Multiple Tabs: Introduce Multiple Tabs Support (#2221)

This commit is contained in:
Alexander Zinchuk 2023-01-28 02:15:26 +01:00
parent 58c71fd908
commit 2a7c78c12a
264 changed files with 13529 additions and 5138 deletions

View File

@ -9,9 +9,15 @@
"jsx-expressions",
"no-async-without-await",
"teactn",
"no-null"
"no-null",
"eslint-multitab-tt"
],
"rules": {
"eslint-multitab-tt/no-immediate-global": "error",
"eslint-multitab-tt/must-update-global-after-await": "off",
"eslint-multitab-tt/set-global-only-variable": "error",
"eslint-multitab-tt/no-getactions-in-actions": "error",
"eslint-multitab-tt/must-specify-action-handler-return-type": "error",
"indent": [
"error",
2,

View File

@ -0,0 +1,19 @@
"use strict";
module.exports = {
root: true,
extends: [
"eslint:recommended",
"plugin:eslint-plugin/recommended",
"plugin:node/recommended",
],
env: {
node: true,
},
overrides: [
{
files: ["tests/**/*.js"],
env: { mocha: true },
},
],
};

View File

@ -0,0 +1,22 @@
/**
* @fileoverview eslint-multitab-tt
* @author undrfined
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const requireIndex = require("requireindex");
//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------
// import all rules in lib/rules
module.exports.rules = requireIndex(__dirname + "/rules");

View File

@ -0,0 +1,39 @@
/**
* @fileoverview Must specify action handler return type
* @author undrfined
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Must specify action handler return type",
recommended: false,
url: null,
},
fixable: null,
schema: [],
messages: {
mustSpecifyActionHandlerReturnType: "Must specify action handler return type",
}
},
create(context) {
return {
ArrowFunctionExpression: (node) => {
if(node.parent.type === "CallExpression" && node.parent.callee.name === 'addActionHandler' && !node.returnType) {
context.report({
node,
messageId: "mustSpecifyActionHandlerReturnType",
})
}
}
};
},
};

View File

@ -0,0 +1,108 @@
/**
* @fileoverview Must update global after await
* @author undrfined
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
// TODO This rule is not working properly
module.exports = {
meta: {
type: "problem",
docs: {
description: "Must update global after await",
recommended: false,
url: null,
},
fixable: null,
schema: [],
messages: {
mustUpdateGlobalAfterAwait: "Global is outdated because of await here -> {{before}}, use global = getGlobal() to update",
}
},
create(context) {
let hasAssignmentOnBlockLevel;
let blocks = 0;
let d;
let hasAwait = false;
let hasAwaitOnBlockLevel;
let assigned;
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
function endFunction() {
hasAwait = false;
assigned = undefined;
d = undefined;
hasAssignmentOnBlockLevel = undefined;
hasAwaitOnBlockLevel = undefined;
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
'FunctionDeclaration:exit': endFunction,
'FunctionExpression:exit': endFunction,
'ArrowFunctionExpression:exit': endFunction,
'AwaitExpression:exit': (node) => {
if(!node) return;
hasAwait = true;
hasAwaitOnBlockLevel = blocks;
d = node;
},
'BlockStatement': () => {
blocks += 1;
},
'BlockStatement:exit': () => {
blocks -= 1;
if(hasAwaitOnBlockLevel && blocks === hasAwaitOnBlockLevel) {
hasAwaitOnBlockLevel = undefined;
}
},
'ReturnStatement:exit': (node) => {
if(hasAwait && hasAwaitOnBlockLevel && blocks === hasAwaitOnBlockLevel && node.parent.type === 'BlockExpression') {
endFunction();
}
},
'AssignmentExpression': (node) => {
if(node.left.type !== "Identifier" || node.left.name !== "global") return;
if(node.right.type !== "CallExpression" || node.right.callee.name !== "getGlobal") return;
if(hasAwaitOnBlockLevel && blocks === hasAwaitOnBlockLevel) {
hasAwait = false;
hasAwaitOnBlockLevel = undefined;
d = undefined;
} else {
hasAssignmentOnBlockLevel = blocks;
assigned = node;
}
},
Identifier: (node) => {
if(node.name !== "global") return;
if(node.parent === assigned) return;
if(hasAwait) {
if(hasAssignmentOnBlockLevel !== undefined && hasAssignmentOnBlockLevel <= blocks) {
endFunction();
return;
}
context.report({
node,
messageId: "mustUpdateGlobalAfterAwait",
data: {
before: d ? d.loc.start.line + ':' + d.loc.start.column : 'unknown'
},
})
}
},
"Program:exit": endFunction,
};
},
};

View File

@ -0,0 +1,40 @@
/**
* @fileoverview Forbid usage of getActions in actions
* @author undrfined
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Forbid usage of getActions in action handlers",
recommended: false,
url: null,
},
fixable: null,
schema: [],
messages: {
noGetActionsInActions: "Do not use getActions inside action handlers, instead use the second argument of the action handler",
}
},
create(context) {
return {
CallExpression: (node) => {
if(!context.getPhysicalFilename().substring(context.getCwd().length).startsWith('/src/global')) return;
if(node.callee.name === 'getActions') {
context.report({
node,
messageId: 'noGetActionsInActions',
})
}
}
};
},
};

View File

@ -0,0 +1,42 @@
/**
* @fileoverview No immediate global
* @author undrfined
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "No immediate global",
recommended: false,
url: null,
},
fixable: null,
schema: [],
messages: {
noImmediateGlobal: "Only use getGlobal() to assign to global variable",
}
},
create(context) {
return {
CallExpression: (node) => {
if(!context.getPhysicalFilename().substring(context.getCwd().length).startsWith('/src/global')) return;
if(node.callee.name === 'getGlobal'
&& node.parent.type !== 'AssignmentExpression'
) {
context.report({
node,
messageId: "noImmediateGlobal",
})
}
}
};
},
};

View File

@ -0,0 +1,53 @@
/**
* @fileoverview setGlobal must only be used with 'global' variable
* @author undrfined
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "setGlobal must only be used with 'global' variable",
recommended: false,
url: null,
},
fixable: null,
schema: [],
hasSuggestions: true,
messages: {
setGlobalOnlyVariable: "setGlobal must only be used with 'global' variable",
}
},
create(context) {
return {
CallExpression: (node) => {
if(node.callee.name === 'setGlobal') {
if(node.arguments[0] && node.arguments[0].type !== 'Identifier' || node.arguments[0].name !== 'global') {
context.report({
node,
messageId: 'setGlobalOnlyVariable',
...(node.parent.type === 'ExpressionStatement' && {
suggest: [{
desc: "Move the global assignment before the setGlobal call",
*fix(fixer) {
const sc = context.getSourceCode();
const parent = node.parent;
yield fixer.insertTextBefore(parent, 'global = ' + sc.getText(node.arguments[0]) + ';\n');
yield fixer.replaceText(node.arguments[0], 'global');
},
}]
}),
})
}
}
}
};
},
};

3647
dev/eslint-multitab/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"name": "eslint-plugin-eslint-multitab-tt",
"version": "0.0.0",
"description": "eslint-multitab-tt",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin"
],
"author": "undrfined",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"scripts": {
"lint": "npm-run-all \"lint:*\"",
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
"lint:js": "eslint .",
"test": "mocha tests --recursive",
"update:eslint-docs": "eslint-doc-generator"
},
"dependencies": {
"requireindex": "^1.2.0"
},
"devDependencies": {
"chalk": "^5.2.0",
"eslint": "^8.19.0",
"eslint-doc-generator": "^1.0.0",
"eslint-plugin-eslint-plugin": "^5.0.0",
"eslint-plugin-node": "^11.1.0",
"mocha": "^10.0.0",
"npm-run-all": "^4.1.5"
},
"engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
},
"peerDependencies": {
"eslint": ">=7"
},
"license": "ISC"
}

924
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,8 @@
"test:record": "playwright codegen localhost:1235",
"prepare": "husky install",
"statoscope:validate": "statoscope validate --input public/build-stats.json",
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json"
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json",
"postinstall": "(cd dev/eslint-multitab && npm i)"
},
"engines": {
"node": "^18",
@ -76,6 +77,7 @@
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-react-app": "^7.0.1",
"eslint-import-resolver-webpack": "^0.13.2",
"eslint-plugin-eslint-multitab-tt": "file:dev/eslint-multitab",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",

View File

@ -6,13 +6,16 @@ import type { GlobalState } from './global/types';
import type { UiLoaderPage } from './components/common/UiLoader';
import { INACTIVE_MARKER, PAGE_TITLE } from './config';
import { PLATFORM_ENV } from './util/environment';
import { IS_MULTITAB_SUPPORTED, PLATFORM_ENV } from './util/environment';
import { selectTabState } from './global/selectors';
import { updateSizes } from './util/windowSize';
import { addActiveTabChangeListener } from './util/activeTabMonitor';
import { hasStoredSession } from './util/sessions';
import buildClassName from './util/buildClassName';
import { parseInitialLocationHash } from './util/routing';
import useFlag from './hooks/useFlag';
import usePrevious from './hooks/usePrevious';
import useAppLayout from './hooks/useAppLayout';
import Auth from './components/auth/Auth';
import Main from './components/main/Main.async';
@ -20,14 +23,13 @@ import LockScreen from './components/main/LockScreen.async';
import AppInactive from './components/main/AppInactive';
import Transition from './components/ui/Transition';
import UiLoader from './components/common/UiLoader';
import { parseInitialLocationHash } from './util/routing';
import useAppLayout from './hooks/useAppLayout';
// import Test from './components/test/TestNoRedundancy';
type StateProps = {
authState: GlobalState['authState'];
isScreenLocked?: boolean;
hasPasscode?: boolean;
isInactiveAuth?: boolean;
hasWebAuthTokenFailed?: boolean;
};
@ -43,23 +45,14 @@ const App: FC<StateProps> = ({
isScreenLocked,
hasPasscode,
hasWebAuthTokenFailed,
isInactiveAuth,
}) => {
const { disconnect } = getActions();
const [isInactive, markInactive] = useFlag(false);
const [isInactive, markInactive, unmarkInactive] = useFlag(false);
const { isMobile } = useAppLayout();
const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
useEffect(() => {
updateSizes();
addActiveTabChangeListener(() => {
disconnect();
document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`;
markInactive();
});
}, [disconnect, markInactive]);
// Prevent drop on elements that do not accept it
useEffect(() => {
const body = document.body;
@ -144,13 +137,35 @@ const App: FC<StateProps> = ({
activeKey = AppScreens.main;
}
useEffect(() => {
updateSizes();
if (IS_MULTITAB_SUPPORTED) return;
addActiveTabChangeListener(() => {
disconnect();
document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`;
markInactive();
});
}, [activeKey, disconnect, markInactive]);
useEffect(() => {
if (isInactiveAuth) {
document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`;
markInactive();
} else {
unmarkInactive();
}
}, [isInactiveAuth, markInactive, unmarkInactive]);
const prevActiveKey = usePrevious(activeKey);
// eslint-disable-next-line consistent-return
function renderContent(isActive: boolean) {
function renderContent() {
switch (activeKey) {
case AppScreens.auth:
return <Auth isActive={isActive} />;
return <Auth />;
case AppScreens.main:
return <Main isMobile={isMobile} />;
case AppScreens.lock:
@ -183,6 +198,7 @@ export default withGlobal(
authState: global.authState,
isScreenLocked: global.passcode?.isScreenLocked,
hasPasscode: global.passcode?.hasPasscode,
isInactiveAuth: selectTabState(global).isInactive,
hasWebAuthTokenFailed: global.hasWebAuthTokenFailed || global.hasWebAuthTokenPasswordRequired,
};
},

View File

@ -1,2 +1,8 @@
// export { initApi, callApi, cancelApiProgress } from './provider';
export { initApi, callApi, cancelApiProgress } from './worker/provider';
export {
initApi, callApi, cancelApiProgress, cancelApiProgressMaster, callApiLocal,
handleMethodCallback,
handleMethodResponse,
updateFullLocalDb,
updateLocalDb,
} from './worker/provider';

View File

@ -1,7 +1,15 @@
import BigInt from 'big-integer';
import type { Api as GramJs } from '../../lib/gramjs';
import type { ApiMessage } from '../types';
import { omitVirtualClassFields } from './apiBuilders/helpers';
import { DATA_BROADCAST_CHANNEL_NAME } from '../../config';
import { constructors } from '../../lib/gramjs/tl';
import { throttle } from '../../util/schedulers';
interface LocalDb {
// eslint-disable-next-line no-restricted-globals
const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
export interface LocalDb {
localMessages: Record<string, ApiMessage>;
// Used for loading avatars and media through in-memory Gram JS instances.
chats: Record<string, GramJs.Chat | GramJs.Channel>;
@ -13,21 +21,99 @@ interface LocalDb {
webDocuments: Record<string, GramJs.TypeWebDocument>;
}
const LOCAL_DB_INITIAL = {
localMessages: {},
chats: {},
users: {},
messages: {},
documents: {},
stickerSets: {},
photos: {},
webDocuments: {},
};
const channel = IS_MULTITAB_SUPPORTED ? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) : undefined;
const localDb: LocalDb = LOCAL_DB_INITIAL;
let batchedUpdates: {
name: string;
prop: string;
value: any;
}[] = [];
const throttledLocalDbUpdate = throttle(() => {
channel!.postMessage({
type: 'localDbUpdate',
batchedUpdates,
});
batchedUpdates = [];
}, 100);
function createProxy(name: string, object: any) {
return new Proxy(object, {
get(target, prop: string, value: any) {
return Reflect.get(target, prop, value);
},
set(target, prop: string, value: any) {
batchedUpdates.push({ name, prop, value });
throttledLocalDbUpdate();
return Reflect.set(target, prop, value);
},
});
}
function convertToVirtualClass(value: any): any {
if (value instanceof Uint8Array) return Buffer.from(value);
if (typeof value === 'object' && Object.keys(value).length === 1 && Object.keys(value)[0] === 'value') {
return BigInt(value.value);
}
if (Array.isArray(value)) {
return value.map(convertToVirtualClass);
}
if (typeof value !== 'object' || !('CONSTRUCTOR_ID' in value)) {
return value;
}
const path = value.className.split('.');
const VirtualClass = path.reduce((acc: any, field: string) => {
return acc[field];
}, constructors);
const valueOmited = omitVirtualClassFields(value);
const valueConverted = Object.keys(valueOmited).reduce((acc, key) => {
acc[key] = convertToVirtualClass(valueOmited[key]);
return acc;
}, {} as Record<string, any>);
return new VirtualClass(valueConverted);
}
function createLocalDbInitial(initial?: LocalDb): LocalDb {
return [
'localMessages', 'chats', 'users', 'messages', 'documents', 'stickerSets', 'photos', 'webDocuments',
]
.reduce((acc: Record<string, any>, key) => {
const value = initial?.[key as keyof LocalDb] ?? {};
const valueVirtualClass = Object.keys(value).reduce((acc2, key2) => {
acc2[key2] = convertToVirtualClass(value[key2]);
return acc2;
}, {} as Record<string, any>);
acc[key] = IS_MULTITAB_SUPPORTED
? createProxy(key, valueVirtualClass)
: valueVirtualClass;
return acc;
}, {} as LocalDb) as LocalDb;
}
const localDb: LocalDb = createLocalDbInitial();
export default localDb;
export function clearLocalDb() {
Object.assign(localDb, LOCAL_DB_INITIAL);
export function broadcastLocalDbUpdateFull() {
if (!channel) return;
channel.postMessage({
type: 'localDbUpdateFull',
localDb: Object.keys(localDb).reduce((acc: Record<string, any>, key) => {
acc[key] = { ...localDb[key as keyof LocalDb] };
return acc;
}, {} as Record<string, any>),
});
}
export function updateFullLocalDb(initial: LocalDb) {
Object.assign(localDb, createLocalDbInitial(initial));
}
export function clearLocalDb() {
Object.assign(localDb, createLocalDbInitial());
}

View File

@ -992,7 +992,7 @@ export function updateChatMemberBannedRights({
export function updateChatAdmin({
chat, user, adminRights, customTitle = '',
}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; customTitle: string }) {
}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; customTitle?: string }) {
const channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel;
const userId = buildInputEntity(user.id, user.accessHash) as GramJs.InputUser;

View File

@ -145,12 +145,12 @@ export function setIsPremium({ isPremium }: { isPremium: boolean }) {
client.setIsPremium(isPremium);
}
export async function destroy(noLogOut = false) {
export async function destroy(noLogOut = false, noClearLocalDb = false) {
if (!noLogOut) {
await invokeRequest(new GramJs.auth.LogOut());
}
clearLocalDb();
if (!noClearLocalDb) clearLocalDb();
await client.destroy();
}

View File

@ -100,3 +100,7 @@ export {
acceptPhoneCall, confirmPhoneCall, requestPhoneCall, decodePhoneCallData, createPhoneCallState,
destroyPhoneCallState, encodePhoneCallData,
} from './phoneCallState';
export {
broadcastLocalDbUpdateFull,
} from '../localDb';

View File

@ -91,7 +91,7 @@ export async function updatePrivateLink({
export async function fetchExportedChatInvites({
peer, admin, limit = 0, isRevoked,
}: { peer: ApiChat; admin: ApiUser; limit: number; isRevoked?: boolean }) {
}: { peer: ApiChat; admin: ApiUser; limit?: number; isRevoked?: boolean }) {
const exportedInvites = await invokeRequest(new GramJs.messages.GetExportedChatInvites({
peer: buildInputPeer(peer.id, peer.accessHash),
adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser,
@ -209,7 +209,7 @@ export async function deleteRevokedExportedChatInvites({
export async function fetchChatInviteImporters({
peer, link, offsetDate = 0, offsetUser, limit = 0, isRequested,
}: {
peer: ApiChat; link?: string; offsetDate: number; offsetUser?: ApiUser; limit: number; isRequested?: boolean;
peer: ApiChat; link?: string; offsetDate?: number; offsetUser?: ApiUser; limit?: number; isRequested?: boolean;
}) {
const result = await invokeRequest(new GramJs.messages.GetChatInviteImporters({
peer: buildInputPeer(peer.id, peer.accessHash),

View File

@ -1,6 +1,6 @@
import type { TelegramClient } from '../../../lib/gramjs';
import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiOnProgress, ApiParsedMedia, ApiPreparedMedia } from '../../types';
import type { ApiOnProgress, ApiParsedMedia } from '../../types';
import {
ApiMediaFormat,
} from '../../types';
@ -52,11 +52,11 @@ export default async function downloadMedia(
void cacheApi.save(cacheName, url, parsed);
}
const prepared = mediaFormat === ApiMediaFormat.Progressive ? '' : prepareMedia(parsed as string | Blob);
const dataBlob = mediaFormat === ApiMediaFormat.Progressive ? '' : parsed as string | Blob;
const arrayBuffer = mediaFormat === ApiMediaFormat.Progressive ? parsed as ArrayBuffer : undefined;
return {
prepared,
dataBlob,
arrayBuffer,
mimeType,
fullSize,
@ -262,14 +262,6 @@ async function parseMedia(
return undefined;
}
function prepareMedia(mediaData: Exclude<ApiParsedMedia, ArrayBuffer>): ApiPreparedMedia {
if (mediaData instanceof Blob) {
return URL.createObjectURL(mediaData);
}
return mediaData;
}
function getMimeType(data: Uint8Array, fallbackMimeType = 'image/jpeg') {
if (data.length < 4) {
return fallbackMimeType;

View File

@ -637,7 +637,7 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment,
export async function pinMessage({
chat, messageId, isUnpin, isOneSide, isSilent,
}: { chat: ApiChat; messageId: number; isUnpin: boolean; isOneSide: boolean; isSilent: boolean }) {
}: { chat: ApiChat; messageId: number; isUnpin: boolean; isOneSide?: boolean; isSilent?: boolean }) {
await invokeRequest(new GramJs.messages.UpdatePinnedMessage({
peer: buildInputPeer(chat.id, chat.accessHash),
id: messageId,

View File

@ -370,8 +370,8 @@ export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broa
isSilent,
shouldShowPreviews,
}: {
isSilent: boolean;
shouldShowPreviews: boolean;
isSilent?: boolean;
shouldShowPreviews?: boolean;
}) {
let peer: GramJs.TypeInputNotifyPeer;
if (peerType === 'contact') {

View File

@ -5,9 +5,11 @@ import type {
ApiOnProgress,
} from '../types';
import type { Methods, MethodArgs, MethodResponse } from './methods/types';
import type { LocalDb } from './localDb';
import { API_THROTTLE_RESET_UPDATES, API_UPDATE_THROTTLE } from '../../config';
import { throttle, throttleWithTickEnd } from '../../util/schedulers';
import { updateFullLocalDb } from './localDb';
import { init as initUpdater } from './updater';
import { init as initAuth } from './methods/auth';
import { init as initChats } from './methods/chats';
@ -24,7 +26,7 @@ import * as methods from './methods';
let onUpdate: OnApiUpdate;
export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs, initialLocalDb?: LocalDb) {
onUpdate = _onUpdate;
initUpdater(handleUpdate);
@ -39,6 +41,8 @@ export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArg
initCalls(handleUpdate);
initPayments(handleUpdate);
if (initialLocalDb) updateFullLocalDb(initialLocalDb);
await initClient(handleUpdate, initialArgs);
}

View File

@ -1,11 +1,15 @@
import type { Api } from '../../../lib/gramjs';
import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
import type { Methods, MethodArgs, MethodResponse } from '../methods/types';
import type { WorkerMessageEvent, OriginRequest } from './types';
import type { WorkerMessageEvent, OriginRequest, ThenArg } from './types';
import type { LocalDb } from '../localDb';
import type { TypedBroadcastChannel } from '../../../util/multitab';
import { DEBUG } from '../../../config';
import { IS_MULTITAB_SUPPORTED } from '../../../util/environment';
import { DATA_BROADCAST_CHANNEL_NAME, DEBUG } from '../../../config';
import generateIdFor from '../../../util/generateIdFor';
import { pause } from '../../../util/schedulers';
import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole';
type RequestStates = {
messageId: string;
@ -20,10 +24,44 @@ const HEALTH_CHECK_MIN_DELAY = 5 * 1000; // 5 sec
let worker: Worker;
const requestStates = new Map<string, RequestStates>();
const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
const savedLocalDb: LocalDb = {
localMessages: {},
chats: {},
users: {},
messages: {},
documents: {},
stickerSets: {},
photos: {},
webDocuments: {},
};
// TODO Re-use `util/WorkerConnector.ts`
let isMasterTab = true;
subscribeToMasterChange((isMasterTabNew) => {
isMasterTab = isMasterTabNew;
});
const channel = IS_MULTITAB_SUPPORTED
? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) as TypedBroadcastChannel
: undefined;
export function initApiOnMasterTab(initialArgs: ApiInitialArgs) {
if (!channel) return;
channel.postMessage({
type: 'initApi',
token: getCurrentTabId(),
initialArgs,
});
}
export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
if (!isMasterTab) {
initApiOnMasterTab(initialArgs);
return Promise.resolve();
}
if (!worker) {
if (DEBUG) {
// eslint-disable-next-line no-console
@ -40,11 +78,33 @@ export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
return makeRequest({
type: 'initApi',
args: [initialArgs],
args: [initialArgs, savedLocalDb],
});
}
export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
export function updateLocalDb(name: keyof LocalDb, prop: string, value: any) {
savedLocalDb[name][prop] = value;
}
export function updateFullLocalDb(initial: LocalDb) {
Object.assign(savedLocalDb, initial);
}
export function callApiOnMasterTab(payload: any) {
if (!channel) return;
channel.postMessage({
type: 'callApi',
token: getCurrentTabId(),
...payload,
});
}
/*
* Call a worker method on this tab's worker, without transferring to master tab
* Mostly needed to disconnect worker when re-electing master
*/
export function callApiLocal<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
if (!worker) {
if (DEBUG) {
// eslint-disable-next-line no-console
@ -86,6 +146,51 @@ export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<
return promise as MethodResponse<T>;
}
export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
if (!worker && isMasterTab) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn('API is not initialized');
}
return undefined;
}
const promise = isMasterTab ? makeRequest({
type: 'callMethod',
name: fnName,
args,
}) : makeRequestToMaster({
name: fnName,
args,
});
// Some TypeScript magic to make sure `VirtualClass` is never returned from any method
if (DEBUG) {
(async () => {
try {
type ForbiddenTypes =
Api.VirtualClass<any>
| (Api.VirtualClass<any> | undefined)[];
type ForbiddenResponses =
ForbiddenTypes
| (AnyLiteral & { [k: string]: ForbiddenTypes });
// Unwrap all chained promises
const response = await promise;
// Make sure responses do not include `VirtualClass` instances
const allowedResponse: Exclude<typeof response, ForbiddenResponses> = response;
// Suppress "unused variable" constraint
void allowedResponse;
} catch (err) {
// Do noting
}
})();
}
return promise as MethodResponse<T>;
}
export function cancelApiProgress(progressCallback: ApiOnProgress) {
progressCallback.isCanceled = true;
@ -94,6 +199,20 @@ export function cancelApiProgress(progressCallback: ApiOnProgress) {
return;
}
if (isMasterTab) {
cancelApiProgressMaster(messageId);
} else {
if (!channel) return;
channel.postMessage({
type: 'cancelApiProgress',
token: getCurrentTabId(),
messageId,
});
}
}
export function cancelApiProgressMaster(messageId: string) {
worker.postMessage({
type: 'cancelProgress',
messageId,
@ -105,22 +224,79 @@ function subscribeToWorker(onUpdate: OnApiUpdate) {
if (data.type === 'update') {
onUpdate(data.update);
} else if (data.type === 'methodResponse') {
const requestState = requestStates.get(data.messageId);
if (requestState) {
if (data.error) {
requestState.reject(data.error);
} else {
requestState.resolve(data.response);
}
}
handleMethodResponse(data);
} else if (data.type === 'methodCallback') {
requestStates.get(data.messageId)?.callback?.(...data.callbackArgs);
handleMethodCallback(data);
} else if (data.type === 'unhandledError') {
throw new Error(data.error?.message);
}
});
}
export function handleMethodResponse(data: { messageId: string;
response?: ThenArg<MethodResponse<keyof Methods>>;
error?: { message: string };
}) {
const requestState = requestStates.get(data.messageId);
if (requestState) {
if (data.error) {
requestState.reject(data.error);
} else {
requestState.resolve(data.response);
}
}
}
export function handleMethodCallback(data: { messageId: string;
callbackArgs: any[];
}) {
requestStates.get(data.messageId)?.callback?.(...data.callbackArgs);
}
function makeRequestToMaster(message: {
messageId?: string;
name: keyof Methods;
args: MethodArgs<keyof Methods>;
withCallback?: boolean;
}) {
const messageId = generateIdFor(requestStates);
const payload = {
messageId,
...message,
};
const requestState = { messageId } as RequestStates;
// Re-wrap type because of `postMessage`
const promise: Promise<MethodResponse<keyof Methods>> = new Promise((resolve, reject) => {
Object.assign(requestState, { resolve, reject });
});
if ('args' in payload && 'name' in payload && typeof payload.args[1] === 'function') {
payload.withCallback = true;
const callback = payload.args.pop() as AnyToVoidFunction;
requestState.callback = callback;
requestStatesByCallback.set(callback, requestState);
}
requestStates.set(messageId, requestState);
promise
.catch(() => undefined)
.finally(() => {
requestStates.delete(messageId);
if (requestState.callback) {
requestStatesByCallback.delete(requestState.callback);
}
});
callApiOnMasterTab(payload);
return promise;
}
function makeRequest(message: OriginRequest) {
const messageId = generateIdFor(requestStates);
const payload: OriginRequest = {

View File

@ -1,5 +1,6 @@
import type { ApiInitialArgs, ApiUpdate } from '../../types';
import type { Methods, MethodArgs, MethodResponse } from '../methods/types';
import type { LocalDb } from '../localDb';
export type ThenArg<T> = T extends Promise<infer U> ? U : T;
@ -27,7 +28,7 @@ export interface WorkerMessageEvent {
export type OriginRequest = {
type: 'initApi';
messageId?: string;
args: [ApiInitialArgs];
args: [ApiInitialArgs, LocalDb];
} | {
type: 'callMethod';
messageId?: string;

View File

@ -21,7 +21,7 @@ onmessage = async (message: OriginMessageEvent) => {
switch (data.type) {
case 'initApi': {
await initApi(onUpdate, data.args[0]);
await initApi(onUpdate, data.args[0], data.args[1]);
break;
}
case 'callMethod': {

View File

@ -1,6 +1,6 @@
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
import type { ApiUser } from './users';
import type { ApiLimitType } from '../../global/types';
import type { ApiLimitType, CallbackAction } from '../../global/types';
export interface ApiInitialArgs {
userAgent: string;
@ -107,7 +107,7 @@ export type ApiNotification = {
title?: string;
message: string;
actionText?: string;
action: VoidFunction;
action?: CallbackAction;
className?: string;
};

View File

@ -1,5 +1,6 @@
import { getActions, getGlobal } from '../global';
import { IS_MULTITAB_SUPPORTED } from '../util/environment';
import { DEBUG } from '../config';
// eslint-disable-next-line import/no-cycle
@ -11,7 +12,7 @@ if (DEBUG) {
console.log('>>> FINISH LOAD MAIN BUNDLE');
}
const { connectionState, passcode: { isScreenLocked } } = getGlobal();
if (!connectionState && !isScreenLocked) {
const { passcode: { isScreenLocked }, connectionState } = getGlobal();
if (!connectionState && !isScreenLocked && !IS_MULTITAB_SUPPORTED) {
getActions().initApi();
}

View File

@ -1,11 +1,10 @@
import type { FC } from '../../lib/teact/teact';
import React, { useEffect, memo } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { GlobalState } from '../../global/types';
import '../../global/actions/initial';
import { pick } from '../../util/iteratees';
import { PLATFORM_ENV } from '../../util/environment';
import useHistoryBack from '../../hooks/useHistoryBack';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
@ -19,26 +18,15 @@ import AuthQrCode from './AuthQrCode';
import './Auth.scss';
type OwnProps = {
isActive: boolean;
};
type StateProps = Pick<GlobalState, 'authState'>;
type StateProps = Pick<GlobalState, 'authState' | 'hasWebAuthTokenPasswordRequired'>;
const Auth: FC<OwnProps & StateProps> = ({
isActive, authState, hasWebAuthTokenPasswordRequired,
const Auth: FC<StateProps> = ({
authState,
}) => {
const {
reset, initApi, returnToAuthPhoneNumber, goToAuthQrCode,
returnToAuthPhoneNumber, goToAuthQrCode,
} = getActions();
useEffect(() => {
if (isActive && !hasWebAuthTokenPasswordRequired) {
reset();
initApi();
}
}, [isActive, reset, initApi, hasWebAuthTokenPasswordRequired]);
const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
const handleChangeAuthorizationMethod = () => {
@ -102,6 +90,10 @@ const Auth: FC<OwnProps & StateProps> = ({
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => pick(global, ['authState', 'hasWebAuthTokenPasswordRequired']),
export default memo(withGlobal(
(global): StateProps => {
return {
authState: global.authState,
};
},
)(Auth));

View File

@ -82,6 +82,10 @@ const AuthCode: FC<StateProps> = ({
}
}, [authError, clearAuthError, code, isTracking, setAuthCode]);
function handleReturnToAuthPhoneNumber() {
returnToAuthPhoneNumber();
}
return (
<div id="auth-code-form" className="custom-scroll">
<div className="auth-form">
@ -95,7 +99,7 @@ const AuthCode: FC<StateProps> = ({
{authPhoneNumber}
<div
className="auth-number-edit"
onClick={returnToAuthPhoneNumber}
onClick={handleReturnToAuthPhoneNumber}
role="button"
tabIndex={0}
title={lang('WrongNumber')}

View File

@ -203,6 +203,10 @@ const AuthPhoneNumber: FC<StateProps> = ({
}
}
const handleGoToAuthQrCode = useCallback(() => {
goToAuthQrCode();
}, [goToAuthQrCode]);
const isAuthReady = authState === 'authorizationStateWaitPhoneNumber';
return (
@ -242,7 +246,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
)
)}
{isAuthReady && (
<Button isText ripple isLoading={authIsLoadingQrCode} onClick={goToAuthQrCode}>
<Button isText ripple isLoading={authIsLoadingQrCode} onClick={handleGoToAuthQrCode}>
{lang('Login.QR.Login')}
</Button>
)}

View File

@ -129,6 +129,10 @@ const AuthCode: FC<StateProps> = ({
});
}, [markIsLoading, setSettingOption, suggestedLanguage, unmarkIsLoading]);
const habdleReturnToAuthPhoneNumber = useCallback(() => {
returnToAuthPhoneNumber();
}, [returnToAuthPhoneNumber]);
const isAuthReady = authState === 'authorizationStateWaitQrCode';
return (
@ -162,7 +166,7 @@ const AuthCode: FC<StateProps> = ({
<li><span>{lang('Login.QR.Help3')}</span></li>
</ol>
{isAuthReady && (
<Button isText onClick={returnToAuthPhoneNumber}>{lang('Login.QR.Cancel')}</Button>
<Button isText onClick={habdleReturnToAuthPhoneNumber}>{lang('Login.QR.Cancel')}</Button>
)}
{suggestedLanguage && suggestedLanguage !== language && continueText && (
<Button isText isLoading={isLoading} onClick={handleLangChange}>{continueText}</Button>

View File

@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../global';
import type { ApiGroupCall, ApiUser } from '../../api/types';
import { selectActiveGroupCall, selectPhoneCallUser } from '../../global/selectors/calls';
import { selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import useLang from '../../hooks/useLang';
@ -33,6 +34,10 @@ const ActiveCallHeader: FC<StateProps> = ({
};
}, [isCallPanelVisible]);
function handleToggleGroupCallPanel() {
toggleGroupCallPanel();
}
if (!groupCall && !phoneCallUser) return undefined;
return (
@ -41,7 +46,7 @@ const ActiveCallHeader: FC<StateProps> = ({
'ActiveCallHeader',
isCallPanelVisible && 'open',
)}
onClick={toggleGroupCallPanel}
onClick={handleToggleGroupCallPanel}
>
<span className="title">{phoneCallUser?.firstName || groupCall?.title || lang('VoipGroupVoiceChat')}</span>
</div>
@ -50,10 +55,11 @@ const ActiveCallHeader: FC<StateProps> = ({
export default memo(withGlobal(
(global): StateProps => {
const tabState = selectTabState(global);
return {
groupCall: selectActiveGroupCall(global),
isCallPanelVisible: global.isCallPanelVisible,
phoneCallUser: selectPhoneCallUser(global),
groupCall: tabState.isMasterTab ? selectActiveGroupCall(global) : undefined,
isCallPanelVisible: tabState.isCallPanelVisible,
phoneCallUser: tabState.isMasterTab ? selectPhoneCallUser(global) : undefined,
};
},
)(ActiveCallHeader));

View File

@ -25,6 +25,7 @@ import {
selectGroupCallParticipant,
selectIsAdminInActiveGroupCall,
} from '../../../global/selectors/calls';
import { selectTabState } from '../../../global/selectors';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
@ -249,6 +250,10 @@ const GroupCall: FC<OwnProps & StateProps> = ({
}
}, [isLeaving, leaveGroupCall, shouldEndGroupCall]);
const handleToggleGroupCallPresentation = useCallback(() => {
toggleGroupCallPresentation();
}, [toggleGroupCallPresentation]);
return (
<Modal
isOpen={!isCallPanelVisible && !isLeaving}
@ -293,7 +298,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
{IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand && (
<MenuItem
icon="share-screen-outlined"
onClick={toggleGroupCallPresentation}
onClick={handleToggleGroupCallPresentation}
>
{lang(hasPresentation ? 'VoipChatStopScreenCapture' : 'VoipChatStartScreenCapture')}
</MenuItem>
@ -416,7 +421,7 @@ export default memo(withGlobal<OwnProps>(
isSpeakerEnabled: !isSpeakerDisabled,
participantsCount,
meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!),
isCallPanelVisible: Boolean(global.isCallPanelVisible),
isCallPanelVisible: Boolean(selectTabState(global).isCallPanelVisible),
isAdmin: selectIsAdminInActiveGroupCall(global),
participants,
};

View File

@ -1,6 +1,6 @@
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useMemo } from '../../../lib/teact/teact';
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import useLang from '../../../hooks/useLang';
@ -36,15 +36,23 @@ const GroupCallParticipantList: FC<OwnProps & StateProps> = ({
return Object.keys(participants || {});
}, [participants]);
const handleLoadMoreGroupCallParticipants = useCallback(() => {
loadMoreGroupCallParticipants();
}, [loadMoreGroupCallParticipants]);
const [viewportIds, getMore] = useInfiniteScroll(
loadMoreGroupCallParticipants,
handleLoadMoreGroupCallParticipants,
participantsIds,
participantsIds.length >= participantsCount,
);
function handleCreateGroupCallInviteLink() {
createGroupCallInviteLink();
}
return (
<div className="participants">
<div className="invite-btn" onClick={createGroupCallInviteLink}>
<div className="invite-btn" onClick={handleCreateGroupCallInviteLink}>
<div className="icon">
<i className="icon-add-user" />
</div>

View File

@ -108,7 +108,7 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
}
toggleGroupCallMute({
participantId: id,
participantId: id!,
value: isAdmin ? !shouldRaiseHand : !isMutedByMe,
});
}, [closeDropdown, toggleGroupCallMute, id, isAdmin, shouldRaiseHand, isMutedByMe]);
@ -131,12 +131,12 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
runThrottled(() => {
if (value === VOLUME_ZERO) {
toggleGroupCallMute({
participantId: id,
participantId: id!,
value: true,
});
} else {
setGroupCallParticipantVolume({
participantId: id,
participantId: id!,
volume: Math.floor(value * GROUP_CALL_VOLUME_MULTIPLIER),
});
}

View File

@ -9,7 +9,7 @@ import type { AnimationLevel } from '../../../types';
import { selectChatGroupCall } from '../../../global/selectors/calls';
import buildClassName from '../../../util/buildClassName';
import { selectChat } from '../../../global/selectors';
import { selectChat, selectTabState } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import Button from '../../ui/Button';
@ -42,17 +42,17 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
animationLevel,
}) => {
const {
joinGroupCall,
requestMasterAndJoinGroupCall,
subscribeToGroupCallUpdates,
} = getActions();
const lang = useLang();
const handleJoinGroupCall = useCallback(() => {
joinGroupCall({
requestMasterAndJoinGroupCall({
chatId,
});
}, [joinGroupCall, chatId]);
}, [requestMasterAndJoinGroupCall, chatId]);
const participants = groupCall?.participants;
@ -128,6 +128,7 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }) => {
const chat = selectChat(global, chatId)!;
const groupCall = selectChatGroupCall(global, chatId);
const activeGroupCallId = selectTabState(global).isMasterTab ? global.groupCalls.activeGroupCallId : undefined;
return {
groupCall,
usersById: global.users.byId,
@ -135,7 +136,7 @@ export default memo(withGlobal<OwnProps>(
activeGroupCallId: global.groupCalls.activeGroupCallId,
isActive: ((!groupCall ? (chat && chat.isCallNotEmpty && chat.isCallActive)
: (groupCall.participantsCount > 0 && groupCall.isLoaded)))
&& (global.groupCalls.activeGroupCallId !== groupCall?.id),
&& (activeGroupCallId !== groupCall?.id),
animationLevel: global.settings.byKey.animationLevel,
};
},

View File

@ -14,6 +14,7 @@ import {
IS_REQUEST_FULLSCREEN_SUPPORTED,
} from '../../../util/environment';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import { selectTabState } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { selectPhoneCallUser } from '../../../global/selectors/calls';
import useLang from '../../../hooks/useLang';
@ -52,7 +53,7 @@ const PhoneCall: FC<StateProps> = ({
}) => {
const lang = useLang();
const {
hangUp, acceptCall, playGroupCallSound, toggleGroupCallPanel, connectToActivePhoneCall,
hangUp, requestMasterAndAcceptCall, playGroupCallSound, toggleGroupCallPanel, connectToActivePhoneCall,
} = getActions();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
@ -347,7 +348,7 @@ const PhoneCall: FC<StateProps> = ({
)}
{isIncomingRequested && (
<PhoneCallButton
onClick={acceptCall}
onClick={requestMasterAndAcceptCall}
icon="phone-discard"
isDisabled={isDiscarded}
label={lang('lng_call_accept')}
@ -370,12 +371,13 @@ const PhoneCall: FC<StateProps> = ({
export default memo(withGlobal(
(global): StateProps => {
const { phoneCall, currentUserId } = global;
const { isCallPanelVisible, isMasterTab } = selectTabState(global);
return {
isCallPanelVisible: Boolean(global.isCallPanelVisible),
isCallPanelVisible: Boolean(isCallPanelVisible),
user: selectPhoneCallUser(global),
isOutgoing: phoneCall?.adminId === currentUserId,
phoneCall,
phoneCall: isMasterTab ? phoneCall : undefined,
animationLevel: global.settings.byKey.animationLevel,
};
},

View File

@ -118,7 +118,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
isMuted: !newAreNotificationsEnabled,
});
} else {
updateChatMutedState({ chatId, isMuted: !newAreNotificationsEnabled });
updateChatMutedState({ chatId: chatId!, isMuted: !newAreNotificationsEnabled });
}
});

View File

@ -80,7 +80,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
const handleDeleteAndStop = useCallback(() => {
deleteHistory({ chatId: chat.id, shouldDeleteForAll: true });
blockContact({ contactId: chat.id, accessHash: chat.accessHash });
blockContact({ contactId: chat.id, accessHash: chat.accessHash! });
onClose();
}, [deleteHistory, chat.id, chat.accessHash, blockContact, onClose]);
@ -89,7 +89,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
if (isPrivateChat) {
deleteHistory({ chatId: chat.id, shouldDeleteForAll: false });
} else if (isBasicGroup) {
deleteChatUser({ chatId: chat.id, userId: currentUserId });
deleteChatUser({ chatId: chat.id, userId: currentUserId! });
deleteHistory({ chatId: chat.id, shouldDeleteForAll: false });
} else if ((isChannel || isSuperGroup) && !chat.isCreator) {
leaveChannel({ chatId: chat.id });

View File

@ -17,13 +17,13 @@ type OwnProps = {
const GroupCallLink: FC<OwnProps> = ({
className, groupCall, children,
}) => {
const { joinGroupCall } = getActions();
const { requestMasterAndJoinGroupCall } = getActions();
const handleClick = useCallback(() => {
if (groupCall) {
joinGroupCall({ id: groupCall.id, accessHash: groupCall.accessHash });
requestMasterAndJoinGroupCall({ id: groupCall.id, accessHash: groupCall.accessHash });
}
}, [groupCall, joinGroupCall]);
}, [groupCall, requestMasterAndJoinGroupCall]);
if (!groupCall) {
return children;

View File

@ -37,7 +37,6 @@ type StateProps = {
const PinMessageModal: FC<OwnProps & StateProps> = ({
isOpen,
messageId,
chatId,
isChannel,
isGroup,
isSuperGroup,
@ -49,17 +48,17 @@ const PinMessageModal: FC<OwnProps & StateProps> = ({
const handlePinMessageForAll = useCallback(() => {
pinMessage({
chatId, messageId, isUnpin: false,
messageId, isUnpin: false,
});
onClose();
}, [pinMessage, chatId, messageId, onClose]);
}, [pinMessage, messageId, onClose]);
const handlePinMessage = useCallback(() => {
pinMessage({
chatId, messageId, isUnpin: false, isOneSide: true, isSilent: true,
messageId, isUnpin: false, isOneSide: true, isSilent: true,
});
onClose();
}, [chatId, messageId, onClose, pinMessage]);
}, [messageId, onClose, pinMessage]);
const lang = useLang();

View File

@ -14,6 +14,7 @@ import { MediaViewerOrigin } from '../../types';
import { IS_TOUCH_ENV } from '../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import {
selectTabState,
selectChat, selectCurrentMessageList, selectThreadMessagesCount, selectUser, selectUserStatus,
} from '../../global/selectors';
import { getUserStatus, isChatChannel, isUserOnline } from '../../global/helpers';
@ -336,7 +337,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf;
const { animationLevel } = global.settings.byKey;
const { mediaId, avatarOwnerId } = global.mediaViewer;
const { mediaId, avatarOwnerId } = selectTabState(global).mediaViewer;
const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined;

View File

@ -47,7 +47,7 @@ const ReportModal: FC<OwnProps> = ({
const handleReport = useCallback(() => {
switch (subject) {
case 'messages':
reportMessages({ messageIds, reason: selectedReason, description });
reportMessages({ messageIds: messageIds!, reason: selectedReason, description });
exitMessageSelectMode();
break;
case 'peer':

View File

@ -3,7 +3,7 @@ import React, { useCallback, memo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import useLang from '../../hooks/useLang';
import { selectChatMessage } from '../../global/selectors';
import { selectChatMessage, selectTabState } from '../../global/selectors';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import Modal from '../ui/Modal';
@ -40,6 +40,10 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
}, CLOSE_ANIMATION_DURATION);
}, [closeSeenByModal, openChat]);
const handleCloseSeenByModal = useCallback(() => {
closeSeenByModal();
}, [closeSeenByModal]);
const renderingMemberIds = useCurrentOrPrev(memberIds, true);
return (
@ -64,7 +68,7 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
<Button
className="confirm-dialog-button"
isText
onClick={closeSeenByModal}
onClick={handleCloseSeenByModal}
>
{lang('Close')}
</Button>
@ -74,7 +78,7 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { chatId, messageId } = global.seenByModal || {};
const { chatId, messageId } = selectTabState(global).seenByModal || {};
if (!chatId || !messageId) {
return {};
}

View File

@ -79,7 +79,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
const prevStickerSet = usePrevious(stickerSet);
const renderingStickerSet = stickerSet || prevStickerSet;
const isAdded = !renderingStickerSet?.isArchived && renderingStickerSet?.installedDate;
const isAdded = Boolean(!renderingStickerSet?.isArchived && renderingStickerSet?.installedDate);
const isEmoji = renderingStickerSet?.isEmoji;
const isButtonLocked = !isAdded && isSetPremium && !isCurrentUserPremium;

View File

@ -2,12 +2,16 @@ import React, { useEffect } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global';
import { ApiMediaFormat } from '../../api/types';
import type { GlobalState } from '../../global/types';
import type { TabState } from '../../global/types';
import type { ThemeKey } from '../../types';
import type { FC } from '../../lib/teact/teact';
import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting
import { selectIsRightColumnShown, selectTheme } from '../../global/selectors';
import {
selectIsRightColumnShown,
selectTheme,
selectTabState,
} from '../../global/selectors';
import { DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR } from '../../config';
import useFlag from '../../hooks/useFlag';
import useShowTransition from '../../hooks/useShowTransition';
@ -41,7 +45,7 @@ type OwnProps = {
isMobile?: boolean;
};
type StateProps = Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
type StateProps = Pick<TabState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
isRightColumnShown?: boolean;
leftColumnWidth?: number;
theme: ThemeKey;
@ -74,7 +78,7 @@ function preloadAvatars() {
const preloadTasks = {
main: () => Promise.all([
loadModule(Bundles.Main, 'Main')
loadModule(Bundles.Main)
.then(preloadFonts),
preloadAvatars(),
preloadImage(reactionThumbsPath),
@ -176,12 +180,13 @@ const UiLoader: FC<OwnProps & StateProps> = ({
export default withGlobal<OwnProps>(
(global, { isMobile }): StateProps => {
const theme = selectTheme(global);
const tabState = selectTabState(global);
return {
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
uiReadyState: global.uiReadyState,
shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations,
uiReadyState: tabState.uiReadyState,
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
leftColumnWidth: global.leftColumnWidth,
leftColumnWidth: tabState.leftColumnWidth,
theme,
};
},

View File

@ -52,11 +52,11 @@ export default function useAnimatedEmoji(
if (!container) return;
sendEmojiInteraction({
chatId,
messageId,
chatId: chatId!,
messageId: messageId!,
localEffect,
emoji,
interactions: interactions.current,
emoji: emoji!,
interactions: interactions.current!,
});
startedInteractions.current = undefined;
interactions.current = undefined;
@ -91,7 +91,7 @@ export default function useAnimatedEmoji(
interactWithAnimatedEmoji({
localEffect,
emoji,
emoji: emoji!,
x,
y,
startSize: size,
@ -131,8 +131,8 @@ export default function useAnimatedEmoji(
sendWatchingEmojiInteraction({
id,
chatId,
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji,
chatId: chatId!,
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect)! : emoji!,
startSize: size,
x,
y,

View File

@ -8,7 +8,7 @@ import { LeftColumnContent, SettingsScreens } from '../../types';
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/environment';
import captureEscKeyListener from '../../util/captureEscKeyListener';
import { selectCurrentChat, selectIsForumPanelOpen } from '../../global/selectors';
import { selectTabState, selectCurrentChat, selectIsForumPanelOpen } from '../../global/selectors';
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
import { useResize } from '../../hooks/useResize';
import { useHotkeys } from '../../hooks/useHotkeys';
@ -113,13 +113,13 @@ const LeftColumn: FC<StateProps> = ({
function fullReset() {
setContent(LeftColumnContent.ChatList);
setContactsFilter('');
setGlobalSearchClosing(true);
setGlobalSearchClosing({ isClosing: true });
resetChatCreation();
setTimeout(() => {
setGlobalSearchQuery({ query: '' });
setGlobalSearchDate({ date: undefined });
setGlobalSearchChatId({ id: undefined });
setGlobalSearchClosing(false);
setGlobalSearchClosing({ isClosing: false });
setLastResetTime(Date.now());
}, RESET_TRANSITION_DELAY_MS);
}
@ -376,13 +376,15 @@ const LeftColumn: FC<StateProps> = ({
if (nextSettingsScreen !== undefined) {
setContent(LeftColumnContent.Settings);
setSettingsScreen(nextSettingsScreen);
requestNextSettingsScreen(undefined);
requestNextSettingsScreen({ screen: undefined });
}
}, [nextSettingsScreen, requestNextSettingsScreen]);
const {
initResize, resetResize, handleMouseUp,
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth, '--left-column-width');
} = useResize(resizeRef, (n) => setLeftColumnWidth({
leftColumnWidth: n,
}), resetLeftColumnWidth, leftColumnWidth, '--left-column-width');
const handleSettingsScreenSelect = useCallback((screen: SettingsScreens) => {
setContent(LeftColumnContent.Settings);
@ -479,30 +481,29 @@ const LeftColumn: FC<StateProps> = ({
export default memo(withGlobal(
(global): StateProps => {
const tabState = selectTabState(global);
const {
globalSearch: {
query,
date,
},
chatFolders: {
activeChatFolder,
},
shouldSkipHistoryAnimations,
leftColumnWidth,
activeChatFolder,
nextSettingsScreen,
} = tabState;
const {
currentUserId,
passcode: {
hasPasscode,
},
settings: {
nextScreen: nextSettingsScreen,
},
isUpdateAvailable,
} = global;
const currentChat = selectCurrentChat(global);
const isChatOpen = Boolean(currentChat?.id);
const isForumPanelOpen = selectIsForumPanelOpen(global);
const forumPanelChatId = global.forumPanelChatId;
const forumPanelChatId = tabState.forumPanelChatId;
return {
searchQuery: query,
@ -517,7 +518,7 @@ export default memo(withGlobal(
isUpdateAvailable,
isForumPanelOpen,
forumPanelChatId,
isClosingSearch: global.globalSearch.isClosing,
isClosingSearch: tabState.globalSearch.isClosing,
};
},
)(LeftColumn));

View File

@ -34,7 +34,7 @@ import {
selectNotifyExceptions,
selectUserStatus,
selectTopicFromMessage,
selectThreadParam,
selectThreadParam, selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
@ -323,7 +323,7 @@ export default memo(withGlobal<OwnProps>(
type: messageListType,
} = selectCurrentMessageList(global) || {};
const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID;
const isSelectedForum = chatId === global.forumPanelChatId;
const isSelectedForum = chatId === selectTabState(global).forumPanelChatId;
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;

View File

@ -14,6 +14,7 @@ import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { selectCurrentLimit } from '../../../global/selectors/limits';
import { selectTabState, selectIsForumPanelOpen } from '../../../global/selectors';
import useShowTransition from '../../../hooks/useShowTransition';
import useLang from '../../../hooks/useLang';
import useHistoryBack from '../../../hooks/useHistoryBack';
@ -22,7 +23,6 @@ import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManag
import Transition from '../../ui/Transition';
import TabList from '../../ui/TabList';
import ChatList from './ChatList';
import { selectIsForumPanelOpen } from '../../../global/selectors';
type OwnProps = {
onScreenSelect: (screen: SettingsScreens) => void;
@ -69,10 +69,10 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
const lang = useLang();
useEffect(() => {
if (lastSyncTime) {
if (lastSyncTime && !orderedFolderIds) {
loadChatFolders();
}
}, [lastSyncTime, loadChatFolders]);
}, [lastSyncTime, loadChatFolders, orderedFolderIds]);
const allChatsFolder = useMemo(() => {
return {
@ -117,7 +117,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
}, [displayedFolders, folderCountersById, maxFolders]);
const handleSwitchTab = useCallback((index: number) => {
setActiveChatFolder(index, { forceOnHeavyAnimation: true });
setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true });
}, [setActiveChatFolder]);
// Prevent `activeTab` pointing at non-existing folder after update
@ -127,7 +127,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
}
if (activeChatFolder >= folderTabs.length) {
setActiveChatFolder(FIRST_FOLDER_INDEX);
setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX });
}
}, [activeChatFolder, folderTabs, setActiveChatFolder]);
@ -140,10 +140,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
selectorToPreventScroll: '.chat-list',
onSwipe: ((e, direction) => {
if (direction === SwipeDirection.Left) {
setActiveChatFolder(Math.min(activeChatFolder + 1, folderTabs.length - 1), { forceOnHeavyAnimation: true });
setActiveChatFolder(
{ activeChatFolder: Math.min(activeChatFolder + 1, folderTabs.length - 1) },
{ forceOnHeavyAnimation: true },
);
return true;
} else if (direction === SwipeDirection.Right) {
setActiveChatFolder(Math.max(0, activeChatFolder - 1), { forceOnHeavyAnimation: true });
setActiveChatFolder({ activeChatFolder: Math.max(0, activeChatFolder - 1) }, { forceOnHeavyAnimation: true });
return true;
}
@ -156,13 +159,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
isNotInFirstFolderRef.current = !isInFirstFolder;
useEffect(() => (isNotInFirstFolderRef.current ? captureEscKeyListener(() => {
if (isNotInFirstFolderRef.current) {
setActiveChatFolder(FIRST_FOLDER_INDEX);
setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX });
}
}) : undefined), [activeChatFolder, setActiveChatFolder]);
useHistoryBack({
isActive: !isInFirstFolder,
onBack: () => setActiveChatFolder(FIRST_FOLDER_INDEX, { forceOnHeavyAnimation: true }),
onBack: () => setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX }, { forceOnHeavyAnimation: true }),
});
useEffect(() => {
@ -179,7 +182,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
const folder = Number(digit) - 1;
if (folder > folderTabs.length - 1) return;
setActiveChatFolder(folder, { forceOnHeavyAnimation: true });
setActiveChatFolder({ activeChatFolder: folder }, { forceOnHeavyAnimation: true });
e.preventDefault();
}
};
@ -245,12 +248,11 @@ export default memo(withGlobal<OwnProps>(
chatFolders: {
byId: chatFoldersById,
orderedIds: orderedFolderIds,
activeChatFolder,
},
currentUserId,
lastSyncTime,
shouldSkipHistoryAnimations,
} = global;
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
return {
chatFoldersById,

View File

@ -12,7 +12,9 @@ import {
TOPICS_SLICE, TOPIC_HEIGHT_PX, TOPIC_LIST_SENSITIVE_AREA,
} from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { selectChat, selectCurrentMessageList, selectIsForumPanelOpen } from '../../../global/selectors';
import {
selectChat, selectCurrentMessageList, selectIsForumPanelOpen, selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { getOrderedTopics } from '../../../global/helpers';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
@ -264,7 +266,7 @@ export default memo(withGlobal<OwnProps>(
(global, ownProps, detachWhenChanged): StateProps => {
detachWhenChanged(selectIsForumPanelOpen(global));
const chatId = global.forumPanelChatId;
const chatId = selectTabState(global).forumPanelChatId;
const chat = chatId ? selectChat(global, chatId) : undefined;
const {
chatId: currentChatId,

View File

@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../../global';
import type { AnimationLevel, ISettings } from '../../../types';
import { LeftColumnContent, SettingsScreens } from '../../../types';
import type { ApiChat } from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { TabState, GlobalState } from '../../../global/types';
import {
ANIMATION_LEVEL_MAX,
@ -25,7 +25,7 @@ import { formatDateToString } from '../../../util/dateFormat';
import switchTheme from '../../../util/switchTheme';
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
import { clearWebsync } from '../../../util/websync';
import { selectCurrentMessageList, selectTheme } from '../../../global/selectors';
import { selectCurrentMessageList, selectTabState, selectTheme } from '../../../global/selectors';
import { isChatArchived } from '../../../global/helpers';
import useLang from '../../../hooks/useLang';
import useConnectionStatus from '../../../hooks/useConnectionStatus';
@ -74,7 +74,7 @@ type StateProps =
areChatsLoaded?: boolean;
hasPasscode?: boolean;
}
& Pick<GlobalState, 'connectionState' | 'isSyncing' | 'canInstall'>;
& Pick<GlobalState, 'connectionState' | 'isSyncing'> & Pick<TabState, 'canInstall'>;
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
const LEGACY_VERSION_URL = 'https://web.telegram.org/?legacy=1';
@ -153,7 +153,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
if (hasPasscode) {
lockScreen();
} else {
requestNextSettingsScreen(SettingsScreens.PasscodeDisabled);
requestNextSettingsScreen({ screen: SettingsScreens.PasscodeDisabled });
}
}, [hasPasscode, lockScreen, requestNextSettingsScreen]);
@ -461,9 +461,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const tabState = selectTabState(global);
const {
query: searchQuery, fetchingStatus, chatId, date,
} = global.globalSearch;
} = tabState.globalSearch;
const { currentUserId, connectionState, isSyncing } = global;
const { byId: chatsById } = global.chats;
const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey;
@ -483,7 +484,7 @@ export default memo(withGlobal<OwnProps>(
isConnectionStatusMinimized,
areChatsLoaded: Boolean(global.chats.listIds.active),
hasPasscode: Boolean(global.passcode.hasPasscode),
canInstall: Boolean(global.canInstall),
canInstall: Boolean(tabState.canInstall),
};
},
)(LeftMainHeader));

View File

@ -4,6 +4,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChat } from '../../../api/types';
import { selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees';
import { filterUsersByName, isUserBot, sortChatIds } from '../../../global/helpers';
import useLang from '../../../hooks/useLang';
@ -139,7 +140,7 @@ export default memo(withGlobal<OwnProps>(
fetchingStatus,
globalResults,
localResults,
} = global.globalSearch;
} = selectTabState(global).globalSearch;
const { userIds: globalUserIds } = globalResults || {};
const { userIds: localUserIds } = localResults || {};

View File

@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
import { ChatCreationProgress } from '../../../types';
import { selectTabState } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import useHistoryBack from '../../../hooks/useHistoryBack';
@ -198,7 +199,7 @@ export default memo(withGlobal<OwnProps>(
const {
progress: creationProgress,
error: creationError,
} = global.chatCreation || {};
} = selectTabState(global).chatCreation || {};
return {
creationProgress,

View File

@ -30,7 +30,6 @@ const AudioResults: FC<OwnProps & StateProps> = ({
theme,
isVoice,
searchQuery,
searchChatId,
isLoading,
chatsById,
usersById,
@ -52,12 +51,10 @@ const AudioResults: FC<OwnProps & StateProps> = ({
runThrottled(() => {
searchMessagesGlobal({
type: currentType,
query: searchQuery,
chatId: searchChatId,
});
});
}
}, [currentType, lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
}, [currentType, lastSyncTime, searchMessagesGlobal]);
const foundMessages = useMemo(() => {
if (!foundIds || !globalMessagesByChatId) {

View File

@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiMessage } from '../../../api/types';
import { LoadMoreDirection } from '../../../types';
import { selectTabState } from '../../../global/selectors';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
@ -39,7 +40,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
const ChatMessageResults: FC<OwnProps & StateProps> = ({
searchQuery,
currentUserId,
dateSearchQuery,
foundIds,
globalMessagesByChatId,
@ -61,12 +61,10 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
runThrottled(() => {
searchMessagesGlobal({
type: 'text',
query: searchQuery,
chatId: currentUserId,
});
});
}
}, [currentUserId, lastSyncTime, searchMessagesGlobal, searchQuery]);
}, [lastSyncTime, searchMessagesGlobal]);
const handleTopicClick = useCallback(
(id: number) => {
@ -171,7 +169,7 @@ export default memo(withGlobal<OwnProps>(
const { currentUserId, messages: { byChatId: globalMessagesByChatId }, lastSyncTime } = global;
const {
fetchingStatus, resultsByType, foundTopicIds, chatId: searchChatId,
} = global.globalSearch;
} = selectTabState(global).globalSearch;
const { foundIds } = (resultsByType?.text) || {};

View File

@ -7,6 +7,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChat, ApiMessage } from '../../../api/types';
import { LoadMoreDirection } from '../../../types';
import { selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees';
import {
sortChatIds,
@ -81,11 +82,10 @@ const ChatResults: FC<OwnProps & StateProps> = ({
runThrottled(() => {
searchMessagesGlobal({
type: 'text',
query: searchQuery,
});
});
}
}, [lastSyncTime, searchMessagesGlobal, searchQuery]);
}, [lastSyncTime, searchMessagesGlobal]);
const handleChatClick = useCallback(
(id: string) => {
@ -303,7 +303,7 @@ export default memo(withGlobal<OwnProps>(
} = global;
const {
fetchingStatus, globalResults, localResults, resultsByType,
} = global.globalSearch;
} = selectTabState(global).globalSearch;
const { chatIds: globalChatIds, userIds: globalUserIds } = globalResults || {};
const { chatIds: localChatIds, userIds: localUserIds } = localResults || {};
const { byChatId: globalMessagesByChatId } = messages;

View File

@ -35,7 +35,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
const FileResults: FC<OwnProps & StateProps> = ({
searchQuery,
searchChatId,
isLoading,
chatsById,
usersById,
@ -64,12 +63,10 @@ const FileResults: FC<OwnProps & StateProps> = ({
runThrottled(() => {
searchMessagesGlobal({
type: CURRENT_TYPE,
query: searchQuery,
chatId: searchChatId,
});
});
}
}, [lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
}, [lastSyncTime, searchMessagesGlobal]);
const foundMessages = useMemo(() => {
if (!foundIds || !globalMessagesByChatId) {

View File

@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
import { GlobalSearchContent } from '../../../types';
import { selectTabState } from '../../../global/selectors';
import { parseDateString } from '../../../util/dateFormat';
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
import useLang from '../../../hooks/useLang';
@ -148,7 +149,7 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { currentContent, chatId } = global.globalSearch;
const { currentContent, chatId } = selectTabState(global).globalSearch;
return { currentContent, chatId };
},

View File

@ -33,7 +33,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
const LinkResults: FC<OwnProps & StateProps> = ({
searchQuery,
searchChatId,
isLoading,
chatsById,
usersById,
@ -62,12 +61,10 @@ const LinkResults: FC<OwnProps & StateProps> = ({
runThrottled(() => {
searchMessagesGlobal({
type: CURRENT_TYPE,
query: searchQuery,
chatId: searchChatId,
});
});
}
}, [lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
}, [lastSyncTime, searchMessagesGlobal]);
const foundMessages = useMemo(() => {
if (!foundIds || !globalMessagesByChatId) {

View File

@ -33,7 +33,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
const MediaResults: FC<OwnProps & StateProps> = ({
searchQuery,
searchChatId,
isLoading,
globalMessagesByChatId,
foundIds,
@ -60,12 +59,10 @@ const MediaResults: FC<OwnProps & StateProps> = ({
runThrottled(() => {
searchMessagesGlobal({
type: CURRENT_TYPE,
query: searchQuery,
chatId: searchChatId,
});
});
}
}, [lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
}, [lastSyncTime, searchMessagesGlobal]);
const foundMessages = useMemo(() => {
if (!foundIds || !globalMessagesByChatId) {

View File

@ -68,6 +68,10 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
}, SEARCH_CLOSE_TIMEOUT_MS);
}, [openChat, addRecentlyFoundChatId, onReset]);
const handleClearRecentlyFoundChats = useCallback(() => {
clearRecentlyFoundChats();
}, [clearRecentlyFoundChats]);
const lang = useLang();
return (
@ -94,7 +98,7 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
size="smaller"
color="translucent"
ariaLabel="Clear recent chats"
onClick={clearRecentlyFoundChats}
onClick={handleClearRecentlyFoundChats}
isRtl={lang.isRtl}
>
<i className="icon-close" />
@ -116,7 +120,7 @@ export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { userIds: topUserIds } = global.topPeers;
const usersById = global.users.byId;
const { recentlyFoundChatIds } = global.globalSearch;
const { recentlyFoundChatIds } = global;
const { animationLevel } = global.settings.byKey;
return {

View File

@ -4,7 +4,7 @@ import type {
} from '../../../../api/types';
import type { ISettings } from '../../../../types';
import { selectChat, selectTheme } from '../../../../global/selectors';
import { selectChat, selectTabState, selectTheme } from '../../../../global/selectors';
export type StateProps = {
theme: ISettings['theme'];
@ -21,11 +21,12 @@ export type StateProps = {
export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
return (global: GlobalState, props: any) => {
const tabState = selectTabState(global);
const { byId: chatsById } = global.chats;
const { byId: usersById } = global.users;
const {
fetchingStatus, resultsByType, chatId,
} = global.globalSearch;
} = tabState.globalSearch;
// One component is used for two different types of results.
// The differences between them are only in the isVoice property.
@ -35,7 +36,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
const { byChatId: globalMessagesByChatId } = global.messages;
const foundIds = resultsByType?.[currentType]?.foundIds;
const activeDownloads = global.activeDownloads.byChatId;
const activeDownloads = tabState.activeDownloads.byChatId;
return {
theme: selectTheme(global),

View File

@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiUser } from '../../../api/types';
import { selectTabState } from '../../../global/selectors';
import { filterUsersByName, getUserFullName } from '../../../global/helpers';
import { unique } from '../../../util/iteratees';
import useLang from '../../../hooks/useLang';
@ -102,7 +103,7 @@ export default memo(withGlobal<OwnProps>(
usersById,
blockedIds,
contactIds: contactList?.userIds,
localContactIds: global.userSearch.localUserIds,
localContactIds: selectTabState(global).userSearch.localUserIds,
currentUserId,
};
},

View File

@ -11,7 +11,7 @@ import { ProfileEditProgress } from '../../../types';
import { PURCHASE_USERNAME, TME_LINK_PREFIX, USERNAME_PURCHASE_ERROR } from '../../../config';
import { throttle } from '../../../util/schedulers';
import { selectUser } from '../../../global/selectors';
import { selectTabState, selectUser } from '../../../global/selectors';
import { getChatAvatarHash } from '../../../global/helpers';
import { selectCurrentLimit } from '../../../global/selectors/limits';
import renderText from '../../common/helpers/renderText';
@ -165,6 +165,8 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
const trimmedLastName = lastName.trim();
const trimmedBio = bio.trim();
if (!editableUsername) return;
if (!trimmedFirstName.length) {
setError(ERROR_FIRST_NAME_MISSING);
return;
@ -292,7 +294,7 @@ export default memo(withGlobal<OwnProps>(
const { currentUserId } = global;
const {
progress, isUsernameAvailable, checkedUsername, error: editUsernameError,
} = global.profileEdit || {};
} = selectTabState(global).profileEdit || {};
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
const maxBioLength = selectCurrentLimit(global, 'aboutLength');

View File

@ -4,7 +4,7 @@ import React, {
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ISettings, TimeFormat } from '../../../types';
import type { AnimationLevel, ISettings, TimeFormat } from '../../../types';
import { SettingsScreens } from '../../../types';
import {
@ -95,7 +95,7 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
document.body.classList.toggle(`animation-level-${i}`, newLevel === i);
});
setSettingOption({ animationLevel: newLevel });
setSettingOption({ animationLevel: newLevel as AnimationLevel });
}, [setSettingOption]);
const handleMessageTextSizeChange = useCallback((newSize: number) => {
@ -120,14 +120,14 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
}, [animationLevel, setSettingOption, theme]);
const handleTimeFormatChange = useCallback((newTimeFormat: string) => {
setSettingOption({ timeFormat: newTimeFormat });
setSettingOption({ timeFormat: newTimeFormat as TimeFormat });
setSettingOption({ wasTimeFormatSetManually: true });
setTimeFormat(newTimeFormat as TimeFormat);
}, [setSettingOption]);
const handleMessageSendComboChange = useCallback((newCombo: string) => {
setSettingOption({ messageSendKeyCombo: newCombo });
setSettingOption({ messageSendKeyCombo: newCombo as ISettings['messageSendKeyCombo'] });
}, [setSettingOption]);
useHistoryBack({

View File

@ -56,7 +56,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
setThemeSettings,
} = getActions();
const themeRef = useRef<string>();
const themeRef = useRef<ThemeKey>();
themeRef.current = theme;
// Due to the parent Transition, this component never gets unmounted,
// that's why we use throttled API call on every update.
@ -94,20 +94,20 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
}, [setThemeSettings, theme]);
const handleWallPaperSelect = useCallback((slug: string) => {
setThemeSettings({ theme: themeRef.current, background: slug });
setThemeSettings({ theme: themeRef.current!, background: slug });
const currentWallpaper = loadedWallpapers && loadedWallpapers.find((wallpaper) => wallpaper.slug === slug);
if (currentWallpaper?.document.thumbnail) {
getAverageColor(currentWallpaper.document.thumbnail.dataUri)
.then((color) => {
const patternColor = getPatternColor(color);
const rgbColor = `#${rgb2hex(color)}`;
setThemeSettings({ theme: themeRef.current, backgroundColor: rgbColor, patternColor });
setThemeSettings({ theme: themeRef.current!, backgroundColor: rgbColor, patternColor });
});
}
}, [loadedWallpapers, setThemeSettings]);
const handleWallPaperBlurChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setThemeSettings({ theme: themeRef.current, isBlurred: e.target.checked });
setThemeSettings({ theme: themeRef.current!, isBlurred: e.target.checked });
}, [setThemeSettings]);
const lang = useLang();

View File

@ -59,7 +59,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
}) => {
const { setThemeSettings } = getActions();
const themeRef = useRef<string>();
const themeRef = useRef<ThemeKey>();
themeRef.current = theme;
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
@ -156,7 +156,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
if (!isFirstRunRef.current) {
const patternColor = getPatternColor(rgb);
setThemeSettings({
theme: themeRef.current,
theme: themeRef.current!,
background: undefined,
backgroundColor: color,
patternColor,

View File

@ -58,7 +58,7 @@ const SettingsHeader: FC<OwnProps> = ({
const handleSignOutMessage = useCallback(() => {
closeSignOutConfirmation();
signOut();
signOut({ forceInitApi: true });
}, [closeSignOutConfirmation, signOut]);
const SettingsMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {

View File

@ -48,7 +48,7 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
void setLanguage(langCode as LangCode, () => {
unmarkIsLoading();
setSettingOption({ language: langCode });
setSettingOption({ language: langCode as LangCode });
loadAttachBots(); // Should be refetched every language change
});

View File

@ -108,6 +108,10 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
}
}, [isCurrentUserPremium, lang, onScreenSelect, showNotification]);
const handleUpdateContentSettings = useCallback((isChecked: boolean) => {
updateContentSettings(isChecked);
}, [updateContentSettings]);
function getVisibilityValue(setting?: ApiPrivacySettings) {
const { visibility } = setting || {};
const blockCount = setting ? setting.blockChatIds.length + setting.blockUserIds.length : 0;
@ -318,7 +322,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
subLabel={lang('lng_settings_sensitive_about')}
checked={Boolean(isSensitiveEnabled)}
disabled={!canChangeSensitive}
onCheck={updateContentSettings}
onCheck={handleUpdateContentSettings}
/>
</div>
)}

View File

@ -171,7 +171,7 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
const handleVisibilityChange = useCallback((value) => {
setPrivacyVisibility({
privacyKey,
privacyKey: privacyKey!,
visibility: value,
});
}, [privacyKey, setPrivacyVisibility]);

View File

@ -86,7 +86,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
const handleSubmit = useCallback(() => {
setPrivacySettings({
privacyKey: getPrivacyKey(screen),
privacyKey: getPrivacyKey(screen)!,
isAllowList: Boolean(isAllowList),
contactsIds: newSelectedContactIds,
});

View File

@ -4,6 +4,8 @@ import React, {
} from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiChatFolder } from '../../../../api/types';
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
import { findIntersectionWithSet } from '../../../../util/iteratees';
@ -142,9 +144,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
dispatch({ type: 'setIsLoading', payload: true });
if (state.mode === 'edit') {
editChatFolder({ id: state.folderId, folderUpdate: state.folder });
editChatFolder({ id: state.folderId!, folderUpdate: state.folder });
} else {
addChatFolder({ folder: state.folder });
addChatFolder({ folder: state.folder as ApiChatFolder });
}
setTimeout(() => {

View File

@ -142,7 +142,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
dispatch({ type: 'setEmail', payload: value });
updateRecoveryEmail({
...state,
email: value,
email: value!,
onSuccess: () => {
onScreenSelect(SettingsScreens.TwoFaCongratulations);
},

View File

@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect } from '../../lib/teact/teact';
import { getActions } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { GlobalState } from '../../global/types';
import type { TabState } from '../../global/types';
import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
@ -10,7 +10,7 @@ import useFlag from '../../hooks/useFlag';
import RecipientPicker from '../common/RecipientPicker';
export type OwnProps = {
requestedAttachBotInChat?: GlobalState['requestedAttachBotInChat'];
requestedAttachBotInChat?: TabState['requestedAttachBotInChat'];
};
const AttachBotRecipientPicker: FC<OwnProps> = ({

View File

@ -1,11 +1,12 @@
import React, { memo, useRef } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { GlobalState } from '../../global/types';
import type { TabState } from '../../global/types';
import type { FC } from '../../lib/teact/teact';
import { pick } from '../../util/iteratees';
import buildStyle from '../../util/buildStyle';
import { selectTabState } from '../../global/selectors';
import useWindowSize from '../../hooks/useWindowSize';
import useOnChange from '../../hooks/useOnChange';
@ -15,7 +16,7 @@ import useAppLayout from '../../hooks/useAppLayout';
import styles from './ConfettiContainer.module.scss';
type StateProps = {
confetti?: GlobalState['confetti'];
confetti?: TabState['confetti'];
};
interface Confetti {
@ -203,5 +204,5 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
};
export default memo(withGlobal(
(global): StateProps => pick(global, ['confetti']),
(global): StateProps => pick(selectTabState(global), ['confetti']),
)(ConfettiContainer));

View File

@ -18,7 +18,7 @@ const DeleteFolderDialog: FC<OwnProps> = ({
const handleDeleteFolderMessage = useCallback(() => {
closeDeleteChatFolderModal();
deleteChatFolder({ id: deleteFolderDialogId });
deleteChatFolder({ id: deleteFolderDialogId! });
}, [closeDeleteChatFolderModal, deleteChatFolder, deleteFolderDialogId]);
return (

View File

@ -7,6 +7,7 @@ import type {
} from '../../api/types';
import type { AnimationLevel } from '../../types';
import { selectTabState } from '../../global/selectors';
import getReadableErrorText from '../../util/getReadableErrorText';
import { pick } from '../../util/iteratees';
import renderText from '../common/helpers/renderText';
@ -20,7 +21,7 @@ import Avatar from '../common/Avatar';
import './Dialogs.scss';
type StateProps = {
dialogs: (ApiError | ApiInviteInfo)[];
dialogs: (ApiError | ApiInviteInfo | ApiContact)[];
animationLevel: AnimationLevel;
};
@ -198,7 +199,7 @@ function getErrorHeader(error: ApiError) {
export default memo(withGlobal(
(global): StateProps => {
return {
dialogs: global.dialogs,
dialogs: selectTabState(global).dialogs,
animationLevel: global.settings.byKey.animationLevel,
};
},

View File

@ -6,6 +6,7 @@ import type { Thread } from '../../global/types';
import type { ApiMessage } from '../../api/types';
import { ApiMediaFormat } from '../../api/types';
import { selectTabState } from '../../global/selectors';
import { IS_OPFS_SUPPORTED, IS_SERVICE_WORKER_SUPPORTED, MAX_BUFFER_SIZE } from '../../util/environment';
import * as mediaLoader from '../../util/mediaLoader';
import download from '../../util/download';
@ -113,7 +114,7 @@ const DownloadManager: FC<StateProps> = ({
export default memo(withGlobal(
(global): StateProps => {
const activeDownloads = global.activeDownloads.byChatId;
const activeDownloads = selectTabState(global).activeDownloads.byChatId;
const messages = global.messages.byChatId;
return {
activeDownloads,

View File

@ -4,7 +4,7 @@ import React, {
} from '../../lib/teact/teact';
import { getActions } from '../../global';
import type { GlobalState } from '../../global/types';
import type { TabState } from '../../global/types';
import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
@ -12,7 +12,7 @@ import useFlag from '../../hooks/useFlag';
import RecipientPicker from '../common/RecipientPicker';
export type OwnProps = {
requestedDraft?: GlobalState['requestedDraft'];
requestedDraft?: TabState['requestedDraft'];
};
const DraftRecipientPicker: FC<OwnProps> = ({

View File

@ -4,6 +4,7 @@ import React, {
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import { selectTabState } from '../../global/selectors';
import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
@ -74,6 +75,6 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((global): StateProps => {
return {
currentUserId: global.currentUserId,
isManyMessages: (global.forwardMessages.messageIds?.length || 0) > 1,
isManyMessages: (selectTabState(global).forwardMessages.messageIds?.length || 0) > 1,
};
})(ForwardRecipientPicker));

View File

@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect } from '../../lib/teact/teact';
import { getActions } from '../../lib/teact/teactn';
import type { FC } from '../../lib/teact/teact';
import type { GlobalState } from '../../global/types';
import type { TabState } from '../../global/types';
import { MAIN_THREAD_ID } from '../../api/types';
import { withGlobal } from '../../global';
@ -22,7 +22,7 @@ type GameEvents = { eventType: 'share_score' | 'share_game' };
const PLAY_GAME_ACTION_INTERVAL = 5000;
type OwnProps = {
openedGame?: GlobalState['openedGame'];
openedGame?: TabState['openedGame'];
gameTitle?: string;
};

View File

@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useCallback } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import { selectTabState } from '../../global/selectors';
import useLang from '../../hooks/useLang';
import CalendarModal from '../common/CalendarModal';
@ -40,6 +41,6 @@ const HistoryCalendar: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return { selectedAt: global.historyCalendarSelectedAt };
return { selectedAt: selectTabState(global).historyCalendarSelectedAt };
},
)(HistoryCalendar));

View File

@ -90,7 +90,7 @@ const LockScreen: FC<OwnProps & StateProps> = ({
const handleSignOutMessage = useCallback(() => {
closeSignOutConfirmation();
signOut();
signOut({ forceInitApi: true });
}, [closeSignOutConfirmation, signOut]);
if (!shouldRender) {

View File

@ -9,7 +9,7 @@ import type {
ApiAttachBot,
ApiChat, ApiMessage, ApiUser,
} from '../../api/types';
import type { ApiLimitTypeWithModal, GlobalState } from '../../global/types';
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
import '../../global/actions/all';
import {
@ -18,6 +18,7 @@ import {
import { IS_ANDROID } from '../../util/environment';
import {
selectChatMessage,
selectTabState,
selectCurrentMessageList,
selectIsForwardModalOpen,
selectIsMediaViewerOpen,
@ -28,7 +29,6 @@ import {
import buildClassName from '../../util/buildClassName';
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
import { processDeepLink } from '../../util/deeplink';
import { getAllNotificationsCount } from '../../util/folderManager';
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
import { fastRaf } from '../../util/schedulers';
@ -42,6 +42,8 @@ import useShowTransition from '../../hooks/useShowTransition';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import useInterval from '../../hooks/useInterval';
import useAppLayout from '../../hooks/useAppLayout';
import updatePageTitle from '../../util/updatePageTitle';
import updateIcon from '../../util/updateIcon';
import StickerSetModal from '../common/StickerSetModal.async';
import UnreadCount from '../common/UnreadCounter';
@ -84,6 +86,7 @@ export interface OwnProps {
}
type StateProps = {
isMasterTab?: boolean;
chat?: ApiChat;
lastSyncTime?: number;
isLeftColumnOpen: boolean;
@ -109,29 +112,26 @@ type StateProps = {
addedCustomEmojiIds?: string[];
newContactUserId?: string;
newContactByPhoneNumber?: boolean;
openedGame?: GlobalState['openedGame'];
openedGame?: TabState['openedGame'];
gameTitle?: string;
isRatePhoneCallModalOpen?: boolean;
webApp?: GlobalState['webApp'];
webApp?: TabState['webApp'];
isPremiumModalOpen?: boolean;
botTrustRequest?: GlobalState['botTrustRequest'];
botTrustRequest?: TabState['botTrustRequest'];
botTrustRequestBot?: ApiUser;
attachBotToInstall?: ApiAttachBot;
requestedAttachBotInChat?: GlobalState['requestedAttachBotInChat'];
requestedDraft?: GlobalState['requestedDraft'];
requestedAttachBotInChat?: TabState['requestedAttachBotInChat'];
requestedDraft?: TabState['requestedDraft'];
currentUser?: ApiUser;
urlAuth?: GlobalState['urlAuth'];
urlAuth?: TabState['urlAuth'];
limitReached?: ApiLimitTypeWithModal;
deleteFolderDialogId?: number;
isPaymentModalOpen?: boolean;
isReceiptModalOpen?: boolean;
};
const NOTIFICATION_INTERVAL = 1000;
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
let notificationInterval: number | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_isLogged = false;
@ -177,12 +177,14 @@ const Main: FC<OwnProps & StateProps> = ({
isPaymentModalOpen,
isReceiptModalOpen,
deleteFolderDialogId,
isMasterTab,
}) => {
const {
loadAnimatedEmojis,
loadNotificationSettings,
loadNotificationExceptions,
updateIsOnline,
onTabFocusChange,
loadTopInlineBots,
loadEmojiKeywords,
loadCountryList,
@ -224,11 +226,11 @@ const Main: FC<OwnProps & StateProps> = ({
}
}, [isDesktop, isLeftColumnOpen, isMiddleColumnOpen, toggleLeftColumn]);
useInterval(checkAppVersion, APP_OUTDATED_TIMEOUT_MS, true);
useInterval(checkAppVersion, isMasterTab ? APP_OUTDATED_TIMEOUT_MS : undefined, true);
// Initial API calls
useEffect(() => {
if (lastSyncTime) {
if (lastSyncTime && isMasterTab) {
updateIsOnline(true);
loadConfig();
loadAppConfig();
@ -248,33 +250,33 @@ const Main: FC<OwnProps & StateProps> = ({
}, [
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList,
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons,
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons, isMasterTab,
]);
// Language-based API calls
useEffect(() => {
if (lastSyncTime) {
if (lastSyncTime && isMasterTab) {
if (language !== BASE_EMOJI_KEYWORD_LANG) {
loadEmojiKeywords({ language });
loadEmojiKeywords({ language: language! });
}
loadCountryList({ langCode: language });
}
}, [language, lastSyncTime, loadCountryList, loadEmojiKeywords]);
}, [language, lastSyncTime, loadCountryList, loadEmojiKeywords, isMasterTab]);
// Re-fetch cached saved emoji for `localDb`
useEffectWithPrevDeps(([prevLastSyncTime]) => {
if (!prevLastSyncTime && lastSyncTime) {
if (!prevLastSyncTime && lastSyncTime && isMasterTab) {
loadCustomEmojis({
ids: Object.keys(getGlobal().customEmojis.byId),
ignoreCache: true,
});
}
}, [lastSyncTime] as const);
}, [lastSyncTime, isMasterTab] as const);
// Sticker sets
useEffect(() => {
if (lastSyncTime) {
if (lastSyncTime && isMasterTab) {
if (!addedSetIds || !addedCustomEmojiIds) {
loadStickerSets();
loadFavoriteStickers();
@ -284,14 +286,17 @@ const Main: FC<OwnProps & StateProps> = ({
loadAddedStickers();
}
}
}, [lastSyncTime, addedSetIds, loadStickerSets, loadFavoriteStickers, loadAddedStickers, addedCustomEmojiIds]);
}, [
lastSyncTime, addedSetIds, loadStickerSets, loadFavoriteStickers, loadAddedStickers, addedCustomEmojiIds,
isMasterTab,
]);
// Check version when service chat is ready
useEffect(() => {
if (lastSyncTime && isServiceChatReady) {
if (lastSyncTime && isServiceChatReady && isMasterTab) {
checkVersionNotification();
}
}, [lastSyncTime, isServiceChatReady, checkVersionNotification]);
}, [lastSyncTime, isServiceChatReady, checkVersionNotification, isMasterTab]);
// Ensure time format
useEffect(() => {
@ -391,45 +396,18 @@ const Main: FC<OwnProps & StateProps> = ({
);
const handleBlur = useCallback(() => {
updateIsOnline(false);
const initialUnread = getAllNotificationsCount();
let index = 0;
clearInterval(notificationInterval);
notificationInterval = window.setInterval(() => {
if (document.title.includes(INACTIVE_MARKER)) {
updateIcon(false);
return;
}
if (index % 2 === 0) {
const newUnread = getAllNotificationsCount() - initialUnread;
if (newUnread > 0) {
updatePageTitle(`${newUnread} notification${newUnread > 1 ? 's' : ''}`);
updateIcon(true);
}
} else {
updatePageTitle(PAGE_TITLE);
updateIcon(false);
}
index++;
}, NOTIFICATION_INTERVAL);
}, [updateIsOnline]);
onTabFocusChange({ isBlurred: true });
}, [onTabFocusChange]);
const handleFocus = useCallback(() => {
updateIsOnline(true);
clearInterval(notificationInterval);
notificationInterval = undefined;
onTabFocusChange({ isBlurred: false });
if (!document.title.includes(INACTIVE_MARKER)) {
updatePageTitle(PAGE_TITLE);
}
updateIcon(false);
}, [updateIsOnline]);
}, [onTabFocusChange]);
const handleStickerSetModalClose = useCallback(() => {
closeStickerSetModal();
@ -494,27 +472,6 @@ const Main: FC<OwnProps & StateProps> = ({
);
};
function updateIcon(asUnread: boolean) {
document.querySelectorAll<HTMLLinkElement>('link[rel="icon"], link[rel="alternate icon"]')
.forEach((link) => {
if (asUnread) {
if (!link.href.includes('favicon-unread')) {
link.href = link.href.replace('favicon', 'favicon-unread');
}
} else {
link.href = link.href.replace('favicon-unread', 'favicon');
}
});
}
// For some reason setting `document.title` to the same value
// causes increment of Chrome Dev Tools > Performance Monitor > DOM Nodes counter
function updatePageTitle(nextTitle: string) {
if (document.title !== nextTitle) {
document.title = nextTitle;
}
}
export default memo(withGlobal<OwnProps>(
(global, { isMobile }): StateProps => {
const {
@ -523,6 +480,10 @@ export default memo(withGlobal<OwnProps>(
animationLevel, language, wasTimeFormatSetManually,
},
},
lastSyncTime,
} = global;
const {
botTrustRequest,
requestedAttachBotInstall,
requestedAttachBotInChat,
@ -530,16 +491,28 @@ export default memo(withGlobal<OwnProps>(
urlAuth,
webApp,
safeLinkModalUrl,
lastSyncTime,
openedStickerSetShortName,
openedCustomEmojiSetIds,
shouldSkipHistoryAnimations,
} = global;
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
openedGame,
audioPlayer,
isLeftColumnShown,
historyCalendarSelectedAt,
notifications,
dialogs,
newContact,
ratingPhoneCall,
premiumModal,
isMasterTab,
payment,
limitReachedModal,
deleteFolderDialogModal,
} = selectTabState(global);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId)
: undefined;
const openedGame = global.openedGame;
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title;
const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
@ -547,32 +520,32 @@ export default memo(withGlobal<OwnProps>(
return {
lastSyncTime,
isLeftColumnOpen: global.isLeftColumnShown,
isLeftColumnOpen: isLeftColumnShown,
isMiddleColumnOpen: Boolean(chatId),
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
isMediaViewerOpen: selectIsMediaViewerOpen(global),
isForwardModalOpen: selectIsForwardModalOpen(global),
hasNotifications: Boolean(global.notifications.length),
hasDialogs: Boolean(global.dialogs.length),
hasNotifications: Boolean(notifications.length),
hasDialogs: Boolean(dialogs.length),
audioMessage,
safeLinkModalUrl,
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
shouldSkipHistoryAnimations,
openedStickerSetShortName,
openedCustomEmojiSetIds,
isServiceChatReady: selectIsServiceChatReady(global),
activeGroupCallId: global.groupCalls.activeGroupCallId,
activeGroupCallId: isMasterTab ? global.groupCalls.activeGroupCallId : undefined,
animationLevel,
language,
wasTimeFormatSetManually,
isPhoneCallActive: Boolean(global.phoneCall),
isPhoneCallActive: isMasterTab ? Boolean(global.phoneCall) : undefined,
addedSetIds: global.stickers.added.setIds,
addedCustomEmojiIds: global.customEmojis.added.setIds,
newContactUserId: global.newContact?.userId,
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
newContactUserId: newContact?.userId,
newContactByPhoneNumber: newContact?.isByPhoneNumber,
openedGame,
gameTitle,
isRatePhoneCallModalOpen: Boolean(global.ratingPhoneCall),
isRatePhoneCallModalOpen: Boolean(ratingPhoneCall),
botTrustRequest,
botTrustRequestBot: botTrustRequest && selectUser(global, botTrustRequest.botId),
attachBotToInstall: requestedAttachBotInstall?.bot,
@ -580,11 +553,12 @@ export default memo(withGlobal<OwnProps>(
webApp,
currentUser,
urlAuth,
isPremiumModalOpen: global.premiumModal?.isOpen,
limitReached: global.limitReachedModal?.limit,
isPaymentModalOpen: global.payment.isPaymentModalOpen,
isReceiptModalOpen: Boolean(global.payment.receipt),
deleteFolderDialogId: global.deleteFolderDialogModal,
isPremiumModalOpen: premiumModal?.isOpen,
limitReached: limitReachedModal?.limit,
isPaymentModalOpen: payment.isPaymentModalOpen,
isReceiptModalOpen: Boolean(payment.receipt),
deleteFolderDialogId: deleteFolderDialogModal,
isMasterTab,
requestedDraft,
};
},

View File

@ -4,6 +4,7 @@ import { getActions, withGlobal } from '../../global';
import type { ApiNotification } from '../../api/types';
import { selectTabState } from '../../global/selectors';
import { pick } from '../../util/iteratees';
import renderText from '../common/helpers/renderText';
@ -40,5 +41,5 @@ const Notifications: FC<StateProps> = ({ notifications }) => {
};
export default memo(withGlobal(
(global): StateProps => pick(global, ['notifications']),
(global): StateProps => pick(selectTabState(global), ['notifications']),
)(Notifications));

View File

@ -5,7 +5,7 @@ import { getActions, getGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiUser } from '../../api/types';
import type { GlobalState } from '../../global/types';
import type { TabState } from '../../global/types';
import { ensureProtocol } from '../../util/ensureProtocol';
import renderText from '../common/helpers/renderText';
@ -20,7 +20,7 @@ import Checkbox from '../ui/Checkbox';
import styles from './UrlAuthModal.module.scss';
export type OwnProps = {
urlAuth?: GlobalState['urlAuth'];
urlAuth?: TabState['urlAuth'];
currentUser?: ApiUser;
};

View File

@ -5,12 +5,14 @@ import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiAttachBot, ApiChat, ApiUser } from '../../api/types';
import type { GlobalState } from '../../global/types';
import type { TabState } from '../../global/types';
import type { ThemeKey } from '../../types';
import type { PopupOptions, WebAppInboundEvent } from './hooks/useWebAppFrame';
import { TME_LINK_PREFIX } from '../../config';
import { selectCurrentChat, selectTheme, selectUser } from '../../global/selectors';
import {
selectCurrentChat, selectTabState, selectTheme, selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { extractCurrentThemeParams, validateHexColor } from '../../util/themeStyle';
@ -41,7 +43,7 @@ type WebAppButton = {
};
export type OwnProps = {
webApp?: GlobalState['webApp'];
webApp?: TabState['webApp'];
};
type StateProps = {
@ -50,7 +52,7 @@ type StateProps = {
attachBot?: ApiAttachBot;
theme?: ThemeKey;
isPaymentModalOpen?: boolean;
paymentStatus?: GlobalState['payment']['status'];
paymentStatus?: TabState['payment']['status'];
};
const NBSP = '\u00A0';
@ -504,7 +506,7 @@ export default memo(withGlobal<OwnProps>(
const bot = botId ? selectUser(global, botId) : undefined;
const chat = selectCurrentChat(global);
const theme = selectTheme(global);
const { isPaymentModalOpen, status } = global.payment;
const { isPaymentModalOpen, status } = selectTabState(global).payment;
return {
attachBot,

View File

@ -10,7 +10,7 @@ import type { AnimationLevel } from '../../../types';
import { formatCurrency } from '../../../util/formatCurrency';
import renderText from '../../common/helpers/renderText';
import { getUserFirstOrLastName } from '../../../global/helpers';
import { selectUser } from '../../../global/selectors';
import { selectTabState, selectUser } from '../../../global/selectors';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLang from '../../../hooks/useLang';
@ -160,7 +160,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const { forUserId, monthlyCurrency, monthlyAmount } = global.giftPremiumModal || {};
const { forUserId, monthlyCurrency, monthlyAmount } = selectTabState(global).giftPremiumModal || {};
const user = forUserId ? selectUser(global, forUserId) : undefined;
const gifts = user ? user.fullInfo?.premiumGifts : undefined;

View File

@ -15,7 +15,7 @@ import PremiumFeatureModal, {
import { TME_LINK_PREFIX } from '../../../config';
import { formatCurrency } from '../../../util/formatCurrency';
import buildClassName from '../../../util/buildClassName';
import { selectIsCurrentUserPremium, selectUser } from '../../../global/selectors';
import { selectTabState, selectIsCurrentUserPremium, selectUser } from '../../../global/selectors';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import { selectPremiumLimit } from '../../../global/selectors/limits';
import renderText from '../../common/helpers/renderText';
@ -309,16 +309,19 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
premiumModal,
} = selectTabState(global);
return {
currentUserId: global.currentUserId,
promo: global.premiumModal?.promo,
isClosing: global.premiumModal?.isClosing,
isSuccess: global.premiumModal?.isSuccess,
isGift: global.premiumModal?.isGift,
monthsAmount: global.premiumModal?.monthsAmount,
fromUser: global.premiumModal?.fromUserId ? selectUser(global, global.premiumModal.fromUserId) : undefined,
toUser: global.premiumModal?.toUserId ? selectUser(global, global.premiumModal.toUserId) : undefined,
initialSection: global.premiumModal?.initialSection,
promo: premiumModal?.promo,
isClosing: premiumModal?.isClosing,
isSuccess: premiumModal?.isSuccess,
isGift: premiumModal?.isGift,
monthsAmount: premiumModal?.monthsAmount,
fromUser: premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined,
toUser: premiumModal?.toUserId ? selectUser(global, premiumModal.toUserId) : undefined,
initialSection: premiumModal?.initialSection,
isPremium: selectIsCurrentUserPremium(global),
limitChannels: selectPremiumLimit(global, 'channels'),
limitFolders: selectPremiumLimit(global, 'dialogFilters'),

View File

@ -14,7 +14,7 @@ import {
selectChatMessage,
selectChatMessages,
selectChatScheduledMessages,
selectCurrentMediaSearch,
selectCurrentMediaSearch, selectTabState,
selectIsChatWithSelf,
selectListedIds,
selectOutlyingIds,
@ -218,13 +218,15 @@ const MediaViewer: FC<StateProps> = ({
const handleFooterClick = useCallback(() => {
handleClose();
if (!chatId || !mediaId) return;
if (isMobile) {
setTimeout(() => {
toggleChatInfo(false, { forceSyncOnIOs: true });
focusMessage({ chatId, threadId, mediaId });
toggleChatInfo({ force: false }, { forceSyncOnIOs: true });
focusMessage({ chatId, threadId, messageId: mediaId });
}, ANIMATION_DURATION);
} else {
focusMessage({ chatId, threadId, mediaId });
focusMessage({ chatId, threadId, messageId: mediaId });
}
}, [handleClose, isMobile, chatId, threadId, focusMessage, toggleChatInfo, mediaId]);
@ -370,6 +372,7 @@ const MediaViewer: FC<StateProps> = ({
export default memo(withGlobal(
(global): StateProps => {
const { mediaViewer, shouldSkipHistoryAnimations } = selectTabState(global);
const {
chatId,
threadId,
@ -377,12 +380,12 @@ export default memo(withGlobal(
avatarOwnerId,
origin,
isHidden,
} = global.mediaViewer;
} = mediaViewer;
const {
animationLevel,
} = global.settings.byKey;
const { shouldSkipHistoryAnimations, currentUserId } = global;
const { currentUserId } = global;
let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId);
if (origin === MediaViewerOrigin.SearchResult) {

View File

@ -10,7 +10,7 @@ import { MediaViewerOrigin } from '../../types';
import { IS_TOUCH_ENV } from '../../util/environment';
import {
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser,
selectChat, selectChatMessage, selectTabState, selectIsMessageProtected, selectScheduledMessage, selectUser,
} from '../../global/selectors';
import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
import { renderMessageText } from '../common/helpers/renderMessageText';
@ -216,7 +216,7 @@ export default memo(withGlobal<OwnProps>(
isMuted,
playbackRate,
isHidden,
} = global.mediaViewer;
} = selectTabState(global).mediaViewer;
if (origin === MediaViewerOrigin.SearchResult) {
if (!(chatId && mediaId)) {

View File

@ -56,9 +56,11 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
const handleFocusMessage = useCallback(() => {
closeMediaViewer();
if (!chatId || !messageId) return;
if (isMobile) {
setTimeout(() => {
toggleChatInfo(false, { forceSyncOnIOs: true });
toggleChatInfo({ force: false }, { forceSyncOnIOs: true });
focusMessage({ chatId, messageId });
}, ANIMATION_DURATION);
} else {

View File

@ -81,12 +81,12 @@ const VideoPlayer: FC<OwnProps> = ({
const handleEnterFullscreen = useCallback(() => {
// Yandex browser doesn't support PIP when video is hidden
if (IS_YA_BROWSER) return;
setMediaViewerHidden(true);
setMediaViewerHidden({ isHidden: true });
}, [setMediaViewerHidden]);
const handleLeaveFullscreen = useCallback(() => {
if (IS_YA_BROWSER) return;
setMediaViewerHidden(false);
setMediaViewerHidden({ isHidden: false });
}, [setMediaViewerHidden]);
const [

View File

@ -15,6 +15,7 @@ import {
selectIsMessageFocused,
selectChat,
selectTopicFromMessage,
selectTabState,
} from '../../global/selectors';
import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
@ -242,7 +243,10 @@ export default memo(withGlobal<OwnProps>(
: undefined;
const isFocused = selectIsMessageFocused(global, message);
const { direction: focusDirection, noHighlight: noFocusHighlight } = (isFocused && global.focusedMessage) || {};
const {
direction: focusDirection,
noHighlight: noFocusHighlight,
} = (isFocused && selectTabState(global).focusedMessage) || {};
const chat = selectChat(global, message.chatId);
const isChat = chat && (isChatChannel(chat) || userId === message.chatId);

View File

@ -27,7 +27,7 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
content,
}) => {
const {
openMediaViewer, uploadProfilePhoto, showNotification, requestNextSettingsScreen,
openMediaViewer, uploadProfilePhoto, showNotification,
} = getActions();
const { isOutgoing } = message;
@ -42,10 +42,15 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
showNotification({
title: lang('ApplyAvatarHintTitle'),
message: lang('ApplyAvatarHint'),
action: () => requestNextSettingsScreen(SettingsScreens.Main),
action: {
action: 'requestNextSettingsScreen',
payload: {
screen: SettingsScreens.Main,
},
},
actionText: lang('Open'),
});
}, [lang, requestNextSettingsScreen, showNotification]);
}, [lang, showNotification]);
const handleSetSuggestedAvatar = useCallback((file: File) => {
setCropModalBlob(undefined);

View File

@ -14,7 +14,7 @@ import * as mediaLoader from '../../util/mediaLoader';
import {
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal,
} from '../../global/helpers';
import { selectChat, selectSender } from '../../global/selectors';
import { selectChat, selectTabState, selectSender } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { makeTrackId } from '../../util/audioPlayer';
import { clearMediaSession } from '../../util/mediaSession';
@ -320,7 +320,7 @@ export default withGlobal<OwnProps>(
(global, { message }): StateProps => {
const sender = selectSender(global, message);
const chat = selectChat(global, message.chatId);
const { volume, playbackRate, isMuted } = global.audioPlayer;
const { volume, playbackRate, isMuted } = selectTabState(global).audioPlayer;
return {
sender,

View File

@ -58,13 +58,13 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
const handleAddContact = useCallback(() => {
openAddContactDialog({ userId: chatId });
if (isAutoArchived) {
toggleChatArchived({ chatId });
toggleChatArchived({ id: chatId });
}
}, [openAddContactDialog, isAutoArchived, toggleChatArchived, chatId]);
const handleConfirmBlock = useCallback(() => {
closeBlockUserModal();
blockContact({ contactId: chatId, accessHash });
blockContact({ contactId: chatId, accessHash: accessHash! });
if (canReportSpam && shouldReportSpam) {
reportSpam({ chatId });
}
@ -84,7 +84,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
closeBlockUserModal();
reportSpam({ chatId });
if (isBasicGroup) {
deleteChatUser({ chatId, userId: currentUserId });
deleteChatUser({ chatId, userId: currentUserId! });
deleteHistory({ chatId, shouldDeleteForAll: false });
} else {
leaveChannel({ chatId });

Some files were not shown because too many files have changed in this diff Show More