Multiple Tabs: Introduce Multiple Tabs Support (#2221)
This commit is contained in:
parent
58c71fd908
commit
2a7c78c12a
@ -9,9 +9,15 @@
|
||||
"jsx-expressions",
|
||||
"no-async-without-await",
|
||||
"teactn",
|
||||
"no-null"
|
||||
"no-null",
|
||||
"eslint-multitab-tt"
|
||||
],
|
||||
"rules": {
|
||||
"eslint-multitab-tt/no-immediate-global": "error",
|
||||
"eslint-multitab-tt/must-update-global-after-await": "off",
|
||||
"eslint-multitab-tt/set-global-only-variable": "error",
|
||||
"eslint-multitab-tt/no-getactions-in-actions": "error",
|
||||
"eslint-multitab-tt/must-specify-action-handler-return-type": "error",
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
|
||||
19
dev/eslint-multitab/.eslintrc.js
Normal file
19
dev/eslint-multitab/.eslintrc.js
Normal 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 },
|
||||
},
|
||||
],
|
||||
};
|
||||
22
dev/eslint-multitab/lib/index.js
Normal file
22
dev/eslint-multitab/lib/index.js
Normal 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");
|
||||
|
||||
|
||||
|
||||
@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
108
dev/eslint-multitab/lib/rules/must-update-global-after-await.js
Normal file
108
dev/eslint-multitab/lib/rules/must-update-global-after-await.js
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
40
dev/eslint-multitab/lib/rules/no-getactions-in-actions.js
Normal file
40
dev/eslint-multitab/lib/rules/no-getactions-in-actions.js
Normal 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',
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
42
dev/eslint-multitab/lib/rules/no-immediate-global.js
Normal file
42
dev/eslint-multitab/lib/rules/no-immediate-global.js
Normal 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",
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
53
dev/eslint-multitab/lib/rules/set-global-only-variable.js
Normal file
53
dev/eslint-multitab/lib/rules/set-global-only-variable.js
Normal 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
3647
dev/eslint-multitab/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
dev/eslint-multitab/package.json
Normal file
39
dev/eslint-multitab/package.json
Normal 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
924
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,8 @@
|
||||
"test:record": "playwright codegen localhost:1235",
|
||||
"prepare": "husky install",
|
||||
"statoscope:validate": "statoscope validate --input public/build-stats.json",
|
||||
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json"
|
||||
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json",
|
||||
"postinstall": "(cd dev/eslint-multitab && npm i)"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18",
|
||||
@ -76,6 +77,7 @@
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-import-resolver-webpack": "^0.13.2",
|
||||
"eslint-plugin-eslint-multitab-tt": "file:dev/eslint-multitab",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
|
||||
48
src/App.tsx
48
src/App.tsx
@ -6,13 +6,16 @@ import type { GlobalState } from './global/types';
|
||||
import type { UiLoaderPage } from './components/common/UiLoader';
|
||||
|
||||
import { INACTIVE_MARKER, PAGE_TITLE } from './config';
|
||||
import { PLATFORM_ENV } from './util/environment';
|
||||
import { IS_MULTITAB_SUPPORTED, PLATFORM_ENV } from './util/environment';
|
||||
import { selectTabState } from './global/selectors';
|
||||
import { updateSizes } from './util/windowSize';
|
||||
import { addActiveTabChangeListener } from './util/activeTabMonitor';
|
||||
import { hasStoredSession } from './util/sessions';
|
||||
import buildClassName from './util/buildClassName';
|
||||
import { parseInitialLocationHash } from './util/routing';
|
||||
import useFlag from './hooks/useFlag';
|
||||
import usePrevious from './hooks/usePrevious';
|
||||
import useAppLayout from './hooks/useAppLayout';
|
||||
|
||||
import Auth from './components/auth/Auth';
|
||||
import Main from './components/main/Main.async';
|
||||
@ -20,14 +23,13 @@ import LockScreen from './components/main/LockScreen.async';
|
||||
import AppInactive from './components/main/AppInactive';
|
||||
import Transition from './components/ui/Transition';
|
||||
import UiLoader from './components/common/UiLoader';
|
||||
import { parseInitialLocationHash } from './util/routing';
|
||||
import useAppLayout from './hooks/useAppLayout';
|
||||
// import Test from './components/test/TestNoRedundancy';
|
||||
|
||||
type StateProps = {
|
||||
authState: GlobalState['authState'];
|
||||
isScreenLocked?: boolean;
|
||||
hasPasscode?: boolean;
|
||||
isInactiveAuth?: boolean;
|
||||
hasWebAuthTokenFailed?: boolean;
|
||||
};
|
||||
|
||||
@ -43,23 +45,14 @@ const App: FC<StateProps> = ({
|
||||
isScreenLocked,
|
||||
hasPasscode,
|
||||
hasWebAuthTokenFailed,
|
||||
isInactiveAuth,
|
||||
}) => {
|
||||
const { disconnect } = getActions();
|
||||
|
||||
const [isInactive, markInactive] = useFlag(false);
|
||||
const [isInactive, markInactive, unmarkInactive] = useFlag(false);
|
||||
const { isMobile } = useAppLayout();
|
||||
const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
||||
|
||||
useEffect(() => {
|
||||
updateSizes();
|
||||
addActiveTabChangeListener(() => {
|
||||
disconnect();
|
||||
document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`;
|
||||
|
||||
markInactive();
|
||||
});
|
||||
}, [disconnect, markInactive]);
|
||||
|
||||
// Prevent drop on elements that do not accept it
|
||||
useEffect(() => {
|
||||
const body = document.body;
|
||||
@ -144,13 +137,35 @@ const App: FC<StateProps> = ({
|
||||
activeKey = AppScreens.main;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateSizes();
|
||||
|
||||
if (IS_MULTITAB_SUPPORTED) return;
|
||||
|
||||
addActiveTabChangeListener(() => {
|
||||
disconnect();
|
||||
document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`;
|
||||
|
||||
markInactive();
|
||||
});
|
||||
}, [activeKey, disconnect, markInactive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInactiveAuth) {
|
||||
document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`;
|
||||
markInactive();
|
||||
} else {
|
||||
unmarkInactive();
|
||||
}
|
||||
}, [isInactiveAuth, markInactive, unmarkInactive]);
|
||||
|
||||
const prevActiveKey = usePrevious(activeKey);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
function renderContent(isActive: boolean) {
|
||||
function renderContent() {
|
||||
switch (activeKey) {
|
||||
case AppScreens.auth:
|
||||
return <Auth isActive={isActive} />;
|
||||
return <Auth />;
|
||||
case AppScreens.main:
|
||||
return <Main isMobile={isMobile} />;
|
||||
case AppScreens.lock:
|
||||
@ -183,6 +198,7 @@ export default withGlobal(
|
||||
authState: global.authState,
|
||||
isScreenLocked: global.passcode?.isScreenLocked,
|
||||
hasPasscode: global.passcode?.hasPasscode,
|
||||
isInactiveAuth: selectTabState(global).isInactive,
|
||||
hasWebAuthTokenFailed: global.hasWebAuthTokenFailed || global.hasWebAuthTokenPasswordRequired,
|
||||
};
|
||||
},
|
||||
|
||||
@ -1,2 +1,8 @@
|
||||
// export { initApi, callApi, cancelApiProgress } from './provider';
|
||||
export { initApi, callApi, cancelApiProgress } from './worker/provider';
|
||||
export {
|
||||
initApi, callApi, cancelApiProgress, cancelApiProgressMaster, callApiLocal,
|
||||
handleMethodCallback,
|
||||
handleMethodResponse,
|
||||
updateFullLocalDb,
|
||||
updateLocalDb,
|
||||
} from './worker/provider';
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import BigInt from 'big-integer';
|
||||
import type { Api as GramJs } from '../../lib/gramjs';
|
||||
import type { ApiMessage } from '../types';
|
||||
import { omitVirtualClassFields } from './apiBuilders/helpers';
|
||||
import { DATA_BROADCAST_CHANNEL_NAME } from '../../config';
|
||||
import { constructors } from '../../lib/gramjs/tl';
|
||||
import { throttle } from '../../util/schedulers';
|
||||
|
||||
interface LocalDb {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
|
||||
|
||||
export interface LocalDb {
|
||||
localMessages: Record<string, ApiMessage>;
|
||||
// Used for loading avatars and media through in-memory Gram JS instances.
|
||||
chats: Record<string, GramJs.Chat | GramJs.Channel>;
|
||||
@ -13,21 +21,99 @@ interface LocalDb {
|
||||
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
||||
}
|
||||
|
||||
const LOCAL_DB_INITIAL = {
|
||||
localMessages: {},
|
||||
chats: {},
|
||||
users: {},
|
||||
messages: {},
|
||||
documents: {},
|
||||
stickerSets: {},
|
||||
photos: {},
|
||||
webDocuments: {},
|
||||
};
|
||||
const channel = IS_MULTITAB_SUPPORTED ? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) : undefined;
|
||||
|
||||
const localDb: LocalDb = LOCAL_DB_INITIAL;
|
||||
let batchedUpdates: {
|
||||
name: string;
|
||||
prop: string;
|
||||
value: any;
|
||||
}[] = [];
|
||||
const throttledLocalDbUpdate = throttle(() => {
|
||||
channel!.postMessage({
|
||||
type: 'localDbUpdate',
|
||||
batchedUpdates,
|
||||
});
|
||||
batchedUpdates = [];
|
||||
}, 100);
|
||||
|
||||
function createProxy(name: string, object: any) {
|
||||
return new Proxy(object, {
|
||||
get(target, prop: string, value: any) {
|
||||
return Reflect.get(target, prop, value);
|
||||
},
|
||||
set(target, prop: string, value: any) {
|
||||
batchedUpdates.push({ name, prop, value });
|
||||
throttledLocalDbUpdate();
|
||||
return Reflect.set(target, prop, value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function convertToVirtualClass(value: any): any {
|
||||
if (value instanceof Uint8Array) return Buffer.from(value);
|
||||
if (typeof value === 'object' && Object.keys(value).length === 1 && Object.keys(value)[0] === 'value') {
|
||||
return BigInt(value.value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(convertToVirtualClass);
|
||||
}
|
||||
|
||||
if (typeof value !== 'object' || !('CONSTRUCTOR_ID' in value)) {
|
||||
return value;
|
||||
}
|
||||
const path = value.className.split('.');
|
||||
const VirtualClass = path.reduce((acc: any, field: string) => {
|
||||
return acc[field];
|
||||
}, constructors);
|
||||
|
||||
const valueOmited = omitVirtualClassFields(value);
|
||||
const valueConverted = Object.keys(valueOmited).reduce((acc, key) => {
|
||||
acc[key] = convertToVirtualClass(valueOmited[key]);
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
return new VirtualClass(valueConverted);
|
||||
}
|
||||
|
||||
function createLocalDbInitial(initial?: LocalDb): LocalDb {
|
||||
return [
|
||||
'localMessages', 'chats', 'users', 'messages', 'documents', 'stickerSets', 'photos', 'webDocuments',
|
||||
]
|
||||
.reduce((acc: Record<string, any>, key) => {
|
||||
const value = initial?.[key as keyof LocalDb] ?? {};
|
||||
const valueVirtualClass = Object.keys(value).reduce((acc2, key2) => {
|
||||
acc2[key2] = convertToVirtualClass(value[key2]);
|
||||
return acc2;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
acc[key] = IS_MULTITAB_SUPPORTED
|
||||
? createProxy(key, valueVirtualClass)
|
||||
: valueVirtualClass;
|
||||
return acc;
|
||||
}, {} as LocalDb) as LocalDb;
|
||||
}
|
||||
|
||||
const localDb: LocalDb = createLocalDbInitial();
|
||||
|
||||
export default localDb;
|
||||
|
||||
export function clearLocalDb() {
|
||||
Object.assign(localDb, LOCAL_DB_INITIAL);
|
||||
export function broadcastLocalDbUpdateFull() {
|
||||
if (!channel) return;
|
||||
|
||||
channel.postMessage({
|
||||
type: 'localDbUpdateFull',
|
||||
localDb: Object.keys(localDb).reduce((acc: Record<string, any>, key) => {
|
||||
acc[key] = { ...localDb[key as keyof LocalDb] };
|
||||
return acc;
|
||||
}, {} as Record<string, any>),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateFullLocalDb(initial: LocalDb) {
|
||||
Object.assign(localDb, createLocalDbInitial(initial));
|
||||
}
|
||||
|
||||
export function clearLocalDb() {
|
||||
Object.assign(localDb, createLocalDbInitial());
|
||||
}
|
||||
|
||||
@ -992,7 +992,7 @@ export function updateChatMemberBannedRights({
|
||||
|
||||
export function updateChatAdmin({
|
||||
chat, user, adminRights, customTitle = '',
|
||||
}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; customTitle: string }) {
|
||||
}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; customTitle?: string }) {
|
||||
const channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel;
|
||||
const userId = buildInputEntity(user.id, user.accessHash) as GramJs.InputUser;
|
||||
|
||||
|
||||
@ -145,12 +145,12 @@ export function setIsPremium({ isPremium }: { isPremium: boolean }) {
|
||||
client.setIsPremium(isPremium);
|
||||
}
|
||||
|
||||
export async function destroy(noLogOut = false) {
|
||||
export async function destroy(noLogOut = false, noClearLocalDb = false) {
|
||||
if (!noLogOut) {
|
||||
await invokeRequest(new GramJs.auth.LogOut());
|
||||
}
|
||||
|
||||
clearLocalDb();
|
||||
if (!noClearLocalDb) clearLocalDb();
|
||||
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
@ -100,3 +100,7 @@ export {
|
||||
acceptPhoneCall, confirmPhoneCall, requestPhoneCall, decodePhoneCallData, createPhoneCallState,
|
||||
destroyPhoneCallState, encodePhoneCallData,
|
||||
} from './phoneCallState';
|
||||
|
||||
export {
|
||||
broadcastLocalDbUpdateFull,
|
||||
} from '../localDb';
|
||||
|
||||
@ -91,7 +91,7 @@ export async function updatePrivateLink({
|
||||
|
||||
export async function fetchExportedChatInvites({
|
||||
peer, admin, limit = 0, isRevoked,
|
||||
}: { peer: ApiChat; admin: ApiUser; limit: number; isRevoked?: boolean }) {
|
||||
}: { peer: ApiChat; admin: ApiUser; limit?: number; isRevoked?: boolean }) {
|
||||
const exportedInvites = await invokeRequest(new GramJs.messages.GetExportedChatInvites({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser,
|
||||
@ -209,7 +209,7 @@ export async function deleteRevokedExportedChatInvites({
|
||||
export async function fetchChatInviteImporters({
|
||||
peer, link, offsetDate = 0, offsetUser, limit = 0, isRequested,
|
||||
}: {
|
||||
peer: ApiChat; link?: string; offsetDate: number; offsetUser?: ApiUser; limit: number; isRequested?: boolean;
|
||||
peer: ApiChat; link?: string; offsetDate?: number; offsetUser?: ApiUser; limit?: number; isRequested?: boolean;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.GetChatInviteImporters({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { TelegramClient } from '../../../lib/gramjs';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import type { ApiOnProgress, ApiParsedMedia, ApiPreparedMedia } from '../../types';
|
||||
import type { ApiOnProgress, ApiParsedMedia } from '../../types';
|
||||
import {
|
||||
ApiMediaFormat,
|
||||
} from '../../types';
|
||||
@ -52,11 +52,11 @@ export default async function downloadMedia(
|
||||
void cacheApi.save(cacheName, url, parsed);
|
||||
}
|
||||
|
||||
const prepared = mediaFormat === ApiMediaFormat.Progressive ? '' : prepareMedia(parsed as string | Blob);
|
||||
const dataBlob = mediaFormat === ApiMediaFormat.Progressive ? '' : parsed as string | Blob;
|
||||
const arrayBuffer = mediaFormat === ApiMediaFormat.Progressive ? parsed as ArrayBuffer : undefined;
|
||||
|
||||
return {
|
||||
prepared,
|
||||
dataBlob,
|
||||
arrayBuffer,
|
||||
mimeType,
|
||||
fullSize,
|
||||
@ -262,14 +262,6 @@ async function parseMedia(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function prepareMedia(mediaData: Exclude<ApiParsedMedia, ArrayBuffer>): ApiPreparedMedia {
|
||||
if (mediaData instanceof Blob) {
|
||||
return URL.createObjectURL(mediaData);
|
||||
}
|
||||
|
||||
return mediaData;
|
||||
}
|
||||
|
||||
function getMimeType(data: Uint8Array, fallbackMimeType = 'image/jpeg') {
|
||||
if (data.length < 4) {
|
||||
return fallbackMimeType;
|
||||
|
||||
@ -637,7 +637,7 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment,
|
||||
|
||||
export async function pinMessage({
|
||||
chat, messageId, isUnpin, isOneSide, isSilent,
|
||||
}: { chat: ApiChat; messageId: number; isUnpin: boolean; isOneSide: boolean; isSilent: boolean }) {
|
||||
}: { chat: ApiChat; messageId: number; isUnpin: boolean; isOneSide?: boolean; isSilent?: boolean }) {
|
||||
await invokeRequest(new GramJs.messages.UpdatePinnedMessage({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
id: messageId,
|
||||
|
||||
@ -370,8 +370,8 @@ export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broa
|
||||
isSilent,
|
||||
shouldShowPreviews,
|
||||
}: {
|
||||
isSilent: boolean;
|
||||
shouldShowPreviews: boolean;
|
||||
isSilent?: boolean;
|
||||
shouldShowPreviews?: boolean;
|
||||
}) {
|
||||
let peer: GramJs.TypeInputNotifyPeer;
|
||||
if (peerType === 'contact') {
|
||||
|
||||
@ -5,9 +5,11 @@ import type {
|
||||
ApiOnProgress,
|
||||
} from '../types';
|
||||
import type { Methods, MethodArgs, MethodResponse } from './methods/types';
|
||||
import type { LocalDb } from './localDb';
|
||||
|
||||
import { API_THROTTLE_RESET_UPDATES, API_UPDATE_THROTTLE } from '../../config';
|
||||
import { throttle, throttleWithTickEnd } from '../../util/schedulers';
|
||||
import { updateFullLocalDb } from './localDb';
|
||||
import { init as initUpdater } from './updater';
|
||||
import { init as initAuth } from './methods/auth';
|
||||
import { init as initChats } from './methods/chats';
|
||||
@ -24,7 +26,7 @@ import * as methods from './methods';
|
||||
|
||||
let onUpdate: OnApiUpdate;
|
||||
|
||||
export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
|
||||
export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs, initialLocalDb?: LocalDb) {
|
||||
onUpdate = _onUpdate;
|
||||
|
||||
initUpdater(handleUpdate);
|
||||
@ -39,6 +41,8 @@ export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArg
|
||||
initCalls(handleUpdate);
|
||||
initPayments(handleUpdate);
|
||||
|
||||
if (initialLocalDb) updateFullLocalDb(initialLocalDb);
|
||||
|
||||
await initClient(handleUpdate, initialArgs);
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import type { Api } from '../../../lib/gramjs';
|
||||
import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
|
||||
import type { Methods, MethodArgs, MethodResponse } from '../methods/types';
|
||||
import type { WorkerMessageEvent, OriginRequest } from './types';
|
||||
import type { WorkerMessageEvent, OriginRequest, ThenArg } from './types';
|
||||
import type { LocalDb } from '../localDb';
|
||||
import type { TypedBroadcastChannel } from '../../../util/multitab';
|
||||
|
||||
import { DEBUG } from '../../../config';
|
||||
import { IS_MULTITAB_SUPPORTED } from '../../../util/environment';
|
||||
import { DATA_BROADCAST_CHANNEL_NAME, DEBUG } from '../../../config';
|
||||
import generateIdFor from '../../../util/generateIdFor';
|
||||
import { pause } from '../../../util/schedulers';
|
||||
import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole';
|
||||
|
||||
type RequestStates = {
|
||||
messageId: string;
|
||||
@ -20,10 +24,44 @@ const HEALTH_CHECK_MIN_DELAY = 5 * 1000; // 5 sec
|
||||
let worker: Worker;
|
||||
const requestStates = new Map<string, RequestStates>();
|
||||
const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
||||
const savedLocalDb: LocalDb = {
|
||||
localMessages: {},
|
||||
chats: {},
|
||||
users: {},
|
||||
messages: {},
|
||||
documents: {},
|
||||
stickerSets: {},
|
||||
photos: {},
|
||||
webDocuments: {},
|
||||
};
|
||||
|
||||
// TODO Re-use `util/WorkerConnector.ts`
|
||||
|
||||
let isMasterTab = true;
|
||||
subscribeToMasterChange((isMasterTabNew) => {
|
||||
isMasterTab = isMasterTabNew;
|
||||
});
|
||||
|
||||
const channel = IS_MULTITAB_SUPPORTED
|
||||
? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) as TypedBroadcastChannel
|
||||
: undefined;
|
||||
|
||||
export function initApiOnMasterTab(initialArgs: ApiInitialArgs) {
|
||||
if (!channel) return;
|
||||
|
||||
channel.postMessage({
|
||||
type: 'initApi',
|
||||
token: getCurrentTabId(),
|
||||
initialArgs,
|
||||
});
|
||||
}
|
||||
|
||||
export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
|
||||
if (!isMasterTab) {
|
||||
initApiOnMasterTab(initialArgs);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!worker) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -40,11 +78,33 @@ export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
|
||||
|
||||
return makeRequest({
|
||||
type: 'initApi',
|
||||
args: [initialArgs],
|
||||
args: [initialArgs, savedLocalDb],
|
||||
});
|
||||
}
|
||||
|
||||
export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
|
||||
export function updateLocalDb(name: keyof LocalDb, prop: string, value: any) {
|
||||
savedLocalDb[name][prop] = value;
|
||||
}
|
||||
|
||||
export function updateFullLocalDb(initial: LocalDb) {
|
||||
Object.assign(savedLocalDb, initial);
|
||||
}
|
||||
|
||||
export function callApiOnMasterTab(payload: any) {
|
||||
if (!channel) return;
|
||||
|
||||
channel.postMessage({
|
||||
type: 'callApi',
|
||||
token: getCurrentTabId(),
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Call a worker method on this tab's worker, without transferring to master tab
|
||||
* Mostly needed to disconnect worker when re-electing master
|
||||
*/
|
||||
export function callApiLocal<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
|
||||
if (!worker) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -86,6 +146,51 @@ export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<
|
||||
return promise as MethodResponse<T>;
|
||||
}
|
||||
|
||||
export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
|
||||
if (!worker && isMasterTab) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('API is not initialized');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const promise = isMasterTab ? makeRequest({
|
||||
type: 'callMethod',
|
||||
name: fnName,
|
||||
args,
|
||||
}) : makeRequestToMaster({
|
||||
name: fnName,
|
||||
args,
|
||||
});
|
||||
|
||||
// Some TypeScript magic to make sure `VirtualClass` is never returned from any method
|
||||
if (DEBUG) {
|
||||
(async () => {
|
||||
try {
|
||||
type ForbiddenTypes =
|
||||
Api.VirtualClass<any>
|
||||
| (Api.VirtualClass<any> | undefined)[];
|
||||
type ForbiddenResponses =
|
||||
ForbiddenTypes
|
||||
| (AnyLiteral & { [k: string]: ForbiddenTypes });
|
||||
|
||||
// Unwrap all chained promises
|
||||
const response = await promise;
|
||||
// Make sure responses do not include `VirtualClass` instances
|
||||
const allowedResponse: Exclude<typeof response, ForbiddenResponses> = response;
|
||||
// Suppress "unused variable" constraint
|
||||
void allowedResponse;
|
||||
} catch (err) {
|
||||
// Do noting
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return promise as MethodResponse<T>;
|
||||
}
|
||||
|
||||
export function cancelApiProgress(progressCallback: ApiOnProgress) {
|
||||
progressCallback.isCanceled = true;
|
||||
|
||||
@ -94,6 +199,20 @@ export function cancelApiProgress(progressCallback: ApiOnProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMasterTab) {
|
||||
cancelApiProgressMaster(messageId);
|
||||
} else {
|
||||
if (!channel) return;
|
||||
|
||||
channel.postMessage({
|
||||
type: 'cancelApiProgress',
|
||||
token: getCurrentTabId(),
|
||||
messageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelApiProgressMaster(messageId: string) {
|
||||
worker.postMessage({
|
||||
type: 'cancelProgress',
|
||||
messageId,
|
||||
@ -105,22 +224,79 @@ function subscribeToWorker(onUpdate: OnApiUpdate) {
|
||||
if (data.type === 'update') {
|
||||
onUpdate(data.update);
|
||||
} else if (data.type === 'methodResponse') {
|
||||
const requestState = requestStates.get(data.messageId);
|
||||
if (requestState) {
|
||||
if (data.error) {
|
||||
requestState.reject(data.error);
|
||||
} else {
|
||||
requestState.resolve(data.response);
|
||||
}
|
||||
}
|
||||
handleMethodResponse(data);
|
||||
} else if (data.type === 'methodCallback') {
|
||||
requestStates.get(data.messageId)?.callback?.(...data.callbackArgs);
|
||||
handleMethodCallback(data);
|
||||
} else if (data.type === 'unhandledError') {
|
||||
throw new Error(data.error?.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function handleMethodResponse(data: { messageId: string;
|
||||
response?: ThenArg<MethodResponse<keyof Methods>>;
|
||||
error?: { message: string };
|
||||
}) {
|
||||
const requestState = requestStates.get(data.messageId);
|
||||
if (requestState) {
|
||||
if (data.error) {
|
||||
requestState.reject(data.error);
|
||||
} else {
|
||||
requestState.resolve(data.response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleMethodCallback(data: { messageId: string;
|
||||
callbackArgs: any[];
|
||||
}) {
|
||||
requestStates.get(data.messageId)?.callback?.(...data.callbackArgs);
|
||||
}
|
||||
|
||||
function makeRequestToMaster(message: {
|
||||
messageId?: string;
|
||||
name: keyof Methods;
|
||||
args: MethodArgs<keyof Methods>;
|
||||
withCallback?: boolean;
|
||||
}) {
|
||||
const messageId = generateIdFor(requestStates);
|
||||
const payload = {
|
||||
messageId,
|
||||
...message,
|
||||
};
|
||||
|
||||
const requestState = { messageId } as RequestStates;
|
||||
|
||||
// Re-wrap type because of `postMessage`
|
||||
const promise: Promise<MethodResponse<keyof Methods>> = new Promise((resolve, reject) => {
|
||||
Object.assign(requestState, { resolve, reject });
|
||||
});
|
||||
|
||||
if ('args' in payload && 'name' in payload && typeof payload.args[1] === 'function') {
|
||||
payload.withCallback = true;
|
||||
|
||||
const callback = payload.args.pop() as AnyToVoidFunction;
|
||||
requestState.callback = callback;
|
||||
requestStatesByCallback.set(callback, requestState);
|
||||
}
|
||||
|
||||
requestStates.set(messageId, requestState);
|
||||
|
||||
promise
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
requestStates.delete(messageId);
|
||||
|
||||
if (requestState.callback) {
|
||||
requestStatesByCallback.delete(requestState.callback);
|
||||
}
|
||||
});
|
||||
|
||||
callApiOnMasterTab(payload);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function makeRequest(message: OriginRequest) {
|
||||
const messageId = generateIdFor(requestStates);
|
||||
const payload: OriginRequest = {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ApiInitialArgs, ApiUpdate } from '../../types';
|
||||
import type { Methods, MethodArgs, MethodResponse } from '../methods/types';
|
||||
import type { LocalDb } from '../localDb';
|
||||
|
||||
export type ThenArg<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
@ -27,7 +28,7 @@ export interface WorkerMessageEvent {
|
||||
export type OriginRequest = {
|
||||
type: 'initApi';
|
||||
messageId?: string;
|
||||
args: [ApiInitialArgs];
|
||||
args: [ApiInitialArgs, LocalDb];
|
||||
} | {
|
||||
type: 'callMethod';
|
||||
messageId?: string;
|
||||
|
||||
@ -21,7 +21,7 @@ onmessage = async (message: OriginMessageEvent) => {
|
||||
|
||||
switch (data.type) {
|
||||
case 'initApi': {
|
||||
await initApi(onUpdate, data.args[0]);
|
||||
await initApi(onUpdate, data.args[0], data.args[1]);
|
||||
break;
|
||||
}
|
||||
case 'callMethod': {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
|
||||
import type { ApiUser } from './users';
|
||||
import type { ApiLimitType } from '../../global/types';
|
||||
import type { ApiLimitType, CallbackAction } from '../../global/types';
|
||||
|
||||
export interface ApiInitialArgs {
|
||||
userAgent: string;
|
||||
@ -107,7 +107,7 @@ export type ApiNotification = {
|
||||
title?: string;
|
||||
message: string;
|
||||
actionText?: string;
|
||||
action: VoidFunction;
|
||||
action?: CallbackAction;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { getActions, getGlobal } from '../global';
|
||||
|
||||
import { IS_MULTITAB_SUPPORTED } from '../util/environment';
|
||||
import { DEBUG } from '../config';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
@ -11,7 +12,7 @@ if (DEBUG) {
|
||||
console.log('>>> FINISH LOAD MAIN BUNDLE');
|
||||
}
|
||||
|
||||
const { connectionState, passcode: { isScreenLocked } } = getGlobal();
|
||||
if (!connectionState && !isScreenLocked) {
|
||||
const { passcode: { isScreenLocked }, connectionState } = getGlobal();
|
||||
if (!connectionState && !isScreenLocked && !IS_MULTITAB_SUPPORTED) {
|
||||
getActions().initApi();
|
||||
}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useEffect, memo } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
|
||||
import '../../global/actions/initial';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import { PLATFORM_ENV } from '../../util/environment';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
@ -19,26 +18,15 @@ import AuthQrCode from './AuthQrCode';
|
||||
|
||||
import './Auth.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isActive: boolean;
|
||||
};
|
||||
type StateProps = Pick<GlobalState, 'authState'>;
|
||||
|
||||
type StateProps = Pick<GlobalState, 'authState' | 'hasWebAuthTokenPasswordRequired'>;
|
||||
|
||||
const Auth: FC<OwnProps & StateProps> = ({
|
||||
isActive, authState, hasWebAuthTokenPasswordRequired,
|
||||
const Auth: FC<StateProps> = ({
|
||||
authState,
|
||||
}) => {
|
||||
const {
|
||||
reset, initApi, returnToAuthPhoneNumber, goToAuthQrCode,
|
||||
returnToAuthPhoneNumber, goToAuthQrCode,
|
||||
} = getActions();
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive && !hasWebAuthTokenPasswordRequired) {
|
||||
reset();
|
||||
initApi();
|
||||
}
|
||||
}, [isActive, reset, initApi, hasWebAuthTokenPasswordRequired]);
|
||||
|
||||
const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
||||
|
||||
const handleChangeAuthorizationMethod = () => {
|
||||
@ -102,6 +90,10 @@ const Auth: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => pick(global, ['authState', 'hasWebAuthTokenPasswordRequired']),
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
return {
|
||||
authState: global.authState,
|
||||
};
|
||||
},
|
||||
)(Auth));
|
||||
|
||||
@ -82,6 +82,10 @@ const AuthCode: FC<StateProps> = ({
|
||||
}
|
||||
}, [authError, clearAuthError, code, isTracking, setAuthCode]);
|
||||
|
||||
function handleReturnToAuthPhoneNumber() {
|
||||
returnToAuthPhoneNumber();
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="auth-code-form" className="custom-scroll">
|
||||
<div className="auth-form">
|
||||
@ -95,7 +99,7 @@ const AuthCode: FC<StateProps> = ({
|
||||
{authPhoneNumber}
|
||||
<div
|
||||
className="auth-number-edit"
|
||||
onClick={returnToAuthPhoneNumber}
|
||||
onClick={handleReturnToAuthPhoneNumber}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={lang('WrongNumber')}
|
||||
|
||||
@ -203,6 +203,10 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoToAuthQrCode = useCallback(() => {
|
||||
goToAuthQrCode();
|
||||
}, [goToAuthQrCode]);
|
||||
|
||||
const isAuthReady = authState === 'authorizationStateWaitPhoneNumber';
|
||||
|
||||
return (
|
||||
@ -242,7 +246,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
||||
)
|
||||
)}
|
||||
{isAuthReady && (
|
||||
<Button isText ripple isLoading={authIsLoadingQrCode} onClick={goToAuthQrCode}>
|
||||
<Button isText ripple isLoading={authIsLoadingQrCode} onClick={handleGoToAuthQrCode}>
|
||||
{lang('Login.QR.Login')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -129,6 +129,10 @@ const AuthCode: FC<StateProps> = ({
|
||||
});
|
||||
}, [markIsLoading, setSettingOption, suggestedLanguage, unmarkIsLoading]);
|
||||
|
||||
const habdleReturnToAuthPhoneNumber = useCallback(() => {
|
||||
returnToAuthPhoneNumber();
|
||||
}, [returnToAuthPhoneNumber]);
|
||||
|
||||
const isAuthReady = authState === 'authorizationStateWaitQrCode';
|
||||
|
||||
return (
|
||||
@ -162,7 +166,7 @@ const AuthCode: FC<StateProps> = ({
|
||||
<li><span>{lang('Login.QR.Help3')}</span></li>
|
||||
</ol>
|
||||
{isAuthReady && (
|
||||
<Button isText onClick={returnToAuthPhoneNumber}>{lang('Login.QR.Cancel')}</Button>
|
||||
<Button isText onClick={habdleReturnToAuthPhoneNumber}>{lang('Login.QR.Cancel')}</Button>
|
||||
)}
|
||||
{suggestedLanguage && suggestedLanguage !== language && continueText && (
|
||||
<Button isText isLoading={isLoading} onClick={handleLangChange}>{continueText}</Button>
|
||||
|
||||
@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../global';
|
||||
import type { ApiGroupCall, ApiUser } from '../../api/types';
|
||||
|
||||
import { selectActiveGroupCall, selectPhoneCallUser } from '../../global/selectors/calls';
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
@ -33,6 +34,10 @@ const ActiveCallHeader: FC<StateProps> = ({
|
||||
};
|
||||
}, [isCallPanelVisible]);
|
||||
|
||||
function handleToggleGroupCallPanel() {
|
||||
toggleGroupCallPanel();
|
||||
}
|
||||
|
||||
if (!groupCall && !phoneCallUser) return undefined;
|
||||
|
||||
return (
|
||||
@ -41,7 +46,7 @@ const ActiveCallHeader: FC<StateProps> = ({
|
||||
'ActiveCallHeader',
|
||||
isCallPanelVisible && 'open',
|
||||
)}
|
||||
onClick={toggleGroupCallPanel}
|
||||
onClick={handleToggleGroupCallPanel}
|
||||
>
|
||||
<span className="title">{phoneCallUser?.firstName || groupCall?.title || lang('VoipGroupVoiceChat')}</span>
|
||||
</div>
|
||||
@ -50,10 +55,11 @@ const ActiveCallHeader: FC<StateProps> = ({
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
return {
|
||||
groupCall: selectActiveGroupCall(global),
|
||||
isCallPanelVisible: global.isCallPanelVisible,
|
||||
phoneCallUser: selectPhoneCallUser(global),
|
||||
groupCall: tabState.isMasterTab ? selectActiveGroupCall(global) : undefined,
|
||||
isCallPanelVisible: tabState.isCallPanelVisible,
|
||||
phoneCallUser: tabState.isMasterTab ? selectPhoneCallUser(global) : undefined,
|
||||
};
|
||||
},
|
||||
)(ActiveCallHeader));
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
selectGroupCallParticipant,
|
||||
selectIsAdminInActiveGroupCall,
|
||||
} from '../../../global/selectors/calls';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
@ -249,6 +250,10 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [isLeaving, leaveGroupCall, shouldEndGroupCall]);
|
||||
|
||||
const handleToggleGroupCallPresentation = useCallback(() => {
|
||||
toggleGroupCallPresentation();
|
||||
}, [toggleGroupCallPresentation]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={!isCallPanelVisible && !isLeaving}
|
||||
@ -293,7 +298,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
||||
{IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand && (
|
||||
<MenuItem
|
||||
icon="share-screen-outlined"
|
||||
onClick={toggleGroupCallPresentation}
|
||||
onClick={handleToggleGroupCallPresentation}
|
||||
>
|
||||
{lang(hasPresentation ? 'VoipChatStopScreenCapture' : 'VoipChatStartScreenCapture')}
|
||||
</MenuItem>
|
||||
@ -416,7 +421,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isSpeakerEnabled: !isSpeakerDisabled,
|
||||
participantsCount,
|
||||
meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!),
|
||||
isCallPanelVisible: Boolean(global.isCallPanelVisible),
|
||||
isCallPanelVisible: Boolean(selectTabState(global).isCallPanelVisible),
|
||||
isAdmin: selectIsAdminInActiveGroupCall(global),
|
||||
participants,
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -36,15 +36,23 @@ const GroupCallParticipantList: FC<OwnProps & StateProps> = ({
|
||||
return Object.keys(participants || {});
|
||||
}, [participants]);
|
||||
|
||||
const handleLoadMoreGroupCallParticipants = useCallback(() => {
|
||||
loadMoreGroupCallParticipants();
|
||||
}, [loadMoreGroupCallParticipants]);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(
|
||||
loadMoreGroupCallParticipants,
|
||||
handleLoadMoreGroupCallParticipants,
|
||||
participantsIds,
|
||||
participantsIds.length >= participantsCount,
|
||||
);
|
||||
|
||||
function handleCreateGroupCallInviteLink() {
|
||||
createGroupCallInviteLink();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="participants">
|
||||
<div className="invite-btn" onClick={createGroupCallInviteLink}>
|
||||
<div className="invite-btn" onClick={handleCreateGroupCallInviteLink}>
|
||||
<div className="icon">
|
||||
<i className="icon-add-user" />
|
||||
</div>
|
||||
|
||||
@ -108,7 +108,7 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
toggleGroupCallMute({
|
||||
participantId: id,
|
||||
participantId: id!,
|
||||
value: isAdmin ? !shouldRaiseHand : !isMutedByMe,
|
||||
});
|
||||
}, [closeDropdown, toggleGroupCallMute, id, isAdmin, shouldRaiseHand, isMutedByMe]);
|
||||
@ -131,12 +131,12 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
||||
runThrottled(() => {
|
||||
if (value === VOLUME_ZERO) {
|
||||
toggleGroupCallMute({
|
||||
participantId: id,
|
||||
participantId: id!,
|
||||
value: true,
|
||||
});
|
||||
} else {
|
||||
setGroupCallParticipantVolume({
|
||||
participantId: id,
|
||||
participantId: id!,
|
||||
volume: Math.floor(value * GROUP_CALL_VOLUME_MULTIPLIER),
|
||||
});
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import type { AnimationLevel } from '../../../types';
|
||||
|
||||
import { selectChatGroupCall } from '../../../global/selectors/calls';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { selectChat } from '../../../global/selectors';
|
||||
import { selectChat, selectTabState } from '../../../global/selectors';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
@ -42,17 +42,17 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
||||
animationLevel,
|
||||
}) => {
|
||||
const {
|
||||
joinGroupCall,
|
||||
requestMasterAndJoinGroupCall,
|
||||
subscribeToGroupCallUpdates,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const handleJoinGroupCall = useCallback(() => {
|
||||
joinGroupCall({
|
||||
requestMasterAndJoinGroupCall({
|
||||
chatId,
|
||||
});
|
||||
}, [joinGroupCall, chatId]);
|
||||
}, [requestMasterAndJoinGroupCall, chatId]);
|
||||
|
||||
const participants = groupCall?.participants;
|
||||
|
||||
@ -128,6 +128,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }) => {
|
||||
const chat = selectChat(global, chatId)!;
|
||||
const groupCall = selectChatGroupCall(global, chatId);
|
||||
const activeGroupCallId = selectTabState(global).isMasterTab ? global.groupCalls.activeGroupCallId : undefined;
|
||||
return {
|
||||
groupCall,
|
||||
usersById: global.users.byId,
|
||||
@ -135,7 +136,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
activeGroupCallId: global.groupCalls.activeGroupCallId,
|
||||
isActive: ((!groupCall ? (chat && chat.isCallNotEmpty && chat.isCallActive)
|
||||
: (groupCall.participantsCount > 0 && groupCall.isLoaded)))
|
||||
&& (global.groupCalls.activeGroupCallId !== groupCall?.id),
|
||||
&& (activeGroupCallId !== groupCall?.id),
|
||||
animationLevel: global.settings.byKey.animationLevel,
|
||||
};
|
||||
},
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
IS_REQUEST_FULLSCREEN_SUPPORTED,
|
||||
} from '../../../util/environment';
|
||||
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { selectPhoneCallUser } from '../../../global/selectors/calls';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -52,7 +53,7 @@ const PhoneCall: FC<StateProps> = ({
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const {
|
||||
hangUp, acceptCall, playGroupCallSound, toggleGroupCallPanel, connectToActivePhoneCall,
|
||||
hangUp, requestMasterAndAcceptCall, playGroupCallSound, toggleGroupCallPanel, connectToActivePhoneCall,
|
||||
} = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -347,7 +348,7 @@ const PhoneCall: FC<StateProps> = ({
|
||||
)}
|
||||
{isIncomingRequested && (
|
||||
<PhoneCallButton
|
||||
onClick={acceptCall}
|
||||
onClick={requestMasterAndAcceptCall}
|
||||
icon="phone-discard"
|
||||
isDisabled={isDiscarded}
|
||||
label={lang('lng_call_accept')}
|
||||
@ -370,12 +371,13 @@ const PhoneCall: FC<StateProps> = ({
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const { phoneCall, currentUserId } = global;
|
||||
const { isCallPanelVisible, isMasterTab } = selectTabState(global);
|
||||
|
||||
return {
|
||||
isCallPanelVisible: Boolean(global.isCallPanelVisible),
|
||||
isCallPanelVisible: Boolean(isCallPanelVisible),
|
||||
user: selectPhoneCallUser(global),
|
||||
isOutgoing: phoneCall?.adminId === currentUserId,
|
||||
phoneCall,
|
||||
phoneCall: isMasterTab ? phoneCall : undefined,
|
||||
animationLevel: global.settings.byKey.animationLevel,
|
||||
};
|
||||
},
|
||||
|
||||
@ -118,7 +118,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
isMuted: !newAreNotificationsEnabled,
|
||||
});
|
||||
} else {
|
||||
updateChatMutedState({ chatId, isMuted: !newAreNotificationsEnabled });
|
||||
updateChatMutedState({ chatId: chatId!, isMuted: !newAreNotificationsEnabled });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleDeleteAndStop = useCallback(() => {
|
||||
deleteHistory({ chatId: chat.id, shouldDeleteForAll: true });
|
||||
blockContact({ contactId: chat.id, accessHash: chat.accessHash });
|
||||
blockContact({ contactId: chat.id, accessHash: chat.accessHash! });
|
||||
|
||||
onClose();
|
||||
}, [deleteHistory, chat.id, chat.accessHash, blockContact, onClose]);
|
||||
@ -89,7 +89,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
||||
if (isPrivateChat) {
|
||||
deleteHistory({ chatId: chat.id, shouldDeleteForAll: false });
|
||||
} else if (isBasicGroup) {
|
||||
deleteChatUser({ chatId: chat.id, userId: currentUserId });
|
||||
deleteChatUser({ chatId: chat.id, userId: currentUserId! });
|
||||
deleteHistory({ chatId: chat.id, shouldDeleteForAll: false });
|
||||
} else if ((isChannel || isSuperGroup) && !chat.isCreator) {
|
||||
leaveChannel({ chatId: chat.id });
|
||||
|
||||
@ -17,13 +17,13 @@ type OwnProps = {
|
||||
const GroupCallLink: FC<OwnProps> = ({
|
||||
className, groupCall, children,
|
||||
}) => {
|
||||
const { joinGroupCall } = getActions();
|
||||
const { requestMasterAndJoinGroupCall } = getActions();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (groupCall) {
|
||||
joinGroupCall({ id: groupCall.id, accessHash: groupCall.accessHash });
|
||||
requestMasterAndJoinGroupCall({ id: groupCall.id, accessHash: groupCall.accessHash });
|
||||
}
|
||||
}, [groupCall, joinGroupCall]);
|
||||
}, [groupCall, requestMasterAndJoinGroupCall]);
|
||||
|
||||
if (!groupCall) {
|
||||
return children;
|
||||
|
||||
@ -37,7 +37,6 @@ type StateProps = {
|
||||
const PinMessageModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen,
|
||||
messageId,
|
||||
chatId,
|
||||
isChannel,
|
||||
isGroup,
|
||||
isSuperGroup,
|
||||
@ -49,17 +48,17 @@ const PinMessageModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handlePinMessageForAll = useCallback(() => {
|
||||
pinMessage({
|
||||
chatId, messageId, isUnpin: false,
|
||||
messageId, isUnpin: false,
|
||||
});
|
||||
onClose();
|
||||
}, [pinMessage, chatId, messageId, onClose]);
|
||||
}, [pinMessage, messageId, onClose]);
|
||||
|
||||
const handlePinMessage = useCallback(() => {
|
||||
pinMessage({
|
||||
chatId, messageId, isUnpin: false, isOneSide: true, isSilent: true,
|
||||
messageId, isUnpin: false, isOneSide: true, isSilent: true,
|
||||
});
|
||||
onClose();
|
||||
}, [chatId, messageId, onClose, pinMessage]);
|
||||
}, [messageId, onClose, pinMessage]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import { MediaViewerOrigin } from '../../types';
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import {
|
||||
selectTabState,
|
||||
selectChat, selectCurrentMessageList, selectThreadMessagesCount, selectUser, selectUserStatus,
|
||||
} from '../../global/selectors';
|
||||
import { getUserStatus, isChatChannel, isUserOnline } from '../../global/helpers';
|
||||
@ -336,7 +337,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = selectChat(global, userId);
|
||||
const isSavedMessages = !forceShowSelf && user && user.isSelf;
|
||||
const { animationLevel } = global.settings.byKey;
|
||||
const { mediaId, avatarOwnerId } = global.mediaViewer;
|
||||
const { mediaId, avatarOwnerId } = selectTabState(global).mediaViewer;
|
||||
const isForum = chat?.isForum;
|
||||
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
|
||||
const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined;
|
||||
|
||||
@ -47,7 +47,7 @@ const ReportModal: FC<OwnProps> = ({
|
||||
const handleReport = useCallback(() => {
|
||||
switch (subject) {
|
||||
case 'messages':
|
||||
reportMessages({ messageIds, reason: selectedReason, description });
|
||||
reportMessages({ messageIds: messageIds!, reason: selectedReason, description });
|
||||
exitMessageSelectMode();
|
||||
break;
|
||||
case 'peer':
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { useCallback, memo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { selectChatMessage } from '../../global/selectors';
|
||||
import { selectChatMessage, selectTabState } from '../../global/selectors';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
|
||||
import Modal from '../ui/Modal';
|
||||
@ -40,6 +40,10 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
|
||||
}, CLOSE_ANIMATION_DURATION);
|
||||
}, [closeSeenByModal, openChat]);
|
||||
|
||||
const handleCloseSeenByModal = useCallback(() => {
|
||||
closeSeenByModal();
|
||||
}, [closeSeenByModal]);
|
||||
|
||||
const renderingMemberIds = useCurrentOrPrev(memberIds, true);
|
||||
|
||||
return (
|
||||
@ -64,7 +68,7 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={closeSeenByModal}
|
||||
onClick={handleCloseSeenByModal}
|
||||
>
|
||||
{lang('Close')}
|
||||
</Button>
|
||||
@ -74,7 +78,7 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const { chatId, messageId } = global.seenByModal || {};
|
||||
const { chatId, messageId } = selectTabState(global).seenByModal || {};
|
||||
if (!chatId || !messageId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
||||
const prevStickerSet = usePrevious(stickerSet);
|
||||
const renderingStickerSet = stickerSet || prevStickerSet;
|
||||
|
||||
const isAdded = !renderingStickerSet?.isArchived && renderingStickerSet?.installedDate;
|
||||
const isAdded = Boolean(!renderingStickerSet?.isArchived && renderingStickerSet?.installedDate);
|
||||
const isEmoji = renderingStickerSet?.isEmoji;
|
||||
const isButtonLocked = !isAdded && isSetPremium && !isCurrentUserPremium;
|
||||
|
||||
|
||||
@ -2,12 +2,16 @@ import React, { useEffect } from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
import type { ThemeKey } from '../../types';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting
|
||||
import { selectIsRightColumnShown, selectTheme } from '../../global/selectors';
|
||||
import {
|
||||
selectIsRightColumnShown,
|
||||
selectTheme,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR } from '../../config';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
@ -41,7 +45,7 @@ type OwnProps = {
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
|
||||
type StateProps = Pick<TabState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
|
||||
isRightColumnShown?: boolean;
|
||||
leftColumnWidth?: number;
|
||||
theme: ThemeKey;
|
||||
@ -74,7 +78,7 @@ function preloadAvatars() {
|
||||
|
||||
const preloadTasks = {
|
||||
main: () => Promise.all([
|
||||
loadModule(Bundles.Main, 'Main')
|
||||
loadModule(Bundles.Main)
|
||||
.then(preloadFonts),
|
||||
preloadAvatars(),
|
||||
preloadImage(reactionThumbsPath),
|
||||
@ -176,12 +180,13 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
||||
export default withGlobal<OwnProps>(
|
||||
(global, { isMobile }): StateProps => {
|
||||
const theme = selectTheme(global);
|
||||
const tabState = selectTabState(global);
|
||||
|
||||
return {
|
||||
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
||||
uiReadyState: global.uiReadyState,
|
||||
shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations,
|
||||
uiReadyState: tabState.uiReadyState,
|
||||
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
|
||||
leftColumnWidth: global.leftColumnWidth,
|
||||
leftColumnWidth: tabState.leftColumnWidth,
|
||||
theme,
|
||||
};
|
||||
},
|
||||
|
||||
@ -52,11 +52,11 @@ export default function useAnimatedEmoji(
|
||||
if (!container) return;
|
||||
|
||||
sendEmojiInteraction({
|
||||
chatId,
|
||||
messageId,
|
||||
chatId: chatId!,
|
||||
messageId: messageId!,
|
||||
localEffect,
|
||||
emoji,
|
||||
interactions: interactions.current,
|
||||
emoji: emoji!,
|
||||
interactions: interactions.current!,
|
||||
});
|
||||
startedInteractions.current = undefined;
|
||||
interactions.current = undefined;
|
||||
@ -91,7 +91,7 @@ export default function useAnimatedEmoji(
|
||||
|
||||
interactWithAnimatedEmoji({
|
||||
localEffect,
|
||||
emoji,
|
||||
emoji: emoji!,
|
||||
x,
|
||||
y,
|
||||
startSize: size,
|
||||
@ -131,8 +131,8 @@ export default function useAnimatedEmoji(
|
||||
|
||||
sendWatchingEmojiInteraction({
|
||||
id,
|
||||
chatId,
|
||||
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji,
|
||||
chatId: chatId!,
|
||||
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect)! : emoji!,
|
||||
startSize: size,
|
||||
x,
|
||||
y,
|
||||
|
||||
@ -8,7 +8,7 @@ import { LeftColumnContent, SettingsScreens } from '../../types';
|
||||
|
||||
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/environment';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { selectCurrentChat, selectIsForumPanelOpen } from '../../global/selectors';
|
||||
import { selectTabState, selectCurrentChat, selectIsForumPanelOpen } from '../../global/selectors';
|
||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||
import { useResize } from '../../hooks/useResize';
|
||||
import { useHotkeys } from '../../hooks/useHotkeys';
|
||||
@ -113,13 +113,13 @@ const LeftColumn: FC<StateProps> = ({
|
||||
function fullReset() {
|
||||
setContent(LeftColumnContent.ChatList);
|
||||
setContactsFilter('');
|
||||
setGlobalSearchClosing(true);
|
||||
setGlobalSearchClosing({ isClosing: true });
|
||||
resetChatCreation();
|
||||
setTimeout(() => {
|
||||
setGlobalSearchQuery({ query: '' });
|
||||
setGlobalSearchDate({ date: undefined });
|
||||
setGlobalSearchChatId({ id: undefined });
|
||||
setGlobalSearchClosing(false);
|
||||
setGlobalSearchClosing({ isClosing: false });
|
||||
setLastResetTime(Date.now());
|
||||
}, RESET_TRANSITION_DELAY_MS);
|
||||
}
|
||||
@ -376,13 +376,15 @@ const LeftColumn: FC<StateProps> = ({
|
||||
if (nextSettingsScreen !== undefined) {
|
||||
setContent(LeftColumnContent.Settings);
|
||||
setSettingsScreen(nextSettingsScreen);
|
||||
requestNextSettingsScreen(undefined);
|
||||
requestNextSettingsScreen({ screen: undefined });
|
||||
}
|
||||
}, [nextSettingsScreen, requestNextSettingsScreen]);
|
||||
|
||||
const {
|
||||
initResize, resetResize, handleMouseUp,
|
||||
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth, '--left-column-width');
|
||||
} = useResize(resizeRef, (n) => setLeftColumnWidth({
|
||||
leftColumnWidth: n,
|
||||
}), resetLeftColumnWidth, leftColumnWidth, '--left-column-width');
|
||||
|
||||
const handleSettingsScreenSelect = useCallback((screen: SettingsScreens) => {
|
||||
setContent(LeftColumnContent.Settings);
|
||||
@ -479,30 +481,29 @@ const LeftColumn: FC<StateProps> = ({
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
const {
|
||||
globalSearch: {
|
||||
query,
|
||||
date,
|
||||
},
|
||||
chatFolders: {
|
||||
activeChatFolder,
|
||||
},
|
||||
shouldSkipHistoryAnimations,
|
||||
leftColumnWidth,
|
||||
activeChatFolder,
|
||||
nextSettingsScreen,
|
||||
} = tabState;
|
||||
const {
|
||||
currentUserId,
|
||||
passcode: {
|
||||
hasPasscode,
|
||||
},
|
||||
settings: {
|
||||
nextScreen: nextSettingsScreen,
|
||||
},
|
||||
isUpdateAvailable,
|
||||
} = global;
|
||||
|
||||
const currentChat = selectCurrentChat(global);
|
||||
const isChatOpen = Boolean(currentChat?.id);
|
||||
const isForumPanelOpen = selectIsForumPanelOpen(global);
|
||||
const forumPanelChatId = global.forumPanelChatId;
|
||||
const forumPanelChatId = tabState.forumPanelChatId;
|
||||
|
||||
return {
|
||||
searchQuery: query,
|
||||
@ -517,7 +518,7 @@ export default memo(withGlobal(
|
||||
isUpdateAvailable,
|
||||
isForumPanelOpen,
|
||||
forumPanelChatId,
|
||||
isClosingSearch: global.globalSearch.isClosing,
|
||||
isClosingSearch: tabState.globalSearch.isClosing,
|
||||
};
|
||||
},
|
||||
)(LeftColumn));
|
||||
|
||||
@ -34,7 +34,7 @@ import {
|
||||
selectNotifyExceptions,
|
||||
selectUserStatus,
|
||||
selectTopicFromMessage,
|
||||
selectThreadParam,
|
||||
selectThreadParam, selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
@ -323,7 +323,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
type: messageListType,
|
||||
} = selectCurrentMessageList(global) || {};
|
||||
const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID;
|
||||
const isSelectedForum = chatId === global.forumPanelChatId;
|
||||
const isSelectedForum = chatId === selectTabState(global).forumPanelChatId;
|
||||
|
||||
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
||||
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;
|
||||
|
||||
@ -14,6 +14,7 @@ import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import { selectTabState, selectIsForumPanelOpen } from '../../../global/selectors';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
@ -22,7 +23,6 @@ import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManag
|
||||
import Transition from '../../ui/Transition';
|
||||
import TabList from '../../ui/TabList';
|
||||
import ChatList from './ChatList';
|
||||
import { selectIsForumPanelOpen } from '../../../global/selectors';
|
||||
|
||||
type OwnProps = {
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
@ -69,10 +69,10 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
if (lastSyncTime && !orderedFolderIds) {
|
||||
loadChatFolders();
|
||||
}
|
||||
}, [lastSyncTime, loadChatFolders]);
|
||||
}, [lastSyncTime, loadChatFolders, orderedFolderIds]);
|
||||
|
||||
const allChatsFolder = useMemo(() => {
|
||||
return {
|
||||
@ -117,7 +117,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
}, [displayedFolders, folderCountersById, maxFolders]);
|
||||
|
||||
const handleSwitchTab = useCallback((index: number) => {
|
||||
setActiveChatFolder(index, { forceOnHeavyAnimation: true });
|
||||
setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true });
|
||||
}, [setActiveChatFolder]);
|
||||
|
||||
// Prevent `activeTab` pointing at non-existing folder after update
|
||||
@ -127,7 +127,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (activeChatFolder >= folderTabs.length) {
|
||||
setActiveChatFolder(FIRST_FOLDER_INDEX);
|
||||
setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX });
|
||||
}
|
||||
}, [activeChatFolder, folderTabs, setActiveChatFolder]);
|
||||
|
||||
@ -140,10 +140,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
selectorToPreventScroll: '.chat-list',
|
||||
onSwipe: ((e, direction) => {
|
||||
if (direction === SwipeDirection.Left) {
|
||||
setActiveChatFolder(Math.min(activeChatFolder + 1, folderTabs.length - 1), { forceOnHeavyAnimation: true });
|
||||
setActiveChatFolder(
|
||||
{ activeChatFolder: Math.min(activeChatFolder + 1, folderTabs.length - 1) },
|
||||
{ forceOnHeavyAnimation: true },
|
||||
);
|
||||
return true;
|
||||
} else if (direction === SwipeDirection.Right) {
|
||||
setActiveChatFolder(Math.max(0, activeChatFolder - 1), { forceOnHeavyAnimation: true });
|
||||
setActiveChatFolder({ activeChatFolder: Math.max(0, activeChatFolder - 1) }, { forceOnHeavyAnimation: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -156,13 +159,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
isNotInFirstFolderRef.current = !isInFirstFolder;
|
||||
useEffect(() => (isNotInFirstFolderRef.current ? captureEscKeyListener(() => {
|
||||
if (isNotInFirstFolderRef.current) {
|
||||
setActiveChatFolder(FIRST_FOLDER_INDEX);
|
||||
setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX });
|
||||
}
|
||||
}) : undefined), [activeChatFolder, setActiveChatFolder]);
|
||||
|
||||
useHistoryBack({
|
||||
isActive: !isInFirstFolder,
|
||||
onBack: () => setActiveChatFolder(FIRST_FOLDER_INDEX, { forceOnHeavyAnimation: true }),
|
||||
onBack: () => setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX }, { forceOnHeavyAnimation: true }),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -179,7 +182,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
const folder = Number(digit) - 1;
|
||||
if (folder > folderTabs.length - 1) return;
|
||||
|
||||
setActiveChatFolder(folder, { forceOnHeavyAnimation: true });
|
||||
setActiveChatFolder({ activeChatFolder: folder }, { forceOnHeavyAnimation: true });
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
@ -245,12 +248,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
chatFolders: {
|
||||
byId: chatFoldersById,
|
||||
orderedIds: orderedFolderIds,
|
||||
activeChatFolder,
|
||||
},
|
||||
currentUserId,
|
||||
lastSyncTime,
|
||||
shouldSkipHistoryAnimations,
|
||||
} = global;
|
||||
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
||||
|
||||
return {
|
||||
chatFoldersById,
|
||||
|
||||
@ -12,7 +12,9 @@ import {
|
||||
TOPICS_SLICE, TOPIC_HEIGHT_PX, TOPIC_LIST_SENSITIVE_AREA,
|
||||
} from '../../../config';
|
||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||
import { selectChat, selectCurrentMessageList, selectIsForumPanelOpen } from '../../../global/selectors';
|
||||
import {
|
||||
selectChat, selectCurrentMessageList, selectIsForumPanelOpen, selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getOrderedTopics } from '../../../global/helpers';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
@ -264,7 +266,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, ownProps, detachWhenChanged): StateProps => {
|
||||
detachWhenChanged(selectIsForumPanelOpen(global));
|
||||
|
||||
const chatId = global.forumPanelChatId;
|
||||
const chatId = selectTabState(global).forumPanelChatId;
|
||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||
const {
|
||||
chatId: currentChatId,
|
||||
|
||||
@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import type { AnimationLevel, ISettings } from '../../../types';
|
||||
import { LeftColumnContent, SettingsScreens } from '../../../types';
|
||||
import type { ApiChat } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { TabState, GlobalState } from '../../../global/types';
|
||||
|
||||
import {
|
||||
ANIMATION_LEVEL_MAX,
|
||||
@ -25,7 +25,7 @@ import { formatDateToString } from '../../../util/dateFormat';
|
||||
import switchTheme from '../../../util/switchTheme';
|
||||
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
||||
import { clearWebsync } from '../../../util/websync';
|
||||
import { selectCurrentMessageList, selectTheme } from '../../../global/selectors';
|
||||
import { selectCurrentMessageList, selectTabState, selectTheme } from '../../../global/selectors';
|
||||
import { isChatArchived } from '../../../global/helpers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||
@ -74,7 +74,7 @@ type StateProps =
|
||||
areChatsLoaded?: boolean;
|
||||
hasPasscode?: boolean;
|
||||
}
|
||||
& Pick<GlobalState, 'connectionState' | 'isSyncing' | 'canInstall'>;
|
||||
& Pick<GlobalState, 'connectionState' | 'isSyncing'> & Pick<TabState, 'canInstall'>;
|
||||
|
||||
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
|
||||
const LEGACY_VERSION_URL = 'https://web.telegram.org/?legacy=1';
|
||||
@ -153,7 +153,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
if (hasPasscode) {
|
||||
lockScreen();
|
||||
} else {
|
||||
requestNextSettingsScreen(SettingsScreens.PasscodeDisabled);
|
||||
requestNextSettingsScreen({ screen: SettingsScreens.PasscodeDisabled });
|
||||
}
|
||||
}, [hasPasscode, lockScreen, requestNextSettingsScreen]);
|
||||
|
||||
@ -461,9 +461,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
const {
|
||||
query: searchQuery, fetchingStatus, chatId, date,
|
||||
} = global.globalSearch;
|
||||
} = tabState.globalSearch;
|
||||
const { currentUserId, connectionState, isSyncing } = global;
|
||||
const { byId: chatsById } = global.chats;
|
||||
const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey;
|
||||
@ -483,7 +484,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isConnectionStatusMinimized,
|
||||
areChatsLoaded: Boolean(global.chats.listIds.active),
|
||||
hasPasscode: Boolean(global.passcode.hasPasscode),
|
||||
canInstall: Boolean(global.canInstall),
|
||||
canInstall: Boolean(tabState.canInstall),
|
||||
};
|
||||
},
|
||||
)(LeftMainHeader));
|
||||
|
||||
@ -4,6 +4,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat } from '../../../api/types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
import { filterUsersByName, isUserBot, sortChatIds } from '../../../global/helpers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -139,7 +140,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
fetchingStatus,
|
||||
globalResults,
|
||||
localResults,
|
||||
} = global.globalSearch;
|
||||
} = selectTabState(global).globalSearch;
|
||||
const { userIds: globalUserIds } = globalResults || {};
|
||||
const { userIds: localUserIds } = localResults || {};
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { ChatCreationProgress } from '../../../types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
|
||||
@ -198,7 +199,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const {
|
||||
progress: creationProgress,
|
||||
error: creationError,
|
||||
} = global.chatCreation || {};
|
||||
} = selectTabState(global).chatCreation || {};
|
||||
|
||||
return {
|
||||
creationProgress,
|
||||
|
||||
@ -30,7 +30,6 @@ const AudioResults: FC<OwnProps & StateProps> = ({
|
||||
theme,
|
||||
isVoice,
|
||||
searchQuery,
|
||||
searchChatId,
|
||||
isLoading,
|
||||
chatsById,
|
||||
usersById,
|
||||
@ -52,12 +51,10 @@ const AudioResults: FC<OwnProps & StateProps> = ({
|
||||
runThrottled(() => {
|
||||
searchMessagesGlobal({
|
||||
type: currentType,
|
||||
query: searchQuery,
|
||||
chatId: searchChatId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [currentType, lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
|
||||
}, [currentType, lastSyncTime, searchMessagesGlobal]);
|
||||
|
||||
const foundMessages = useMemo(() => {
|
||||
if (!foundIds || !globalMessagesByChatId) {
|
||||
|
||||
@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import type { ApiChat, ApiMessage } from '../../../api/types';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||
@ -39,7 +40,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
|
||||
|
||||
const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
||||
searchQuery,
|
||||
currentUserId,
|
||||
dateSearchQuery,
|
||||
foundIds,
|
||||
globalMessagesByChatId,
|
||||
@ -61,12 +61,10 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
||||
runThrottled(() => {
|
||||
searchMessagesGlobal({
|
||||
type: 'text',
|
||||
query: searchQuery,
|
||||
chatId: currentUserId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [currentUserId, lastSyncTime, searchMessagesGlobal, searchQuery]);
|
||||
}, [lastSyncTime, searchMessagesGlobal]);
|
||||
|
||||
const handleTopicClick = useCallback(
|
||||
(id: number) => {
|
||||
@ -171,7 +169,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const { currentUserId, messages: { byChatId: globalMessagesByChatId }, lastSyncTime } = global;
|
||||
const {
|
||||
fetchingStatus, resultsByType, foundTopicIds, chatId: searchChatId,
|
||||
} = global.globalSearch;
|
||||
} = selectTabState(global).globalSearch;
|
||||
|
||||
const { foundIds } = (resultsByType?.text) || {};
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
import type { ApiChat, ApiMessage } from '../../../api/types';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
import {
|
||||
sortChatIds,
|
||||
@ -81,11 +82,10 @@ const ChatResults: FC<OwnProps & StateProps> = ({
|
||||
runThrottled(() => {
|
||||
searchMessagesGlobal({
|
||||
type: 'text',
|
||||
query: searchQuery,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [lastSyncTime, searchMessagesGlobal, searchQuery]);
|
||||
}, [lastSyncTime, searchMessagesGlobal]);
|
||||
|
||||
const handleChatClick = useCallback(
|
||||
(id: string) => {
|
||||
@ -303,7 +303,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
} = global;
|
||||
const {
|
||||
fetchingStatus, globalResults, localResults, resultsByType,
|
||||
} = global.globalSearch;
|
||||
} = selectTabState(global).globalSearch;
|
||||
const { chatIds: globalChatIds, userIds: globalUserIds } = globalResults || {};
|
||||
const { chatIds: localChatIds, userIds: localUserIds } = localResults || {};
|
||||
const { byChatId: globalMessagesByChatId } = messages;
|
||||
|
||||
@ -35,7 +35,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
|
||||
|
||||
const FileResults: FC<OwnProps & StateProps> = ({
|
||||
searchQuery,
|
||||
searchChatId,
|
||||
isLoading,
|
||||
chatsById,
|
||||
usersById,
|
||||
@ -64,12 +63,10 @@ const FileResults: FC<OwnProps & StateProps> = ({
|
||||
runThrottled(() => {
|
||||
searchMessagesGlobal({
|
||||
type: CURRENT_TYPE,
|
||||
query: searchQuery,
|
||||
chatId: searchChatId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
|
||||
}, [lastSyncTime, searchMessagesGlobal]);
|
||||
|
||||
const foundMessages = useMemo(() => {
|
||||
if (!foundIds || !globalMessagesByChatId) {
|
||||
|
||||
@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { GlobalSearchContent } from '../../../types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { parseDateString } from '../../../util/dateFormat';
|
||||
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -148,7 +149,7 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const { currentContent, chatId } = global.globalSearch;
|
||||
const { currentContent, chatId } = selectTabState(global).globalSearch;
|
||||
|
||||
return { currentContent, chatId };
|
||||
},
|
||||
|
||||
@ -33,7 +33,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
|
||||
|
||||
const LinkResults: FC<OwnProps & StateProps> = ({
|
||||
searchQuery,
|
||||
searchChatId,
|
||||
isLoading,
|
||||
chatsById,
|
||||
usersById,
|
||||
@ -62,12 +61,10 @@ const LinkResults: FC<OwnProps & StateProps> = ({
|
||||
runThrottled(() => {
|
||||
searchMessagesGlobal({
|
||||
type: CURRENT_TYPE,
|
||||
query: searchQuery,
|
||||
chatId: searchChatId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
|
||||
}, [lastSyncTime, searchMessagesGlobal]);
|
||||
|
||||
const foundMessages = useMemo(() => {
|
||||
if (!foundIds || !globalMessagesByChatId) {
|
||||
|
||||
@ -33,7 +33,6 @@ const runThrottled = throttle((cb) => cb(), 500, true);
|
||||
|
||||
const MediaResults: FC<OwnProps & StateProps> = ({
|
||||
searchQuery,
|
||||
searchChatId,
|
||||
isLoading,
|
||||
globalMessagesByChatId,
|
||||
foundIds,
|
||||
@ -60,12 +59,10 @@ const MediaResults: FC<OwnProps & StateProps> = ({
|
||||
runThrottled(() => {
|
||||
searchMessagesGlobal({
|
||||
type: CURRENT_TYPE,
|
||||
query: searchQuery,
|
||||
chatId: searchChatId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [lastSyncTime, searchMessagesGlobal, searchQuery, searchChatId]);
|
||||
}, [lastSyncTime, searchMessagesGlobal]);
|
||||
|
||||
const foundMessages = useMemo(() => {
|
||||
if (!foundIds || !globalMessagesByChatId) {
|
||||
|
||||
@ -68,6 +68,10 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
|
||||
}, SEARCH_CLOSE_TIMEOUT_MS);
|
||||
}, [openChat, addRecentlyFoundChatId, onReset]);
|
||||
|
||||
const handleClearRecentlyFoundChats = useCallback(() => {
|
||||
clearRecentlyFoundChats();
|
||||
}, [clearRecentlyFoundChats]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
return (
|
||||
@ -94,7 +98,7 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
ariaLabel="Clear recent chats"
|
||||
onClick={clearRecentlyFoundChats}
|
||||
onClick={handleClearRecentlyFoundChats}
|
||||
isRtl={lang.isRtl}
|
||||
>
|
||||
<i className="icon-close" />
|
||||
@ -116,7 +120,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const { userIds: topUserIds } = global.topPeers;
|
||||
const usersById = global.users.byId;
|
||||
const { recentlyFoundChatIds } = global.globalSearch;
|
||||
const { recentlyFoundChatIds } = global;
|
||||
const { animationLevel } = global.settings.byKey;
|
||||
|
||||
return {
|
||||
|
||||
@ -4,7 +4,7 @@ import type {
|
||||
} from '../../../../api/types';
|
||||
import type { ISettings } from '../../../../types';
|
||||
|
||||
import { selectChat, selectTheme } from '../../../../global/selectors';
|
||||
import { selectChat, selectTabState, selectTheme } from '../../../../global/selectors';
|
||||
|
||||
export type StateProps = {
|
||||
theme: ISettings['theme'];
|
||||
@ -21,11 +21,12 @@ export type StateProps = {
|
||||
|
||||
export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
||||
return (global: GlobalState, props: any) => {
|
||||
const tabState = selectTabState(global);
|
||||
const { byId: chatsById } = global.chats;
|
||||
const { byId: usersById } = global.users;
|
||||
const {
|
||||
fetchingStatus, resultsByType, chatId,
|
||||
} = global.globalSearch;
|
||||
} = tabState.globalSearch;
|
||||
|
||||
// One component is used for two different types of results.
|
||||
// The differences between them are only in the isVoice property.
|
||||
@ -35,7 +36,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
||||
const { byChatId: globalMessagesByChatId } = global.messages;
|
||||
const foundIds = resultsByType?.[currentType]?.foundIds;
|
||||
|
||||
const activeDownloads = global.activeDownloads.byChatId;
|
||||
const activeDownloads = tabState.activeDownloads.byChatId;
|
||||
|
||||
return {
|
||||
theme: selectTheme(global),
|
||||
|
||||
@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { filterUsersByName, getUserFullName } from '../../../global/helpers';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -102,7 +103,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
usersById,
|
||||
blockedIds,
|
||||
contactIds: contactList?.userIds,
|
||||
localContactIds: global.userSearch.localUserIds,
|
||||
localContactIds: selectTabState(global).userSearch.localUserIds,
|
||||
currentUserId,
|
||||
};
|
||||
},
|
||||
|
||||
@ -11,7 +11,7 @@ import { ProfileEditProgress } from '../../../types';
|
||||
|
||||
import { PURCHASE_USERNAME, TME_LINK_PREFIX, USERNAME_PURCHASE_ERROR } from '../../../config';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { selectTabState, selectUser } from '../../../global/selectors';
|
||||
import { getChatAvatarHash } from '../../../global/helpers';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
@ -165,6 +165,8 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
const trimmedLastName = lastName.trim();
|
||||
const trimmedBio = bio.trim();
|
||||
|
||||
if (!editableUsername) return;
|
||||
|
||||
if (!trimmedFirstName.length) {
|
||||
setError(ERROR_FIRST_NAME_MISSING);
|
||||
return;
|
||||
@ -292,7 +294,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const { currentUserId } = global;
|
||||
const {
|
||||
progress, isUsernameAvailable, checkedUsername, error: editUsernameError,
|
||||
} = global.profileEdit || {};
|
||||
} = selectTabState(global).profileEdit || {};
|
||||
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
|
||||
|
||||
const maxBioLength = selectCurrentLimit(global, 'aboutLength');
|
||||
|
||||
@ -4,7 +4,7 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ISettings, TimeFormat } from '../../../types';
|
||||
import type { AnimationLevel, ISettings, TimeFormat } from '../../../types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import {
|
||||
@ -95,7 +95,7 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
||||
document.body.classList.toggle(`animation-level-${i}`, newLevel === i);
|
||||
});
|
||||
|
||||
setSettingOption({ animationLevel: newLevel });
|
||||
setSettingOption({ animationLevel: newLevel as AnimationLevel });
|
||||
}, [setSettingOption]);
|
||||
|
||||
const handleMessageTextSizeChange = useCallback((newSize: number) => {
|
||||
@ -120,14 +120,14 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
||||
}, [animationLevel, setSettingOption, theme]);
|
||||
|
||||
const handleTimeFormatChange = useCallback((newTimeFormat: string) => {
|
||||
setSettingOption({ timeFormat: newTimeFormat });
|
||||
setSettingOption({ timeFormat: newTimeFormat as TimeFormat });
|
||||
setSettingOption({ wasTimeFormatSetManually: true });
|
||||
|
||||
setTimeFormat(newTimeFormat as TimeFormat);
|
||||
}, [setSettingOption]);
|
||||
|
||||
const handleMessageSendComboChange = useCallback((newCombo: string) => {
|
||||
setSettingOption({ messageSendKeyCombo: newCombo });
|
||||
setSettingOption({ messageSendKeyCombo: newCombo as ISettings['messageSendKeyCombo'] });
|
||||
}, [setSettingOption]);
|
||||
|
||||
useHistoryBack({
|
||||
|
||||
@ -56,7 +56,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
|
||||
setThemeSettings,
|
||||
} = getActions();
|
||||
|
||||
const themeRef = useRef<string>();
|
||||
const themeRef = useRef<ThemeKey>();
|
||||
themeRef.current = theme;
|
||||
// Due to the parent Transition, this component never gets unmounted,
|
||||
// that's why we use throttled API call on every update.
|
||||
@ -94,20 +94,20 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
|
||||
}, [setThemeSettings, theme]);
|
||||
|
||||
const handleWallPaperSelect = useCallback((slug: string) => {
|
||||
setThemeSettings({ theme: themeRef.current, background: slug });
|
||||
setThemeSettings({ theme: themeRef.current!, background: slug });
|
||||
const currentWallpaper = loadedWallpapers && loadedWallpapers.find((wallpaper) => wallpaper.slug === slug);
|
||||
if (currentWallpaper?.document.thumbnail) {
|
||||
getAverageColor(currentWallpaper.document.thumbnail.dataUri)
|
||||
.then((color) => {
|
||||
const patternColor = getPatternColor(color);
|
||||
const rgbColor = `#${rgb2hex(color)}`;
|
||||
setThemeSettings({ theme: themeRef.current, backgroundColor: rgbColor, patternColor });
|
||||
setThemeSettings({ theme: themeRef.current!, backgroundColor: rgbColor, patternColor });
|
||||
});
|
||||
}
|
||||
}, [loadedWallpapers, setThemeSettings]);
|
||||
|
||||
const handleWallPaperBlurChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setThemeSettings({ theme: themeRef.current, isBlurred: e.target.checked });
|
||||
setThemeSettings({ theme: themeRef.current!, isBlurred: e.target.checked });
|
||||
}, [setThemeSettings]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -59,7 +59,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
const { setThemeSettings } = getActions();
|
||||
|
||||
const themeRef = useRef<string>();
|
||||
const themeRef = useRef<ThemeKey>();
|
||||
themeRef.current = theme;
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -156,7 +156,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
|
||||
if (!isFirstRunRef.current) {
|
||||
const patternColor = getPatternColor(rgb);
|
||||
setThemeSettings({
|
||||
theme: themeRef.current,
|
||||
theme: themeRef.current!,
|
||||
background: undefined,
|
||||
backgroundColor: color,
|
||||
patternColor,
|
||||
|
||||
@ -58,7 +58,7 @@ const SettingsHeader: FC<OwnProps> = ({
|
||||
|
||||
const handleSignOutMessage = useCallback(() => {
|
||||
closeSignOutConfirmation();
|
||||
signOut();
|
||||
signOut({ forceInitApi: true });
|
||||
}, [closeSignOutConfirmation, signOut]);
|
||||
|
||||
const SettingsMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
|
||||
@ -48,7 +48,7 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
void setLanguage(langCode as LangCode, () => {
|
||||
unmarkIsLoading();
|
||||
|
||||
setSettingOption({ language: langCode });
|
||||
setSettingOption({ language: langCode as LangCode });
|
||||
|
||||
loadAttachBots(); // Should be refetched every language change
|
||||
});
|
||||
|
||||
@ -108,6 +108,10 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [isCurrentUserPremium, lang, onScreenSelect, showNotification]);
|
||||
|
||||
const handleUpdateContentSettings = useCallback((isChecked: boolean) => {
|
||||
updateContentSettings(isChecked);
|
||||
}, [updateContentSettings]);
|
||||
|
||||
function getVisibilityValue(setting?: ApiPrivacySettings) {
|
||||
const { visibility } = setting || {};
|
||||
const blockCount = setting ? setting.blockChatIds.length + setting.blockUserIds.length : 0;
|
||||
@ -318,7 +322,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
subLabel={lang('lng_settings_sensitive_about')}
|
||||
checked={Boolean(isSensitiveEnabled)}
|
||||
disabled={!canChangeSensitive}
|
||||
onCheck={updateContentSettings}
|
||||
onCheck={handleUpdateContentSettings}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -171,7 +171,7 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleVisibilityChange = useCallback((value) => {
|
||||
setPrivacyVisibility({
|
||||
privacyKey,
|
||||
privacyKey: privacyKey!,
|
||||
visibility: value,
|
||||
});
|
||||
}, [privacyKey, setPrivacyVisibility]);
|
||||
|
||||
@ -86,7 +86,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
setPrivacySettings({
|
||||
privacyKey: getPrivacyKey(screen),
|
||||
privacyKey: getPrivacyKey(screen)!,
|
||||
isAllowList: Boolean(isAllowList),
|
||||
contactsIds: newSelectedContactIds,
|
||||
});
|
||||
|
||||
@ -4,6 +4,8 @@ import React, {
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiChatFolder } from '../../../../api/types';
|
||||
|
||||
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
|
||||
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||
import { findIntersectionWithSet } from '../../../../util/iteratees';
|
||||
@ -142,9 +144,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
|
||||
dispatch({ type: 'setIsLoading', payload: true });
|
||||
if (state.mode === 'edit') {
|
||||
editChatFolder({ id: state.folderId, folderUpdate: state.folder });
|
||||
editChatFolder({ id: state.folderId!, folderUpdate: state.folder });
|
||||
} else {
|
||||
addChatFolder({ folder: state.folder });
|
||||
addChatFolder({ folder: state.folder as ApiChatFolder });
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@ -142,7 +142,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
dispatch({ type: 'setEmail', payload: value });
|
||||
updateRecoveryEmail({
|
||||
...state,
|
||||
email: value,
|
||||
email: value!,
|
||||
onSuccess: () => {
|
||||
onScreenSelect(SettingsScreens.TwoFaCongratulations);
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
@ -10,7 +10,7 @@ import useFlag from '../../hooks/useFlag';
|
||||
import RecipientPicker from '../common/RecipientPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
requestedAttachBotInChat?: GlobalState['requestedAttachBotInChat'];
|
||||
requestedAttachBotInChat?: TabState['requestedAttachBotInChat'];
|
||||
};
|
||||
|
||||
const AttachBotRecipientPicker: FC<OwnProps> = ({
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import { pick } from '../../util/iteratees';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
|
||||
import useWindowSize from '../../hooks/useWindowSize';
|
||||
import useOnChange from '../../hooks/useOnChange';
|
||||
@ -15,7 +16,7 @@ import useAppLayout from '../../hooks/useAppLayout';
|
||||
import styles from './ConfettiContainer.module.scss';
|
||||
|
||||
type StateProps = {
|
||||
confetti?: GlobalState['confetti'];
|
||||
confetti?: TabState['confetti'];
|
||||
};
|
||||
|
||||
interface Confetti {
|
||||
@ -203,5 +204,5 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => pick(global, ['confetti']),
|
||||
(global): StateProps => pick(selectTabState(global), ['confetti']),
|
||||
)(ConfettiContainer));
|
||||
|
||||
@ -18,7 +18,7 @@ const DeleteFolderDialog: FC<OwnProps> = ({
|
||||
|
||||
const handleDeleteFolderMessage = useCallback(() => {
|
||||
closeDeleteChatFolderModal();
|
||||
deleteChatFolder({ id: deleteFolderDialogId });
|
||||
deleteChatFolder({ id: deleteFolderDialogId! });
|
||||
}, [closeDeleteChatFolderModal, deleteChatFolder, deleteFolderDialogId]);
|
||||
|
||||
return (
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
} from '../../api/types';
|
||||
import type { AnimationLevel } from '../../types';
|
||||
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import getReadableErrorText from '../../util/getReadableErrorText';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
@ -20,7 +21,7 @@ import Avatar from '../common/Avatar';
|
||||
import './Dialogs.scss';
|
||||
|
||||
type StateProps = {
|
||||
dialogs: (ApiError | ApiInviteInfo)[];
|
||||
dialogs: (ApiError | ApiInviteInfo | ApiContact)[];
|
||||
animationLevel: AnimationLevel;
|
||||
};
|
||||
|
||||
@ -198,7 +199,7 @@ function getErrorHeader(error: ApiError) {
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
return {
|
||||
dialogs: global.dialogs,
|
||||
dialogs: selectTabState(global).dialogs,
|
||||
animationLevel: global.settings.byKey.animationLevel,
|
||||
};
|
||||
},
|
||||
|
||||
@ -6,6 +6,7 @@ import type { Thread } from '../../global/types';
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import { IS_OPFS_SUPPORTED, IS_SERVICE_WORKER_SUPPORTED, MAX_BUFFER_SIZE } from '../../util/environment';
|
||||
import * as mediaLoader from '../../util/mediaLoader';
|
||||
import download from '../../util/download';
|
||||
@ -113,7 +114,7 @@ const DownloadManager: FC<StateProps> = ({
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const activeDownloads = global.activeDownloads.byChatId;
|
||||
const activeDownloads = selectTabState(global).activeDownloads.byChatId;
|
||||
const messages = global.messages.byChatId;
|
||||
return {
|
||||
activeDownloads,
|
||||
|
||||
@ -4,7 +4,7 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
@ -12,7 +12,7 @@ import useFlag from '../../hooks/useFlag';
|
||||
import RecipientPicker from '../common/RecipientPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
requestedDraft?: GlobalState['requestedDraft'];
|
||||
requestedDraft?: TabState['requestedDraft'];
|
||||
};
|
||||
|
||||
const DraftRecipientPicker: FC<OwnProps> = ({
|
||||
|
||||
@ -4,6 +4,7 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
|
||||
@ -74,6 +75,6 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
return {
|
||||
currentUserId: global.currentUserId,
|
||||
isManyMessages: (global.forwardMessages.messageIds?.length || 0) > 1,
|
||||
isManyMessages: (selectTabState(global).forwardMessages.messageIds?.length || 0) > 1,
|
||||
};
|
||||
})(ForwardRecipientPicker));
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../lib/teact/teactn';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import { withGlobal } from '../../global';
|
||||
@ -22,7 +22,7 @@ type GameEvents = { eventType: 'share_score' | 'share_game' };
|
||||
const PLAY_GAME_ACTION_INTERVAL = 5000;
|
||||
|
||||
type OwnProps = {
|
||||
openedGame?: GlobalState['openedGame'];
|
||||
openedGame?: TabState['openedGame'];
|
||||
gameTitle?: string;
|
||||
};
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import CalendarModal from '../common/CalendarModal';
|
||||
@ -40,6 +41,6 @@ const HistoryCalendar: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
return { selectedAt: global.historyCalendarSelectedAt };
|
||||
return { selectedAt: selectTabState(global).historyCalendarSelectedAt };
|
||||
},
|
||||
)(HistoryCalendar));
|
||||
|
||||
@ -90,7 +90,7 @@ const LockScreen: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleSignOutMessage = useCallback(() => {
|
||||
closeSignOutConfirmation();
|
||||
signOut();
|
||||
signOut({ forceInitApi: true });
|
||||
}, [closeSignOutConfirmation, signOut]);
|
||||
|
||||
if (!shouldRender) {
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
ApiAttachBot,
|
||||
ApiChat, ApiMessage, ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { ApiLimitTypeWithModal, GlobalState } from '../../global/types';
|
||||
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
||||
|
||||
import '../../global/actions/all';
|
||||
import {
|
||||
@ -18,6 +18,7 @@ import {
|
||||
import { IS_ANDROID } from '../../util/environment';
|
||||
import {
|
||||
selectChatMessage,
|
||||
selectTabState,
|
||||
selectCurrentMessageList,
|
||||
selectIsForwardModalOpen,
|
||||
selectIsMediaViewerOpen,
|
||||
@ -28,7 +29,6 @@ import {
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||
import { processDeepLink } from '../../util/deeplink';
|
||||
import { getAllNotificationsCount } from '../../util/folderManager';
|
||||
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
|
||||
import { fastRaf } from '../../util/schedulers';
|
||||
|
||||
@ -42,6 +42,8 @@ import useShowTransition from '../../hooks/useShowTransition';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useInterval from '../../hooks/useInterval';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import updatePageTitle from '../../util/updatePageTitle';
|
||||
import updateIcon from '../../util/updateIcon';
|
||||
|
||||
import StickerSetModal from '../common/StickerSetModal.async';
|
||||
import UnreadCount from '../common/UnreadCounter';
|
||||
@ -84,6 +86,7 @@ export interface OwnProps {
|
||||
}
|
||||
|
||||
type StateProps = {
|
||||
isMasterTab?: boolean;
|
||||
chat?: ApiChat;
|
||||
lastSyncTime?: number;
|
||||
isLeftColumnOpen: boolean;
|
||||
@ -109,29 +112,26 @@ type StateProps = {
|
||||
addedCustomEmojiIds?: string[];
|
||||
newContactUserId?: string;
|
||||
newContactByPhoneNumber?: boolean;
|
||||
openedGame?: GlobalState['openedGame'];
|
||||
openedGame?: TabState['openedGame'];
|
||||
gameTitle?: string;
|
||||
isRatePhoneCallModalOpen?: boolean;
|
||||
webApp?: GlobalState['webApp'];
|
||||
webApp?: TabState['webApp'];
|
||||
isPremiumModalOpen?: boolean;
|
||||
botTrustRequest?: GlobalState['botTrustRequest'];
|
||||
botTrustRequest?: TabState['botTrustRequest'];
|
||||
botTrustRequestBot?: ApiUser;
|
||||
attachBotToInstall?: ApiAttachBot;
|
||||
requestedAttachBotInChat?: GlobalState['requestedAttachBotInChat'];
|
||||
requestedDraft?: GlobalState['requestedDraft'];
|
||||
requestedAttachBotInChat?: TabState['requestedAttachBotInChat'];
|
||||
requestedDraft?: TabState['requestedDraft'];
|
||||
currentUser?: ApiUser;
|
||||
urlAuth?: GlobalState['urlAuth'];
|
||||
urlAuth?: TabState['urlAuth'];
|
||||
limitReached?: ApiLimitTypeWithModal;
|
||||
deleteFolderDialogId?: number;
|
||||
isPaymentModalOpen?: boolean;
|
||||
isReceiptModalOpen?: boolean;
|
||||
};
|
||||
|
||||
const NOTIFICATION_INTERVAL = 1000;
|
||||
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
||||
|
||||
let notificationInterval: number | undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let DEBUG_isLogged = false;
|
||||
|
||||
@ -177,12 +177,14 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
isPaymentModalOpen,
|
||||
isReceiptModalOpen,
|
||||
deleteFolderDialogId,
|
||||
isMasterTab,
|
||||
}) => {
|
||||
const {
|
||||
loadAnimatedEmojis,
|
||||
loadNotificationSettings,
|
||||
loadNotificationExceptions,
|
||||
updateIsOnline,
|
||||
onTabFocusChange,
|
||||
loadTopInlineBots,
|
||||
loadEmojiKeywords,
|
||||
loadCountryList,
|
||||
@ -224,11 +226,11 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [isDesktop, isLeftColumnOpen, isMiddleColumnOpen, toggleLeftColumn]);
|
||||
|
||||
useInterval(checkAppVersion, APP_OUTDATED_TIMEOUT_MS, true);
|
||||
useInterval(checkAppVersion, isMasterTab ? APP_OUTDATED_TIMEOUT_MS : undefined, true);
|
||||
|
||||
// Initial API calls
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
if (lastSyncTime && isMasterTab) {
|
||||
updateIsOnline(true);
|
||||
loadConfig();
|
||||
loadAppConfig();
|
||||
@ -248,33 +250,33 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
}, [
|
||||
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
||||
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList,
|
||||
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons,
|
||||
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons, isMasterTab,
|
||||
]);
|
||||
|
||||
// Language-based API calls
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
if (lastSyncTime && isMasterTab) {
|
||||
if (language !== BASE_EMOJI_KEYWORD_LANG) {
|
||||
loadEmojiKeywords({ language });
|
||||
loadEmojiKeywords({ language: language! });
|
||||
}
|
||||
|
||||
loadCountryList({ langCode: language });
|
||||
}
|
||||
}, [language, lastSyncTime, loadCountryList, loadEmojiKeywords]);
|
||||
}, [language, lastSyncTime, loadCountryList, loadEmojiKeywords, isMasterTab]);
|
||||
|
||||
// Re-fetch cached saved emoji for `localDb`
|
||||
useEffectWithPrevDeps(([prevLastSyncTime]) => {
|
||||
if (!prevLastSyncTime && lastSyncTime) {
|
||||
if (!prevLastSyncTime && lastSyncTime && isMasterTab) {
|
||||
loadCustomEmojis({
|
||||
ids: Object.keys(getGlobal().customEmojis.byId),
|
||||
ignoreCache: true,
|
||||
});
|
||||
}
|
||||
}, [lastSyncTime] as const);
|
||||
}, [lastSyncTime, isMasterTab] as const);
|
||||
|
||||
// Sticker sets
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
if (lastSyncTime && isMasterTab) {
|
||||
if (!addedSetIds || !addedCustomEmojiIds) {
|
||||
loadStickerSets();
|
||||
loadFavoriteStickers();
|
||||
@ -284,14 +286,17 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
loadAddedStickers();
|
||||
}
|
||||
}
|
||||
}, [lastSyncTime, addedSetIds, loadStickerSets, loadFavoriteStickers, loadAddedStickers, addedCustomEmojiIds]);
|
||||
}, [
|
||||
lastSyncTime, addedSetIds, loadStickerSets, loadFavoriteStickers, loadAddedStickers, addedCustomEmojiIds,
|
||||
isMasterTab,
|
||||
]);
|
||||
|
||||
// Check version when service chat is ready
|
||||
useEffect(() => {
|
||||
if (lastSyncTime && isServiceChatReady) {
|
||||
if (lastSyncTime && isServiceChatReady && isMasterTab) {
|
||||
checkVersionNotification();
|
||||
}
|
||||
}, [lastSyncTime, isServiceChatReady, checkVersionNotification]);
|
||||
}, [lastSyncTime, isServiceChatReady, checkVersionNotification, isMasterTab]);
|
||||
|
||||
// Ensure time format
|
||||
useEffect(() => {
|
||||
@ -391,45 +396,18 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
updateIsOnline(false);
|
||||
|
||||
const initialUnread = getAllNotificationsCount();
|
||||
let index = 0;
|
||||
|
||||
clearInterval(notificationInterval);
|
||||
notificationInterval = window.setInterval(() => {
|
||||
if (document.title.includes(INACTIVE_MARKER)) {
|
||||
updateIcon(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index % 2 === 0) {
|
||||
const newUnread = getAllNotificationsCount() - initialUnread;
|
||||
if (newUnread > 0) {
|
||||
updatePageTitle(`${newUnread} notification${newUnread > 1 ? 's' : ''}`);
|
||||
updateIcon(true);
|
||||
}
|
||||
} else {
|
||||
updatePageTitle(PAGE_TITLE);
|
||||
updateIcon(false);
|
||||
}
|
||||
|
||||
index++;
|
||||
}, NOTIFICATION_INTERVAL);
|
||||
}, [updateIsOnline]);
|
||||
onTabFocusChange({ isBlurred: true });
|
||||
}, [onTabFocusChange]);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
updateIsOnline(true);
|
||||
|
||||
clearInterval(notificationInterval);
|
||||
notificationInterval = undefined;
|
||||
onTabFocusChange({ isBlurred: false });
|
||||
|
||||
if (!document.title.includes(INACTIVE_MARKER)) {
|
||||
updatePageTitle(PAGE_TITLE);
|
||||
}
|
||||
|
||||
updateIcon(false);
|
||||
}, [updateIsOnline]);
|
||||
}, [onTabFocusChange]);
|
||||
|
||||
const handleStickerSetModalClose = useCallback(() => {
|
||||
closeStickerSetModal();
|
||||
@ -494,27 +472,6 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function updateIcon(asUnread: boolean) {
|
||||
document.querySelectorAll<HTMLLinkElement>('link[rel="icon"], link[rel="alternate icon"]')
|
||||
.forEach((link) => {
|
||||
if (asUnread) {
|
||||
if (!link.href.includes('favicon-unread')) {
|
||||
link.href = link.href.replace('favicon', 'favicon-unread');
|
||||
}
|
||||
} else {
|
||||
link.href = link.href.replace('favicon-unread', 'favicon');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For some reason setting `document.title` to the same value
|
||||
// causes increment of Chrome Dev Tools > Performance Monitor > DOM Nodes counter
|
||||
function updatePageTitle(nextTitle: string) {
|
||||
if (document.title !== nextTitle) {
|
||||
document.title = nextTitle;
|
||||
}
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { isMobile }): StateProps => {
|
||||
const {
|
||||
@ -523,6 +480,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
animationLevel, language, wasTimeFormatSetManually,
|
||||
},
|
||||
},
|
||||
lastSyncTime,
|
||||
} = global;
|
||||
|
||||
const {
|
||||
botTrustRequest,
|
||||
requestedAttachBotInstall,
|
||||
requestedAttachBotInChat,
|
||||
@ -530,16 +491,28 @@ export default memo(withGlobal<OwnProps>(
|
||||
urlAuth,
|
||||
webApp,
|
||||
safeLinkModalUrl,
|
||||
lastSyncTime,
|
||||
openedStickerSetShortName,
|
||||
openedCustomEmojiSetIds,
|
||||
shouldSkipHistoryAnimations,
|
||||
} = global;
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
||||
openedGame,
|
||||
audioPlayer,
|
||||
isLeftColumnShown,
|
||||
historyCalendarSelectedAt,
|
||||
notifications,
|
||||
dialogs,
|
||||
newContact,
|
||||
ratingPhoneCall,
|
||||
premiumModal,
|
||||
isMasterTab,
|
||||
payment,
|
||||
limitReachedModal,
|
||||
deleteFolderDialogModal,
|
||||
} = selectTabState(global);
|
||||
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
||||
const audioMessage = audioChatId && audioMessageId
|
||||
? selectChatMessage(global, audioChatId, audioMessageId)
|
||||
: undefined;
|
||||
const openedGame = global.openedGame;
|
||||
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
|
||||
const gameTitle = gameMessage?.content.game?.title;
|
||||
const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
|
||||
@ -547,32 +520,32 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
return {
|
||||
lastSyncTime,
|
||||
isLeftColumnOpen: global.isLeftColumnShown,
|
||||
isLeftColumnOpen: isLeftColumnShown,
|
||||
isMiddleColumnOpen: Boolean(chatId),
|
||||
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
|
||||
isMediaViewerOpen: selectIsMediaViewerOpen(global),
|
||||
isForwardModalOpen: selectIsForwardModalOpen(global),
|
||||
hasNotifications: Boolean(global.notifications.length),
|
||||
hasDialogs: Boolean(global.dialogs.length),
|
||||
hasNotifications: Boolean(notifications.length),
|
||||
hasDialogs: Boolean(dialogs.length),
|
||||
audioMessage,
|
||||
safeLinkModalUrl,
|
||||
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
|
||||
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
|
||||
shouldSkipHistoryAnimations,
|
||||
openedStickerSetShortName,
|
||||
openedCustomEmojiSetIds,
|
||||
isServiceChatReady: selectIsServiceChatReady(global),
|
||||
activeGroupCallId: global.groupCalls.activeGroupCallId,
|
||||
activeGroupCallId: isMasterTab ? global.groupCalls.activeGroupCallId : undefined,
|
||||
animationLevel,
|
||||
language,
|
||||
wasTimeFormatSetManually,
|
||||
isPhoneCallActive: Boolean(global.phoneCall),
|
||||
isPhoneCallActive: isMasterTab ? Boolean(global.phoneCall) : undefined,
|
||||
addedSetIds: global.stickers.added.setIds,
|
||||
addedCustomEmojiIds: global.customEmojis.added.setIds,
|
||||
newContactUserId: global.newContact?.userId,
|
||||
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
|
||||
newContactUserId: newContact?.userId,
|
||||
newContactByPhoneNumber: newContact?.isByPhoneNumber,
|
||||
openedGame,
|
||||
gameTitle,
|
||||
isRatePhoneCallModalOpen: Boolean(global.ratingPhoneCall),
|
||||
isRatePhoneCallModalOpen: Boolean(ratingPhoneCall),
|
||||
botTrustRequest,
|
||||
botTrustRequestBot: botTrustRequest && selectUser(global, botTrustRequest.botId),
|
||||
attachBotToInstall: requestedAttachBotInstall?.bot,
|
||||
@ -580,11 +553,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
webApp,
|
||||
currentUser,
|
||||
urlAuth,
|
||||
isPremiumModalOpen: global.premiumModal?.isOpen,
|
||||
limitReached: global.limitReachedModal?.limit,
|
||||
isPaymentModalOpen: global.payment.isPaymentModalOpen,
|
||||
isReceiptModalOpen: Boolean(global.payment.receipt),
|
||||
deleteFolderDialogId: global.deleteFolderDialogModal,
|
||||
isPremiumModalOpen: premiumModal?.isOpen,
|
||||
limitReached: limitReachedModal?.limit,
|
||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||
isReceiptModalOpen: Boolean(payment.receipt),
|
||||
deleteFolderDialogId: deleteFolderDialogModal,
|
||||
isMasterTab,
|
||||
requestedDraft,
|
||||
};
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiNotification } from '../../api/types';
|
||||
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
@ -40,5 +41,5 @@ const Notifications: FC<StateProps> = ({ notifications }) => {
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => pick(global, ['notifications']),
|
||||
(global): StateProps => pick(selectTabState(global), ['notifications']),
|
||||
)(Notifications));
|
||||
|
||||
@ -5,7 +5,7 @@ import { getActions, getGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiUser } from '../../api/types';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
|
||||
import { ensureProtocol } from '../../util/ensureProtocol';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
@ -20,7 +20,7 @@ import Checkbox from '../ui/Checkbox';
|
||||
import styles from './UrlAuthModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
urlAuth?: GlobalState['urlAuth'];
|
||||
urlAuth?: TabState['urlAuth'];
|
||||
currentUser?: ApiUser;
|
||||
};
|
||||
|
||||
|
||||
@ -5,12 +5,14 @@ import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiAttachBot, ApiChat, ApiUser } from '../../api/types';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
import type { ThemeKey } from '../../types';
|
||||
import type { PopupOptions, WebAppInboundEvent } from './hooks/useWebAppFrame';
|
||||
|
||||
import { TME_LINK_PREFIX } from '../../config';
|
||||
import { selectCurrentChat, selectTheme, selectUser } from '../../global/selectors';
|
||||
import {
|
||||
selectCurrentChat, selectTabState, selectTheme, selectUser,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { extractCurrentThemeParams, validateHexColor } from '../../util/themeStyle';
|
||||
|
||||
@ -41,7 +43,7 @@ type WebAppButton = {
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
webApp?: GlobalState['webApp'];
|
||||
webApp?: TabState['webApp'];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -50,7 +52,7 @@ type StateProps = {
|
||||
attachBot?: ApiAttachBot;
|
||||
theme?: ThemeKey;
|
||||
isPaymentModalOpen?: boolean;
|
||||
paymentStatus?: GlobalState['payment']['status'];
|
||||
paymentStatus?: TabState['payment']['status'];
|
||||
};
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
@ -504,7 +506,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const bot = botId ? selectUser(global, botId) : undefined;
|
||||
const chat = selectCurrentChat(global);
|
||||
const theme = selectTheme(global);
|
||||
const { isPaymentModalOpen, status } = global.payment;
|
||||
const { isPaymentModalOpen, status } = selectTabState(global).payment;
|
||||
|
||||
return {
|
||||
attachBot,
|
||||
|
||||
@ -10,7 +10,7 @@ import type { AnimationLevel } from '../../../types';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { getUserFirstOrLastName } from '../../../global/helpers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { selectTabState, selectUser } from '../../../global/selectors';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -160,7 +160,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
const { forUserId, monthlyCurrency, monthlyAmount } = global.giftPremiumModal || {};
|
||||
const { forUserId, monthlyCurrency, monthlyAmount } = selectTabState(global).giftPremiumModal || {};
|
||||
const user = forUserId ? selectUser(global, forUserId) : undefined;
|
||||
const gifts = user ? user.fullInfo?.premiumGifts : undefined;
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import PremiumFeatureModal, {
|
||||
import { TME_LINK_PREFIX } from '../../../config';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { selectIsCurrentUserPremium, selectUser } from '../../../global/selectors';
|
||||
import { selectTabState, selectIsCurrentUserPremium, selectUser } from '../../../global/selectors';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
import { selectPremiumLimit } from '../../../global/selectors/limits';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
@ -309,16 +309,19 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
const {
|
||||
premiumModal,
|
||||
} = selectTabState(global);
|
||||
return {
|
||||
currentUserId: global.currentUserId,
|
||||
promo: global.premiumModal?.promo,
|
||||
isClosing: global.premiumModal?.isClosing,
|
||||
isSuccess: global.premiumModal?.isSuccess,
|
||||
isGift: global.premiumModal?.isGift,
|
||||
monthsAmount: global.premiumModal?.monthsAmount,
|
||||
fromUser: global.premiumModal?.fromUserId ? selectUser(global, global.premiumModal.fromUserId) : undefined,
|
||||
toUser: global.premiumModal?.toUserId ? selectUser(global, global.premiumModal.toUserId) : undefined,
|
||||
initialSection: global.premiumModal?.initialSection,
|
||||
promo: premiumModal?.promo,
|
||||
isClosing: premiumModal?.isClosing,
|
||||
isSuccess: premiumModal?.isSuccess,
|
||||
isGift: premiumModal?.isGift,
|
||||
monthsAmount: premiumModal?.monthsAmount,
|
||||
fromUser: premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined,
|
||||
toUser: premiumModal?.toUserId ? selectUser(global, premiumModal.toUserId) : undefined,
|
||||
initialSection: premiumModal?.initialSection,
|
||||
isPremium: selectIsCurrentUserPremium(global),
|
||||
limitChannels: selectPremiumLimit(global, 'channels'),
|
||||
limitFolders: selectPremiumLimit(global, 'dialogFilters'),
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
selectChatMessage,
|
||||
selectChatMessages,
|
||||
selectChatScheduledMessages,
|
||||
selectCurrentMediaSearch,
|
||||
selectCurrentMediaSearch, selectTabState,
|
||||
selectIsChatWithSelf,
|
||||
selectListedIds,
|
||||
selectOutlyingIds,
|
||||
@ -218,13 +218,15 @@ const MediaViewer: FC<StateProps> = ({
|
||||
const handleFooterClick = useCallback(() => {
|
||||
handleClose();
|
||||
|
||||
if (!chatId || !mediaId) return;
|
||||
|
||||
if (isMobile) {
|
||||
setTimeout(() => {
|
||||
toggleChatInfo(false, { forceSyncOnIOs: true });
|
||||
focusMessage({ chatId, threadId, mediaId });
|
||||
toggleChatInfo({ force: false }, { forceSyncOnIOs: true });
|
||||
focusMessage({ chatId, threadId, messageId: mediaId });
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
focusMessage({ chatId, threadId, mediaId });
|
||||
focusMessage({ chatId, threadId, messageId: mediaId });
|
||||
}
|
||||
}, [handleClose, isMobile, chatId, threadId, focusMessage, toggleChatInfo, mediaId]);
|
||||
|
||||
@ -370,6 +372,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const { mediaViewer, shouldSkipHistoryAnimations } = selectTabState(global);
|
||||
const {
|
||||
chatId,
|
||||
threadId,
|
||||
@ -377,12 +380,12 @@ export default memo(withGlobal(
|
||||
avatarOwnerId,
|
||||
origin,
|
||||
isHidden,
|
||||
} = global.mediaViewer;
|
||||
} = mediaViewer;
|
||||
const {
|
||||
animationLevel,
|
||||
} = global.settings.byKey;
|
||||
|
||||
const { shouldSkipHistoryAnimations, currentUserId } = global;
|
||||
const { currentUserId } = global;
|
||||
let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId);
|
||||
|
||||
if (origin === MediaViewerOrigin.SearchResult) {
|
||||
|
||||
@ -10,7 +10,7 @@ import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import {
|
||||
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser,
|
||||
selectChat, selectChatMessage, selectTabState, selectIsMessageProtected, selectScheduledMessage, selectUser,
|
||||
} from '../../global/selectors';
|
||||
import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
|
||||
import { renderMessageText } from '../common/helpers/renderMessageText';
|
||||
@ -216,7 +216,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isMuted,
|
||||
playbackRate,
|
||||
isHidden,
|
||||
} = global.mediaViewer;
|
||||
} = selectTabState(global).mediaViewer;
|
||||
|
||||
if (origin === MediaViewerOrigin.SearchResult) {
|
||||
if (!(chatId && mediaId)) {
|
||||
|
||||
@ -56,9 +56,11 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
|
||||
const handleFocusMessage = useCallback(() => {
|
||||
closeMediaViewer();
|
||||
|
||||
if (!chatId || !messageId) return;
|
||||
|
||||
if (isMobile) {
|
||||
setTimeout(() => {
|
||||
toggleChatInfo(false, { forceSyncOnIOs: true });
|
||||
toggleChatInfo({ force: false }, { forceSyncOnIOs: true });
|
||||
focusMessage({ chatId, messageId });
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
|
||||
@ -81,12 +81,12 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
const handleEnterFullscreen = useCallback(() => {
|
||||
// Yandex browser doesn't support PIP when video is hidden
|
||||
if (IS_YA_BROWSER) return;
|
||||
setMediaViewerHidden(true);
|
||||
setMediaViewerHidden({ isHidden: true });
|
||||
}, [setMediaViewerHidden]);
|
||||
|
||||
const handleLeaveFullscreen = useCallback(() => {
|
||||
if (IS_YA_BROWSER) return;
|
||||
setMediaViewerHidden(false);
|
||||
setMediaViewerHidden({ isHidden: false });
|
||||
}, [setMediaViewerHidden]);
|
||||
|
||||
const [
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
selectIsMessageFocused,
|
||||
selectChat,
|
||||
selectTopicFromMessage,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -242,7 +243,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
: undefined;
|
||||
|
||||
const isFocused = selectIsMessageFocused(global, message);
|
||||
const { direction: focusDirection, noHighlight: noFocusHighlight } = (isFocused && global.focusedMessage) || {};
|
||||
const {
|
||||
direction: focusDirection,
|
||||
noHighlight: noFocusHighlight,
|
||||
} = (isFocused && selectTabState(global).focusedMessage) || {};
|
||||
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const isChat = chat && (isChatChannel(chat) || userId === message.chatId);
|
||||
|
||||
@ -27,7 +27,7 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
|
||||
content,
|
||||
}) => {
|
||||
const {
|
||||
openMediaViewer, uploadProfilePhoto, showNotification, requestNextSettingsScreen,
|
||||
openMediaViewer, uploadProfilePhoto, showNotification,
|
||||
} = getActions();
|
||||
|
||||
const { isOutgoing } = message;
|
||||
@ -42,10 +42,15 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
|
||||
showNotification({
|
||||
title: lang('ApplyAvatarHintTitle'),
|
||||
message: lang('ApplyAvatarHint'),
|
||||
action: () => requestNextSettingsScreen(SettingsScreens.Main),
|
||||
action: {
|
||||
action: 'requestNextSettingsScreen',
|
||||
payload: {
|
||||
screen: SettingsScreens.Main,
|
||||
},
|
||||
},
|
||||
actionText: lang('Open'),
|
||||
});
|
||||
}, [lang, requestNextSettingsScreen, showNotification]);
|
||||
}, [lang, showNotification]);
|
||||
|
||||
const handleSetSuggestedAvatar = useCallback((file: File) => {
|
||||
setCropModalBlob(undefined);
|
||||
|
||||
@ -14,7 +14,7 @@ import * as mediaLoader from '../../util/mediaLoader';
|
||||
import {
|
||||
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal,
|
||||
} from '../../global/helpers';
|
||||
import { selectChat, selectSender } from '../../global/selectors';
|
||||
import { selectChat, selectTabState, selectSender } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { makeTrackId } from '../../util/audioPlayer';
|
||||
import { clearMediaSession } from '../../util/mediaSession';
|
||||
@ -320,7 +320,7 @@ export default withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const sender = selectSender(global, message);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const { volume, playbackRate, isMuted } = global.audioPlayer;
|
||||
const { volume, playbackRate, isMuted } = selectTabState(global).audioPlayer;
|
||||
|
||||
return {
|
||||
sender,
|
||||
|
||||
@ -58,13 +58,13 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
const handleAddContact = useCallback(() => {
|
||||
openAddContactDialog({ userId: chatId });
|
||||
if (isAutoArchived) {
|
||||
toggleChatArchived({ chatId });
|
||||
toggleChatArchived({ id: chatId });
|
||||
}
|
||||
}, [openAddContactDialog, isAutoArchived, toggleChatArchived, chatId]);
|
||||
|
||||
const handleConfirmBlock = useCallback(() => {
|
||||
closeBlockUserModal();
|
||||
blockContact({ contactId: chatId, accessHash });
|
||||
blockContact({ contactId: chatId, accessHash: accessHash! });
|
||||
if (canReportSpam && shouldReportSpam) {
|
||||
reportSpam({ chatId });
|
||||
}
|
||||
@ -84,7 +84,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
closeBlockUserModal();
|
||||
reportSpam({ chatId });
|
||||
if (isBasicGroup) {
|
||||
deleteChatUser({ chatId, userId: currentUserId });
|
||||
deleteChatUser({ chatId, userId: currentUserId! });
|
||||
deleteHistory({ chatId, shouldDeleteForAll: false });
|
||||
} else {
|
||||
leaveChannel({ chatId });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user