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", "jsx-expressions",
"no-async-without-await", "no-async-without-await",
"teactn", "teactn",
"no-null" "no-null",
"eslint-multitab-tt"
], ],
"rules": { "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": [ "indent": [
"error", "error",
2, 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", "test:record": "playwright codegen localhost:1235",
"prepare": "husky install", "prepare": "husky install",
"statoscope:validate": "statoscope validate --input public/build-stats.json", "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": { "engines": {
"node": "^18", "node": "^18",
@ -76,6 +77,7 @@
"eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-import-resolver-webpack": "^0.13.2", "eslint-import-resolver-webpack": "^0.13.2",
"eslint-plugin-eslint-multitab-tt": "file:dev/eslint-multitab",
"eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1", "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 type { UiLoaderPage } from './components/common/UiLoader';
import { INACTIVE_MARKER, PAGE_TITLE } from './config'; 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 { updateSizes } from './util/windowSize';
import { addActiveTabChangeListener } from './util/activeTabMonitor'; import { addActiveTabChangeListener } from './util/activeTabMonitor';
import { hasStoredSession } from './util/sessions'; import { hasStoredSession } from './util/sessions';
import buildClassName from './util/buildClassName'; import buildClassName from './util/buildClassName';
import { parseInitialLocationHash } from './util/routing';
import useFlag from './hooks/useFlag'; import useFlag from './hooks/useFlag';
import usePrevious from './hooks/usePrevious'; import usePrevious from './hooks/usePrevious';
import useAppLayout from './hooks/useAppLayout';
import Auth from './components/auth/Auth'; import Auth from './components/auth/Auth';
import Main from './components/main/Main.async'; 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 AppInactive from './components/main/AppInactive';
import Transition from './components/ui/Transition'; import Transition from './components/ui/Transition';
import UiLoader from './components/common/UiLoader'; import UiLoader from './components/common/UiLoader';
import { parseInitialLocationHash } from './util/routing';
import useAppLayout from './hooks/useAppLayout';
// import Test from './components/test/TestNoRedundancy'; // import Test from './components/test/TestNoRedundancy';
type StateProps = { type StateProps = {
authState: GlobalState['authState']; authState: GlobalState['authState'];
isScreenLocked?: boolean; isScreenLocked?: boolean;
hasPasscode?: boolean; hasPasscode?: boolean;
isInactiveAuth?: boolean;
hasWebAuthTokenFailed?: boolean; hasWebAuthTokenFailed?: boolean;
}; };
@ -43,23 +45,14 @@ const App: FC<StateProps> = ({
isScreenLocked, isScreenLocked,
hasPasscode, hasPasscode,
hasWebAuthTokenFailed, hasWebAuthTokenFailed,
isInactiveAuth,
}) => { }) => {
const { disconnect } = getActions(); const { disconnect } = getActions();
const [isInactive, markInactive] = useFlag(false); const [isInactive, markInactive, unmarkInactive] = useFlag(false);
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android'; 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 // Prevent drop on elements that do not accept it
useEffect(() => { useEffect(() => {
const body = document.body; const body = document.body;
@ -144,13 +137,35 @@ const App: FC<StateProps> = ({
activeKey = AppScreens.main; 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); const prevActiveKey = usePrevious(activeKey);
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
function renderContent(isActive: boolean) { function renderContent() {
switch (activeKey) { switch (activeKey) {
case AppScreens.auth: case AppScreens.auth:
return <Auth isActive={isActive} />; return <Auth />;
case AppScreens.main: case AppScreens.main:
return <Main isMobile={isMobile} />; return <Main isMobile={isMobile} />;
case AppScreens.lock: case AppScreens.lock:
@ -183,6 +198,7 @@ export default withGlobal(
authState: global.authState, authState: global.authState,
isScreenLocked: global.passcode?.isScreenLocked, isScreenLocked: global.passcode?.isScreenLocked,
hasPasscode: global.passcode?.hasPasscode, hasPasscode: global.passcode?.hasPasscode,
isInactiveAuth: selectTabState(global).isInactive,
hasWebAuthTokenFailed: global.hasWebAuthTokenFailed || global.hasWebAuthTokenPasswordRequired, hasWebAuthTokenFailed: global.hasWebAuthTokenFailed || global.hasWebAuthTokenPasswordRequired,
}; };
}, },

View File

@ -1,2 +1,8 @@
// export { initApi, callApi, cancelApiProgress } from './provider'; // 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 { Api as GramJs } from '../../lib/gramjs';
import type { ApiMessage } from '../types'; 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>; localMessages: Record<string, ApiMessage>;
// Used for loading avatars and media through in-memory Gram JS instances. // Used for loading avatars and media through in-memory Gram JS instances.
chats: Record<string, GramJs.Chat | GramJs.Channel>; chats: Record<string, GramJs.Chat | GramJs.Channel>;
@ -13,21 +21,99 @@ interface LocalDb {
webDocuments: Record<string, GramJs.TypeWebDocument>; webDocuments: Record<string, GramJs.TypeWebDocument>;
} }
const LOCAL_DB_INITIAL = { const channel = IS_MULTITAB_SUPPORTED ? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) : undefined;
localMessages: {},
chats: {},
users: {},
messages: {},
documents: {},
stickerSets: {},
photos: {},
webDocuments: {},
};
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 default localDb;
export function clearLocalDb() { export function broadcastLocalDbUpdateFull() {
Object.assign(localDb, LOCAL_DB_INITIAL); 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({ export function updateChatAdmin({
chat, user, adminRights, customTitle = '', 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 channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel;
const userId = buildInputEntity(user.id, user.accessHash) as GramJs.InputUser; 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); client.setIsPremium(isPremium);
} }
export async function destroy(noLogOut = false) { export async function destroy(noLogOut = false, noClearLocalDb = false) {
if (!noLogOut) { if (!noLogOut) {
await invokeRequest(new GramJs.auth.LogOut()); await invokeRequest(new GramJs.auth.LogOut());
} }
clearLocalDb(); if (!noClearLocalDb) clearLocalDb();
await client.destroy(); await client.destroy();
} }

View File

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

View File

@ -91,7 +91,7 @@ export async function updatePrivateLink({
export async function fetchExportedChatInvites({ export async function fetchExportedChatInvites({
peer, admin, limit = 0, isRevoked, 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({ const exportedInvites = await invokeRequest(new GramJs.messages.GetExportedChatInvites({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser, adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser,
@ -209,7 +209,7 @@ export async function deleteRevokedExportedChatInvites({
export async function fetchChatInviteImporters({ export async function fetchChatInviteImporters({
peer, link, offsetDate = 0, offsetUser, limit = 0, isRequested, 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({ const result = await invokeRequest(new GramJs.messages.GetChatInviteImporters({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),

View File

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

View File

@ -637,7 +637,7 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment,
export async function pinMessage({ export async function pinMessage({
chat, messageId, isUnpin, isOneSide, isSilent, 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({ await invokeRequest(new GramJs.messages.UpdatePinnedMessage({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
id: messageId, id: messageId,

View File

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

View File

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

View File

@ -1,11 +1,15 @@
import type { Api } from '../../../lib/gramjs'; import type { Api } from '../../../lib/gramjs';
import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types'; import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
import type { Methods, MethodArgs, MethodResponse } from '../methods/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 generateIdFor from '../../../util/generateIdFor';
import { pause } from '../../../util/schedulers'; import { pause } from '../../../util/schedulers';
import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole';
type RequestStates = { type RequestStates = {
messageId: string; messageId: string;
@ -20,10 +24,44 @@ const HEALTH_CHECK_MIN_DELAY = 5 * 1000; // 5 sec
let worker: Worker; let worker: Worker;
const requestStates = new Map<string, RequestStates>(); const requestStates = new Map<string, RequestStates>();
const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>(); const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
const savedLocalDb: LocalDb = {
localMessages: {},
chats: {},
users: {},
messages: {},
documents: {},
stickerSets: {},
photos: {},
webDocuments: {},
};
// TODO Re-use `util/WorkerConnector.ts` // 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) { export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
if (!isMasterTab) {
initApiOnMasterTab(initialArgs);
return Promise.resolve();
}
if (!worker) { if (!worker) {
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -40,11 +78,33 @@ export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
return makeRequest({ return makeRequest({
type: 'initApi', 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 (!worker) {
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // 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>; 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) { export function cancelApiProgress(progressCallback: ApiOnProgress) {
progressCallback.isCanceled = true; progressCallback.isCanceled = true;
@ -94,6 +199,20 @@ export function cancelApiProgress(progressCallback: ApiOnProgress) {
return; return;
} }
if (isMasterTab) {
cancelApiProgressMaster(messageId);
} else {
if (!channel) return;
channel.postMessage({
type: 'cancelApiProgress',
token: getCurrentTabId(),
messageId,
});
}
}
export function cancelApiProgressMaster(messageId: string) {
worker.postMessage({ worker.postMessage({
type: 'cancelProgress', type: 'cancelProgress',
messageId, messageId,
@ -105,22 +224,79 @@ function subscribeToWorker(onUpdate: OnApiUpdate) {
if (data.type === 'update') { if (data.type === 'update') {
onUpdate(data.update); onUpdate(data.update);
} else if (data.type === 'methodResponse') { } else if (data.type === 'methodResponse') {
const requestState = requestStates.get(data.messageId); handleMethodResponse(data);
if (requestState) {
if (data.error) {
requestState.reject(data.error);
} else {
requestState.resolve(data.response);
}
}
} else if (data.type === 'methodCallback') { } else if (data.type === 'methodCallback') {
requestStates.get(data.messageId)?.callback?.(...data.callbackArgs); handleMethodCallback(data);
} else if (data.type === 'unhandledError') { } else if (data.type === 'unhandledError') {
throw new Error(data.error?.message); 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) { function makeRequest(message: OriginRequest) {
const messageId = generateIdFor(requestStates); const messageId = generateIdFor(requestStates);
const payload: OriginRequest = { const payload: OriginRequest = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -129,6 +129,10 @@ const AuthCode: FC<StateProps> = ({
}); });
}, [markIsLoading, setSettingOption, suggestedLanguage, unmarkIsLoading]); }, [markIsLoading, setSettingOption, suggestedLanguage, unmarkIsLoading]);
const habdleReturnToAuthPhoneNumber = useCallback(() => {
returnToAuthPhoneNumber();
}, [returnToAuthPhoneNumber]);
const isAuthReady = authState === 'authorizationStateWaitQrCode'; const isAuthReady = authState === 'authorizationStateWaitQrCode';
return ( return (
@ -162,7 +166,7 @@ const AuthCode: FC<StateProps> = ({
<li><span>{lang('Login.QR.Help3')}</span></li> <li><span>{lang('Login.QR.Help3')}</span></li>
</ol> </ol>
{isAuthReady && ( {isAuthReady && (
<Button isText onClick={returnToAuthPhoneNumber}>{lang('Login.QR.Cancel')}</Button> <Button isText onClick={habdleReturnToAuthPhoneNumber}>{lang('Login.QR.Cancel')}</Button>
)} )}
{suggestedLanguage && suggestedLanguage !== language && continueText && ( {suggestedLanguage && suggestedLanguage !== language && continueText && (
<Button isText isLoading={isLoading} onClick={handleLangChange}>{continueText}</Button> <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 type { ApiGroupCall, ApiUser } from '../../api/types';
import { selectActiveGroupCall, selectPhoneCallUser } from '../../global/selectors/calls'; import { selectActiveGroupCall, selectPhoneCallUser } from '../../global/selectors/calls';
import { selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -33,6 +34,10 @@ const ActiveCallHeader: FC<StateProps> = ({
}; };
}, [isCallPanelVisible]); }, [isCallPanelVisible]);
function handleToggleGroupCallPanel() {
toggleGroupCallPanel();
}
if (!groupCall && !phoneCallUser) return undefined; if (!groupCall && !phoneCallUser) return undefined;
return ( return (
@ -41,7 +46,7 @@ const ActiveCallHeader: FC<StateProps> = ({
'ActiveCallHeader', 'ActiveCallHeader',
isCallPanelVisible && 'open', isCallPanelVisible && 'open',
)} )}
onClick={toggleGroupCallPanel} onClick={handleToggleGroupCallPanel}
> >
<span className="title">{phoneCallUser?.firstName || groupCall?.title || lang('VoipGroupVoiceChat')}</span> <span className="title">{phoneCallUser?.firstName || groupCall?.title || lang('VoipGroupVoiceChat')}</span>
</div> </div>
@ -50,10 +55,11 @@ const ActiveCallHeader: FC<StateProps> = ({
export default memo(withGlobal( export default memo(withGlobal(
(global): StateProps => { (global): StateProps => {
const tabState = selectTabState(global);
return { return {
groupCall: selectActiveGroupCall(global), groupCall: tabState.isMasterTab ? selectActiveGroupCall(global) : undefined,
isCallPanelVisible: global.isCallPanelVisible, isCallPanelVisible: tabState.isCallPanelVisible,
phoneCallUser: selectPhoneCallUser(global), phoneCallUser: tabState.isMasterTab ? selectPhoneCallUser(global) : undefined,
}; };
}, },
)(ActiveCallHeader)); )(ActiveCallHeader));

View File

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

View File

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

View File

@ -108,7 +108,7 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
} }
toggleGroupCallMute({ toggleGroupCallMute({
participantId: id, participantId: id!,
value: isAdmin ? !shouldRaiseHand : !isMutedByMe, value: isAdmin ? !shouldRaiseHand : !isMutedByMe,
}); });
}, [closeDropdown, toggleGroupCallMute, id, isAdmin, shouldRaiseHand, isMutedByMe]); }, [closeDropdown, toggleGroupCallMute, id, isAdmin, shouldRaiseHand, isMutedByMe]);
@ -131,12 +131,12 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
runThrottled(() => { runThrottled(() => {
if (value === VOLUME_ZERO) { if (value === VOLUME_ZERO) {
toggleGroupCallMute({ toggleGroupCallMute({
participantId: id, participantId: id!,
value: true, value: true,
}); });
} else { } else {
setGroupCallParticipantVolume({ setGroupCallParticipantVolume({
participantId: id, participantId: id!,
volume: Math.floor(value * GROUP_CALL_VOLUME_MULTIPLIER), 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 { selectChatGroupCall } from '../../../global/selectors/calls';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectChat } from '../../../global/selectors'; import { selectChat, selectTabState } from '../../../global/selectors';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -42,17 +42,17 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
animationLevel, animationLevel,
}) => { }) => {
const { const {
joinGroupCall, requestMasterAndJoinGroupCall,
subscribeToGroupCallUpdates, subscribeToGroupCallUpdates,
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
const handleJoinGroupCall = useCallback(() => { const handleJoinGroupCall = useCallback(() => {
joinGroupCall({ requestMasterAndJoinGroupCall({
chatId, chatId,
}); });
}, [joinGroupCall, chatId]); }, [requestMasterAndJoinGroupCall, chatId]);
const participants = groupCall?.participants; const participants = groupCall?.participants;
@ -128,6 +128,7 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }) => { (global, { chatId }) => {
const chat = selectChat(global, chatId)!; const chat = selectChat(global, chatId)!;
const groupCall = selectChatGroupCall(global, chatId); const groupCall = selectChatGroupCall(global, chatId);
const activeGroupCallId = selectTabState(global).isMasterTab ? global.groupCalls.activeGroupCallId : undefined;
return { return {
groupCall, groupCall,
usersById: global.users.byId, usersById: global.users.byId,
@ -135,7 +136,7 @@ export default memo(withGlobal<OwnProps>(
activeGroupCallId: global.groupCalls.activeGroupCallId, activeGroupCallId: global.groupCalls.activeGroupCallId,
isActive: ((!groupCall ? (chat && chat.isCallNotEmpty && chat.isCallActive) isActive: ((!groupCall ? (chat && chat.isCallNotEmpty && chat.isCallActive)
: (groupCall.participantsCount > 0 && groupCall.isLoaded))) : (groupCall.participantsCount > 0 && groupCall.isLoaded)))
&& (global.groupCalls.activeGroupCallId !== groupCall?.id), && (activeGroupCallId !== groupCall?.id),
animationLevel: global.settings.byKey.animationLevel, animationLevel: global.settings.byKey.animationLevel,
}; };
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,11 +52,11 @@ export default function useAnimatedEmoji(
if (!container) return; if (!container) return;
sendEmojiInteraction({ sendEmojiInteraction({
chatId, chatId: chatId!,
messageId, messageId: messageId!,
localEffect, localEffect,
emoji, emoji: emoji!,
interactions: interactions.current, interactions: interactions.current!,
}); });
startedInteractions.current = undefined; startedInteractions.current = undefined;
interactions.current = undefined; interactions.current = undefined;
@ -91,7 +91,7 @@ export default function useAnimatedEmoji(
interactWithAnimatedEmoji({ interactWithAnimatedEmoji({
localEffect, localEffect,
emoji, emoji: emoji!,
x, x,
y, y,
startSize: size, startSize: size,
@ -131,8 +131,8 @@ export default function useAnimatedEmoji(
sendWatchingEmojiInteraction({ sendWatchingEmojiInteraction({
id, id,
chatId, chatId: chatId!,
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji, emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect)! : emoji!,
startSize: size, startSize: size,
x, x,
y, 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 { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/environment';
import captureEscKeyListener from '../../util/captureEscKeyListener'; 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 useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
import { useResize } from '../../hooks/useResize'; import { useResize } from '../../hooks/useResize';
import { useHotkeys } from '../../hooks/useHotkeys'; import { useHotkeys } from '../../hooks/useHotkeys';
@ -113,13 +113,13 @@ const LeftColumn: FC<StateProps> = ({
function fullReset() { function fullReset() {
setContent(LeftColumnContent.ChatList); setContent(LeftColumnContent.ChatList);
setContactsFilter(''); setContactsFilter('');
setGlobalSearchClosing(true); setGlobalSearchClosing({ isClosing: true });
resetChatCreation(); resetChatCreation();
setTimeout(() => { setTimeout(() => {
setGlobalSearchQuery({ query: '' }); setGlobalSearchQuery({ query: '' });
setGlobalSearchDate({ date: undefined }); setGlobalSearchDate({ date: undefined });
setGlobalSearchChatId({ id: undefined }); setGlobalSearchChatId({ id: undefined });
setGlobalSearchClosing(false); setGlobalSearchClosing({ isClosing: false });
setLastResetTime(Date.now()); setLastResetTime(Date.now());
}, RESET_TRANSITION_DELAY_MS); }, RESET_TRANSITION_DELAY_MS);
} }
@ -376,13 +376,15 @@ const LeftColumn: FC<StateProps> = ({
if (nextSettingsScreen !== undefined) { if (nextSettingsScreen !== undefined) {
setContent(LeftColumnContent.Settings); setContent(LeftColumnContent.Settings);
setSettingsScreen(nextSettingsScreen); setSettingsScreen(nextSettingsScreen);
requestNextSettingsScreen(undefined); requestNextSettingsScreen({ screen: undefined });
} }
}, [nextSettingsScreen, requestNextSettingsScreen]); }, [nextSettingsScreen, requestNextSettingsScreen]);
const { const {
initResize, resetResize, handleMouseUp, 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) => { const handleSettingsScreenSelect = useCallback((screen: SettingsScreens) => {
setContent(LeftColumnContent.Settings); setContent(LeftColumnContent.Settings);
@ -479,30 +481,29 @@ const LeftColumn: FC<StateProps> = ({
export default memo(withGlobal( export default memo(withGlobal(
(global): StateProps => { (global): StateProps => {
const tabState = selectTabState(global);
const { const {
globalSearch: { globalSearch: {
query, query,
date, date,
}, },
chatFolders: {
activeChatFolder,
},
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
leftColumnWidth, leftColumnWidth,
activeChatFolder,
nextSettingsScreen,
} = tabState;
const {
currentUserId, currentUserId,
passcode: { passcode: {
hasPasscode, hasPasscode,
}, },
settings: {
nextScreen: nextSettingsScreen,
},
isUpdateAvailable, isUpdateAvailable,
} = global; } = global;
const currentChat = selectCurrentChat(global); const currentChat = selectCurrentChat(global);
const isChatOpen = Boolean(currentChat?.id); const isChatOpen = Boolean(currentChat?.id);
const isForumPanelOpen = selectIsForumPanelOpen(global); const isForumPanelOpen = selectIsForumPanelOpen(global);
const forumPanelChatId = global.forumPanelChatId; const forumPanelChatId = tabState.forumPanelChatId;
return { return {
searchQuery: query, searchQuery: query,
@ -517,7 +518,7 @@ export default memo(withGlobal(
isUpdateAvailable, isUpdateAvailable,
isForumPanelOpen, isForumPanelOpen,
forumPanelChatId, forumPanelChatId,
isClosingSearch: global.globalSearch.isClosing, isClosingSearch: tabState.globalSearch.isClosing,
}; };
}, },
)(LeftColumn)); )(LeftColumn));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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