diff --git a/.eslintignore b/.eslintignore
index 3c6b311c31..488082ca3b 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -24,6 +24,11 @@ password_preload.js
sass.config.js
*.worker.config.js
+babel.config.js
+minify.js
+eslint-local-rules.js
+eslint-rules/
+
# Not too sure why we can't have eslint on those files atm
ts/webworker/workers/node/util/util.worker.ts
ts/webworker/workers/node/libsession/libsession.worker.ts
diff --git a/.eslintrc.js b/.eslintrc.js
index a0c94a147b..8c37e25948 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -29,7 +29,7 @@ module.exports = {
'plugin:import/typescript',
],
- plugins: ['mocha', 'more', '@typescript-eslint'],
+ plugins: ['mocha', 'more', '@typescript-eslint', 'local-rules'],
parser: '@typescript-eslint/parser',
parserOptions: { project: ['tsconfig.json'] },
@@ -122,7 +122,7 @@ module.exports = {
ignoreRegExpLiterals: true,
},
],
- 'no-restricted-imports': [
+ '@typescript-eslint/no-restricted-imports': [
'error',
{
paths: [
@@ -132,9 +132,22 @@ module.exports = {
message:
"Don't import from 'react-use' directly. Please use a default import for each hook from 'react-use/lib' instead.",
},
+ {
+ name: 'zod',
+ allowTypeImports: true,
+ message:
+ "Don't import from 'zod' directly. Please use a default import from 'ts/utils/zod' instead.",
+ },
],
},
],
+ 'local-rules/styled-components-transient-props': [
+ 'error',
+ {
+ additionalValidProps: ['as', 'forwardedAs', 'theme'],
+ ignoreComponentPatterns: ['^Motion', '^Animated'],
+ },
+ ],
},
overrides: [
{
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000000..98c03dd1ca
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,504 @@
+/* eslint-disable no-console */
+
+const fs = require('fs');
+const path = require('path');
+const { SourceMapConsumer } = require('source-map');
+const { globSync } = require('glob');
+
+const fileFilter = process.env.SESSION_RC_FILE_FILTER;
+const allowErrors = process.env.SESSION_RC_ALLOW_ERRORS;
+
+// File patterns for babel
+const babelInclude = 'ts/**/*.js';
+const babelIgnore = ['ts/test/**'];
+
+// This can be turned into an array if we need more than 1 file ignored by the react compiler
+const fileIgnoredByReactCompiler =
+ // NOTE: [react-compiler] we are telling the compiler to not attempt to compile this
+ // file in the babel config as it is highly complex and has a lot of very fine tuned
+ // callbacks, its probably not worth trying to refactor at this stage
+ path.normalize('ts/components/conversation/composition/CompositionTextArea.js');
+
+// Project root directory (where babel.config.js lives)
+const PROJECT_ROOT = __dirname;
+
+// ANSI color codes
+const c = code => (process.env.NO_COLOR ? '' : `\x1b[${code}m`);
+const colors = {
+ reset: c(0),
+ bright: c(1),
+ dim: c(2),
+ black: c(30),
+ red: c(31),
+ green: c(32),
+ yellow: c(33),
+ blue: c(34),
+ magenta: c(35),
+ cyan: c(36),
+ white: c(37),
+ bgRed: c(41),
+ bgYellow: c(43),
+};
+
+// Cache for source map consumers
+const sourceMapCache = new Map();
+
+// Number of lines to show before/after error
+const CONTEXT_LINES = 2;
+
+// Collect all errors grouped by file
+const errorsByFile = new Map();
+const skippedFiles = new Set();
+const filenameMap = new Map();
+const pendingPromises = [];
+
+// File size tracking - measure all js files at startup
+function getTotalSize() {
+ const files = globSync('ts/**/*.js', {
+ cwd: PROJECT_ROOT,
+ ignore: babelIgnore,
+ });
+ let total = 0;
+ for (const file of files) {
+ try {
+ total += fs.statSync(path.join(PROJECT_ROOT, file)).size;
+ } catch {
+ // Skip files that don't exist
+ }
+ }
+ return { total, count: files.length };
+}
+
+const initialStats = getTotalSize();
+const startTime = Date.now();
+
+// Progress tracking
+let filesProcessed = 0;
+const totalFiles = initialStats.count;
+
+function updateProgress() {
+ filesProcessed++;
+ process.stdout.write(
+ `\r${colors.dim}Compiling... ${filesProcessed}/${totalFiles}${colors.reset}`
+ );
+}
+
+function clearProgress() {
+ process.stdout.write('\r\x1b[K');
+}
+
+function formatBytes(bytes) {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
+}
+
+function printSizeSummary() {
+ clearProgress();
+
+ const finalStats = getTotalSize();
+ const inputSize = initialStats.total;
+ const outputSize = finalStats.total;
+
+ if (inputSize === 0) return;
+
+ const diff = outputSize - inputSize;
+ const percent = ((diff / inputSize) * 100).toFixed(1);
+ const sign = diff >= 0 ? '+' : '';
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+
+ console.log(
+ `${colors.green}Compiled ${formatBytes(inputSize)} → ${formatBytes(outputSize)} (${sign}${percent}%) [${finalStats.count} files in ${elapsed}s]${colors.reset}`
+ );
+}
+
+function resolveFilename(filename) {
+ const resultName = filenameMap.get(filename);
+ if (resultName) {
+ return resultName;
+ }
+
+ const relativeFilename = path.relative(PROJECT_ROOT, filename);
+ filenameMap.set(filename, relativeFilename);
+ return relativeFilename;
+}
+
+async function getOriginalLocation(jsFile, line, column) {
+ try {
+ const mapFile = `${jsFile}.map`;
+
+ if (!fs.existsSync(mapFile)) {
+ return null;
+ }
+
+ let consumer = sourceMapCache.get(mapFile);
+ if (!consumer) {
+ const rawSourceMap = JSON.parse(fs.readFileSync(mapFile, 'utf-8'));
+ consumer = await new SourceMapConsumer(rawSourceMap);
+ sourceMapCache.set(mapFile, consumer);
+ }
+
+ const original = consumer.originalPositionFor({ line, column });
+ return original.source ? original : null;
+ } catch (err) {
+ return null;
+ }
+}
+
+function getSourceContent(jsFile) {
+ try {
+ const mapFile = `${jsFile}.map`;
+ if (!fs.existsSync(mapFile)) {
+ return null;
+ }
+ const rawSourceMap = JSON.parse(fs.readFileSync(mapFile, 'utf-8'));
+ if (rawSourceMap.sourcesContent && rawSourceMap.sourcesContent[0]) {
+ return {
+ content: rawSourceMap.sourcesContent[0],
+ sourceName: rawSourceMap.sources[0],
+ };
+ }
+ return null;
+ } catch (err) {
+ return null;
+ }
+}
+
+async function processErrorInternal(filename, event) {
+ const errorLoc = event.detail?.options?.loc;
+ const fnLoc = event.fnLoc;
+ const reason = event.detail?.options?.reason || 'Unknown error';
+ const category = event.detail?.options?.category || 'Unknown';
+
+ const errorData = {
+ reason,
+ category,
+ lines: [],
+ };
+
+ if (!fnLoc || !errorLoc) {
+ errorData.lines.push(` ${colors.yellow}Reason:${colors.reset} [${category}] ${reason}`);
+ return errorData;
+ }
+
+ // Try to get original TS source from source map
+ const sourceData = getSourceContent(filename);
+ let source;
+ let sourceName;
+
+ if (sourceData) {
+ source = sourceData.content;
+ sourceName = sourceData.sourceName;
+ } else {
+ source = fs.readFileSync(filename, 'utf-8');
+ sourceName = filename;
+ }
+
+ const lines = source.split('\n');
+
+ // Map JS locations to TS locations
+ let errStartLine = errorLoc.start.line;
+ let errEndLine = errorLoc.end.line;
+ let errColStart = errorLoc.start.column;
+ let errColEnd = errorLoc.end.column;
+
+ if (sourceData) {
+ const errStartOrig = await getOriginalLocation(
+ filename,
+ errorLoc.start.line,
+ errorLoc.start.column
+ );
+ const errEndOrig = await getOriginalLocation(filename, errorLoc.end.line, errorLoc.end.column);
+
+ if (errStartOrig) {
+ errStartLine = errStartOrig.line;
+ errColStart = errStartOrig.column;
+ }
+ if (errEndOrig) {
+ errEndLine = errEndOrig.line;
+ errColEnd = errEndOrig.column;
+ }
+ }
+
+ // Validate line numbers are within bounds
+ const maxLine = lines.length;
+ errStartLine = Math.max(1, Math.min(errStartLine, maxLine));
+ errEndLine = Math.max(1, Math.min(errEndLine, maxLine));
+
+ // Show context around the error
+ const contextStart = Math.max(0, errStartLine - CONTEXT_LINES - 1);
+ const contextEnd = Math.min(lines.length, errEndLine + CONTEXT_LINES);
+
+ errorData.sourceName = sourceName;
+ errorData.errStartLine = errStartLine;
+ errorData.errColStart = errColStart;
+ errorData.errColEnd = errColEnd;
+
+ errorData.lines.push(` ${colors.yellow}Reason:${colors.reset} [${category}] ${reason}`);
+ errorData.lines.push(
+ ` ${colors.yellow}Source:${colors.reset} ${colors.dim}${sourceName}${colors.reset} line ${errStartLine}, columns ${errColStart}-${errColEnd}`
+ );
+
+ // Only show source context if we have valid lines to show
+ if (contextEnd > contextStart && lines.length > 0) {
+ errorData.lines.push(` ${colors.dim}${'─'.repeat(60)}${colors.reset}`);
+
+ for (let i = contextStart; i < contextEnd; i++) {
+ const lineNum = i + 1;
+ const lineNumStr = lineNum.toString().padStart(4, ' ');
+ const line = lines[i] ?? '';
+
+ const isErrorLine = lineNum >= errStartLine && lineNum <= errEndLine;
+
+ if (isErrorLine) {
+ const gutter = `${colors.bgRed}${colors.black} ${lineNumStr} ${colors.reset}`;
+
+ if (errColStart !== null && errColEnd !== null && lineNum === errStartLine) {
+ // Clamp column values to line length
+ const safeColStart = Math.min(errColStart, line.length);
+ const safeColEnd = Math.min(errColEnd, line.length);
+
+ const before = line.substring(0, safeColStart);
+ const highlight = line.substring(safeColStart, safeColEnd);
+ const after = line.substring(safeColEnd);
+
+ if (highlight.length > 0) {
+ errorData.lines.push(
+ ` ${gutter} ${before}${colors.bgYellow}${colors.black}${highlight}${colors.reset}${after}`
+ );
+
+ const underline =
+ ' '.repeat(safeColStart) + '^'.repeat(Math.max(1, safeColEnd - safeColStart));
+ errorData.lines.push(
+ ` ${colors.dim} ${colors.reset} ${colors.red}${underline}${colors.reset}`
+ );
+ } else {
+ // No highlight range, just show the line in red
+ errorData.lines.push(` ${gutter} ${colors.red}${line}${colors.reset}`);
+ }
+ } else {
+ errorData.lines.push(` ${gutter} ${colors.red}${line}${colors.reset}`);
+ }
+ } else {
+ const gutter = `${colors.dim} ${lineNumStr} ${colors.reset}`;
+ errorData.lines.push(` ${gutter} ${line}`);
+ }
+ }
+
+ errorData.lines.push(` ${colors.dim}${'─'.repeat(60)}${colors.reset}`);
+ } else {
+ // No source context available
+ errorData.lines.push(
+ ` ${colors.dim}(source context not available - line ${errStartLine} may be out of range for file with ${lines.length} lines)${colors.reset}`
+ );
+ }
+
+ return errorData;
+}
+
+async function processError(filename, event) {
+ const errorData = await processErrorInternal(filename, event);
+ if (!errorsByFile.has(filename)) {
+ errorsByFile.set(filename, []);
+ }
+ errorsByFile.get(filename).push(errorData);
+}
+
+function printAllErrors() {
+ console.log(`\n${colors.red}${colors.bright}${'═'.repeat(70)}${colors.reset}`);
+ console.log(`${colors.red}${colors.bright} REACT COMPILER ERRORS SUMMARY${colors.reset}`);
+ console.log(`${colors.red}${colors.bright}${'═'.repeat(70)}${colors.reset}`);
+
+ let totalErrors = 0;
+
+ const errorsByFileArray = [];
+ errorsByFile.forEach((errors, filename) => {
+ const relativeFilename = resolveFilename(filename);
+ totalErrors += errors.length;
+ errorsByFileArray.push({ errors, filename: relativeFilename });
+ });
+
+ errorsByFileArray.sort((a, b) => b.errors.length - a.errors.length);
+
+ if (fileFilter) {
+ console.log(
+ `${colors.red} File filter specified, filtering output for: ${fileFilter} ${colors.reset}`
+ );
+ }
+
+ let hiddenFiles = 0;
+
+ errorsByFileArray.forEach(({ errors, filename }) => {
+ if (fileFilter && !filename.includes(fileFilter)) {
+ hiddenFiles++;
+ return;
+ }
+ console.log(
+ `\n${colors.cyan}${colors.bright}📁 ${filename}${colors.reset} ${colors.dim}(${errors.length} error${errors.length > 1 ? 's' : ''})${colors.reset}`
+ );
+ console.log(`${colors.dim}${'─'.repeat(70)}${colors.reset}`);
+ errors.forEach((error, index) => {
+ if (errors.length > 1) {
+ console.log(`\n ${colors.magenta}Error ${index + 1}:${colors.reset}`);
+ }
+ error.lines.forEach(line => console.log(line));
+ });
+ });
+
+ const logTotals = () => {
+ console.log(`\n${colors.red}${colors.bright}${'═'.repeat(70)}${colors.reset}`);
+ console.log(
+ `${colors.red}${colors.bright} Total: ${totalErrors} error${totalErrors > 1 ? 's' : ''} in ${errorsByFile.size} file${errorsByFile.size > 1 ? 's' : ''}${colors.reset}`
+ );
+ console.log(`${colors.red}${colors.bright}${'═'.repeat(70)}${colors.reset}\n`);
+ };
+
+ console.log(
+ `${colors.red} ${errorsByFile.size} files could not be compiled with the react compiler: ${colors.reset}`
+ );
+ errorsByFileArray.forEach(({ filename, errors }) =>
+ console.log(` - (${errors.length}) ${colors.red}${colors.bright}${filename}${colors.reset}`)
+ );
+ logTotals();
+ console.log(
+ `${colors.red} Babel compilation complete with react compiler errors. Note: react compiler errors mean the file was not compiled with the react compiler, so is left in the same state it was in before compilation. ${colors.reset}\n`
+ );
+
+ if (hiddenFiles > 0) {
+ console.log(
+ `${colors.red} ${hiddenFiles} files were hidden from the compiler output because of the filter: ${fileFilter} ${colors.reset}`
+ );
+ }
+}
+
+function printSkippedFiles() {
+ const multiple = skippedFiles.size > 1;
+ console.log(
+ `${colors.yellow}${skippedFiles.size} file${multiple ? 's were' : ' was'} skipped by the react compiler based on the config: ${colors.reset}`
+ );
+ skippedFiles.forEach(filename => {
+ const resolvedFileName = resolveFilename(filename);
+ console.log(`${colors.yellow} - ${resolvedFileName} ${colors.reset}`);
+ });
+}
+
+async function handleExit() {
+ await Promise.all(pendingPromises);
+ printSizeSummary();
+ if (errorsByFile.size > 0) {
+ printAllErrors();
+ } else {
+ console.log(
+ `${colors.green}${totalFiles - skippedFiles.size} files were successfully parsed by the React Compiler ${colors.reset}`
+ );
+ }
+
+ if (skippedFiles.size) {
+ printSkippedFiles();
+ }
+
+ if (errorsByFile.size > 0) {
+ if (allowErrors) {
+ console.log(
+ `${colors.red}SESSION_RC_ALLOW_ERRORS was enabled, the compiler will report no errors and the build will continue! ${colors.reset}`
+ );
+ } else {
+ process.exit(1);
+ }
+ }
+}
+
+let cleanupRegistered = false;
+function registerCleanup() {
+ if (cleanupRegistered) {
+ return;
+ }
+ cleanupRegistered = true;
+
+ process.on('beforeExit', () => {
+ void handleExit();
+ });
+}
+
+// Always register cleanup to print size summary
+registerCleanup();
+
+const packageJson = require('./package.json');
+
+const electron = packageJson.devDependencies.electron;
+if (!electron) {
+ throw new Error('Unable to find electron version in package.json devDependencies');
+}
+console.log(`Babel is targeting Electron ${electron}`);
+
+const react = packageJson.dependencies.react;
+if (!react) {
+ throw new Error('Unable to find react version in package.json dependencies');
+}
+
+const reactTargetMajor = react.split('.')[0];
+if (!reactTargetMajor || Number.isNaN(Number.parseInt(reactTargetMajor))) {
+ throw new Error(`Unable to parse react version from package.json dependencies: "${react}"`);
+}
+
+console.log(`React compiler is targeting React ${reactTargetMajor}`);
+
+/** @type {import('@types/babel__core').TransformOptions} */
+module.exports = {
+ targets: {
+ electron,
+ },
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ modules: 'commonjs',
+ targets: {
+ electron,
+ },
+ bugfixes: true, // Use smaller transforms
+ exclude: [
+ // Exclude transforms Electron doesn't need
+ 'transform-typeof-symbol',
+ 'transform-regenerator',
+ 'transform-async-to-generator',
+ ],
+ },
+ ],
+ ],
+ plugins: [
+ // Progress tracking plugin
+ function progressPlugin() {
+ return {
+ post() {
+ updateProgress();
+ },
+ };
+ },
+ [
+ require.resolve('babel-plugin-react-compiler'),
+ {
+ target: reactTargetMajor,
+ sources: filename => {
+ // Skip specific file(s) from React Compiler
+ if (filename.includes(fileIgnoredByReactCompiler)) {
+ skippedFiles.add(filename);
+ registerCleanup();
+ return false;
+ }
+ return true;
+ },
+ logger: {
+ logEvent(filename, event) {
+ if (event.kind === 'CompileError') {
+ registerCleanup();
+ pendingPromises.push(processError(filename, event));
+ }
+ },
+ },
+ },
+ ],
+ ],
+ only: [babelInclude],
+ ignore: babelIgnore,
+};
diff --git a/eslint-local-rules.js b/eslint-local-rules.js
new file mode 100644
index 0000000000..8e72f9de15
--- /dev/null
+++ b/eslint-local-rules.js
@@ -0,0 +1,5 @@
+// Local ESLint rules index, reads rules from './eslint-rules'.
+module.exports = {
+ // eslint-disable-next-line global-require
+ 'styled-components-transient-props': require('./eslint-rules/styled-components-transient-props'),
+};
diff --git a/eslint-rules/styled-components-transient-props.js b/eslint-rules/styled-components-transient-props.js
new file mode 100644
index 0000000000..d9ddb59375
--- /dev/null
+++ b/eslint-rules/styled-components-transient-props.js
@@ -0,0 +1,359 @@
+/**
+ * ESLint rule to enforce $ prefix for transient props in styled-components
+ * Requires: @emotion/is-prop-valid
+ */
+
+const isPropValid = require('@emotion/is-prop-valid').default;
+
+// Pre-compiled regex
+const STYLED_COMPONENT_PATTERN = /^Styled[A-Z]/;
+
+// Character codes for fast comparison
+const CHAR_$ = 36;
+const CHAR_a = 97;
+const CHAR_d = 100;
+const CHAR_z = 122;
+const CHAR_HYPHEN = 45;
+
+const propValidCache = new Map();
+
+function isValidDOMAttributeCached(name) {
+ let result = propValidCache.get(name);
+ if (result === undefined) {
+ result = isPropValid(name);
+ propValidCache.set(name, result);
+ }
+ return result;
+}
+
+function isValidDOMAttribute(name) {
+ const firstChar = name.charCodeAt(0);
+
+ if (
+ firstChar === CHAR_d &&
+ name.length > 5 &&
+ name.charCodeAt(4) === CHAR_HYPHEN &&
+ name.startsWith('data-')
+ ) {
+ return true;
+ }
+ if (
+ firstChar === CHAR_a &&
+ name.length > 5 &&
+ name.charCodeAt(4) === CHAR_HYPHEN &&
+ name.startsWith('aria-')
+ ) {
+ return true;
+ }
+
+ return isValidDOMAttributeCached(name);
+}
+
+function isTransientProp(name) {
+ return name.charCodeAt(0) === CHAR_$;
+}
+
+function isNativeElement(name) {
+ const firstChar = name.charCodeAt(0);
+ return firstChar >= CHAR_a && firstChar <= CHAR_z;
+}
+
+function isStyledIdentifier(node) {
+ if (node?.type !== 'Identifier') {
+ return false;
+ }
+ const name = node.name;
+ return name === 'styled' || name === 'css';
+}
+
+function getRootIdentifier(node) {
+ if (!node) {
+ return null;
+ }
+
+ switch (node.type) {
+ case 'Identifier':
+ return node;
+
+ case 'MemberExpression':
+ return getRootIdentifier(node.object);
+
+ case 'CallExpression':
+ return getRootIdentifier(node.callee);
+
+ default:
+ return null;
+ }
+}
+
+function isStyledTag(tag) {
+ const root = getRootIdentifier(tag);
+ return isStyledIdentifier(root);
+}
+
+/**
+ * Determines if a styled-components tag targets a DOM element directly.
+ *
+ * Returns:
+ * - true: styled.div, styled.span, styled('div'), etc. (props go to DOM)
+ * - false: styled(Component), styled(OtherStyledComponent) (props go to component)
+ */
+function isStyledDOMElement(tag) {
+ if (!tag) {
+ return false;
+ }
+
+ // styled.div`` or styled.div.attrs({})``
+ if (tag.type === 'MemberExpression') {
+ let current = tag;
+
+ while (current?.type === 'MemberExpression') {
+ const obj = current.object;
+
+ if (obj?.type === 'Identifier' && (obj.name === 'styled' || obj.name === 'css')) {
+ const prop = current.property;
+ if (prop?.type === 'Identifier') {
+ return isNativeElement(prop.name);
+ }
+ }
+
+ if (obj?.type === 'MemberExpression') {
+ current = obj;
+ continue;
+ }
+
+ if (obj?.type === 'CallExpression') {
+ return isStyledDOMElement(obj);
+ }
+
+ break;
+ }
+
+ return false;
+ }
+
+ // styled(Component)`` or styled('div')``
+ if (tag.type === 'CallExpression') {
+ const callee = tag.callee;
+
+ if (callee?.type === 'Identifier' && callee.name === 'styled') {
+ const args = tag.arguments;
+ if (args && args.length > 0) {
+ const firstArg = args[0];
+
+ if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
+ return isNativeElement(firstArg.value);
+ }
+
+ if (firstArg.type === 'Identifier') {
+ return false;
+ }
+ }
+ }
+
+ if (callee?.type === 'MemberExpression') {
+ if (callee.property?.name === 'attrs') {
+ return isStyledDOMElement(callee.object);
+ }
+ return isStyledDOMElement(callee);
+ }
+ }
+
+ return false;
+}
+
+const createMessageData = prop => ({ prop });
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'Enforce $ prefix for transient props in styled-components to prevent passing non-DOM attributes to HTML elements',
+ category: 'Best Practices',
+ recommended: true,
+ url: 'https://styled-components.com/docs/api#transient-props',
+ },
+ fixable: null,
+ messages: {
+ missingTransientPrefix:
+ 'Prop "{{prop}}" should use transient prop syntax "${{prop}}" to avoid passing to DOM. See https://styled-components.com/docs/api#transient-props',
+ missingTransientPrefixInline:
+ 'Inline type prop "{{prop}}" should use transient prop syntax "${{prop}}".',
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ additionalValidProps: {
+ type: 'array',
+ items: { type: 'string' },
+ default: [],
+ },
+ ignoreComponentPatterns: {
+ type: 'array',
+ items: { type: 'string' },
+ default: [],
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ },
+
+ create(context) {
+ const options = context.options[0] || {};
+ const additionalValidProps =
+ options.additionalValidProps?.length > 0 ? new Set(options.additionalValidProps) : null;
+ const ignorePatterns = options.ignoreComponentPatterns;
+ const ignoreComponentPatterns =
+ ignorePatterns?.length > 0 ? ignorePatterns.map(p => new RegExp(p)) : null;
+
+ // Track styled components that wrap DOM elements vs other components
+ const styledDOMComponents = new Set();
+ const styledNonDOMComponents = new Set();
+
+ function shouldIgnoreComponent(name) {
+ if (!ignoreComponentPatterns) {
+ return false;
+ }
+ for (let i = 0; i < ignoreComponentPatterns.length; i++) {
+ if (ignoreComponentPatterns[i].test(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function isValidProp(name) {
+ if (isTransientProp(name)) {
+ return true;
+ }
+ if (additionalValidProps?.has(name)) {
+ return true;
+ }
+ return isValidDOMAttribute(name);
+ }
+
+ function checkTypeMembers(members) {
+ for (let i = 0; i < members.length; i++) {
+ const member = members[i];
+ const key = member.key;
+ if (member.type === 'TSPropertySignature' && key?.type === 'Identifier') {
+ const propName = key.name;
+ if (!isValidProp(propName)) {
+ context.report({
+ node: key,
+ messageId: 'missingTransientPrefixInline',
+ data: createMessageData(propName),
+ });
+ }
+ }
+ }
+ }
+
+ function isStyledDeclaration(init) {
+ return init?.type === 'TaggedTemplateExpression' && isStyledTag(init.tag);
+ }
+
+ return {
+ // Collect styled component names and categorize them
+ VariableDeclarator(node) {
+ const id = node.id;
+ if (id?.type === 'Identifier' && isStyledDeclaration(node.init)) {
+ const name = id.name;
+ const tag = node.init.tag;
+
+ if (isStyledDOMElement(tag)) {
+ styledDOMComponents.add(name);
+ } else {
+ styledNonDOMComponents.add(name);
+ }
+ }
+ },
+
+ // Check inline type parameters in styled-components (e.g., styled.div<{ customProp: boolean }>)
+ TaggedTemplateExpression(node) {
+ const tag = node.tag;
+ if (!isStyledTag(tag)) {
+ return;
+ }
+
+ // Only check if this is a styled DOM element
+ if (!isStyledDOMElement(tag)) {
+ return;
+ }
+
+ const typeParams = tag.typeParameters || tag.callee?.typeParameters;
+ const params = typeParams?.params;
+ if (!params) {
+ return;
+ }
+
+ for (let i = 0; i < params.length; i++) {
+ const param = params[i];
+ if (param.type === 'TSTypeLiteral' && param.members) {
+ checkTypeMembers(param.members);
+ }
+ }
+ },
+
+ // Check JSX attributes passed to styled components
+ JSXAttribute(node) {
+ const nodeName = node.name;
+ if (!nodeName || nodeName.type !== 'JSXIdentifier') {
+ return;
+ }
+
+ const propName = nodeName.name;
+ if (isValidProp(propName)) {
+ return;
+ }
+
+ const jsxElement = node.parent;
+ if (jsxElement?.type !== 'JSXOpeningElement') {
+ return;
+ }
+
+ const elementName = jsxElement.name;
+ if (!elementName) {
+ return;
+ }
+
+ let componentName;
+ const elementType = elementName.type;
+
+ if (elementType === 'JSXIdentifier') {
+ componentName = elementName.name;
+ } else if (elementType === 'JSXMemberExpression') {
+ componentName = elementName.property?.name;
+ }
+
+ if (!componentName || isNativeElement(componentName)) {
+ return;
+ }
+
+ if (shouldIgnoreComponent(componentName)) {
+ return;
+ }
+
+ // Skip if this is a styled component that wraps a non-DOM component
+ if (styledNonDOMComponents.has(componentName)) {
+ return;
+ }
+
+ // Check if it's a known styled DOM component or matches naming pattern
+ if (
+ styledDOMComponents.has(componentName) ||
+ STYLED_COMPONENT_PATTERN.test(componentName)
+ ) {
+ context.report({
+ node: nodeName,
+ messageId: 'missingTransientPrefix',
+ data: createMessageData(propName),
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/minify.js b/minify.js
new file mode 100644
index 0000000000..eae089ec34
--- /dev/null
+++ b/minify.js
@@ -0,0 +1,184 @@
+#!/usr/bin/env node
+/* eslint-disable no-console */
+
+/**
+ * Minify all JS files in ts/ directory using Terser
+ * Usage: node build/minify.js [--no-mangle] [--keep-console]
+ */
+
+const fs = require('fs');
+const os = require('os');
+const { globSync } = require('glob');
+const { minify } = require('terser');
+
+// ANSI colors
+const c = code => (process.env.NO_COLOR ? '' : `\x1b[${code}m`);
+const green = c(32);
+const yellow = c(33);
+const dim = c(2);
+const reset = c(0);
+
+// Parse CLI args
+const args = process.argv.slice(2);
+const noMangle = args.includes('--no-mangle');
+const keepConsole = args.includes('--keep-console');
+const skipMinify = process.env.SKIP_MINIFY === '1';
+
+if (skipMinify) {
+ console.log(`${yellow}Skipping minification (SKIP_MINIFY=1)${reset}`);
+ process.exit(0);
+}
+
+function formatBytes(bytes) {
+ if (bytes < 1024) {
+ return `${bytes} B`;
+ }
+ if (bytes < 1024 * 1024) {
+ return `${(bytes / 1024).toFixed(1)} KB`;
+ }
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
+}
+
+async function main() {
+ const startTime = Date.now();
+ const files = globSync('ts/**/*.js', { ignore: ['ts/**/*.min.js'] });
+
+ if (files.length === 0) {
+ console.log(`${yellow}No files found to minify${reset}`);
+ return;
+ }
+
+ console.log(`${dim}Minifying ${files.length} files...${reset}`);
+
+ let totalInputSize = 0;
+ let totalOutputSize = 0;
+ let errors = 0;
+
+ /** @type {import('terser').MinifyOptions} */
+ const terserOptions = {
+ // Electron 34 = Chromium 132, supports ES2024+
+ ecma: 2024,
+ module: true,
+ compress: {
+ ecma: 2024,
+ passes: 2, // more than 2 passes doesn't seem to improve anything
+ dead_code: true,
+ drop_debugger: true,
+ drop_console: !keepConsole ? ['log', 'debug', 'info'] : false,
+ // Aggressive optimizations safe for modern V8
+ arrows: true, // Convert functions to arrows where safe
+ arguments: true, // Replace arguments[i] with named params
+ booleans_as_integers: false, // Keep booleans as booleans (clearer)
+ booleans: true,
+ collapse_vars: true,
+ comparisons: true,
+ computed_props: true,
+ conditionals: true,
+ dead_code: true,
+ directives: true,
+ evaluate: true,
+ expression: false,
+ hoist_funs: true,
+ hoist_props: true, // Hoist properties from objects
+ hoist_vars: false, // Don't hoist vars (can break things)
+ if_return: true,
+ inline: 3, // Aggressive function inlining
+ join_vars: true,
+ keep_fnames: noMangle,
+ keep_classnames: noMangle,
+ loops: true,
+
+ negate_iife: true,
+ properties: true, // Rewrite property access
+ reduce_funcs: true,
+ reduce_vars: true,
+ sequences: true,
+ side_effects: true, // Drop side-effect-free code
+ switches: true,
+ toplevel: false, // Don't touch top-level (module exports)
+ typeofs: true,
+ unsafe_arrows: true, // Safe for Electron
+ unsafe_methods: true, // Safe for Electron
+ unsafe_proto: true, // Safe for Electron
+ unused: true,
+ },
+ mangle: noMangle
+ ? false
+ : {
+ toplevel: false, // Preserve module exports
+ eval: false, // Don't mangle when eval is present
+ properties: false, // Don't mangle properties (can break things)
+ },
+ format: {
+ ecma: 2024,
+ comments: false,
+ webkit: false, // No Safari workarounds needed
+ wrap_func_args: false,
+ },
+ };
+
+ // Process files in parallel with concurrency based on CPU cores
+ const CONCURRENCY = os.cpus().length;
+ const chunks = [];
+ for (let i = 0; i < files.length; i += CONCURRENCY) {
+ chunks.push(files.slice(i, i + CONCURRENCY));
+ }
+
+ let processed = 0;
+
+ for (const chunk of chunks) {
+ await Promise.all(
+ chunk.map(async file => {
+ try {
+ const inputCode = fs.readFileSync(file, 'utf-8');
+ const inputSize = Buffer.byteLength(inputCode, 'utf-8');
+ totalInputSize += inputSize;
+
+ const result = await minify(inputCode, terserOptions);
+
+ if (result.code) {
+ fs.writeFileSync(file, result.code, 'utf-8');
+ const outputSize = Buffer.byteLength(result.code, 'utf-8');
+ totalOutputSize += outputSize;
+ } else {
+ // No output means no change needed
+ totalOutputSize += inputSize;
+ }
+ } catch (err) {
+ console.error(`\n${yellow}Failed to minify ${file}: ${err.message}${reset}`);
+ errors++;
+ // Keep original file, count its size
+ try {
+ const size = fs.statSync(file).size;
+ totalOutputSize += size;
+ } catch {
+ // Ignore
+ }
+ } finally {
+ processed++;
+ process.stdout.write(`\r${dim}Minifying... ${processed}/${files.length}${reset}`);
+ }
+ })
+ );
+ }
+
+ // Clear progress line
+ process.stdout.write('\r\x1b[K');
+
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+ const saved = totalInputSize - totalOutputSize;
+ const percent = ((saved / totalInputSize) * 100).toFixed(1);
+
+ console.log(
+ `${green}Minified ${formatBytes(totalInputSize)} → ${formatBytes(totalOutputSize)} (-${percent}%) [${files.length} files in ${elapsed}s]${reset}`
+ );
+
+ if (errors > 0) {
+ console.log(`${yellow}${errors} file(s) failed to minify${reset}`);
+ }
+}
+
+main().catch(err => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/package.json b/package.json
index 31e4d431e6..745440b83d 100644
--- a/package.json
+++ b/package.json
@@ -15,23 +15,35 @@
"main": "ts/mains/main_node.js",
"resolutions": {
"lodash": "^4.17.21",
- "react": "18.3.1",
- "@types/react": "18.3.3",
- "@babel/runtime": "^7.26.10",
- "nanoid": "^3.3.8"
+ "react": "19.2.1",
+ "@types/react": "^19.0.0",
+ "nanoid": "^3.3.8",
+ "csstype": "^3.1.3",
+ "reselect": "5.1.1"
},
"scripts": {
"start-prod": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod$MULTI electron .",
"start-prod:pretty": "yarn start-prod | pino-pretty",
"start-dev": "cross-env NODE_ENV=development NODE_APP_INSTANCE=devprod$MULTI electron .",
- "build": "yarn print-deps && yarn clean && yarn protobuf && yarn update-git-info && yarn sass && yarn build:locales-soft && tsc && yarn build:workers",
"start-dev:pretty": "yarn start-dev | pino-pretty",
- "build:workers": "yarn worker:utils && yarn worker:libsession && yarn worker:image_processor",
+ "start-perf": "cross-env NODE_ENV=development NODE_APP_INSTANCE=devprod$MULTI electron --inspect-brk=9229 --enable-precise-memory-info --js-flags=\"--expose-gc --allow-natives-syntax\" .",
+ "start-dev:perf": "yarn start-perf | pino-pretty",
+ "build-no-compiler": "yarn build-pre && tsc && yarn build-post",
"build:locales": "python3 ./tools/localization/generateLocales.py --generate-types --print-problems --error-on-problems --error-old-dynamic-variables",
"build:locales-soft": "python3 ./tools/localization/generateLocales.py --generate-types --print-problems --print-problem-strings",
+ "build-pre": "yarn print-deps && yarn clean && yarn protobuf && yarn update-git-info && yarn sass && yarn build:locales-soft",
+ "build-ts": "tsc",
+ "build-ts-source-maps": "tsc --sourceMap --inlineSources",
+ "build-ts-source-maps:watch": "tsc --sourceMap --inlineSources --watch",
+ "build-compile": "babel ts --out-dir ts --extensions .js",
+ "build-minify": "node minify.js",
+ "build:workers": "yarn worker:utils && yarn worker:libsession && yarn worker:image_processor",
+ "build-post": "yarn build:workers",
+ "build": "yarn build-pre && yarn build-ts && yarn build-compile && yarn build-post",
+ "build:dev": "yarn build-pre && yarn build-ts-source-maps && cross-env SESSION_RC_ALLOW_ERRORS=1 yarn build-compile && yarn build-post",
"protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js --force-long",
"sass": "rimraf --glob 'stylesheets/dist/' && webpack --config=./sass.config.js",
- "clean": "rimraf --glob 'ts/**/*.js' 'ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map tsconfig.tsbuildinfo'",
+ "clean": "rimraf --glob 'ts/**/*.js' 'ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' 'tsconfig.tsbuildinfo'",
"lint": "yarn format --cache && eslint --cache .",
"format": "prettier --list-different --write \"*.{css,js,json,scss,ts,tsx}\" \"./**/*.{css,js,json,scss,ts,tsx}\"",
"start-prod-test": "cross-env NODE_ENV=production NODE_APP_INSTANCE=$MULTI electron .",
@@ -48,14 +60,13 @@
"dedup": "npx --yes yarn-deduplicate yarn.lock",
"prepare": "husky",
"print-deps": "node -v && python3 --version",
- "watch": "yarn clean && yarn update-git-info && concurrently -n PBJS,SASS,LOCALES,UTIL,LIBSESSION,IMAGE,TS -c yellow,green,red,blue,blue,blue,green \"yarn protobuf\" \"yarn sass --watch\" \"yarn build:locales-soft\" \"yarn worker:utils --watch\" \"yarn worker:libsession --watch\" \"yarn worker:image_processor --watch\" \"yarn tsc --watch\""
+ "watch": "yarn clean && yarn update-git-info && concurrently -n PBJS,SASS,LOCALES,UTIL,LIBSESSION,IMAGE,TS -c yellow,green,red,blue,blue,blue,green \"yarn protobuf\" \"yarn sass --watch\" \"yarn build:locales-soft\" \"yarn worker:utils --watch\" \"yarn worker:libsession --watch\" \"yarn worker:image_processor --watch\" \"yarn build-ts-source-maps:watch\""
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
- "@reduxjs/toolkit": "^1.9.7",
+ "@reduxjs/toolkit": "^2.11.1",
"@signalapp/better-sqlite3": "9.0.13",
- "@types/react-test-renderer": "^18.3.1",
"abort-controller": "3.0.0",
"auto-bind": "^4.0.0",
"blob-util": "2.0.2",
@@ -90,38 +101,43 @@
"pino": "^9.6.0",
"protobufjs": "^7.4.0",
"punycode": "^2.3.1",
- "qrcode.react": "^4.2.0",
- "react": "18.3.1",
- "react-contexify": "^6.0.0",
- "react-dom": "18.3.1",
- "react-draggable": "^4.4.6",
- "react-error-boundary": "^5.0.0",
- "react-h5-audio-player": "^3.9.3",
- "react-intersection-observer": "^9.16.0",
- "react-redux": "8.1.3",
- "react-test-renderer": "^18.3.1",
- "react-toastify": "^10.0.0",
- "react-use": "^17.6.0",
- "react-virtualized": "^9.22.6",
- "read-last-lines": "^1.8.0",
- "redux": "4.2.1",
- "redux-persist": "^6.0.0",
- "redux-promise-middleware": "^6.2.0",
- "rimraf": "6.0.1",
+ "qrcode.react": "4.2.0",
+ "react": "19.2.1",
+ "react-contexify": "6.0.0",
+ "react-dom": "19.2.1",
+ "react-draggable": "4.5.0",
+ "react-h5-audio-player": "3.10.1",
+ "react-intersection-observer": "10.0.0",
+ "react-redux": "9.2.0",
+ "react-toastify": "10.0.0",
+ "react-use": "17.6.0",
+ "react-virtualized": "9.22.6",
+ "read-last-lines": "1.8.0",
+ "redux": "5.0.1",
+ "redux-promise-middleware": "6.2.0",
+ "reselect": "5.1.1",
+ "rimraf": "6.1.2",
"sanitize.css": "^12.0.1",
"semver": "^7.7.1",
"sharp": "https://github.com/session-foundation/sharp/releases/download/v0.34.5/sharp-0.34.5.tgz",
- "styled-components": "^6.1.15",
+ "styled-components": "6.1.19",
"uuid": "11.1.0",
"webrtc-adapter": "^4.2.2",
- "zod": "^3.24.2"
+ "zod": "4.1.13"
},
"devDependencies": {
+ "@babel/cli": "7.28.3",
+ "@babel/core": "7.28.5",
+ "@babel/preset-env": "7.28.5",
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
- "@testing-library/jest-dom": "^6.4.6",
- "@testing-library/react": "^15.0.7",
- "@testing-library/user-event": "^14.6.1",
+ "@emotion/is-prop-valid": "^1.4.0",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/jest-dom": "6.9.1",
+ "@testing-library/react": "16.3.0",
+ "@testing-library/user-event": "14.6.1",
+ "@types/babel__core": "7.20.5",
+ "@types/babel__preset-env": "7.10.0",
"@types/buffer-crc32": "^0.2.4",
"@types/bytebuffer": "^5.0.49",
"@types/chai": "4.3.20",
@@ -129,38 +145,45 @@
"@types/config": "3.3.5",
"@types/dompurify": "^3.2.0",
"@types/electron-localshortcut": "^3.1.3",
+ "@types/eslint": "~9.6.1",
+ "@types/eslint-plugin-mocha": "~10.4.0",
+ "@types/events": "~3.0.3",
"@types/filesize": "5.0.2",
"@types/firstline": "^2.0.4",
"@types/fs-extra": "11.0.4",
"@types/jsdom": "^21.1.7",
+ "@types/jsdom-global": "~3.0.7",
"@types/libsodium-wrappers-sumo": "^0.7.8",
"@types/linkify-it": "^5.0.0",
"@types/lodash": "^4.17.16",
"@types/mocha": "5.2.7",
"@types/node-fetch": "^2.5.7",
- "@types/react": "18.3.18",
- "@types/react-dom": "18.3.5",
- "@types/react-redux": "^7.1.33",
- "@types/react-virtualized": "^9.21.30",
+ "@types/punycode": "~2.1.4",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@types/react-virtualized": "~9.22.3",
"@types/rimraf": "4.0.5",
+ "@types/sass-loader": "~8.0.10",
"@types/semver": "7.5.8",
"@types/sinon": "9.0.11",
- "@types/styled-components": "^5.1.34",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "8.26.1",
+ "babel-plugin-react-compiler": "^1.0.0",
"chai": "^4.5.0",
"chai-as-promised": "^7.1.2",
"chai-bytes": "^0.1.2",
"concurrently": "^9.2.0",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
+ "csstype": "^3.2.3",
"electron": "34.2.0",
- "electron-builder": "26.0.0-alpha.8",
+ "electron-builder": "26.0.12",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "10.1.1",
"eslint-import-resolver-typescript": "3.9.1",
- "eslint-plugin-import": "2.31.0",
+ "eslint-plugin-import": "2.32.0",
+ "eslint-plugin-local-rules": "^3.0.2",
"eslint-plugin-mocha": "^10.5.0",
"eslint-plugin-more": "^1.0.5",
"eslint-plugin-react": "7.37.4",
@@ -176,13 +199,17 @@
"pino-pretty": "13.0.0",
"prettier": "3.5.3",
"protobufjs-cli": "^1.1.3",
+ "react-error-boundary": "5.0.0",
"sass": "^1.85.1",
"sass-loader": "^16.0.5",
"sinon": "19.0.2",
+ "source-map": "^0.7.6",
+ "terser": "^5.44.1",
"ts-loader": "^9.5.2",
"typescript": "^5.8.2",
- "webpack": "^5.98.0",
- "webpack-cli": "^6.0.1"
+ "typescript-eslint": "8.49.0",
+ "webpack": "5.103.0",
+ "webpack-cli": "6.0.1"
},
"engines": {
"node": "20.18.2"
@@ -224,7 +251,9 @@
},
"win": {
"asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries",
- "publisherName": "Oxen Labs",
+ "signtoolOptions": {
+ "publisherName": "Oxen Labs"
+ },
"verifyUpdateCodeSignature": false,
"icon": "build/icon.ico",
"target": [
@@ -314,6 +343,8 @@
"!node_modules/lamejs/testdata/*",
"!node_modules/@reduxjs/**/*",
"node_modules/@reduxjs/**/*.js",
+ "node_modules/@reduxjs/**/*.cjs",
+ "node_modules/@reduxjs/**/*.mjs",
"node_modules/@reduxjs/toolkit/package.json",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}",
"!**/node_modules/.bin",
diff --git a/preload.js b/preload.js
index 6197a3bc08..0f5c021129 100644
--- a/preload.js
+++ b/preload.js
@@ -178,7 +178,7 @@ window.readyForUpdates = () => {
};
ipc.on('get-theme-setting', () => {
- const theme = window.Events.getThemeSetting();
+ const theme = window.getSettingValue('theme');
ipc.send('get-success-theme-setting', theme);
});
diff --git a/ts/components/EmptyMessageView.tsx b/ts/components/EmptyMessageView.tsx
index 726d3ea053..3a74788824 100644
--- a/ts/components/EmptyMessageView.tsx
+++ b/ts/components/EmptyMessageView.tsx
@@ -101,7 +101,7 @@ export const EmptyMessageView = () => {
$flexDirection="column"
$justifyContent="center"
$alignItems="center"
- margin="0 auto"
+ $margin="0 auto"
>
diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx
index 3abbeea507..40ad373d29 100644
--- a/ts/components/MemberListItem.tsx
+++ b/ts/components/MemberListItem.tsx
@@ -47,10 +47,10 @@ const AvatarItem = (props: { memberPubkey: string; isAdmin: boolean }) => {
};
const StyledSessionMemberItem = styled.button<{
- inMentions?: boolean;
+ $inMentions?: boolean;
selected?: boolean;
- disableBg?: boolean;
- withBorder?: boolean;
+ $disableBg?: boolean;
+ $withBorder?: boolean;
}>`
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
display: flex;
@@ -59,23 +59,23 @@ const StyledSessionMemberItem = styled.button<{
flex-shrink: 0;
font-family: var(--font-default);
padding: 0px var(--margins-sm);
- height: ${props => (props.inMentions ? '40px' : '50px')};
+ height: ${props => (props.$inMentions ? '40px' : '50px')};
width: 100%;
transition: var(--default-duration);
background-color: ${props =>
- !props.disableBg && props.selected
+ !props.$disableBg && props.selected
? 'var(--conversation-tab-background-selected-color) !important'
: null};
- ${props => props.inMentions && 'max-width: 300px;'}
+ ${props => props.$inMentions && 'max-width: 300px;'}
${props =>
- props.withBorder &&
+ props.$withBorder &&
`&:not(button:last-child) {
border-bottom: 1px solid var(--border-color);
}`}
${props =>
- !props.inMentions
+ !props.$inMentions
? css`
&:hover {
background-color: var(--conversation-tab-background-hover-color);
@@ -130,8 +130,8 @@ const ResendContainer = ({
@@ -141,8 +141,8 @@ const ResendContainer = ({
return null;
};
-const StyledGroupStatusText = styled.span<{ isFailure: boolean }>`
- color: ${props => (props.isFailure ? 'var(--danger-color)' : 'var(--text-secondary-color)')};
+const StyledGroupStatusText = styled.span<{ $isFailure: boolean }>`
+ color: ${props => (props.$isFailure ? 'var(--danger-color)' : 'var(--text-secondary-color)')};
font-size: var(--font-size-xs);
margin-top: var(--margins-xs);
min-width: 100px; // min-width so that the dialog does not resize when the status change to sending
@@ -199,7 +199,7 @@ const GroupStatusText = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: Gro
return (
{statusText}
@@ -332,10 +332,10 @@ export const MemberListItem = ({
isSelected ? onUnselect?.(pubkey) : onSelect?.(pubkey);
}}
data-testid={dataTestId}
- inMentions={inMentions}
+ $inMentions={inMentions}
selected={isSelected}
- disableBg={disableBg}
- withBorder={withBorder}
+ $disableBg={disableBg}
+ $withBorder={withBorder}
disabled={disabled}
>
@@ -343,9 +343,9 @@ export const MemberListItem = ({
(items: Array, key: string): { [key: string]: T } {
- // Yep, we can't index into item without knowing what it is. True. But we want to.
- const pairs = map(items, item => [(item as any)[key] as string, item]);
-
- return fromPairs(pairs);
-}
const StyledGutter = styled.div`
width: var(--left-panel-width) !important;
transition: none;
`;
-async function createSessionInboxStore() {
- // Here we set up a full redux store with initial state for our LeftPane Root
- const conversations = ConvoHub.use()
- .getConversations()
- .map(conversation => conversation.getConversationModelProps());
-
- const userGroups: UserGroupState['userGroups'] = {};
-
- (await UserGroupsWrapperActions.getAllGroups()).forEach(m => {
- userGroups[m.pubkeyHex] = makeUserGroupGetRedux(m);
- });
-
- const initialState: StateType = {
- conversations: {
- ...getEmptyConversationState(),
- conversationLookup: makeLookup(conversations, 'id'),
- },
- user: {
- ourDisplayNameInProfile: (await UserUtils.getOurProfile()).displayName || '',
- ourNumber: UserUtils.getOurPubKeyStrFromCache(),
- uploadingNewAvatarCurrentUser: false,
- uploadingNewAvatarCurrentUserFailed: false,
- },
- section: initialSectionState,
- defaultRooms: initialDefaultRoomState,
- search: initialSearchState,
- theme: initialThemeState,
- primaryColor: initialPrimaryColorState,
- onionPaths: initialOnionPathState,
- modals: initialModalState,
- userConfig: initialUserConfigState,
- stagedAttachments: getEmptyStagedAttachmentsState(),
- call: initialCallState,
- sogsRoomInfo: initialSogsRoomInfoState,
- settings: getSettingsInitialState(),
- groups: initialGroupState,
- userGroups: { userGroups },
- releasedFeatures: initialReleasedFeaturesState,
- debug: initialDebugState,
- networkModal: initialNetworkModalState,
- networkData: initialNetworkDataState,
- proBackendData: initialProBackendDataState,
- };
-
- return createStore(initialState);
-}
-
-// Do this only if we created a new account id, or if we already received the initial configuration message
-const triggerSyncIfNeeded = async () => {
- const us = UserUtils.getOurPubKeyStrFromCache();
- await ConvoHub.use().get(us).setDidApproveMe(true, true);
- await ConvoHub.use().get(us).setIsApproved(true, true);
- const didWeHandleAConfigurationMessageAlready =
- (await Data.getItemById(SettingsKey.hasSyncedInitialConfigurationItem))?.value || false;
- if (didWeHandleAConfigurationMessageAlready) {
- await forceSyncConfigurationNowIfNeeded();
- }
-};
-
-/**
- * We only need to regenerate the last message of groups/communities once,
- * and we can remove it in a few months safely
- */
-async function regenerateLastMessagesGroupsCommunities() {
- if (Storage.getBoolOr(SettingsKey.lastMessageGroupsRegenerated, false)) {
- return; // already regenerated once
- }
-
- ConvoHub.use()
- .getConversations()
- .filter(m => m.isClosedGroupV2() || m.isOpenGroupV2())
- .forEach(m => {
- m.updateLastMessage();
- });
- await Storage.put(SettingsKey.lastMessageGroupsRegenerated, true);
-}
-
-/**
- * This function is called only once: on app startup with a logged in user
- */
-export const doAppStartUp = async () => {
- window.openConversationWithMessages = openConversationWithMessages;
- window.inboxStore = await createSessionInboxStore();
- window.getState = window.inboxStore.getState;
-
- window.inboxStore?.dispatch(
- updateAllOnStorageReady({
- hasBlindedMsgRequestsEnabled: Storage.getBoolOr(
- SettingsKey.hasBlindedMsgRequestsEnabled,
- false
- ),
- settingsLinkPreview: Storage.getBoolOr(SettingsKey.settingsLinkPreview, false),
- hasFollowSystemThemeEnabled: Storage.getBoolOr(
- SettingsKey.hasFollowSystemThemeEnabled,
- false
- ),
- hasShiftSendEnabled: Storage.getBoolOr(SettingsKey.hasShiftSendEnabled, false),
- hideRecoveryPassword: Storage.getBoolOr(SettingsKey.hideRecoveryPassword, false),
- showOnboardingAccountJustCreated: Storage.getBoolOr(
- SettingsKey.showOnboardingAccountJustCreated,
- true
- ),
- })
- );
-
- // eslint-disable-next-line more/no-then
- void SnodePool.getFreshSwarmFor(UserUtils.getOurPubKeyStrFromCache()).then(async () => {
- window.log.debug('appStartup: got our fresh swarm, starting polling');
- // trigger any other actions that need to be done after the swarm is ready
- window.inboxStore?.dispatch(networkDataActions.fetchInfoFromSeshServer() as any);
- window.inboxStore?.dispatch(
- proBackendDataActions.refreshGetProDetailsFromProBackend({}) as any
- );
- if (window.inboxStore) {
- if (getDataFeatureFlag('useLocalDevNet') && isTestIntegration()) {
- /**
- * When running on the local dev net (during the regression tests), the network is too fast
- * and we show the DonateCTA before we got the time to grab the recovery phrase.
- * This sleepFor is there to give some time so we can grab the recovery phrase.
- * The regression test this is about is `Donate CTA, DB age >= 7 days`
- */
- await sleepFor(1000);
- }
- if (window.inboxStore?.dispatch) {
- void handleTriggeredProCTAs(window.inboxStore.dispatch);
- }
- }
- // we want to (try) to fetch from the revocation server before we process
- // incoming messages, as some might have a pro proof that has been revoked
- await UpdateProRevocationList.runOnStartup();
- void getSwarmPollingInstance().start();
- // trigger a sync message if needed for our other devices
- void triggerSyncIfNeeded();
- void loadDefaultRooms();
- }); // refresh our swarm on start to speed up the first message fetching event
-
- window.inboxStore?.dispatch(groupInfoActions.loadMetaDumpsFromDB() as any); // this loads the dumps from DB and fills the 03-groups slice with the corresponding details
-
- // this generates the key to encrypt attachments locally
- await Data.generateAttachmentKeyIfEmpty();
-
- void Data.cleanupOrphanedAttachments();
-
- // Note: do not make this a debounce call (as for some reason it doesn't work with promises)
- await AvatarReupload.addAvatarReuploadJob();
-
- /* Postpone a little bit of the polling of sogs messages to let the swarm messages come in first. */
- global.setTimeout(() => {
- void getOpenGroupManager().startPolling();
- }, 10000);
-
- global.setTimeout(() => {
- // init the messageQueue. In the constructor, we add all not send messages
- // this call does nothing except calling the constructor, which will continue sending message in the pipeline
- void MessageQueue.use().processAllPending();
- }, 3000);
-
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- global.setTimeout(async () => {
- // Schedule a confSyncJob in some time to let anything incoming from the network be applied and see if there is a push needed
- // Note: this also starts periodic jobs, so we don't need to keep doing it
- await UserSync.queueNewJobIfNeeded();
- }, 20000);
-
- global.setTimeout(() => {
- // Schedule all avatarMigrateJobs in some time to let anything incoming from the network be handled first
- void AvatarMigrate.scheduleAllAvatarMigrateJobs();
- }, 1 * DURATION.MINUTES);
-
- void regenerateLastMessagesGroupsCommunities();
-};
-
export const SessionInboxView = () => {
if (!window.inboxStore) {
- return null;
+ throw new Error('window.inboxStore is undefined in SessionInboxView');
}
- const persistor = persistStore(window.inboxStore);
- window.persistStore = persistor;
-
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
diff --git a/ts/components/SessionPasswordPrompt.tsx b/ts/components/SessionPasswordPrompt.tsx
index 070af1a605..ee34b19231 100644
--- a/ts/components/SessionPasswordPrompt.tsx
+++ b/ts/components/SessionPasswordPrompt.tsx
@@ -215,7 +215,7 @@ const SessionPasswordPromptInner = () => {
{loading ? (
<>
-
+
>
diff --git a/ts/components/SessionPopover.tsx b/ts/components/SessionPopover.tsx
index 7a5a95483c..6a27cbebd7 100644
--- a/ts/components/SessionPopover.tsx
+++ b/ts/components/SessionPopover.tsx
@@ -1,6 +1,6 @@
import styled from 'styled-components';
import { type ReactNode, useMemo, useRef } from 'react';
-import { useFeatureFlag } from '../state/ducks/types/releasedFeaturesReduxTypes';
+import { getFeatureFlagMemo } from '../state/ducks/types/releasedFeaturesReduxTypes';
const TIP_LENGTH = 18;
const VIEWPORT_MARGIN = 4;
@@ -25,14 +25,14 @@ const StyledCoordinateMarker = styled.div<{
`;
const StyledPopover = styled.div<{
- readyToShow: boolean;
+ $readyToShow: boolean;
x: number;
y: number;
- maxWidth?: string;
- pointerOffset?: number;
- tooltipStyles?: boolean;
- borderRadius?: number;
- verticalPosition?: VerticalPosition;
+ $maxWidth?: string;
+ $pointerOffset?: number;
+ $tooltipStyles?: boolean;
+ $borderRadius?: number;
+ $verticalPosition?: VerticalPosition;
}>`
background-color: var(--message-bubbles-received-background-color);
color: var(--message-bubbles-received-text-color);
@@ -40,7 +40,7 @@ const StyledPopover = styled.div<{
font-size: var(--font-size-sm);
overflow-wrap: break-word;
- border-radius: ${props => props.borderRadius}px;
+ border-radius: ${props => props.$borderRadius}px;
z-index: 5;
position: fixed;
@@ -51,14 +51,14 @@ const StyledPopover = styled.div<{
justify-content: space-between;
align-items: center;
height: max-content;
- width: ${props => (props.maxWidth ? '100%' : 'max-content')};
- max-width: ${props => props.maxWidth || undefined};
- ${props => !props.readyToShow && 'visibility: hidden;'}
+ width: ${props => (props.$maxWidth ? '100%' : 'max-content')};
+ max-width: ${props => props.$maxWidth || undefined};
+ ${props => !props.$readyToShow && 'visibility: hidden;'}
- ${props => props.tooltipStyles && 'padding: var(--margins-xs) var(--margins-md);'}
+ ${props => props.$tooltipStyles && 'padding: var(--margins-xs) var(--margins-md);'}
${props =>
- props.tooltipStyles &&
+ props.$tooltipStyles &&
`&:after {
content: '';
width: ${TIP_LENGTH}px;
@@ -68,10 +68,10 @@ const StyledPopover = styled.div<{
border-radius: 5px;
transform: scaleY(1.4) rotate(45deg);
// 5.2px allows the tooltip triangle to wrap around the border radius slightly on its limits
- clip-path: ${props.verticalPosition === 'bottom' ? 'polygon(0% 0%, 0px 60%, 60% 0px)' : 'polygon(100% 100%, 5.2px 100%, 100% 5.2px)'};
+ clip-path: ${props.$verticalPosition === 'bottom' ? 'polygon(0% 0%, 0px 60%, 60% 0px)' : 'polygon(100% 100%, 5.2px 100%, 100% 5.2px)'};
position: absolute;
- ${props.verticalPosition === 'bottom' ? 'top' : 'bottom'}: 0;
- left: ${props.pointerOffset ?? 0}px;
+ ${props.$verticalPosition === 'bottom' ? 'top' : 'bottom'}: 0;
+ left: ${props.$pointerOffset ?? 0}px;
}`}
`;
@@ -109,7 +109,7 @@ export const SessionPopoverContent = (props: PopoverProps) => {
verticalPosition = 'top',
} = props;
- const showPopoverAnchors = useFeatureFlag('showPopoverAnchors');
+ const showPopoverAnchors = getFeatureFlagMemo('showPopoverAnchors');
const ref = useRef(null);
@@ -202,16 +202,16 @@ export const SessionPopoverContent = (props: PopoverProps) => {
<>
{children}
diff --git a/ts/components/SessionSearchInput.tsx b/ts/components/SessionSearchInput.tsx
index 0fa4046d44..45a0429291 100644
--- a/ts/components/SessionSearchInput.tsx
+++ b/ts/components/SessionSearchInput.tsx
@@ -1,8 +1,9 @@
import { Dispatch } from '@reduxjs/toolkit';
import { debounce } from 'lodash';
import { useRef, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../state/dispatch';
import { searchActions, type DoSearchActionType, type SearchType } from '../state/ducks/search';
import { getConversationsCount } from '../state/selectors/conversations';
import { useLeftOverlayMode } from '../state/selectors/section';
@@ -64,7 +65,7 @@ function updateSearch(dispatch: Dispatch, searchOpts: DoSearchActionType) {
export const SessionSearchInput = ({ searchType }: { searchType: SearchType }) => {
const [currentSearchTerm, setCurrentSearchTerm] = useState('');
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isGroupCreationSearch = useLeftOverlayMode() === 'closed-group';
const convoCount = useSelector(getConversationsCount);
diff --git a/ts/components/SessionTooltip.tsx b/ts/components/SessionTooltip.tsx
index d9b21c49d2..a9e206d24d 100644
--- a/ts/components/SessionTooltip.tsx
+++ b/ts/components/SessionTooltip.tsx
@@ -51,9 +51,10 @@ export const SessionTooltip = ({
const [hovered, setHovered] = useState(false);
const [debouncedHover, setDebouncedHover] = useState(false);
- const ref = useRef(null);
+ const ref = useRef(null);
- const triggerPos = useTriggerPosition(ref);
+ // FIXME: remove this as cast
+ const triggerPos = useTriggerPosition(ref as RefObject);
useDebounce(
() => {
diff --git a/ts/components/SessionWrapperModal.tsx b/ts/components/SessionWrapperModal.tsx
index df98297379..50a0fbb8f2 100644
--- a/ts/components/SessionWrapperModal.tsx
+++ b/ts/components/SessionWrapperModal.tsx
@@ -35,21 +35,21 @@ type WithExtraRightButton = {
type WithShowExitIcon = { showExitIcon?: boolean };
const StyledModalHeader = styled(Flex)<{
- bigHeader?: boolean;
- scrolled: boolean;
- floatingHeader?: boolean;
+ $bigHeader?: boolean;
+ $scrolled: boolean;
+ $floatingHeader?: boolean;
}>`
- position: ${props => (props.floatingHeader ? 'absolute' : 'relative')};
+ position: ${props => (props.$floatingHeader ? 'absolute' : 'relative')};
font-family: var(--font-default);
- font-size: ${props => (props.bigHeader ? 'var(--font-size-h4)' : 'var(--font-size-xl)')};
+ font-size: ${props => (props.$bigHeader ? 'var(--font-size-h4)' : 'var(--font-size-xl)')};
font-weight: 500;
text-align: center;
line-height: 18px;
background-color: ${props =>
- props.floatingHeader && !props.scrolled
+ props.$floatingHeader && !props.$scrolled
? 'var(--transparent-color)'
: 'var(--modal-background-content-color)'};
- width: ${props => (props.floatingHeader ? '-webkit-fill-available' : 'auto')};
+ width: ${props => (props.$floatingHeader ? '-webkit-fill-available' : 'auto')};
transition-duration: var(--default-duration);
z-index: 3;
@@ -64,7 +64,7 @@ const StyledModalHeader = styled(Flex)<{
background: linear-gradient(to bottom, var(--modal-shadow-color), transparent);
pointer-events: none;
- opacity: ${props => (!props.scrolled ? '0' : '1')};
+ opacity: ${props => (!props.$scrolled ? '0' : '1')};
}
`;
@@ -129,14 +129,14 @@ const StyledModal = styled.div<{
}
`;
-const StyledModalBody = styled.div<{ shouldOverflow: boolean; removeScrollbarGutter?: boolean }>`
- ${props => (!props.removeScrollbarGutter ? 'scrollbar-gutter: stable;' : '')}
+const StyledModalBody = styled.div<{ $shouldOverflow: boolean; $removeScrollbarGutter?: boolean }>`
+ ${props => (!props.$removeScrollbarGutter ? 'scrollbar-gutter: stable;' : '')}
margin: 0;
font-family: var(--font-default);
line-height: var(--font-size-md);
font-size: var(--font-size-md);
height: 100%;
- overflow-y: ${props => (props.shouldOverflow ? 'auto' : 'hidden')};
+ overflow-y: ${props => (props.$shouldOverflow ? 'auto' : 'hidden')};
overflow-x: hidden;
.message {
@@ -144,12 +144,12 @@ const StyledModalBody = styled.div<{ shouldOverflow: boolean; removeScrollbarGut
}
`;
-const StyledTitle = styled.div<{ bigHeader?: boolean }>`
+const StyledTitle = styled.div<{ $bigHeader?: boolean }>`
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding: ${props =>
- props.bigHeader ? 'var(--margins-sm)' : 'var(--margins-xs) var(--margins-sm)'};
+ props.$bigHeader ? 'var(--margins-sm)' : 'var(--margins-xs) var(--margins-sm)'};
`;
export const ModalActionsContainer = ({
@@ -171,7 +171,7 @@ export const ModalActionsContainer = ({
$container={true}
width={'100%'}
$justifyContent="space-evenly"
- maxWidth={maxWidth || '300px'}
+ $maxWidth={maxWidth || '300px'}
$alignItems="center"
$flexGap="var(--margins-md)"
height="unset"
@@ -333,17 +333,17 @@ export const ModalBasicHeader = ({
$flexDirection={'row'}
$justifyContent={'space-between'}
$alignItems={'center'}
- padding={'var(--margins-lg) var(--margins-lg) var(--margins-sm) var(--margins-lg)'}
- bigHeader={bigHeader}
- floatingHeader={floatingHeader}
- scrolled={scrolled}
+ $padding={'var(--margins-lg) var(--margins-lg) var(--margins-sm) var(--margins-lg)'}
+ $bigHeader={bigHeader}
+ $floatingHeader={floatingHeader}
+ $scrolled={scrolled}
>
{extraLeftButton}
{/* Note: this is just here to keep the title centered, no matter the buttons we have */}
@@ -354,7 +354,7 @@ export const ModalBasicHeader = ({
/>
@@ -364,8 +364,8 @@ export const ModalBasicHeader = ({
$container={true}
$flexDirection={'row'}
$alignItems={'center'}
- padding={'0'}
- margin={'0'}
+ $padding={'0'}
+ $margin={'0'}
>
{/* Note: this is just here to keep the title centered, no matter the buttons we have */}
{bodyHeader}
{props.children}
diff --git a/ts/components/SplitViewContainer.tsx b/ts/components/SplitViewContainer.tsx
index b81806ad45..0a37b07845 100644
--- a/ts/components/SplitViewContainer.tsx
+++ b/ts/components/SplitViewContainer.tsx
@@ -2,8 +2,8 @@ import { ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
type SplitViewProps = {
- top: ReactElement;
- bottom: ReactElement;
+ top: ReactElement;
+ bottom: ReactElement;
disableTop: boolean;
};
diff --git a/ts/components/avatar/AvatarPlaceHolder/ClosedGroupAvatar.tsx b/ts/components/avatar/AvatarPlaceHolder/ClosedGroupAvatar.tsx
index be0447bafe..fb2c4f9317 100644
--- a/ts/components/avatar/AvatarPlaceHolder/ClosedGroupAvatar.tsx
+++ b/ts/components/avatar/AvatarPlaceHolder/ClosedGroupAvatar.tsx
@@ -47,9 +47,9 @@ function useGroupMembersAvatars(convoId: string | undefined) {
return sortAndSlice(sortedMembers, us);
}
-const StyledAvatarClosedContainer = styled.div<{ containerSize: number }>`
- width: ${({ containerSize }) => containerSize}px;
- height: ${({ containerSize }) => containerSize}px;
+const StyledAvatarClosedContainer = styled.div<{ $containerSize: number }>`
+ width: ${({ $containerSize }) => $containerSize}px;
+ height: ${({ $containerSize }) => $containerSize}px;
mask-image: url(images/avatar-svg-mask.svg);
${StyledAvatar}:last-child {
@@ -59,6 +59,9 @@ const StyledAvatarClosedContainer = styled.div<{ containerSize: number }>`
}
`;
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+const useAvatarBgColorInternal = useAvatarBgColor;
+
export const ClosedGroupAvatar = ({
convoId,
size: containerSize,
@@ -78,11 +81,11 @@ export const ClosedGroupAvatar = ({
throw new Error(`Invalid avatar size ${containerSize}`);
}
- const { bgColor } = useAvatarBgColor(secondMemberID || convoId);
+ const { bgColor } = useAvatarBgColorInternal(secondMemberID || convoId);
if (firstMemberId && secondMemberID) {
return (
-
+
@@ -92,7 +95,7 @@ export const ClosedGroupAvatar = ({
const isClickable = !!onAvatarClick;
return (
-
+
`
@@ -61,22 +61,22 @@ export const Flex = styled.div`
flex-wrap: ${props => (props.$flexWrap !== undefined ? props.$flexWrap : 'nowrap')};
gap: ${props => props.$flexGap || undefined};
align-items: ${props => props.$alignItems || 'stretch'};
- margin: ${props => props.margin || ''};
- padding: ${props => props.padding || ''};
+ margin: ${props => props.$margin || ''};
+ padding: ${props => props.$padding || ''};
width: ${props => props.width || 'auto'};
- max-width: ${props => props.maxWidth || 'none'};
- min-width: ${props => props.minWidth || 'none'};
+ max-width: ${props => props.$maxWidth || 'none'};
+ min-width: ${props => props.$minWidth || 'none'};
height: ${props => props.height || 'auto'};
- max-height: ${props => props.maxHeight || 'none'};
- min-height: ${props => props.minHeight || 'none'};
+ max-height: ${props => props.$maxHeight || 'none'};
+ min-height: ${props => props.$minHeight || 'none'};
overflow: ${props => (props.overflow !== undefined ? props.overflow : undefined)};
overflow-x: ${props => (props.overflowX !== undefined ? props.overflowX : undefined)};
overflow-y: ${props => (props.overflowY !== undefined ? props.overflowY : undefined)};
direction: ${props => props.dir || undefined};
- padding-inline: ${props => props.paddingInline || undefined};
- padding-block: ${props => props.paddingBlock || undefined};
- margin-inline: ${props => props.marginInline || undefined};
- margin-block: ${props => props.marginBlock || undefined};
+ padding-inline: ${props => props.$paddingInline || undefined};
+ padding-block: ${props => props.$paddingBlock || undefined};
+ margin-inline: ${props => props.$marginInline || undefined};
+ margin-block: ${props => props.$marginBlock || undefined};
`;
export const AnimatedFlex = styled(motion.div) & FlexProps>`
@@ -89,18 +89,18 @@ export const AnimatedFlex = styled(motion.div) & FlexProp
flex-wrap: ${props => (props.$flexWrap !== undefined ? props.$flexWrap : 'nowrap')};
gap: ${props => props.$flexGap || undefined};
align-items: ${props => props.$alignItems || 'stretch'};
- margin: ${props => props.margin || '0'};
- padding: ${props => props.padding || '0'};
+ margin: ${props => props.$margin || '0'};
+ padding: ${props => props.$padding || '0'};
width: ${props => props.width || 'auto'};
- max-width: ${props => props.maxWidth || 'none'};
- min-width: ${props => props.minWidth || 'none'};
+ max-width: ${props => props.$maxWidth || 'none'};
+ min-width: ${props => props.$minWidth || 'none'};
height: ${props => props.height || 'auto'};
- max-height: ${props => props.maxHeight || 'none'};
- min-height: ${props => props.minHeight || 'none'};
+ max-height: ${props => props.$maxHeight || 'none'};
+ min-height: ${props => props.$minHeight || 'none'};
overflow: ${props => (props.overflow !== undefined ? props.overflow : undefined)};
direction: ${props => props.dir || undefined};
- padding-inline: ${props => props.paddingInline || undefined};
- padding-block: ${props => props.paddingBlock || undefined};
- margin-inline: ${props => props.marginInline || undefined};
- margin-block: ${props => props.marginBlock || undefined};
+ padding-inline: ${props => props.$paddingInline || undefined};
+ padding-block: ${props => props.$paddingBlock || undefined};
+ margin-inline: ${props => props.$marginInline || undefined};
+ margin-block: ${props => props.$marginBlock || undefined};
`;
diff --git a/ts/components/basic/Heading.tsx b/ts/components/basic/Heading.tsx
index 210c740c58..7819bd414a 100644
--- a/ts/components/basic/Heading.tsx
+++ b/ts/components/basic/Heading.tsx
@@ -30,11 +30,11 @@ const StyledHeading = styled.h1`
const squashAsH6 = ['h6', 'h7', 'h8', 'h9'];
-const Heading = (props: StyledHeadingProps) => {
+const Heading = ({ dataTestId, ...props }: StyledHeadingProps) => {
const tag = squashAsH6.includes(props.size) ? 'h6' : props.size;
return (
-
+
{props.children}
);
diff --git a/ts/components/basic/MessageBodyHighlight.tsx b/ts/components/basic/MessageBodyHighlight.tsx
index b91461c2bd..8e9a6db122 100644
--- a/ts/components/basic/MessageBodyHighlight.tsx
+++ b/ts/components/basic/MessageBodyHighlight.tsx
@@ -1,6 +1,7 @@
import styled from 'styled-components';
-import { RenderTextCallbackType } from '../../types/Util';
-import { SizeClassType } from '../../util/emoji';
+import type { ReactNode } from 'react';
+import type { RenderTextCallbackType } from '../../types/Util';
+import type { SizeClassType } from '../../util/emoji';
import { AddNewLines } from '../conversation/AddNewLines';
import { Emojify } from '../conversation/Emojify';
import {
@@ -54,7 +55,7 @@ export const MessageBodyHighlight = (props: {
isPublic: boolean;
}) => {
const { text, isGroup, isPublic } = props;
- const results: Array = [];
+ const results: Array = [];
// this is matching what sqlite fts5 is giving us back
const FIND_BEGIN_END = /<>(.+?)<>/g;
diff --git a/ts/components/basic/PillContainer.tsx b/ts/components/basic/PillContainer.tsx
index 407143abfe..e7a1f63ff9 100644
--- a/ts/components/basic/PillContainer.tsx
+++ b/ts/components/basic/PillContainer.tsx
@@ -4,10 +4,10 @@ import { Flex } from './Flex';
type PillContainerProps = {
children: ReactNode;
- margin?: string;
- padding?: string;
+ $margin?: string;
+ $padding?: string;
onClick?: () => void;
- disableHover?: boolean;
+ $disableHover?: boolean;
};
export const StyledPillContainerHoverable = styled(Flex)`
@@ -33,21 +33,21 @@ const StyledPillInner = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
- padding: ${props => props.padding || ''};
- margin: ${props => props.margin || ''};
+ padding: ${props => props.$padding || ''};
+ margin: ${props => props.$margin || ''};
border-radius: 300px;
- cursor: ${props => (props.disableHover ? 'unset' : 'pointer')};
+ cursor: ${props => (props.$disableHover ? 'unset' : 'pointer')};
border: 1px solid var(--border-color);
transition: var(--default-duration);
&:hover {
background: ${props =>
- props.disableHover ? 'none' : 'var(--button-solid-background-hover-color)'};
+ props.$disableHover ? 'none' : 'var(--button-solid-background-hover-color)'};
}
`;
-export const PillContainerHoverable = (props: Omit) => {
+export const PillContainerHoverable = (props: Omit) => {
return (
-
+
{props.children}
);
diff --git a/ts/components/basic/SessionButton.tsx b/ts/components/basic/SessionButton.tsx
index 40399efc6b..f3c951c046 100644
--- a/ts/components/basic/SessionButton.tsx
+++ b/ts/components/basic/SessionButton.tsx
@@ -108,7 +108,7 @@ const StyledOutlineButton = styled(StyledBaseButton)`
}
`;
-const StyledSolidButton = styled(StyledBaseButton)<{ isDarkTheme: boolean }>`
+const StyledSolidButton = styled(StyledBaseButton)<{ $isDarkTheme: boolean }>`
outline: none;
background-color: ${props =>
props.color ? `var(--${props.color}-color)` : `var(--primary-color)`};
@@ -128,7 +128,7 @@ const StyledSolidButton = styled(StyledBaseButton)<{ isDarkTheme: boolean }>`
&:hover {
background-color: var(--transparent-color);
color: ${props =>
- props.isDarkTheme
+ props.$isDarkTheme
? props.color && props.color !== SessionButtonColor.Tertiary
? `var(--${props.color}-color)`
: 'var(--primary-color)'
@@ -150,7 +150,7 @@ export type SessionButtonProps = {
width?: string;
margin?: string;
dataTestId?: SessionDataTestId;
- reference?: RefObject;
+ reference?: RefObject;
className?: string;
style?: CSSProperties;
};
@@ -206,7 +206,7 @@ export const SessionButton = (props: SessionButtonProps) => {
className
)}
role="button"
- isDarkTheme={isDarkTheme}
+ $isDarkTheme={isDarkTheme}
onClick={onClickFn}
ref={reference}
data-testid={dataTestId}
diff --git a/ts/components/basic/SessionToast.tsx b/ts/components/basic/SessionToast.tsx
index c4bc15ec1e..56aa9f714c 100644
--- a/ts/components/basic/SessionToast.tsx
+++ b/ts/components/basic/SessionToast.tsx
@@ -71,7 +71,7 @@ export const SessionToast = (props: Props) => {
$alignItems="center"
onClick={props.onToastClick}
data-testid="session-toast"
- padding="var(--margins-sm) 0"
+ $padding="var(--margins-sm) 0"
>
diff --git a/ts/components/basic/SessionToggle.tsx b/ts/components/basic/SessionToggle.tsx
index 578d9ef0a3..e5b2e244e6 100644
--- a/ts/components/basic/SessionToggle.tsx
+++ b/ts/components/basic/SessionToggle.tsx
@@ -2,7 +2,7 @@ import { type SessionDataTestId } from 'react';
import type { CSSProperties } from 'styled-components';
import styled from 'styled-components';
-const StyledKnob = styled.div<{ active: boolean }>`
+const StyledKnob = styled.div<{ $active: boolean }>`
position: absolute;
top: 0;
left: 0;
@@ -11,7 +11,7 @@ const StyledKnob = styled.div<{ active: boolean }>`
border-radius: 28px;
background-color: var(--toggle-switch-ball-color);
box-shadow: ${props =>
- props.active
+ props.$active
? '-2px 1px 3px var(--toggle-switch-ball-shadow-color);'
: '2px 1px 3px var(--toggle-switch-ball-shadow-color);'};
@@ -19,11 +19,11 @@ const StyledKnob = styled.div<{ active: boolean }>`
transform var(--default-duration) ease,
background-color var(--default-duration) ease;
- transform: ${props => (props.active ? 'translateX(25px)' : '')};
+ transform: ${props => (props.$active ? 'translateX(25px)' : '')};
pointer-events: none; // let the container handle the event
`;
-const StyledSessionToggle = styled.div<{ active: boolean; disabled: boolean }>`
+const StyledSessionToggle = styled.div<{ $active: boolean; disabled: boolean }>`
width: 51px;
height: 25px;
border: 1px solid var(--toggle-switch-off-border-color);
@@ -34,11 +34,11 @@ const StyledSessionToggle = styled.div<{ active: boolean; disabled: boolean }>`
flex-shrink: 0;
background-color: ${props =>
- props.active
+ props.$active
? 'var(--toggle-switch-on-background-color)'
: 'var(--toggle-switch-off-background-color)'};
border-color: ${props =>
- props.active
+ props.$active
? 'var(--toggle-switch-on-border-color)'
: 'var(--toggle-switch-off-border-color)'};
`;
@@ -60,14 +60,14 @@ export const SessionToggle = ({
return (
-
+
);
};
diff --git a/ts/components/basic/Text.tsx b/ts/components/basic/Text.tsx
index 16f361cb71..5b244bbfc8 100644
--- a/ts/components/basic/Text.tsx
+++ b/ts/components/basic/Text.tsx
@@ -3,23 +3,23 @@ import styled, { CSSProperties } from 'styled-components';
type TextProps = {
text: string;
- subtle?: boolean;
- maxWidth?: string;
- padding?: string;
- textAlign?: 'center';
- ellipsisOverflow?: boolean;
+ $subtle?: boolean;
+ $maxWidth?: string;
+ $padding?: string;
+ $textAlign?: 'center';
+ $ellipsisOverflow?: boolean;
};
const StyledDefaultText = styled.div>`
transition: var(--default-duration);
- max-width: ${props => (props.maxWidth ? props.maxWidth : '')};
- padding: ${props => (props.padding ? props.padding : '')};
- text-align: ${props => (props.textAlign ? props.textAlign : '')};
+ max-width: ${props => (props.$maxWidth ? props.$maxWidth : '')};
+ padding: ${props => (props.$padding ? props.$padding : '')};
+ text-align: ${props => (props.$textAlign ? props.$textAlign : '')};
font-family: var(--font-default);
- color: ${props => (props.subtle ? 'var(--text-secondary-color)' : 'var(--text-primary-color)')};
- white-space: ${props => (props.ellipsisOverflow ? 'nowrap' : null)};
- overflow: ${props => (props.ellipsisOverflow ? 'hidden' : null)};
- text-overflow: ${props => (props.ellipsisOverflow ? 'ellipsis' : null)};
+ color: ${props => (props.$subtle ? 'var(--text-secondary-color)' : 'var(--text-primary-color)')};
+ white-space: ${props => (props.$ellipsisOverflow ? 'nowrap' : null)};
+ overflow: ${props => (props.$ellipsisOverflow ? 'hidden' : null)};
+ text-overflow: ${props => (props.$ellipsisOverflow ? 'ellipsis' : null)};
`;
export const Text = (props: TextProps) => {
diff --git a/ts/components/buttons/HelpDeskButton.tsx b/ts/components/buttons/HelpDeskButton.tsx
index 05c6f175d4..49da86d663 100644
--- a/ts/components/buttons/HelpDeskButton.tsx
+++ b/ts/components/buttons/HelpDeskButton.tsx
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { showLinkVisitWarningDialog } from '../dialog/OpenUrlModal';
import { SessionLucideIconButton } from '../icon/SessionIconButton';
import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
@@ -9,7 +9,7 @@ export const HelpDeskButton = ({
iconSize,
iconColor,
}: { style?: React.CSSProperties } & WithIconSize & WithIconColor) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
{
const leftOverlayMode = useLeftOverlayMode();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isToggled = Boolean(leftOverlayMode);
diff --git a/ts/components/buttons/panel/GenericPanelButtonWithAction.tsx b/ts/components/buttons/panel/GenericPanelButtonWithAction.tsx
index 3f5c002182..1a9ef8fd9b 100644
--- a/ts/components/buttons/panel/GenericPanelButtonWithAction.tsx
+++ b/ts/components/buttons/panel/GenericPanelButtonWithAction.tsx
@@ -30,7 +30,7 @@ export const GenericPanelButtonWithAction = (props: GenericPanelButtonProps) =>
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={onClick}
data-testid={rowDataTestId}
- isDarkTheme={isDarkTheme}
+ $isDarkTheme={isDarkTheme}
>
{textElement}
diff --git a/ts/components/buttons/panel/PanelButton.tsx b/ts/components/buttons/panel/PanelButton.tsx
index 6741f8a306..809cac7293 100644
--- a/ts/components/buttons/panel/PanelButton.tsx
+++ b/ts/components/buttons/panel/PanelButton.tsx
@@ -9,9 +9,9 @@ import type { TrArgs } from '../../../localization/localeTools';
import { useIsDarkTheme } from '../../../state/theme/selectors/theme';
// NOTE Used for descendant components
-export const StyledContent = styled.div<{ disabled?: boolean; rowReverse?: boolean }>`
+export const StyledContent = styled.div<{ disabled?: boolean; $rowReverse?: boolean }>`
display: flex;
- flex-direction: ${props => (props.rowReverse ? 'row-reverse' : 'row')};
+ flex-direction: ${props => (props.$rowReverse ? 'row-reverse' : 'row')};
justify-content: space-between;
align-items: center;
width: 100%;
@@ -64,15 +64,18 @@ export function PanelLabelWithDescription({
);
}
-const StyledRoundedPanelButtonGroup = styled.div<{ isSidePanel?: boolean; isDarkTheme?: boolean }>`
+const StyledRoundedPanelButtonGroup = styled.div<{
+ $isSidePanel?: boolean;
+ $isDarkTheme?: boolean;
+}>`
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
background: ${props =>
- props.isSidePanel
+ props.$isSidePanel
? 'var(--background-tertiary-color)'
- : props.isDarkTheme
+ : props.$isDarkTheme
? 'var(--background-primary-color)'
: 'var(--background-secondary-color)'};
border-radius: 16px;
@@ -103,8 +106,8 @@ export const PanelButtonGroup = (
return (
{children}
@@ -113,12 +116,12 @@ export const PanelButtonGroup = (
export const StyledPanelButton = styled.button<{
disabled: boolean;
- color?: string;
- isDarkTheme: boolean;
- defaultCursorWhenDisabled?: boolean;
+ $color?: string;
+ $isDarkTheme: boolean;
+ $defaultCursorWhenDisabled?: boolean;
}>`
cursor: ${props =>
- props.disabled ? (props.defaultCursorWhenDisabled ? 'default' : 'not-allowed') : 'pointer'};
+ props.disabled ? (props.$defaultCursorWhenDisabled ? 'default' : 'not-allowed') : 'pointer'};
display: flex;
align-items: center;
justify-content: space-between;
@@ -127,7 +130,7 @@ export const StyledPanelButton = styled.button<{
font-family: var(--font-default);
width: 100%;
transition: var(--default-duration);
- color: ${props => (props.disabled ? 'var(--disabled-color)' : props.color)};
+ color: ${props => (props.disabled ? 'var(--disabled-color)' : props.$color)};
padding-inline: var(--margins-lg);
padding-block: var(--margins-sm);
min-height: var(--panel-button-container-min-height);
@@ -137,7 +140,7 @@ export const StyledPanelButton = styled.button<{
if (props.disabled) {
return 'transparent'; // let the PanelButtonGroup background be visible
}
- if (props.isDarkTheme) {
+ if (props.$isDarkTheme) {
return 'color-mix(in srgb, var(--background-tertiary-color) 95%, white)';
}
return 'color-mix(in srgb, var(--background-tertiary-color) 95%, black)';
@@ -168,8 +171,8 @@ export const PanelButton = (props: PanelButtonProps) => {
onClick={onClick}
style={style}
data-testid={dataTestId}
- color={color}
- isDarkTheme={isDarkTheme}
+ $color={color}
+ $isDarkTheme={isDarkTheme}
>
{children}
@@ -210,8 +213,8 @@ const PanelButtonTextInternal = (props: PropsWithChildren) => {
width={'100%'}
$flexDirection={'column'}
$alignItems={'flex-start'}
- margin="0 var(--margins-lg) 0 0"
- minWidth="0"
+ $margin="0 var(--margins-lg) 0 0"
+ $minWidth="0"
>
{props.children}
diff --git a/ts/components/buttons/panel/PanelIconButton.tsx b/ts/components/buttons/panel/PanelIconButton.tsx
index d87c9c6642..9f355d54dc 100644
--- a/ts/components/buttons/panel/PanelIconButton.tsx
+++ b/ts/components/buttons/panel/PanelIconButton.tsx
@@ -23,10 +23,10 @@ type PanelIconButtonProps = Omit`
+const IconContainer = styled.div<{ $rowReverse?: boolean }>`
flex-shrink: 0;
margin: ${props =>
- props.rowReverse
+ props.$rowReverse
? '0 var(--margins-sm) 0 var(--margins-lg)'
: '0 var(--margins-lg) 0 var(--margins-sm)'};
padding: 0;
@@ -55,8 +55,8 @@ export const PanelIconButton = (
color={color}
style={{ minHeight: '55px' }}
>
-
- {props.iconElement}
+
+ {props.iconElement}
{subTextProps ? (
`
+const StyledCallActionButton = styled.div<{ $isFullScreen: boolean }>`
.session-icon-button {
background-color: var(--call-buttons-action-background-color);
border-radius: 50%;
transition-duration: var(--default-duration);
- ${props => props.isFullScreen && 'opacity: 0.4;'}
+ ${props => props.$isFullScreen && 'opacity: 0.4;'}
&:hover {
background-color: var(--call-buttons-action-background-hover-color);
- ${props => props.isFullScreen && 'opacity: 1;'}
+ ${props => props.$isFullScreen && 'opacity: 1;'}
}
}
`;
const ShowInFullScreenButton = ({ isFullScreen }: { isFullScreen: boolean }) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const showInFullScreen = () => {
if (isFullScreen) {
@@ -253,7 +254,7 @@ const ShowInFullScreenButton = ({ isFullScreen }: { isFullScreen: boolean }) =>
};
return (
-
+
{
};
return (
-
+
`
+const StyledCallWindowControls = styled.div<{ $isFullScreen: boolean; $makeVisible: boolean }>`
position: absolute;
bottom: 0px;
@@ -366,10 +367,10 @@ const StyledCallWindowControls = styled.div<{ isFullScreen: boolean; makeVisible
display: flex;
justify-content: center;
- opacity: ${props => (props.makeVisible ? 1 : 0)};
+ opacity: ${props => (props.$makeVisible ? 1 : 0)};
${props =>
- props.isFullScreen &&
+ props.$isFullScreen &&
`
opacity: 0.4;
&:hover {
@@ -417,7 +418,7 @@ export const CallWindowControls = ({
};
}, [isFullScreen]);
return (
-
+
{!remoteStreamVideoIsMuted && }
`
+const StyledLocalVideoElement = styled.video<{ $isVideoMuted: boolean }>`
height: 20%;
width: 20%;
bottom: 0;
right: 0;
position: absolute;
- opacity: ${props => (props.isVideoMuted ? 0 : 1)};
+ opacity: ${props => (props.$isVideoMuted ? 0 : 1)};
`;
export const CallInFullScreenContainer = () => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const ongoingCallWithFocused = useSelector(getHasOngoingCallWithFocusedConvo);
const hasOngoingCallFullScreen = useSelector(getCallIsInFullScreen);
@@ -91,12 +92,12 @@ export const CallInFullScreenContainer = () => {
`
+export const StyledVideoElement = styled.video<{ $isVideoMuted: boolean }>`
padding: 0 1rem;
height: 100%;
width: 100%;
- opacity: ${props => (props.isVideoMuted ? 0 : 1)};
+ opacity: ${props => (props.$isVideoMuted ? 0 : 1)};
`;
const StyledDraggableVideoElement = styled(StyledVideoElement)`
@@ -127,7 +127,7 @@ export const DraggableCallContainer = () => {
{remoteStreamVideoIsMuted && ongoingCallPubkey && (
diff --git a/ts/components/calling/InConversationCallContainer.tsx b/ts/components/calling/InConversationCallContainer.tsx
index e69087689d..6423f6bae9 100644
--- a/ts/components/calling/InConversationCallContainer.tsx
+++ b/ts/components/calling/InConversationCallContainer.tsx
@@ -114,9 +114,9 @@ const DurationLabel = () => {
return {dateString};
};
-const StyledSpinner = styled.div<{ fullWidth: boolean }>`
+const StyledSpinner = styled.div<{ $fullWidth: boolean }>`
height: 100%;
- width: ${props => (props.fullWidth ? '100%' : '50%')};
+ width: ${props => (props.$fullWidth ? '100%' : '50%')};
display: flex;
justify-content: center;
align-items: center;
@@ -126,8 +126,8 @@ const StyledSpinner = styled.div<{ fullWidth: boolean }>`
export const VideoLoadingSpinner = (props: { fullWidth: boolean }) => {
return (
-
-
+
+
);
};
@@ -194,7 +194,7 @@ export const InConversationCallContainer = () => {
{remoteStreamVideoIsMuted && (
@@ -207,7 +207,7 @@ export const InConversationCallContainer = () => {
ref={videoRefLocal}
autoPlay={true}
muted={true}
- isVideoMuted={localStreamVideoIsMuted}
+ $isVideoMuted={localStreamVideoIsMuted}
/>
{localStreamVideoIsMuted && (
diff --git a/ts/components/conversation/AddMentions.tsx b/ts/components/conversation/AddMentions.tsx
index de8e9e8dfe..5c516819c3 100644
--- a/ts/components/conversation/AddMentions.tsx
+++ b/ts/components/conversation/AddMentions.tsx
@@ -1,5 +1,5 @@
import styled from 'styled-components';
-import type { ReactNode } from 'react';
+import type { ReactNode, JSX } from 'react';
import { ConvoHub } from '../../session/conversations';
import { isUsAnySogsFromCache } from '../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { PubKey } from '../../session/types';
@@ -15,11 +15,11 @@ interface MentionProps {
children?: ReactNode;
}
-export const StyledMentionAnother = styled.span<{ inComposableElement?: boolean }>`
+export const StyledMentionAnother = styled.span<{ $inComposableElement?: boolean }>`
border-radius: var(--border-radius);
- padding: ${props => (props.inComposableElement ? '0' : '1px')};
- cursor: ${props => (props.inComposableElement ? 'default' : 'auto')};
- ${props => (props.inComposableElement ? 'user-select: all;' : '')}
+ padding: ${props => (props.$inComposableElement ? '0' : '1px')};
+ cursor: ${props => (props.$inComposableElement ? 'default' : 'auto')};
+ ${props => (props.$inComposableElement ? 'user-select: all;' : '')}
unicode-bidi: plaintext;
font-weight: bold;
`;
@@ -40,7 +40,7 @@ export const Mention = (props: MentionProps) => {
@{tr('you')}
{props.children}
@@ -57,7 +57,7 @@ export const Mention = (props: MentionProps) => {
@{resolvedName} {suffix}
{props.children}
diff --git a/ts/components/conversation/Emojify.tsx b/ts/components/conversation/Emojify.tsx
index f8be50e464..26eaf82e24 100644
--- a/ts/components/conversation/Emojify.tsx
+++ b/ts/components/conversation/Emojify.tsx
@@ -1,5 +1,4 @@
import { SizeClassType } from '../../util/emoji';
-
import { RenderTextCallbackType } from '../../types/Util';
type Props = {
@@ -14,7 +13,7 @@ type Props = {
const defaultRenderNonEmoji = (text: string | undefined) => <>{text || ''}>;
-export const Emojify = (props: Props): JSX.Element => {
+export const Emojify = (props: Props) => {
const { text, renderNonEmoji, sizeClass, isGroup, isPublic = false } = props;
if (!renderNonEmoji) {
return <>{defaultRenderNonEmoji(text)}>;
diff --git a/ts/components/conversation/H5AudioPlayer.tsx b/ts/components/conversation/H5AudioPlayer.tsx
index 00e3bf18e6..cb4af92980 100644
--- a/ts/components/conversation/H5AudioPlayer.tsx
+++ b/ts/components/conversation/H5AudioPlayer.tsx
@@ -1,9 +1,10 @@
// Audio Player
import { SessionDataTestId, useEffect, useRef, useState } from 'react';
import H5AudioPlayer, { RHAP_UI } from 'react-h5-audio-player';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { contextMenu } from 'react-contexify';
+import { getAppDispatch } from '../../state/dispatch';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
import { setNextMessageToPlayId } from '../../state/ducks/conversations';
import { useMessageDirection, useMessageSelected } from '../../state/selectors';
@@ -11,11 +12,11 @@ import {
getNextMessageToPlayId,
getSortedMessagesOfSelectedConversation,
} from '../../state/selectors/conversations';
-import { getAudioAutoplay } from '../../state/selectors/userConfig';
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
import { useIsMessageSelectionMode } from '../../state/selectors/selectedConversation';
-import { SessionLucideIconButton } from '../icon/SessionIconButton';
import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
+import { getAudioAutoplay } from '../../state/selectors/settings';
+import { LucideIcon } from '../icon/LucideIcon';
const StyledSpeedButton = styled.div`
padding: var(--margins-xs);
@@ -156,7 +157,7 @@ export const AudioPlayerWithEncryptedFile = (props: {
messageId: string;
}) => {
const { messageId, contentType, src } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [playbackSpeed, setPlaybackSpeed] = useState(1.0);
const { urlToLoad } = useEncryptedFileFetch(src, contentType, false);
const player = useRef(null);
@@ -279,14 +280,10 @@ export const AudioPlayerWithEncryptedFile = (props: {
customProgressBarSection={[RHAP_UI.CURRENT_LEFT_TIME, RHAP_UI.PROGRESS_BAR]}
customIcons={{
play: (
-
+
),
pause: (
- void;
};
-const StyledOverlay = styled.div>`
+const StyledOverlay = styled.div<{ $darkOverlay?: boolean }>`
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: ${props =>
- props.darkOverlay ? 'var(--message-link-preview-background-color)' : 'unset'};
+ props.$darkOverlay ? 'var(--message-link-preview-background-color)' : 'unset'};
`;
+
export const Image = (props: Props) => {
const {
alt,
@@ -178,8 +179,7 @@ export const Image = (props: Props) => {
{closeButton ? (
{
return (
-
+
);
};
@@ -117,7 +117,7 @@ const GroupMarkedAsExpired = () => {
};
export class SessionConversation extends Component {
- private readonly messageContainerRef: RefObject;
+ private readonly messageContainerRef: RefObject;
private dragCounter: number;
private publicMembersRefreshTimeout?: NodeJS.Timeout;
private readonly updateMemberList: () => any;
@@ -647,7 +647,7 @@ const renderImagePreview = async (contentType: string, file: File, fileName: str
};
function OutdatedLegacyGroupBanner() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const weAreAdmin = useSelectedWeAreAdmin();
const selectedConversationKey = useSelectedConversationKey();
diff --git a/ts/components/conversation/SessionEmojiPanel.tsx b/ts/components/conversation/SessionEmojiPanel.tsx
index cbb394c7d3..bba464909c 100644
--- a/ts/components/conversation/SessionEmojiPanel.tsx
+++ b/ts/components/conversation/SessionEmojiPanel.tsx
@@ -11,11 +11,11 @@ import { i18nEmojiData } from '../../util/emoji';
import { hexColorToRGB } from '../../util/hexColorToRGB';
export const StyledEmojiPanel = styled.div<{
- isModal: boolean;
- primaryColor: string;
- theme: ThemeStateType;
- panelBackgroundRGB: string;
- panelTextRGB: string;
+ $isModal: boolean;
+ $primaryColor: string;
+ $theme: ThemeStateType;
+ $panelBackgroundRGB: string;
+ $panelTextRGB: string;
}>`
padding: var(--margins-lg);
z-index: 5;
@@ -34,7 +34,7 @@ export const StyledEmojiPanel = styled.div<{
}
em-emoji-picker {
- ${props => props.panelBackgroundRGB && `background-color: rgb(${props.panelBackgroundRGB})`};
+ ${props => props.$panelBackgroundRGB && `background-color: rgb(${props.$panelBackgroundRGB})`};
border: 1px solid var(--border-color);
padding-bottom: var(--margins-sm);
--font-family: var(--font-default);
@@ -43,14 +43,14 @@ export const StyledEmojiPanel = styled.div<{
--border-radius: 8px;
--color-border: var(--border-color);
--color-border-over: var(--border-color);
- --background-rgb: ${props => props.panelBackgroundRGB};
- --rgb-background: ${props => props.panelBackgroundRGB};
- --rgb-color: ${props => props.panelTextRGB};
- --rgb-input: ${props => props.panelBackgroundRGB};
- --rgb-accent: ${props => props.primaryColor};
+ --background-rgb: ${props => props.$panelBackgroundRGB};
+ --rgb-background: ${props => props.$panelBackgroundRGB};
+ --rgb-color: ${props => props.$panelTextRGB};
+ --rgb-input: ${props => props.$panelBackgroundRGB};
+ --rgb-accent: ${props => props.$primaryColor};
${props =>
- !props.isModal &&
+ !props.$isModal &&
`
&:after {
content: '';
@@ -64,7 +64,7 @@ export const StyledEmojiPanel = styled.div<{
transform: scaleY(1.4) rotate(45deg);
border: 0.7px solid var(--border-color);
clip-path: polygon(100% 100%, 7.2px 100%, 100% 7.2px);
- ${props.panelBackgroundRGB && `background-color: rgb(${props.panelBackgroundRGB})`};
+ ${props.$panelBackgroundRGB && `background-color: rgb(${props.$panelBackgroundRGB})`};
[dir='rtl'] & {
left: 75px;
@@ -128,11 +128,11 @@ export const SessionEmojiPanel = forwardRef((props: Props
return (
diff --git a/ts/components/conversation/SessionMessagesListContainer.tsx b/ts/components/conversation/SessionMessagesListContainer.tsx
index e104092b3d..fa1fffe7c3 100644
--- a/ts/components/conversation/SessionMessagesListContainer.tsx
+++ b/ts/components/conversation/SessionMessagesListContainer.tsx
@@ -32,7 +32,7 @@ import { StyledMentionAnother } from './AddMentions';
import { MessagesContainerRefContext } from '../../contexts/MessagesContainerRefContext';
export type SessionMessageListProps = {
- messageContainerRef: RefObject;
+ messageContainerRef: RefObject;
};
export const messageContainerDomID = 'messages-container';
diff --git a/ts/components/conversation/SessionQuotedMessageComposition.tsx b/ts/components/conversation/SessionQuotedMessageComposition.tsx
index 92ae2a9bc2..5f0de1d1eb 100644
--- a/ts/components/conversation/SessionQuotedMessageComposition.tsx
+++ b/ts/components/conversation/SessionQuotedMessageComposition.tsx
@@ -1,6 +1,7 @@
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { quoteMessage } from '../../state/ducks/conversations';
import { getQuotedMessage } from '../../state/selectors/conversations';
@@ -24,8 +25,8 @@ const QuotedMessageComposition = styled(Flex)`
border-top: 1px solid var(--border-color);
`;
-const QuotedMessageCompositionReply = styled(Flex)<{ hasAttachments: boolean }>`
- ${props => !props.hasAttachments && 'border-left: 3px solid var(--primary-color);'}
+const QuotedMessageCompositionReply = styled(Flex)<{ $hasAttachments: boolean }>`
+ ${props => !props.$hasAttachments && 'border-left: 3px solid var(--primary-color);'}
`;
const Subtle = styled.div`
@@ -67,7 +68,7 @@ function checkHasAttachments(attachments: Array | undefined) {
}
export const SessionQuotedMessageComposition = () => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const quotedMessageProps = useSelector(getQuotedMessage);
const conversationId = useSelectedConversationKey();
@@ -121,13 +122,13 @@ export const SessionQuotedMessageComposition = () => {
$alignItems="center"
width={'100%'}
$flexGrow={1}
- padding={'var(--margins-md)'}
+ $padding={'var(--margins-md)'}
>
{hasAttachments && (
diff --git a/ts/components/conversation/SessionRecording.tsx b/ts/components/conversation/SessionRecording.tsx
index ac7e53f678..f45dfc1b10 100644
--- a/ts/components/conversation/SessionRecording.tsx
+++ b/ts/components/conversation/SessionRecording.tsx
@@ -36,7 +36,7 @@ function getTimestamp() {
}
interface StyledFlexWrapperProps {
- marginHorizontal: string;
+ $marginHorizontal: string;
}
const sharedButtonProps = {
@@ -82,7 +82,7 @@ const StyledFlexWrapper = styled.div`
gap: var(--margins-xs);
.session-button {
- margin: ${props => props.marginHorizontal};
+ margin: ${props => props.$marginHorizontal};
}
`;
@@ -181,7 +181,7 @@ export class SessionRecording extends Component {
return (
-
+
{isRecording && (
{isLoading ? (
-
+
) : null}
{isLoaded && isContentTypeImage ? (
diff --git a/ts/components/conversation/StagedAttachmentList.tsx b/ts/components/conversation/StagedAttachmentList.tsx
index 2a32457b88..b560216d69 100644
--- a/ts/components/conversation/StagedAttachmentList.tsx
+++ b/ts/components/conversation/StagedAttachmentList.tsx
@@ -1,5 +1,5 @@
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import {
removeAllStagedAttachmentsInConversation,
removeStagedAttachmentInConversation,
@@ -54,7 +54,7 @@ const StyledAttachmentsContainer = styled.div`
export const StagedAttachmentList = (props: Props) => {
const { attachments, onAddAttachment, onClickAttachment } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const conversationKey = useSelectedConversationKey();
const onRemoveAllStaged = () => {
diff --git a/ts/components/conversation/SubtleNotification.tsx b/ts/components/conversation/SubtleNotification.tsx
index 7a9c9e1e74..01aa195d69 100644
--- a/ts/components/conversation/SubtleNotification.tsx
+++ b/ts/components/conversation/SubtleNotification.tsx
@@ -36,7 +36,7 @@ import {
import { Localizer } from '../basic/Localizer';
import { tr, type TrArgs } from '../../localization/localeTools';
-const Container = styled.div<{ noExtraPadding: boolean }>`
+const Container = styled.div<{ $noExtraPadding: boolean }>`
display: flex;
flex-direction: row;
justify-content: center;
@@ -44,7 +44,7 @@ const Container = styled.div<{ noExtraPadding: boolean }>`
// add padding only if we have a child.
&:has(*:not(:empty)) {
- padding: ${props => (props.noExtraPadding ? '' : 'var(--margins-lg)')};
+ padding: ${props => (props.$noExtraPadding ? '' : 'var(--margins-lg)')};
}
`;
@@ -64,7 +64,7 @@ function TextNotification({
noExtraPadding: boolean;
}) {
return (
-
+
@@ -99,7 +99,7 @@ export const ConversationOutgoingRequestExplanation = () => {
{tr('messageRequestPendingDescription')}
@@ -214,7 +214,7 @@ const InvitedToGroupControlMessage = () => {
export const InvitedToGroup = () => {
return (
-
+
);
@@ -270,15 +270,35 @@ function useGetMessageDetailsForNoMessages(): TrArgs {
return { token: 'conversationsEmpty', conversation_name: name };
}
-export const NoMessageInConversation = () => {
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useConversationDetailsInternal() {
const selectedConversation = useSelectedConversationKey();
const hasMessages = useSelectedHasMessages();
const isGroupV2 = useSelectedIsGroupV2();
const isInvitePending = useLibGroupInvitePending(selectedConversation);
-
const isPrivate = useSelectedIsPrivate();
const isIncomingRequest = useIsIncomingRequest(selectedConversation);
+ return {
+ selectedConversation,
+ hasMessages,
+ isGroupV2,
+ isInvitePending,
+ isPrivate,
+ isIncomingRequest,
+ };
+}
+
+export const NoMessageInConversation = () => {
+ const {
+ selectedConversation,
+ hasMessages,
+ isGroupV2,
+ isInvitePending,
+ isPrivate,
+ isIncomingRequest,
+ } = useConversationDetailsInternal();
+
const msgDetails = useGetMessageDetailsForNoMessages();
// groupV2 use its own invite logic as part of
diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx
index ab0989b4f2..cbbc1ec4fc 100644
--- a/ts/components/conversation/TimerNotification.tsx
+++ b/ts/components/conversation/TimerNotification.tsx
@@ -1,7 +1,7 @@
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { PubkeyType } from 'libsession_util_nodejs';
import { isNil } from 'lodash';
+import { getAppDispatch } from '../../state/dispatch';
import {
useSelectedConversationDisappearingMode,
@@ -46,7 +46,7 @@ function useFollowSettingsButtonClick({ messageId }: WithMessageId) {
const disabled = useMessageExpirationUpdateDisabled(messageId);
const timespanText = useMessageExpirationUpdateTimespanText(messageId);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const onExit = () => dispatch(updateConfirmModal(null));
const doIt = () => {
@@ -119,9 +119,13 @@ function useOurExpirationMatches({ messageId }: WithMessageId) {
return false;
}
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useSelectedIsPrivateFriendInternal = useSelectedIsPrivateFriend;
+const useMessageAuthorIsUsInternal = useMessageAuthorIsUs;
+
const FollowSettingsButton = ({ messageId }: WithMessageId) => {
- const isPrivateAndFriend = useSelectedIsPrivateFriend();
- const authorIsUs = useMessageAuthorIsUs(messageId);
+ const isPrivateAndFriend = useSelectedIsPrivateFriendInternal();
+ const authorIsUs = useMessageAuthorIsUsInternal(messageId);
const click = useFollowSettingsButtonClick({
messageId,
@@ -184,9 +188,9 @@ export const TimerNotification = (props: WithMessageId) => {
$alignItems="center"
$justifyContent="center"
width="90%"
- maxWidth="700px"
- margin="5px auto 10px auto" // top margin is smaller that bottom one to make the stopwatch icon of expirable message closer to its content
- padding="5px 10px"
+ $maxWidth="700px"
+ $margin="5px auto 10px auto" // top margin is smaller that bottom one to make the stopwatch icon of expirable message closer to its content
+ $padding="5px 10px"
style={{ textAlign: 'center' }}
>
{renderOffIcon && (
@@ -199,7 +203,7 @@ export const TimerNotification = (props: WithMessageId) => {
>
)}
-
+
diff --git a/ts/components/conversation/TypingAnimation.tsx b/ts/components/conversation/TypingAnimation.tsx
index dcccb0b2d0..0530164d63 100644
--- a/ts/components/conversation/TypingAnimation.tsx
+++ b/ts/components/conversation/TypingAnimation.tsx
@@ -12,7 +12,7 @@ const StyledTypingContainer = styled.div`
padding-inline-end: 1px;
`;
-const StyledTypingDot = styled.div<{ index: number }>`
+const StyledTypingDot = styled.div<{ $index: number }>`
border-radius: 50%;
background-color: var(--text-secondary-color);
@@ -60,9 +60,9 @@ const StyledTypingDot = styled.div<{ index: number }>`
}
animation: ${props =>
- props.index === 0
+ props.$index === 0
? 'typing-animation-first'
- : props.index === 1
+ : props.$index === 1
? 'typing-animation-second'
: 'typing-animation-third'}
var(--duration-typing-animation) ease infinite;
@@ -75,12 +75,12 @@ const StyledSpacer = styled.div`
export const TypingAnimation = () => {
return (
-
+
-
+
-
+
);
};
diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx
index fb5d6436b2..9418093aa8 100644
--- a/ts/components/conversation/TypingBubble.tsx
+++ b/ts/components/conversation/TypingBubble.tsx
@@ -8,11 +8,11 @@ interface TypingBubbleProps {
isTyping: boolean;
}
-const TypingBubbleContainer = styled.div>`
- height: ${props => (props.isTyping ? 'auto' : '0px')};
+const TypingBubbleContainer = styled.div<{ $isTyping: boolean }>`
+ height: ${props => (props.$isTyping ? 'auto' : '0px')};
display: flow-root;
- padding-bottom: ${props => (props.isTyping ? '4px' : '0px')};
- padding-top: ${props => (props.isTyping ? '4px' : '0px')};
+ padding-bottom: ${props => (props.$isTyping ? '4px' : '0px')};
+ padding-top: ${props => (props.$isTyping ? '4px' : '0px')};
transition: var(--default-duration);
padding-inline-end: 16px;
padding-inline-start: 4px;
@@ -27,7 +27,7 @@ export const TypingBubble = (props: TypingBubbleProps) => {
}
return (
-
+
);
diff --git a/ts/components/conversation/composition/CharacterCount.tsx b/ts/components/conversation/composition/CharacterCount.tsx
index 8d22af01f9..c50678643d 100644
--- a/ts/components/conversation/composition/CharacterCount.tsx
+++ b/ts/components/conversation/composition/CharacterCount.tsx
@@ -1,6 +1,6 @@
import styled from 'styled-components';
import { Constants } from '../../../session';
-import { useFeatureFlag } from '../../../state/ducks/types/releasedFeaturesReduxTypes';
+import { getFeatureFlagMemo } from '../../../state/ducks/types/releasedFeaturesReduxTypes';
import { SessionTooltip } from '../../SessionTooltip';
import { StyledCTA } from '../../basic/StyledCTA';
import { formatNumber } from '../../../util/i18n/formatting/generics';
@@ -27,8 +27,8 @@ const StyledCharacterCountContainer = styled.div`
inset-inline-end: var(--margins-md);
`;
-const StyledRemainingNumber = styled.span<{ pastLimit: boolean }>`
- color: ${props => (props.pastLimit ? 'var(--danger-color)' : 'var(--text-primary-color)')};
+const StyledRemainingNumber = styled.span<{ $pastLimit: boolean }>`
+ color: ${props => (props.$pastLimit ? 'var(--danger-color)' : 'var(--text-primary-color)')};
`;
function ProCta() {
@@ -52,7 +52,7 @@ function ProCta() {
}
export function CharacterCount({ count }: CharacterCountProps) {
- const alwaysShowFlag = useFeatureFlag('alwaysShowRemainingChars');
+ const alwaysShowFlag = getFeatureFlagMemo('alwaysShowRemainingChars');
const currentUserHasPro = useCurrentUserHasPro();
@@ -74,7 +74,7 @@ export function CharacterCount({ count }: CharacterCountProps) {
})}
dataTestId="tooltip-character-count"
>
-
+
{formatNumber(remaining)}
diff --git a/ts/components/conversation/composition/CompositionBox.tsx b/ts/components/conversation/composition/CompositionBox.tsx
index feddc3679d..4eccfcee40 100644
--- a/ts/components/conversation/composition/CompositionBox.tsx
+++ b/ts/components/conversation/composition/CompositionBox.tsx
@@ -6,7 +6,7 @@ import styled from 'styled-components';
import { AbortController } from 'abort-controller';
import autoBind from 'auto-bind';
-import { Component, createRef, RefObject, KeyboardEvent } from 'react';
+import { Component, createRef, RefObject, KeyboardEvent, type JSX } from 'react';
import { FrequentlyUsed } from 'emoji-mart';
import * as MIME from '../../../types/MIME';
import { SessionEmojiPanel, StyledEmojiPanel } from '../SessionEmojiPanel';
@@ -217,10 +217,10 @@ const StyledCompositionBoxContainer = styled(Flex)`
`;
class CompositionBoxInner extends Component {
- private readonly inputRef: RefObject;
- private readonly fileInput: RefObject;
- private container: RefObject;
- private readonly emojiPanel: RefObject;
+ private readonly inputRef: RefObject;
+ private readonly fileInput: RefObject;
+ private container: RefObject;
+ private readonly emojiPanel: RefObject;
private readonly emojiPanelButton: any;
private linkPreviewAbortController?: AbortController;
diff --git a/ts/components/conversation/composition/CompositionInput.tsx b/ts/components/conversation/composition/CompositionInput.tsx
index 8ec89da178..5a2c48f2da 100644
--- a/ts/components/conversation/composition/CompositionInput.tsx
+++ b/ts/components/conversation/composition/CompositionInput.tsx
@@ -904,7 +904,7 @@ const UnstyledCompositionInput = forwardRef`
height: 100%;
width: 100%;
@@ -920,7 +920,7 @@ const CompositionInput = styled(UnstyledCompositionInput)<{
max-height: 50vh;
min-height: 28px;
padding-top: 0;
- padding-inline-end: ${props => props.scrollbarPadding ?? 0}px;
+ padding-inline-end: ${props => props.$scrollbarPadding ?? 0}px;
padding-inline-start: 0.375em;
padding-bottom: 0.25em;
overflow-y: auto;
diff --git a/ts/components/conversation/composition/CompositionTextArea.tsx b/ts/components/conversation/composition/CompositionTextArea.tsx
index 496406a931..85df5931d7 100644
--- a/ts/components/conversation/composition/CompositionTextArea.tsx
+++ b/ts/components/conversation/composition/CompositionTextArea.tsx
@@ -1,3 +1,7 @@
+// NOTE: [react-compiler] we are telling the compiler to not attempt to compile this
+// file in the babel config as it is highly complex and has a lot of very fine tuned
+// callbacks, its probably not worth trying to refactor at this stage
+
import {
type KeyboardEventHandler,
type KeyboardEvent,
@@ -49,8 +53,8 @@ type Props = {
initialDraft: string;
draft: string;
setDraft: (draft: string) => void;
- container: RefObject;
- inputRef: RefObject;
+ container: RefObject;
+ inputRef: RefObject;
typingEnabled: boolean;
onKeyDown: KeyboardEventHandler;
};
@@ -218,7 +222,7 @@ function useHandleSelect({
}: {
focusedItem: SearchableSuggestion;
handleMentionCleanup: () => void;
- inputRef: RefObject;
+ inputRef: RefObject;
mention: MentionDetails | null;
results: Array;
setDraft: Dispatch;
@@ -326,7 +330,7 @@ function useHandleKeyDown({
handleMentionCheck: (content: string, htmlIndex?: number | null) => void;
handleMentionCleanup: () => void;
handleSelect: (item?: SessionSuggestionDataItem) => void;
- inputRef: RefObject;
+ inputRef: RefObject;
mention: MentionDetails | null;
onKeyDown: KeyboardEventHandler;
results: Array;
@@ -432,7 +436,7 @@ function useHandleKeyUp({
}, [draft, lastBumpTypingMessageLength, selectedConversationKey, setLastBumpTypingMessageLength]);
}
-export const CompositionTextArea = (props: Props) => {
+export function CompositionTextArea(props: Props) {
const { draft, initialDraft, setDraft, inputRef, typingEnabled, onKeyDown } = props;
const [lastBumpTypingMessageLength, setLastBumpTypingMessageLength] = useState(0);
@@ -577,7 +581,7 @@ export const CompositionTextArea = (props: Props) => {
disabled={!typingEnabled}
autoFocus={true}
ref={inputRef}
- scrollbarPadding={140}
+ $scrollbarPadding={140}
autoCorrect="off"
aria-haspopup="listbox"
aria-autocomplete="list"
@@ -599,4 +603,4 @@ export const CompositionTextArea = (props: Props) => {
) : null}
>
);
-};
+}
diff --git a/ts/components/conversation/header/ConversationHeader.tsx b/ts/components/conversation/header/ConversationHeader.tsx
index 00505e8c46..96c9828e50 100644
--- a/ts/components/conversation/header/ConversationHeader.tsx
+++ b/ts/components/conversation/header/ConversationHeader.tsx
@@ -1,8 +1,7 @@
-import { useDispatch } from 'react-redux';
-
import type { PubkeyType } from 'libsession_util_nodejs';
import { useCallback } from 'react';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../state/dispatch';
import {
use05GroupMembers,
@@ -108,7 +107,7 @@ const RecreateGroupContainer = styled.div`
`;
function useShowRecreateModal() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return useCallback(
(name: string, members: Array) => {
@@ -134,15 +133,37 @@ function useShowRecreateModal() {
);
}
-function RecreateGroupButton() {
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useGroupDetailsInternal() {
const isLegacyGroup = useSelectedIsLegacyGroup();
const selectedConvo = useSelectedConversationKey();
-
const name = useConversationUsernameWithFallback(true, selectedConvo);
const members = use05GroupMembers(selectedConvo);
-
const weAreAdmin = useSelectedWeAreAdmin();
+ return {
+ isLegacyGroup,
+ name,
+ members,
+ weAreAdmin,
+ };
+}
+
+async function ensureMemberConvosExist(members: Array) {
+ for (let index = 0; index < members.length; index++) {
+ const m = members[index];
+ /* eslint-disable no-await-in-loop */
+ const memberConvo = await ConvoHub.use().getOrCreateAndWait(m, ConversationTypeEnum.PRIVATE);
+ if (!memberConvo.get('active_at')) {
+ memberConvo.setActiveAt(Constants.CONVERSATION.LAST_JOINED_FALLBACK_TIMESTAMP);
+ await memberConvo.commit();
+ }
+ }
+}
+
+function RecreateGroupButton() {
+ const { isLegacyGroup, name, members, weAreAdmin } = useGroupDetailsInternal();
+
const showRecreateGroupModal = useShowRecreateModal();
if (!isLegacyGroup || !weAreAdmin) {
@@ -156,19 +177,7 @@ function RecreateGroupButton() {
margin="var(--margins-sm)"
onClick={async () => {
try {
- for (let index = 0; index < members.length; index++) {
- const m = members[index];
- /* eslint-disable no-await-in-loop */
- const memberConvo = await ConvoHub.use().getOrCreateAndWait(
- m,
- ConversationTypeEnum.PRIVATE
- );
- if (!memberConvo.get('active_at')) {
- memberConvo.setActiveAt(Constants.CONVERSATION.LAST_JOINED_FALLBACK_TIMESTAMP);
- await memberConvo.commit();
- }
- /* eslint-enable no-await-in-loop */
- }
+ await ensureMemberConvosExist(members);
} catch (e) {
window.log.warn('recreate group: failed to recreate a member convo', e.message);
}
diff --git a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx
index 2deede2094..925c1e4975 100644
--- a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx
+++ b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx
@@ -1,6 +1,7 @@
import { useRef } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
+import { getAppDispatch } from '../../../state/dispatch';
import { deleteMessagesForX } from '../../../interactions/conversations/unsendingInteractions';
import { resetSelectedMessageIds } from '../../../state/ducks/conversations';
@@ -24,7 +25,7 @@ export const SelectionOverlay = () => {
const selectedMessageIds = useSelector(getSelectedMessageIds);
const selectedConversationKey = useSelectedConversationKey();
const isPublic = useSelectedIsPublic();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const ref = useRef(null);
function onCloseOverlay() {
diff --git a/ts/components/conversation/header/ConversationHeaderSubtitle.tsx b/ts/components/conversation/header/ConversationHeaderSubtitle.tsx
index 684fa6b2fc..4de20c6592 100644
--- a/ts/components/conversation/header/ConversationHeaderSubtitle.tsx
+++ b/ts/components/conversation/header/ConversationHeaderSubtitle.tsx
@@ -33,10 +33,10 @@ export const StyledSubtitleContainer = styled.div`
export const StyledSubtitleDotMenu = styled(Flex)``;
-const StyledSubtitleDot = styled.span<{ active: boolean }>`
+const StyledSubtitleDot = styled.span<{ $active: boolean }>`
border-radius: 50%;
background-color: ${props =>
- props.active ? 'var(--text-primary-color)' : 'var(--text-secondary-color)'};
+ props.$active ? 'var(--text-primary-color)' : 'var(--text-secondary-color)'};
height: 5px;
width: 5px;
@@ -61,7 +61,7 @@ export const SubtitleDotMenu = ({
return (
);
})}
diff --git a/ts/components/conversation/header/ConversationHeaderTitle.tsx b/ts/components/conversation/header/ConversationHeaderTitle.tsx
index bc3f64c144..ac842b22c6 100644
--- a/ts/components/conversation/header/ConversationHeaderTitle.tsx
+++ b/ts/components/conversation/header/ConversationHeaderTitle.tsx
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../state/dispatch';
import { useDisappearingMessageSettingText } from '../../../hooks/useParamSelector';
import { useIsRightPanelShowing } from '../../../hooks/useUI';
import { closeRightPanel } from '../../../state/ducks/conversations';
@@ -163,34 +163,71 @@ const StyledName = styled.span`
text-overflow: ellipsis;
`;
-export const ConversationHeaderTitle = ({ showSubtitle }: { showSubtitle: boolean }) => {
- const dispatch = useDispatch();
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useConversationHeaderTitleInternal() {
const convoId = useSelectedConversationKey();
const convoName = useSelectedNicknameOrProfileNameOrShortenedPubkey();
const isRightPanelOn = useIsRightPanelShowing();
const isMe = useSelectedIsNoteToSelf();
-
const isLegacyGroup = useSelectedIsLegacyGroup();
-
+ const isBlocked = useSelectedIsBlocked();
const expirationMode = useSelectedConversationDisappearingMode();
- const [subtitleIndex, setSubtitleIndex] = useState(0);
+ const showProBadgeForUser = useShowProBadgeFor(convoId);
+ const showPro = useProBadgeOnClickCb({
+ context: 'conversation-header-title',
+ args: { userHasPro: showProBadgeForUser, isMe },
+ });
+ const showConvoSettingsCb = useShowConversationSettingsFor(convoId);
+
+ return {
+ convoId,
+ convoName,
+ isRightPanelOn,
+ isMe,
+ isLegacyGroup,
+ expirationMode,
+ isBlocked,
+ showConvoSettingsCb,
+ showPro,
+ };
+}
+
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useSubtitleArrayInternal = useSubtitleArray;
+
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useSubtitleIndex(convoId?: string) {
+ const [subtitleIndex, setSubtitleIndex] = useState(0);
// reset the subtitle selected index when the convoId changes (so the page is always 0 by default)
useEffect(() => {
setSubtitleIndex(0);
}, [convoId]);
- const showConvoSettingsCb = useShowConversationSettingsFor(convoId);
+ return {
+ subtitleIndex,
+ setSubtitleIndex,
+ };
+}
- const subtitles = useSubtitleArray(convoId);
- const isBlocked = useSelectedIsBlocked();
+export const ConversationHeaderTitle = ({ showSubtitle }: { showSubtitle: boolean }) => {
+ const dispatch = getAppDispatch();
- const showProBadgeForUser = useShowProBadgeFor(convoId);
+ const {
+ convoId,
+ convoName,
+ isRightPanelOn,
+ isMe,
+ isLegacyGroup,
+ expirationMode,
+ isBlocked,
+ showConvoSettingsCb,
+ showPro,
+ } = useConversationHeaderTitleInternal();
- const showPro = useProBadgeOnClickCb({
- context: 'conversation-header-title',
- args: { userHasPro: showProBadgeForUser, isMe },
- });
+ const { subtitleIndex, setSubtitleIndex } = useSubtitleIndex(convoId);
+
+ const subtitles = useSubtitleArrayInternal(convoId);
const onHeaderClick = () => {
if (isLegacyGroup || !convoId) {
@@ -228,9 +265,8 @@ export const ConversationHeaderTitle = ({ showSubtitle }: { showSubtitle: boolea
const displayName = isMe ? tr('noteToSelf') : convoName;
- const clampedSubtitleIndex = useMemo(() => {
- return Math.max(0, Math.min(subtitles.length - 1, subtitleIndex));
- }, [subtitles, subtitleIndex]);
+ const clampedSubtitleIndex = Math.max(0, Math.min(subtitles.length - 1, subtitleIndex));
+
const visibleSubtitle = subtitles?.[clampedSubtitleIndex];
const handleTitleCycle = (direction: 1 | -1) => {
diff --git a/ts/components/conversation/media-gallery/AttachmentSection.tsx b/ts/components/conversation/media-gallery/AttachmentSection.tsx
index 9f869b6fa4..28cbaae675 100644
--- a/ts/components/conversation/media-gallery/AttachmentSection.tsx
+++ b/ts/components/conversation/media-gallery/AttachmentSection.tsx
@@ -1,4 +1,5 @@
import styled from 'styled-components';
+import type { JSX } from 'react';
import { missingCaseError } from '../../../util/missingCaseError';
import { MediaItemType } from '../../lightbox/LightboxGallery';
import { DocumentListItem } from './DocumentListItem';
diff --git a/ts/components/conversation/message/message-content/MessageAttachment.tsx b/ts/components/conversation/message/message-content/MessageAttachment.tsx
index c4cef25151..089df9c601 100644
--- a/ts/components/conversation/message/message-content/MessageAttachment.tsx
+++ b/ts/components/conversation/message/message-content/MessageAttachment.tsx
@@ -1,7 +1,8 @@
import { clone } from 'lodash';
import { useCallback } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../../state/dispatch';
import { Data } from '../../../../data/data';
import { MessageModelType, MessageRenderingProps } from '../../../../models/messageType';
import { PropsForAttachment, toggleSelectedMessageId } from '../../../../state/ducks/conversations';
@@ -46,20 +47,20 @@ type Props = {
};
const StyledImageGridContainer = styled.div<{
- messageDirection: MessageModelType;
+ $messageDirection: MessageModelType;
}>`
text-align: center;
position: relative;
overflow: hidden;
display: flex;
- justify-content: ${props => (props.messageDirection === 'incoming' ? 'flex-start' : 'flex-end')};
+ justify-content: ${props => (props.$messageDirection === 'incoming' ? 'flex-start' : 'flex-end')};
`;
export const MessageAttachment = (props: Props) => {
const { messageId, imageBroken, handleImageError, highlight = false } = props;
const isDetailView = useIsDetailMessageView();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const attachmentProps = useSelector((state: StateType) =>
getMessageAttachmentProps(state, messageId)
);
@@ -131,8 +132,8 @@ export const MessageAttachment = (props: Props) => {
}
return (
-
-
+
+
{
if (!firstAttachment.pending && !firstAttachment.error && isAudio(attachments)) {
return (
{
if (multiSelectMode) {
diff --git a/ts/components/conversation/message/message-content/MessageAuthorText.tsx b/ts/components/conversation/message/message-content/MessageAuthorText.tsx
index a8bd9255dc..5db233a941 100644
--- a/ts/components/conversation/message/message-content/MessageAuthorText.tsx
+++ b/ts/components/conversation/message/message-content/MessageAuthorText.tsx
@@ -17,10 +17,10 @@ type Props = {
messageId: string;
};
-const StyledAuthorContainer = styled(Flex)<{ hideAvatar: boolean }>`
+const StyledAuthorContainer = styled(Flex)<{ $hideAvatar: boolean }>`
color: var(--text-primary-color);
text-overflow: ellipsis;
- margin-inline-start: ${props => (props.hideAvatar ? 0 : 'var(--width-avatar-group-msg-list)')};
+ margin-inline-start: ${props => (props.$hideAvatar ? 0 : 'var(--width-avatar-group-msg-list)')};
`;
export const MessageAuthorText = ({ messageId }: Props) => {
@@ -43,7 +43,7 @@ export const MessageAuthorText = ({ messageId }: Props) => {
return (
{
void showUserDetailsCb({ messageId });
}}
diff --git a/ts/components/conversation/message/message-content/MessageBody.tsx b/ts/components/conversation/message/message-content/MessageBody.tsx
index f15cb36a96..46dc547c6d 100644
--- a/ts/components/conversation/message/message-content/MessageBody.tsx
+++ b/ts/components/conversation/message/message-content/MessageBody.tsx
@@ -1,9 +1,9 @@
import LinkifyIt from 'linkify-it';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import type { ReactNode } from 'react';
+import type { ReactNode, JSX } from 'react';
+import { getAppDispatch } from '../../../../state/dispatch';
import { RenderTextCallbackType } from '../../../../types/Util';
import { getEmojiSizeClass, SizeClassType } from '../../../../util/emoji';
import { LinkPreviews } from '../../../../util/linkPreviews';
@@ -86,7 +86,7 @@ const Linkify = (props: LinkifyProps): JSX.Element => {
const { text, isGroup, renderNonLink, isPublic } = props;
const results: Array = [];
let count = 1;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const matchData = linkify.match(text) || [];
let last = 0;
diff --git a/ts/components/conversation/message/message-content/MessageBubble.tsx b/ts/components/conversation/message/message-content/MessageBubble.tsx
index 44b2739783..6b7b3d2129 100644
--- a/ts/components/conversation/message/message-content/MessageBubble.tsx
+++ b/ts/components/conversation/message/message-content/MessageBubble.tsx
@@ -4,14 +4,14 @@ import { Constants } from '../../../../session';
import { tr } from '../../../../localization/localeTools';
import { useMessagesContainerRef } from '../../../../contexts/MessagesContainerRefContext';
-export const StyledMessageBubble = styled.div<{ expanded: boolean }>`
+export const StyledMessageBubble = styled.div<{ $expanded: boolean }>`
position: relative;
display: flex;
flex-direction: column;
width: 100%;
- ${({ expanded }) =>
- !expanded &&
+ ${({ $expanded }) =>
+ !$expanded &&
css`
pre,
.message-body {
@@ -134,7 +134,7 @@ export function MessageBubble({ children }: { children: ReactNode }) {
return (
<>
-
+
{children}
{showReadMore && !expanded ? (
diff --git a/ts/components/conversation/message/message-content/MessageContent.tsx b/ts/components/conversation/message/message-content/MessageContent.tsx
index 0726c3dd54..e8959bee1c 100644
--- a/ts/components/conversation/message/message-content/MessageContent.tsx
+++ b/ts/components/conversation/message/message-content/MessageContent.tsx
@@ -39,26 +39,26 @@ type Props = {
messageId: string;
};
-const StyledMessageContent = styled.div<{ msgDirection: MessageModelType }>`
+const StyledMessageContent = styled.div<{ $msgDirection: MessageModelType }>`
display: flex;
- align-self: ${props => (props.msgDirection === 'incoming' ? 'flex-start' : 'flex-end')};
+ align-self: ${props => (props.$msgDirection === 'incoming' ? 'flex-start' : 'flex-end')};
`;
const StyledMessageOpaqueContent = styled(MessageHighlighter)<{
- isIncoming: boolean;
- highlight: boolean;
- selected: boolean;
+ $isIncoming: boolean;
+ $highlight: boolean;
+ $selected: boolean;
}>`
background: ${props =>
- props.isIncoming
+ props.$isIncoming
? 'var(--message-bubbles-received-background-color)'
: 'var(--message-bubbles-sent-background-color)'};
- align-self: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')};
+ align-self: ${props => (props.$isIncoming ? 'flex-start' : 'flex-end')};
padding: var(--padding-message-content);
border-radius: var(--border-radius-message-box);
width: 100%;
- ${props => props.selected && `box-shadow: var(--drop-shadow);`}
+ ${props => props.$selected && `box-shadow: var(--drop-shadow);`}
`;
const StyledAvatarContainer = styled.div`
@@ -142,7 +142,7 @@ export const MessageContent = (props: Props) => {
className={clsx('module-message__container', `module-message__container--${direction}`)}
role="button"
title={toolTipTitle}
- msgDirection={direction}
+ $msgDirection={direction}
>
{hideAvatar ? null : (
@@ -168,9 +168,9 @@ export const MessageContent = (props: Props) => {
{hasContentBeforeAttachment && (
{!isDeleted && (
<>
diff --git a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx
index 585bf13958..84aa44a119 100644
--- a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx
+++ b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx
@@ -1,7 +1,8 @@
import { SessionDataTestId, MouseEvent, useCallback } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import { clsx } from 'clsx';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../../state/dispatch';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { replyToMessage } from '../../../../interactions/conversationInteractions';
import { MessageRenderingProps } from '../../../../models/messageType';
@@ -35,13 +36,13 @@ type Props = {
enableReactions: boolean;
};
-const StyledMessageContentContainer = styled.div<{ isIncoming: boolean; isDetailView: boolean }>`
+const StyledMessageContentContainer = styled.div<{ $isIncoming: boolean; $isDetailView: boolean }>`
display: flex;
flex-direction: column;
justify-content: flex-start;
- align-items: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')};
- padding-left: ${props => (props.isDetailView || props.isIncoming ? 0 : '25%')};
- padding-right: ${props => (props.isDetailView || !props.isIncoming ? 0 : '25%')};
+ align-items: ${props => (props.$isIncoming ? 'flex-start' : 'flex-end')};
+ padding-left: ${props => (props.$isDetailView || props.$isIncoming ? 0 : '25%')};
+ padding-right: ${props => (props.$isDetailView || !props.$isIncoming ? 0 : '25%')};
width: 100%;
max-width: '100%';
`;
@@ -60,7 +61,7 @@ export const MessageContentWithStatuses = (props: Props) => {
const contentProps = useSelector((state: StateType) =>
getMessageContentWithStatusesSelectorProps(state, props.messageId)
);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const hideAvatar = useHideAvatarInMsgList(props.messageId);
const multiSelectMode = useIsMessageSelectionMode();
@@ -124,7 +125,7 @@ export const MessageContentWithStatuses = (props: Props) => {
};
return (
-
+
{
$flexShrink={0}
// we need this to prevent short messages from being misaligned (incoming)
$alignItems={isIncoming ? 'flex-start' : 'flex-end'}
- maxWidth="100%"
+ $maxWidth="100%"
>
{!isDetailView && }
diff --git a/ts/components/conversation/message/message-content/MessageContextMenu.tsx b/ts/components/conversation/message/message-content/MessageContextMenu.tsx
index 315621110f..9729f23dd1 100644
--- a/ts/components/conversation/message/message-content/MessageContextMenu.tsx
+++ b/ts/components/conversation/message/message-content/MessageContextMenu.tsx
@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
-import { Dispatch, useCallback, useEffect, useRef, useState } from 'react';
+import { Dispatch, RefObject, useCallback, useEffect, useRef, useState } from 'react';
import { isNumber } from 'lodash';
import { ItemParams, Menu, useContextMenu } from 'react-contexify';
-import { useDispatch } from 'react-redux';
import useClickAway from 'react-use/lib/useClickAway';
import useMouse from 'react-use/lib/useMouse';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../../state/dispatch';
import { Data } from '../../../../data/data';
import { MessageInteraction } from '../../../../interactions';
@@ -158,7 +158,7 @@ export const showMessageInfoOverlay = async ({
export const MessageContextMenu = (props: Props) => {
const { messageId, contextMenuId, enableReactions } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const { hideAll } = useContextMenu();
const isLegacyGroup = useSelectedIsLegacyGroup();
@@ -183,8 +183,9 @@ export const MessageContextMenu = (props: Props) => {
const emojiPanelWidth = 354;
const emojiPanelHeight = 435;
- const contextMenuRef = useRef(null);
- const { docX, docY } = useMouse(contextMenuRef);
+ const contextMenuRef = useRef(null);
+ // FIXME: remove as cast
+ const { docX, docY } = useMouse(contextMenuRef as RefObject);
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
diff --git a/ts/components/conversation/message/message-content/MessageGenericAttachment.tsx b/ts/components/conversation/message/message-content/MessageGenericAttachment.tsx
index e65be6f7e6..3920ae29be 100644
--- a/ts/components/conversation/message/message-content/MessageGenericAttachment.tsx
+++ b/ts/components/conversation/message/message-content/MessageGenericAttachment.tsx
@@ -10,7 +10,6 @@ import { LucideIcon } from '../../../icon/LucideIcon';
import { LUCIDE_ICONS_UNICODE } from '../../../icon/lucide';
const StyledGenericAttachmentContainer = styled(MessageHighlighter)<{
- highlight: boolean;
selected: boolean;
}>`
${props => props.selected && 'box-shadow: var(--drop-shadow);'}
@@ -36,7 +35,7 @@ export function MessageGenericAttachment({
return (
) => void;
}) {
- const { className, children, highlight, role, onClick } = props;
+ const { className, children, $highlight, role, onClick } = props;
return (
{
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const direction = useMessageDirection(props.messageId);
const attachments = useMessageAttachments(props.messageId);
const previews = useMessageLinkPreview(props.messageId);
diff --git a/ts/components/conversation/message/message-content/MessageReactBar.tsx b/ts/components/conversation/message/message-content/MessageReactBar.tsx
index 7f884668b0..8e87028efd 100644
--- a/ts/components/conversation/message/message-content/MessageReactBar.tsx
+++ b/ts/components/conversation/message/message-content/MessageReactBar.tsx
@@ -59,9 +59,9 @@ const ReactButton = styled.span`
}
`;
-const StyledContainer = styled.div<{ expirationTimestamp: number | null }>`
+const StyledContainer = styled.div<{ $expirationTimestamp: number | null }>`
position: absolute;
- top: ${props => (props.expirationTimestamp ? '-106px' : '-56px')};
+ top: ${props => (props.$expirationTimestamp ? '-106px' : '-56px')};
display: flex;
flex-direction: column;
min-width: 0;
@@ -154,7 +154,7 @@ export const MessageReactBar = ({ action, additionalAction, messageId }: Props)
const expirationTimestamp = useIsRenderedExpiresInItem(messageId);
return (
-
+
{recentReactions &&
recentReactions.map(emoji => (
diff --git a/ts/components/conversation/message/message-content/MessageReactions.tsx b/ts/components/conversation/message/message-content/MessageReactions.tsx
index b326935d2d..6f73f26a42 100644
--- a/ts/components/conversation/message/message-content/MessageReactions.tsx
+++ b/ts/components/conversation/message/message-content/MessageReactions.tsx
@@ -15,13 +15,13 @@ import { LucideIcon } from '../../../icon/LucideIcon';
import { LUCIDE_ICONS_UNICODE } from '../../../icon/lucide';
export const StyledMessageReactionsContainer = styled(Flex)<{
- noAvatar: boolean;
+ $noAvatar: boolean;
}>`
- ${props => !props.noAvatar && 'margin-inline-start: var(--width-avatar-group-msg-list);'}
+ ${props => !props.$noAvatar && 'margin-inline-start: var(--width-avatar-group-msg-list);'}
`;
-export const StyledMessageReactions = styled(Flex)<{ fullWidth: boolean }>`
- ${props => (props.fullWidth ? '' : 'max-width: 640px;')}
+export const StyledMessageReactions = styled(Flex)<{ $fullWidth: boolean }>`
+ ${props => (props.$fullWidth ? '' : 'max-width: 640px;')}
`;
const StyledReactionOverflow = styled.button`
@@ -59,7 +59,7 @@ const Reactions = (props: ReactionsProps) => {
$container={true}
$flexWrap={inModal ? 'nowrap' : 'wrap'}
$alignItems={'center'}
- fullWidth={inModal}
+ $fullWidth={inModal}
>
{reactions.map(([emoji]) => (
@@ -79,7 +79,7 @@ const CompressedReactions = (props: ExpandReactionsProps) => {
$container={true}
$flexWrap={inModal ? 'nowrap' : 'wrap'}
$alignItems={'center'}
- fullWidth={true}
+ $fullWidth={true}
>
{reactions.slice(0, 4).map(([emoji]) => (
@@ -109,7 +109,7 @@ const CompressedReactions = (props: ExpandReactionsProps) => {
const ExpandedReactions = (props: ExpandReactionsProps) => {
const { handleExpand } = props;
return (
-
+
{
$flexDirection={'column'}
$justifyContent={'center'}
$alignItems={inModal ? 'flex-start' : 'center'}
- noAvatar={noAvatar}
+ $noAvatar={noAvatar}
>
{sortedReacts &&
sortedReacts?.length !== 0 &&
diff --git a/ts/components/conversation/message/message-content/MessageStatus.tsx b/ts/components/conversation/message/message-content/MessageStatus.tsx
index 76d4558a60..54cfe48b07 100644
--- a/ts/components/conversation/message/message-content/MessageStatus.tsx
+++ b/ts/components/conversation/message/message-content/MessageStatus.tsx
@@ -66,24 +66,24 @@ export const MessageStatus = ({ messageId, dataTestId }: Props) => {
};
const MessageStatusContainer = styled.div<{
- isIncoming: boolean;
- isGroup: boolean;
- clickable: boolean;
+ $isIncoming: boolean;
+ $isGroup: boolean;
+ $clickable: boolean;
}>`
display: inline-block;
- align-self: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')};
+ align-self: ${props => (props.$isIncoming ? 'flex-start' : 'flex-end')};
flex-direction: ${props =>
- props.isIncoming
+ props.$isIncoming
? 'row-reverse'
: 'row'}; // we want {icon}{text} for incoming read messages, but {text}{icon} for outgoing messages
margin-bottom: 2px;
margin-inline-start: 5px;
- cursor: ${props => (props.clickable ? 'pointer' : 'inherit')};
+ cursor: ${props => (props.$clickable ? 'pointer' : 'inherit')};
display: flex;
align-items: center;
margin-inline-start: ${props =>
- props.isGroup || !props.isIncoming ? 'var(--width-avatar-group-msg-list)' : 0};
+ props.$isGroup || !props.$isIncoming ? 'var(--width-avatar-group-msg-list)' : 0};
`;
const StyledStatusText = styled.div<{ $textColor: string }>`
@@ -142,9 +142,9 @@ const MessageStatusSending = ({ dataTestId }: Omit) => {
@@ -169,10 +169,13 @@ function IconForExpiringMessageId({
);
}
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useSelectedIsGroupOrCommunityInternal = useSelectedIsGroupOrCommunity;
+
const MessageStatusSent = ({ dataTestId, messageId }: Omit) => {
const isExpiring = useIsExpiring(messageId);
const isMostRecentOutgoingMessage = useIsMostRecentOutgoingMessage(messageId);
- const isGroup = useSelectedIsGroupOrCommunity();
+ const isGroup = useSelectedIsGroupOrCommunityInternal();
// we hide the "sent" message status for a non-expiring messages unless it's the most recent outgoing message
if (!isExpiring && !isMostRecentOutgoingMessage) {
@@ -182,9 +185,9 @@ const MessageStatusSent = ({ dataTestId, messageId }: Omit
@@ -198,7 +201,7 @@ const MessageStatusRead = ({
isIncoming,
}: Omit & { isIncoming: boolean }) => {
const isExpiring = useIsExpiring(messageId);
- const isGroup = useSelectedIsGroupOrCommunity();
+ const isGroup = useSelectedIsGroupOrCommunityInternal();
const isMostRecentOutgoingMessage = useIsMostRecentOutgoingMessage(messageId);
@@ -211,9 +214,9 @@ const MessageStatusRead = ({
@@ -233,9 +236,9 @@ const MessageStatusError = ({ dataTestId }: Omit) => {
onClick={() => {
void saveLogToDesktop();
}}
- isIncoming={false}
- clickable={true}
- isGroup={isGroup}
+ $isIncoming={false}
+ $clickable={true}
+ $isGroup={isGroup}
>
) => void) | undefined;
}>`
position: relative;
@@ -24,9 +24,9 @@ const StyledQuote = styled.div<{
flex-direction: row;
align-items: stretch;
margin: var(--margins-xs);
- ${props => !props.hasAttachment && 'border-left: 4px solid;'}
+ ${props => !props.$hasAttachment && 'border-left: 4px solid;'}
border-color: ${props =>
- props.isIncoming
+ props.$isIncoming
? 'var(--message-bubbles-received-text-color)'
: 'var(--message-bubbles-sent-text-color)'};
cursor: ${props => (props.onClick ? 'pointer' : 'auto')};
@@ -81,8 +81,8 @@ export const Quote = (props: QuoteProps) => {
return (
{
if (onClick && !isSelectionMode) {
onClick(e);
diff --git a/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx b/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx
index 333a17a022..7a3d7e38af 100644
--- a/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx
+++ b/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx
@@ -3,9 +3,9 @@ import { useSelectedConversationKey } from '../../../../../state/selectors/selec
import { ContactName } from '../../../ContactName/ContactName';
import { QuoteProps } from './Quote';
-const StyledQuoteAuthor = styled.div<{ isIncoming: boolean }>`
+const StyledQuoteAuthor = styled.div<{ $isIncoming: boolean }>`
color: ${props =>
- props.isIncoming
+ props.$isIncoming
? 'var(--message-bubbles-received-text-color)'
: 'var(--message-bubbles-sent-text-color)'};
font-size: var(--font-size-md);
@@ -33,7 +33,7 @@ export const QuoteAuthor = (props: QuoteAuthorProps) => {
}
return (
-
+
`
+const StyledQuoteText = styled.div<{ $isIncoming: boolean }>`
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
@@ -27,12 +27,12 @@ const StyledQuoteText = styled.div<{ isIncoming: boolean }>`
white-space: pre-wrap;
color: ${props =>
- props.isIncoming
+ props.$isIncoming
? 'var(--message-bubbles-received-text-color)'
: 'var(--message-bubbles-sent-text-color)'};
a {
color: ${props =>
- props.isIncoming
+ props.$isIncoming
? 'var(--color-received-message-text)'
: 'var(--message-bubbles-sent-text-color)'};
}
@@ -78,7 +78,7 @@ export const QuoteText = (
}
return (
-
+
{
const now = Date.now();
@@ -65,11 +65,11 @@ function useIsExpired(
}
const StyledReadableMessage = styled(ReadableMessage)<{
- isIncoming: boolean;
+ $isIncoming: boolean;
}>`
display: flex;
- justify-content: flex-end; // ${props => (props.isIncoming ? 'flex-start' : 'flex-end')};
- align-items: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')};
+ justify-content: flex-end; // ${props => (props.$isIncoming ? 'flex-start' : 'flex-end')};
+ align-items: ${props => (props.$isIncoming ? 'flex-start' : 'flex-end')};
width: 100%;
flex-direction: column;
`;
@@ -99,9 +99,13 @@ function ExpireTimerControlMessage({
);
}
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useMessageExpirationPropsByIdInternal = useMessageExpirationPropsById;
+const useIsDetailMessageViewInternal = useIsDetailMessageView;
+
export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) => {
- const selected = useMessageExpirationPropsById(props.messageId);
- const isDetailView = useIsDetailMessageView();
+ const selected = useMessageExpirationPropsByIdInternal(props.messageId);
+ const isDetailView = useIsDetailMessageViewInternal();
const { isControlMessage, onClick, onDoubleClickCapture, role, dataTestId } = props;
@@ -134,7 +138,7 @@ export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) =
`
display: flex;
align-items: center;
width: 100%;
letter-spacing: 0.03rem;
- padding: ${props => (props.isDetailView ? '0' : 'var(--margins-xs) var(--margins-lg) 0')};
+ padding: ${props => (props.$isDetailView ? '0' : 'var(--margins-xs) var(--margins-lg) 0')};
&.message-highlighted {
animation: ${highlightedMessageAnimation} var(--duration-message-highlight) ease-in-out;
@@ -59,7 +59,7 @@ const StyledReadableMessage = styled.div<{
${props =>
!props.selected &&
- props.isRightClicked &&
+ props.$isRightClicked &&
`background-color: var(--conversation-tab-background-selected-color);`}
`;
@@ -142,8 +142,8 @@ export const GenericReadableMessage = (props: Props) => {
return (
{
- const groupChange = useMessageGroupUpdateChange(messageId);
+ const groupChange = useMessageGroupUpdateChangeInternal(messageId);
const changeProps = useChangeItem(groupChange);
diff --git a/ts/components/conversation/message/message-item/InteractionNotification.tsx b/ts/components/conversation/message/message-item/InteractionNotification.tsx
index c8ec9bf6a0..7e214d54cd 100644
--- a/ts/components/conversation/message/message-item/InteractionNotification.tsx
+++ b/ts/components/conversation/message/message-item/InteractionNotification.tsx
@@ -86,7 +86,7 @@ export const InteractionNotification = (props: WithMessageId) => {
$flexDirection="row"
$alignItems="center"
$justifyContent="center"
- margin={'var(--margins-md) var(--margins-sm)'}
+ $margin={'var(--margins-md) var(--margins-sm)'}
data-testid="control-message"
>
{text}
diff --git a/ts/components/conversation/message/message-item/MessageRequestResponse.tsx b/ts/components/conversation/message/message-item/MessageRequestResponse.tsx
index 1d6fb9189c..845e9dc3c7 100644
--- a/ts/components/conversation/message/message-item/MessageRequestResponse.tsx
+++ b/ts/components/conversation/message/message-item/MessageRequestResponse.tsx
@@ -31,11 +31,11 @@ export const MessageRequestResponse = ({ messageId }: WithMessageId) => {
$flexDirection="row"
$alignItems="center"
$justifyContent="center"
- margin={'var(--margins-sm)'}
+ $margin={'var(--margins-sm)'}
id={`msg-${messageId}`}
>
-
+
{isUs ? (
) : (
diff --git a/ts/components/conversation/message/message-item/ReadableMessage.tsx b/ts/components/conversation/message/message-item/ReadableMessage.tsx
index 057c03e37d..7fd56bcd1e 100644
--- a/ts/components/conversation/message/message-item/ReadableMessage.tsx
+++ b/ts/components/conversation/message/message-item/ReadableMessage.tsx
@@ -10,7 +10,8 @@ import {
useState,
} from 'react';
import { InView } from 'react-intersection-observer';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
+import { getAppDispatch } from '../../../../state/dispatch';
import { useScrollToLoadedMessage } from '../../../../contexts/ScrollToLoadedMessage';
import { Data } from '../../../../data/data';
import { useHasUnread } from '../../../../hooks/useParamSelector';
@@ -108,7 +109,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
} = props;
const isAppFocused = useSelector(getIsAppFocused);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const selectedConversationKey = useSelectedConversationKey();
const mostRecentMessageId = useSelector(getMostRecentMessageId);
diff --git a/ts/components/conversation/message/reactions/Reaction.tsx b/ts/components/conversation/message/reactions/Reaction.tsx
index a59998b55b..770bf3ad7b 100644
--- a/ts/components/conversation/message/reactions/Reaction.tsx
+++ b/ts/components/conversation/message/reactions/Reaction.tsx
@@ -14,11 +14,10 @@ import { SessionTooltip } from '../../../SessionTooltip';
const StyledReaction = styled.button<{
selected: boolean;
- inModal: boolean;
- showCount: boolean;
+ $showCount: boolean;
}>`
display: flex;
- justify-content: ${props => (props.showCount ? 'flex-start' : 'center')};
+ justify-content: ${props => (props.$showCount ? 'flex-start' : 'center')};
align-items: center;
background-color: var(--message-bubbles-received-background-color);
@@ -29,7 +28,7 @@ const StyledReaction = styled.button<{
padding: 0 7px;
margin: 0 4px var(--margins-sm);
height: 24px;
- min-width: ${props => (props.showCount ? '48px' : '24px')};
+ min-width: ${props => (props.$showCount ? '48px' : '24px')};
span {
width: 100%;
@@ -39,10 +38,10 @@ const StyledReaction = styled.button<{
`;
const StyledReactionContainer = styled.div<{
- inModal: boolean;
+ $inModal: boolean;
}>`
position: relative;
- ${props => props.inModal && 'white-space: nowrap; margin-right: 8px;'}
+ ${props => props.$inModal && 'white-space: nowrap; margin-right: 8px;'}
`;
export type ReactionProps = {
@@ -139,11 +138,10 @@ export const Reaction = (props: ReactionProps) => {
);
const reactionContainer = (
-
+
handlePopupReaction?.(emoji)}
>
diff --git a/ts/components/conversation/right-panel/overlay/RightPanelMedia.tsx b/ts/components/conversation/right-panel/overlay/RightPanelMedia.tsx
index 2e214e8f65..2ee31a35ba 100644
--- a/ts/components/conversation/right-panel/overlay/RightPanelMedia.tsx
+++ b/ts/components/conversation/right-panel/overlay/RightPanelMedia.tsx
@@ -1,7 +1,7 @@
import { compact, flatten, isEqual } from 'lodash';
import { useEffect, useState } from 'react';
-import { useDispatch } from 'react-redux';
import useInterval from 'react-use/lib/useInterval';
+import { getAppDispatch } from '../../../../state/dispatch';
import { Data } from '../../../../data/data';
@@ -95,7 +95,7 @@ async function getMediaGalleryProps(conversationId: string): Promise<{
export const RightPanelMedia = () => {
const [documents, setDocuments] = useState>([]);
const [media, setMedia] = useState>([]);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const selectedConvoKey = useSelectedConversationKey();
const isShowing = useIsRightPanelShowing();
diff --git a/ts/components/conversation/right-panel/overlay/components/Header.tsx b/ts/components/conversation/right-panel/overlay/components/Header.tsx
index d0b6c596a7..4b66f2df53 100644
--- a/ts/components/conversation/right-panel/overlay/components/Header.tsx
+++ b/ts/components/conversation/right-panel/overlay/components/Header.tsx
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../../../state/dispatch';
import { closeRightPanel } from '../../../../../state/ducks/conversations';
import { Flex } from '../../../../basic/Flex';
import { sectionActions } from '../../../../../state/ducks/section';
@@ -32,13 +32,13 @@ type HeaderProps = (
export const Header = (props: HeaderProps) => {
const { children, hideCloseButton, closeButtonOnClick, paddingTop } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
{
$justifyContent={'flex-start'}
$alignItems={'center'}
width={'100%'}
- margin={'-5px auto auto'}
+ $margin={'-5px auto auto'}
>
{children}
diff --git a/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx
index 91b7888597..74e965211b 100644
--- a/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx
+++ b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx
@@ -1,10 +1,11 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import styled from 'styled-components';
// tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey';
import { clipboard } from 'electron';
+import { getAppDispatch } from '../../../../../state/dispatch';
import { PropsForAttachment, closeRightPanel } from '../../../../../state/ducks/conversations';
import { getMessageInfoId } from '../../../../../state/selectors/conversations';
import { Flex } from '../../../../basic/Flex';
@@ -52,6 +53,7 @@ import { PanelIconLucideIcon } from '../../../../buttons/panel/PanelIconButton';
import { sectionActions } from '../../../../../state/ducks/section';
import { useIsIncomingRequest } from '../../../../../hooks/useParamSelector';
import { tr } from '../../../../../localization/localeTools';
+import { AppDispatch } from '../../../../../state/createStore';
// NOTE we override the default max-widths when in the detail isDetailView
const StyledMessageBody = styled.div`
@@ -218,7 +220,7 @@ function CopyMessageBodyButton({ messageId }: WithMessageIdOpt) {
}
function ReplyToMessageButton({ messageId }: WithMessageIdOpt) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (!messageId) {
return null;
}
@@ -240,39 +242,76 @@ function ReplyToMessageButton({ messageId }: WithMessageIdOpt) {
);
}
-export const OverlayMessageInfo = () => {
- const dispatch = useDispatch();
+function closePanel(dispatch: AppDispatch) {
+ dispatch(closeRightPanel());
+ dispatch(sectionActions.resetRightOverlayMode());
+}
+
+function useMessageId() {
+ return useSelector(getMessageInfoId);
+}
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useMessageDetailsInternal(messageId?: string) {
const rightOverlayMode = useRightOverlayMode();
- const messageId = useSelector(getMessageInfoId);
- const messageInfo = useMessageInfo(messageId);
const isDeletable = useMessageIsDeletable(messageId);
const direction = useMessageDirection(messageId);
const timestamp = useMessageTimestamp(messageId);
const serverTimestamp = useMessageServerTimestamp(messageId);
const sender = useMessageSender(messageId);
- const isLegacyGroup = useSelectedIsLegacyGroup();
-
// we close the right panel when switching conversation so the convoId of that message is always the selectedConversationKey
// is always the currently selected conversation
const convoId = useSelectedConversationKey();
-
const isIncomingMessageRequest = useIsIncomingRequest(convoId);
+ const isLegacyGroup = useSelectedIsLegacyGroup();
- const closePanel = useCallback(() => {
- dispatch(closeRightPanel());
- dispatch(sectionActions.resetRightOverlayMode());
- }, [dispatch]);
-
- useKey('Escape', closePanel);
+ return {
+ rightOverlayMode,
+ isDeletable,
+ direction,
+ timestamp,
+ serverTimestamp,
+ sender,
+ convoId,
+ isIncomingMessageRequest,
+ isLegacyGroup,
+ };
+}
- // close the panel if the messageInfo is associated with a deleted message
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useClosePanelIfMessageDeleted(sender?: string) {
+ const dispatch = getAppDispatch();
useEffect(() => {
if (!sender) {
- closePanel();
+ closePanel(dispatch);
}
- }, [sender, closePanel]);
+ }, [sender, dispatch]);
+}
+
+export const OverlayMessageInfo = () => {
+ const dispatch = getAppDispatch();
+
+ const messageId = useMessageId();
+ const messageInfo = useMessageInfo(messageId);
+
+ const {
+ rightOverlayMode,
+ isDeletable,
+ direction,
+ timestamp,
+ serverTimestamp,
+ sender,
+ convoId,
+ isIncomingMessageRequest,
+ isLegacyGroup,
+ } = useMessageDetailsInternal(messageId);
+
+ const closePanelCb = () => closePanel(dispatch);
+
+ useKey('Escape', closePanelCb);
+
+ useClosePanelIfMessageDeleted(sender);
if (!rightOverlayMode || !messageInfo || !convoId || !messageId || !sender) {
return null;
@@ -316,7 +355,7 @@ export const OverlayMessageInfo = () => {
{tr('messageInfo')}
diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx
index 9bede08cb9..7fb9a9dc3e 100644
--- a/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx
+++ b/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx
@@ -191,7 +191,7 @@ function ProMessageFeaturesDetails({ messageId }: { messageId: string }) {
diff --git a/ts/components/dialog/BanOrUnbanUserDialog.tsx b/ts/components/dialog/BanOrUnbanUserDialog.tsx
index f02c70db51..ddb65bd0eb 100644
--- a/ts/components/dialog/BanOrUnbanUserDialog.tsx
+++ b/ts/components/dialog/BanOrUnbanUserDialog.tsx
@@ -1,5 +1,5 @@
import { useRef, useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useFocusMount } from '../../hooks/useFocusMount';
import { useConversationUsernameWithFallback } from '../../hooks/useParamSelector';
@@ -70,7 +70,7 @@ export const BanOrUnBanUserDialog = (props: {
}) => {
const { conversationId, banType, pubkey } = props;
const isBan = banType === 'ban';
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const convo = ConvoHub.use().get(conversationId);
const inputRef = useRef(null);
@@ -182,7 +182,7 @@ export const BanOrUnBanUserDialog = (props: {
onEnterPressed={() => {}}
inputDataTestId={isBan ? 'ban-user-input' : 'unban-user-input'}
/>
-
+
);
diff --git a/ts/components/dialog/DeleteAccountModal.tsx b/ts/components/dialog/DeleteAccountModal.tsx
index 118e40d9f2..ee9bcd1301 100644
--- a/ts/components/dialog/DeleteAccountModal.tsx
+++ b/ts/components/dialog/DeleteAccountModal.tsx
@@ -1,5 +1,5 @@
import { useCallback, useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { updateDeleteAccountModal } from '../../state/ducks/modalDialog';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
@@ -87,7 +87,7 @@ export const DeleteAccountModal = () => {
const [askingConfirmation, setAskingConfirmation] = useState(false);
const [deleteMode, setDeleteMode] = useState(DEVICE_ONLY);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const onDeleteEverythingLocallyOnly = async () => {
if (!isLoading) {
@@ -172,7 +172,7 @@ export const DeleteAccountModal = () => {
setDeleteMode={setDeleteMode}
/>
)}
-
+
);
diff --git a/ts/components/dialog/EditProfilePictureModal.tsx b/ts/components/dialog/EditProfilePictureModal.tsx
index 613ad8e059..a07afebda8 100644
--- a/ts/components/dialog/EditProfilePictureModal.tsx
+++ b/ts/components/dialog/EditProfilePictureModal.tsx
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
-import { useDispatch } from 'react-redux';
import type { AnyAction, Dispatch } from 'redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { ToastUtils, UserUtils } from '../../session/utils';
import {
userSettingsModal,
@@ -29,7 +29,7 @@ import {
ModalBasicHeader,
SessionWrapperModal,
} from '../SessionWrapperModal';
-import { useIsProAvailable } from '../../hooks/useIsProAvailable';
+import { getIsProAvailableMemo } from '../../hooks/useIsProAvailable';
import { SpacerLG, SpacerSM } from '../basic/Text';
import { AvatarSize } from '../avatar/Avatar';
import { ProIconButton } from '../buttons/ProButton';
@@ -51,14 +51,14 @@ const StyledAvatarContainer = styled.div`
position: relative;
`;
-const StyledCTADescription = styled.span<{ reverseDirection: boolean }>`
+const StyledCTADescription = styled.span<{ $reverseDirection: boolean }>`
text-align: center;
cursor: pointer;
font-size: var(--font-size-lg);
color: var(--text-secondary-color);
line-height: normal;
display: inline-flex;
- flex-direction: ${props => (props.reverseDirection ? 'row-reverse' : 'row')};
+ flex-direction: ${props => (props.$reverseDirection ? 'row-reverse' : 'row')};
align-items: center;
gap: var(--margins-xs);
padding: 3px;
@@ -134,12 +134,12 @@ const triggerRemovalProfileAvatar = async (conversationId: string) => {
};
export const EditProfilePictureModal = ({ conversationId }: EditProfilePictureModalProps) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isMe = useIsMe(conversationId);
const isCommunity = useIsPublic(conversationId);
const weHavePro = useCurrentUserHasPro() && isMe;
- const isProAvailable = useIsProAvailable();
+ const isProAvailable = getIsProAvailableMemo();
const avatarPath = useAvatarPath(conversationId) || '';
@@ -312,7 +312,7 @@ export const EditProfilePictureModal = ({ conversationId }: EditProfilePictureMo
$flexGap="var(--margins-sm)"
>
{isMe && proBadgeCb.cb ? (
-
+
{tr(
weHavePro
? 'proAnimatedDisplayPictureModalDescription'
@@ -363,7 +363,7 @@ export const EditProfilePictureModal = ({ conversationId }: EditProfilePictureMo
<>
{isMe && !isProcessing ? : null}
-
+
>
) : (
diff --git a/ts/components/dialog/EnterPasswordModal.tsx b/ts/components/dialog/EnterPasswordModal.tsx
index ac21d2cf08..a81a8e93d3 100644
--- a/ts/components/dialog/EnterPasswordModal.tsx
+++ b/ts/components/dialog/EnterPasswordModal.tsx
@@ -1,5 +1,5 @@
-import { useDispatch } from 'react-redux';
import { useState } from 'react';
+import { getAppDispatch } from '../../state/dispatch';
import { updateEnterPasswordModal } from '../../state/ducks/modalDialog';
import { SpacerSM } from '../basic/Text';
@@ -36,7 +36,7 @@ export const EnterPasswordModal = (props: EnterPasswordModalProps) => {
const [password, setPassword] = useState('');
const [providedError, setProvidedError] = useState(undefined);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const verifyPassword = async () => {
try {
diff --git a/ts/components/dialog/HideRecoveryPasswordDialog.tsx b/ts/components/dialog/HideRecoveryPasswordDialog.tsx
index 1e5e7e065d..d89102c71c 100644
--- a/ts/components/dialog/HideRecoveryPasswordDialog.tsx
+++ b/ts/components/dialog/HideRecoveryPasswordDialog.tsx
@@ -1,5 +1,5 @@
import { isEmpty } from 'lodash';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { SettingsKey } from '../../data/settings-key';
import { updateHideRecoveryPasswordModal, userSettingsModal } from '../../state/ducks/modalDialog';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
@@ -20,7 +20,7 @@ export type HideRecoveryPasswordDialogProps = {
export function HideRecoveryPasswordDialog(props: HideRecoveryPasswordDialogProps) {
const { state } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const onClose = () => {
dispatch(updateHideRecoveryPasswordModal(null));
diff --git a/ts/components/dialog/InviteContactsDialog.tsx b/ts/components/dialog/InviteContactsDialog.tsx
index 6c9d7c8b29..c1b857f22b 100644
--- a/ts/components/dialog/InviteContactsDialog.tsx
+++ b/ts/components/dialog/InviteContactsDialog.tsx
@@ -3,7 +3,7 @@ import useKey from 'react-use/lib/useKey';
import { clone } from 'lodash';
import { PubkeyType } from 'libsession_util_nodejs';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { ConvoHub } from '../../session/conversations';
import { updateGroupMembersModal, updateInviteContactModal } from '../../state/ducks/modalDialog';
import { SpacerLG } from '../basic/Text';
@@ -112,7 +112,7 @@ function ContactsToInvite({
const InviteContactsDialogInner = (props: Props) => {
const { conversationId } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const { contactsToInvite, isSearch, searchTerm, hasSearchResults } = useContactsToInviteTo(
'invite-contact-to',
diff --git a/ts/components/dialog/LocalizedPopupDialog.tsx b/ts/components/dialog/LocalizedPopupDialog.tsx
index 43b78097cd..d3cf92b019 100644
--- a/ts/components/dialog/LocalizedPopupDialog.tsx
+++ b/ts/components/dialog/LocalizedPopupDialog.tsx
@@ -1,7 +1,7 @@
import { isEmpty } from 'lodash';
import { Dispatch } from 'react';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import {
type LocalizedPopupDialogState,
updateLocalizedPopupDialog,
@@ -23,7 +23,7 @@ const StyledScrollDescriptionContainer = styled.div`
`;
export function LocalizedPopupDialog(props: LocalizedPopupDialogState) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
function onClose() {
dispatch(updateLocalizedPopupDialog(null));
diff --git a/ts/components/dialog/ModeratorsAddDialog.tsx b/ts/components/dialog/ModeratorsAddDialog.tsx
index c378486eb4..e906ce5d60 100644
--- a/ts/components/dialog/ModeratorsAddDialog.tsx
+++ b/ts/components/dialog/ModeratorsAddDialog.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
import { compact } from 'lodash';
+import { getAppDispatch } from '../../state/dispatch';
import { sogsV3AddAdmin } from '../../session/apis/open_group_api/sogsv3/sogsV3AddRemoveMods';
import { PubKey } from '../../session/types';
@@ -29,7 +29,7 @@ type Props = {
export const AddModeratorsDialog = (props: Props) => {
const { conversationId } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const convo = ConvoHub.use().get(conversationId);
const [inputBoxValue, setInputBoxValue] = useState('');
@@ -139,7 +139,7 @@ export const AddModeratorsDialog = (props: Props) => {
}
/>
-
+
);
diff --git a/ts/components/dialog/ModeratorsRemoveDialog.tsx b/ts/components/dialog/ModeratorsRemoveDialog.tsx
index 4def277eaf..f658d190b9 100644
--- a/ts/components/dialog/ModeratorsRemoveDialog.tsx
+++ b/ts/components/dialog/ModeratorsRemoveDialog.tsx
@@ -1,6 +1,6 @@
import { compact } from 'lodash';
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { ConvoHub } from '../../session/conversations';
import { PubKey } from '../../session/types';
@@ -61,7 +61,7 @@ export const RemoveModeratorsDialog = (props: Props) => {
const { conversationId } = props;
const [removingInProgress, setRemovingInProgress] = useState(false);
const [modsToRemove, setModsToRemove] = useState>([]);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const closeDialog = () => {
dispatch(updateRemoveModeratorsModal(null));
};
@@ -137,7 +137,7 @@ export const RemoveModeratorsDialog = (props: Props) => {
)}
-
+
);
};
diff --git a/ts/components/dialog/OnionStatusPathDialog.tsx b/ts/components/dialog/OnionStatusPathDialog.tsx
index 12bfe6a6bd..3e1f198f71 100644
--- a/ts/components/dialog/OnionStatusPathDialog.tsx
+++ b/ts/components/dialog/OnionStatusPathDialog.tsx
@@ -1,7 +1,7 @@
import { ipcRenderer } from 'electron';
-import { useState, SessionDataTestId } from 'react';
+import { useState, SessionDataTestId, type JSX } from 'react';
-import { useDispatch } from 'react-redux';
+import useUpdate from 'react-use/lib/useUpdate';
import useHover from 'react-use/lib/useHover';
import styled from 'styled-components';
import useInterval from 'react-use/lib/useInterval';
@@ -9,12 +9,14 @@ import useInterval from 'react-use/lib/useInterval';
import { isEmpty, isTypedArray } from 'lodash';
import { CityResponse, Reader } from 'maxmind';
import useMount from 'react-use/lib/useMount';
+import { useSelector } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { onionPathModal, updateOpenUrlModal } from '../../state/ducks/modalDialog';
import {
+ getFirstOnionPathLength,
+ getIsOnline,
+ getOnionPathsCount,
useFirstOnionPath,
- useFirstOnionPathLength,
- useIsOnline,
- useOnionPathsCount,
} from '../../state/selectors/onions';
import { Flex } from '../basic/Flex';
@@ -30,7 +32,8 @@ import {
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
import { ModalDescription } from './shared/ModalDescriptionContainer';
import { ModalFlexContainer } from './shared/ModalFlexContainer';
-import { useDataFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes';
+import { getDataFeatureFlagMemo } from '../../state/ducks/types/releasedFeaturesReduxTypes';
+import { DURATION } from '../../session/constants';
type StatusLightType = {
glowing?: boolean;
@@ -73,7 +76,7 @@ const StyledGrowingIcon = styled.div`
function useOnionPathWithUsAndNetwork() {
const onionPath = useFirstOnionPath();
- const localDevnet = useDataFeatureFlag('useLocalDevNet');
+ const localDevnet = getDataFeatureFlagMemo('useLocalDevNet');
if (onionPath.length === 0) {
return [];
@@ -86,7 +89,6 @@ function useOnionPathWithUsAndNetwork() {
...onionPath.map((node, index) => {
return {
...node,
-
label: localDevnet ? `SeshNet ${index + 1}` : undefined,
};
}),
@@ -96,16 +98,22 @@ function useOnionPathWithUsAndNetwork() {
];
}
+function useGlowingIndex() {
+ const [glowingIndex, setGlowingIndex] = useState(0);
+ return { glowingIndex, setGlowingIndex };
+}
+
function GlowingNodes() {
const onionPath = useOnionPathWithUsAndNetwork();
- const countDotsTotal = onionPath.length;
+ const { glowingIndex, setGlowingIndex } = useGlowingIndex();
- const [glowingIndex, setGlowingIndex] = useState(0);
- useInterval(() => {
- setGlowingIndex((glowingIndex + 1) % countDotsTotal);
- }, 1000);
+ const increment = () => {
+ setGlowingIndex(prev => (prev + 1) % onionPath.length);
+ };
- return onionPath.map((_snode: Snode | any, index: number) => {
+ useInterval(increment, 1 * DURATION.SECONDS);
+
+ return onionPath.map((_, index) => {
return ;
});
}
@@ -119,28 +127,42 @@ const OnionCountryDisplay = ({ labelText, snodeIp }: { snodeIp?: string; labelTe
let reader: Reader | null;
+function loadCityData(forceUpdate: () => void) {
+ ipcRenderer.once('load-maxmind-data-complete', (_event, content) => {
+ const asArrayBuffer = content as Uint8Array;
+ if (asArrayBuffer && isTypedArray(asArrayBuffer) && !isEmpty(asArrayBuffer)) {
+ reader = new Reader(Buffer.from(asArrayBuffer.buffer));
+ forceUpdate();
+ }
+ });
+ ipcRenderer.send('load-maxmind-data');
+}
+
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useIsOnline() {
+ return useSelector(getIsOnline);
+}
+
const OnionPathModalInner = () => {
+ const forceUpdate = useUpdate();
const nodes = useOnionPathWithUsAndNetwork();
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [_dataLoaded, setDataLoaded] = useState(false);
const isOnline = useIsOnline();
useMount(() => {
- ipcRenderer.once('load-maxmind-data-complete', (_event, content) => {
- const asArrayBuffer = content as Uint8Array;
- if (asArrayBuffer && isTypedArray(asArrayBuffer) && !isEmpty(asArrayBuffer)) {
- reader = new Reader(Buffer.from(asArrayBuffer.buffer));
- setDataLoaded(true); // retrigger a rerender
- }
- });
- ipcRenderer.send('load-maxmind-data');
+ if (!reader) {
+ loadCityData(forceUpdate);
+ }
});
- if (!isOnline || !nodes || nodes.length <= 0) {
- return ;
+ if (!isOnline || !nodes || nodes.length <= 0 || !reader) {
+ return ;
}
+ return ;
+};
+
+const OnionPathModalLoaded = () => {
+ const nodes = useOnionPathWithUsAndNetwork();
return (
{
);
})}
@@ -231,8 +253,8 @@ function OnionPathDot({
width="12"
height="12"
viewBox="0 0 100 100"
- clip-rule="nonzero"
- fill-rule="nonzero"
+ clipRule="nonzero"
+ fillRule="nonzero"
data-testid={dataTestId}
style={{
transition: 'all var(--default-duration) ease-in-out',
@@ -249,7 +271,7 @@ function OnionPathDot({
}
export const OnionPathModal = () => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
`
padding: ${props => (props.$inActionPanel ? 'var(--margins-md)' : '0')};
`;
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useOnionPathsCount() {
+ return useSelector(getOnionPathsCount);
+}
+
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useFirstOnionPathLength() {
+ return useSelector(getFirstOnionPathLength);
+}
+
/**
* A status light specifically for the action panel. Color is based on aggregate node states instead of individual onion node state
*/
diff --git a/ts/components/dialog/OpenUrlModal.tsx b/ts/components/dialog/OpenUrlModal.tsx
index a610f2b720..ebc415ef47 100644
--- a/ts/components/dialog/OpenUrlModal.tsx
+++ b/ts/components/dialog/OpenUrlModal.tsx
@@ -1,8 +1,8 @@
import { shell } from 'electron';
import { isEmpty } from 'lodash';
import { Dispatch } from 'react';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { MessageInteraction } from '../../interactions';
import { OpenUrlModalState, updateOpenUrlModal } from '../../state/ducks/modalDialog';
import {
@@ -23,7 +23,7 @@ const StyledScrollDescriptionContainer = styled.div`
`;
export function OpenUrlModal(props: OpenUrlModalState) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (!props || isEmpty(props) || !props.urlToOpen) {
return null;
diff --git a/ts/components/dialog/ProCTADescription.tsx b/ts/components/dialog/ProCTADescription.tsx
index d06eb9a791..95a34f8a15 100644
--- a/ts/components/dialog/ProCTADescription.tsx
+++ b/ts/components/dialog/ProCTADescription.tsx
@@ -1,5 +1,5 @@
import { ReactNode, useMemo } from 'react';
-import { useCurrentUserHasExpiredPro, useProAccessDetails } from '../../hooks/useHasPro';
+import { useCurrentUserHasExpiredPro } from '../../hooks/useHasPro';
import { Localizer } from '../basic/Localizer';
import { CTADescriptionListItem, StyledCTADescriptionList } from './CTADescriptionList';
import { StyledScrollDescriptionContainer } from './SessionCTA';
@@ -9,6 +9,7 @@ import { formatNumber } from '../../util/i18n/formatting/generics';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { ProIconButton } from '../buttons/ProButton';
import { CTAVariant, type ProCTAVariant } from './cta/types';
+import { useProBackendProDetails } from '../../state/selectors/proBackendData';
const variantsWithoutFeatureList = [
CTAVariant.PRO_GROUP_NON_ADMIN,
@@ -95,7 +96,7 @@ function FeatureList({ variant }: { variant: CTAVariant }) {
}
function ProExpiringSoonDescription() {
- const { data } = useProAccessDetails();
+ const { data } = useProBackendProDetails();
return ;
}
diff --git a/ts/components/dialog/ProCTATitle.tsx b/ts/components/dialog/ProCTATitle.tsx
index 5b33e25787..35f60eabff 100644
--- a/ts/components/dialog/ProCTATitle.tsx
+++ b/ts/components/dialog/ProCTATitle.tsx
@@ -50,7 +50,7 @@ export function ProCTATitle({ variant }: { variant: ProCTAVariant }) {
}, [variant]);
return (
-
+
{titleText}
{
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const { onClickOk, onClickCancel, i18nMessage } = props;
const title = tr('warning');
diff --git a/ts/components/dialog/ReactClearAllModal.tsx b/ts/components/dialog/ReactClearAllModal.tsx
index 270704f486..49e8749467 100644
--- a/ts/components/dialog/ReactClearAllModal.tsx
+++ b/ts/components/dialog/ReactClearAllModal.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useMessageReactsPropsById } from '../../hooks/useParamSelector';
import { clearSogsReactionByServerId } from '../../session/apis/open_group_api/sogsv3/sogsV3ClearReaction';
import { ConvoHub } from '../../session/conversations';
@@ -25,7 +25,7 @@ export const ReactClearAllModal = (props: Props) => {
const [clearingInProgress, setClearingInProgress] = useState(false);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const msgProps = useMessageReactsPropsById(messageId);
if (!msgProps) {
@@ -79,7 +79,7 @@ export const ReactClearAllModal = (props: Props) => {
localizerProps={{ token: 'emojiReactsClearAll', emoji: reaction }}
/>
-
+
);
diff --git a/ts/components/dialog/ReactListModal.tsx b/ts/components/dialog/ReactListModal.tsx
index 0173452073..be8b5e9529 100644
--- a/ts/components/dialog/ReactListModal.tsx
+++ b/ts/components/dialog/ReactListModal.tsx
@@ -1,7 +1,7 @@
import { isEmpty, isEqual } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { useMessageReactsPropsById } from '../../hooks/useParamSelector';
import { isUsAnySogsFromCache } from '../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { UserUtils } from '../../session/utils';
@@ -97,7 +97,7 @@ type ReactionSendersProps = {
const ReactionSenders = (props: ReactionSendersProps) => {
const { messageId, currentReact, senders, me, conversationId } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const handleRemoveReaction = async () => {
await Reactions.sendMessageReaction(messageId, currentReact);
@@ -200,7 +200,7 @@ const handleSenders = (senders: Array, me: string) => {
export const ReactListModal = (props: Props) => {
const { reaction, messageId } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [reactions, setReactions] = useState([]);
const [currentReact, setCurrentReact] = useState('');
diff --git a/ts/components/dialog/SessionCTA.tsx b/ts/components/dialog/SessionCTA.tsx
index 9bad371c9a..aa80864b88 100644
--- a/ts/components/dialog/SessionCTA.tsx
+++ b/ts/components/dialog/SessionCTA.tsx
@@ -1,8 +1,8 @@
import { isNil } from 'lodash';
import { Dispatch, useMemo, type ReactNode } from 'react';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import type { CSSProperties } from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import {
type SessionCTAState,
updateSessionCTA,
@@ -24,7 +24,7 @@ import {
import { SpacerSM, SpacerXL } from '../basic/Text';
import type { MergedLocalizerTokens } from '../../localization/localeTools';
import { SessionButtonShiny } from '../basic/SessionButtonShiny';
-import { useIsProAvailable } from '../../hooks/useIsProAvailable';
+import { getIsProAvailableMemo } from '../../hooks/useIsProAvailable';
import { useCurrentUserHasPro } from '../../hooks/useHasPro';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { Storage } from '../../util/storage';
@@ -77,9 +77,9 @@ const StyledAnimationImage = styled.img`
position: absolute;
`;
-const StyledAnimatedCTAImageContainer = styled.div<{ noColor?: boolean }>`
+const StyledAnimatedCTAImageContainer = styled.div<{ $noColor?: boolean }>`
position: relative;
- ${props => (props.noColor ? 'filter: grayscale(100%) brightness(0.8);' : '')}
+ ${props => (props.$noColor ? 'filter: grayscale(100%) brightness(0.8);' : '')}
`;
function AnimatedCTAImage({
@@ -94,19 +94,19 @@ function AnimatedCTAImage({
noColor?: boolean;
}) {
return (
-
+
);
}
-export const StyledCTATitle = styled.span<{ reverseDirection?: boolean }>`
+export const StyledCTATitle = styled.span<{ $reverseDirection?: boolean }>`
font-size: var(--font-size-h4);
font-weight: bold;
line-height: normal;
display: inline-flex;
- flex-direction: ${props => (props.reverseDirection ? 'row-reverse' : 'row')};
+ flex-direction: ${props => (props.$reverseDirection ? 'row-reverse' : 'row')};
align-items: center;
gap: var(--margins-xs);
padding: 3px;
@@ -231,7 +231,7 @@ function Buttons({
afterActionButtonCallback?: () => void;
actionButtonNextModalAfterCloseCallback?: () => void;
}) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const actionButton = useMemo(() => {
if (!isVariantWithActionButton(variant)) {
@@ -339,7 +339,7 @@ function Buttons({
}
export function SessionCTA(props: SessionCTAState) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const hasPro = useCurrentUserHasPro();
function onClose() {
@@ -393,9 +393,8 @@ export const showSessionCTA = (variant: CTAVariant, dispatch: Dispatch) =>
};
export const useShowSessionCTACb = (variant: CTAVariant) => {
- const dispatch = useDispatch();
-
- const isProAvailable = useIsProAvailable();
+ const dispatch = getAppDispatch();
+ const isProAvailable = getIsProAvailableMemo();
const isProCTA = useIsProCTAVariant(variant);
if (isProCTA && !isProAvailable) {
return () => null;
@@ -405,9 +404,8 @@ export const useShowSessionCTACb = (variant: CTAVariant) => {
};
export const useShowSessionCTACbWithVariant = () => {
- const dispatch = useDispatch();
-
- const isProAvailable = useIsProAvailable();
+ const dispatch = getAppDispatch();
+ const isProAvailable = getIsProAvailableMemo();
return (variant: CTAVariant) => {
if (isProCTAVariant(variant) && !isProAvailable) {
diff --git a/ts/components/dialog/SessionConfirm.tsx b/ts/components/dialog/SessionConfirm.tsx
index e0c2a03c15..a3030f4393 100644
--- a/ts/components/dialog/SessionConfirm.tsx
+++ b/ts/components/dialog/SessionConfirm.tsx
@@ -1,6 +1,6 @@
-import { useDispatch } from 'react-redux';
import { useEffect, useState } from 'react';
import useKey from 'react-use/lib/useKey';
+import { getAppDispatch } from '../../state/dispatch';
import { useLastMessage } from '../../hooks/useParamSelector';
import { updateConversationInteractionState } from '../../interactions/conversationInteractions';
import { ConversationInteractionStatus } from '../../interactions/types';
@@ -63,7 +63,7 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
conversationId,
} = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const lastMessage = useLastMessage(conversationId);
const [isLoading, setIsLoading] = useState(false);
@@ -161,7 +161,7 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
}}
/>
) : null}
-
+
);
diff --git a/ts/components/dialog/SessionNicknameDialog.tsx b/ts/components/dialog/SessionNicknameDialog.tsx
index 9611c3ab17..1424c0be75 100644
--- a/ts/components/dialog/SessionNicknameDialog.tsx
+++ b/ts/components/dialog/SessionNicknameDialog.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { ConvoHub } from '../../session/conversations';
import { changeNickNameModal } from '../../state/ducks/modalDialog';
@@ -64,7 +64,7 @@ export const SessionNicknameDialog = (props: Props) => {
const { conversationId } = props;
// this resolves to the real user name, and not the nickname (if set) like we do usually
const displayName = useConversationRealName(conversationId);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const currentNickname = useNickname(conversationId);
const [nickname, setStateNickname] = useState(currentNickname || '');
diff --git a/ts/components/dialog/StyledRootDialog.tsx b/ts/components/dialog/StyledRootDialog.tsx
index 61d38a0234..5f95c4bd1b 100644
--- a/ts/components/dialog/StyledRootDialog.tsx
+++ b/ts/components/dialog/StyledRootDialog.tsx
@@ -1,6 +1,6 @@
import styled from 'styled-components';
-export const StyledRootDialog = styled.div<{ shouldOverflow: boolean }>`
+export const StyledRootDialog = styled.div<{ $shouldOverflow: boolean }>`
display: flex;
align-items: center;
justify-content: center;
@@ -13,7 +13,7 @@ export const StyledRootDialog = styled.div<{ shouldOverflow: boolean }>`
background-color: var(--modal-background-color);
padding: 5vh var(--margins-lg);
z-index: 100;
- overflow-y: ${props => (props.shouldOverflow ? 'auto' : 'hidden')};
+ overflow-y: ${props => (props.$shouldOverflow ? 'auto' : 'hidden')};
& ~ .index.inbox {
transition: filter var(--duration-modal-to-inbox);
diff --git a/ts/components/dialog/TermsOfServicePrivacyDialog.tsx b/ts/components/dialog/TermsOfServicePrivacyDialog.tsx
index a69b2d6265..4e5b504712 100644
--- a/ts/components/dialog/TermsOfServicePrivacyDialog.tsx
+++ b/ts/components/dialog/TermsOfServicePrivacyDialog.tsx
@@ -1,5 +1,5 @@
import { shell } from 'electron';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { updateTermsOfServicePrivacyModal } from '../../state/onboarding/ducks/modals';
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
import {
@@ -18,7 +18,7 @@ export type TermsOfServicePrivacyDialogProps = {
export function TermsOfServicePrivacyDialog(props: TermsOfServicePrivacyDialogProps) {
const { show } = props;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const onClose = () => {
dispatch(updateTermsOfServicePrivacyModal(null));
diff --git a/ts/components/dialog/UpdateConversationDetailsDialog.tsx b/ts/components/dialog/UpdateConversationDetailsDialog.tsx
index 4c27dcdf0a..2456d534de 100644
--- a/ts/components/dialog/UpdateConversationDetailsDialog.tsx
+++ b/ts/components/dialog/UpdateConversationDetailsDialog.tsx
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import { isEmpty } from 'lodash';
+import { getAppDispatch } from '../../state/dispatch';
import {
useAvatarPath,
@@ -26,7 +26,7 @@ import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/S
import { SpacerMD, SpacerSM } from '../basic/Text';
import { SessionSpinner } from '../loading';
import { tr } from '../../localization/localeTools';
-import { SimpleSessionInput, SimpleSessionTextarea } from '../inputs/SessionInput';
+import { SimpleSessionInput } from '../inputs/SessionInput';
import {
ModalBasicHeader,
ModalActionsContainer,
@@ -45,6 +45,8 @@ import { UploadFirstImageButton } from '../buttons/avatar/UploadFirstImageButton
import { sanitizeDisplayNameOrToast } from '../registration/utils';
import { ProfileManager } from '../../session/profile_manager/ProfileManager';
import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes';
+import { SimpleSessionTextarea } from '../inputs/SimpleSessionTextarea';
+import { ConversationModel } from '../../models/conversation';
/**
* We want the description to be at most 200 visible characters, in addition
@@ -121,41 +123,72 @@ function useDescriptionErrorString({
: tr('updateGroupInformationEnterShorterDescription');
}
-export function UpdateConversationDetailsDialog(props: WithConvoId) {
- const dispatch = useDispatch();
- const { conversationId } = props;
- const isClosedGroup = useIsClosedGroup(conversationId);
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useUpdateConversationDetailsDialogInternal(convo: ConversationModel) {
+ const conversationId = convo.id;
+ const nameOnOpen = convo.getRealSessionUsername();
+ const [avatarPointerOnMount, setAvatarPointerOnMount] = useState('');
+ const [newName, setNewName] = useState(nameOnOpen);
+ const originalGroupDescription = useLibGroupDescription(conversationId);
+ const originalCommunityDescription = useRoomDescription(conversationId);
const isPublic = useIsPublic(conversationId);
- const convo = ConvoHub.use().get(conversationId);
- const isGroupChangePending = useGroupNameChangeFromUIPending();
+ const descriptionOnOpen = isPublic ? originalCommunityDescription : originalGroupDescription;
+ const [newDescription, setNewDescription] = useState(descriptionOnOpen);
+ const avatarPath = useAvatarPath(conversationId) || '';
+ const isMe = useIsMe(conversationId);
const isCommunityChangePending = useChangeDetailsOfRoomPending(conversationId);
+ const isGroupChangePending = useGroupNameChangeFromUIPending();
+ const isClosedGroup = useIsClosedGroup(conversationId);
const isNameChangePending = isPublic ? isCommunityChangePending : isGroupChangePending;
- const isMe = useIsMe(conversationId);
-
- const [avatarPointerOnMount, setAvatarPointerOnMount] = useState('');
- const refreshedAvatarPointer = convo.getAvatarPointer() || '';
useMount(() => {
setAvatarPointerOnMount(convo?.getAvatarPointer() || '');
});
- if (!convo) {
- throw new Error('UpdateGroupOrCommunityDetailsDialog corresponding convo not found');
- }
-
if (!isClosedGroup && !isPublic && !isMe) {
throw new Error(
'UpdateGroupOrCommunityDetailsDialog dialog only works groups/communities or ourselves'
);
}
- const nameOnOpen = convo.getRealSessionUsername();
- const originalGroupDescription = useLibGroupDescription(conversationId);
- const originalCommunityDescription = useRoomDescription(conversationId);
- const descriptionOnOpen = isPublic ? originalCommunityDescription : originalGroupDescription;
- const [newName, setNewName] = useState(nameOnOpen);
- const [newDescription, setNewDescription] = useState(descriptionOnOpen);
- const avatarPath = useAvatarPath(conversationId) || '';
+ return {
+ isNameChangePending,
+ newName,
+ newDescription,
+ isMe,
+ nameOnOpen,
+ descriptionOnOpen,
+ isPublic,
+ avatarPointerOnMount,
+ avatarPath,
+ setNewName,
+ setNewDescription,
+ };
+}
+
+export function UpdateConversationDetailsDialog(props: WithConvoId) {
+ const dispatch = getAppDispatch();
+ const { conversationId } = props;
+ const convo = ConvoHub.use().get(conversationId);
+
+ const refreshedAvatarPointer = convo.getAvatarPointer() || '';
+ const {
+ isNameChangePending,
+ newName,
+ newDescription,
+ isMe,
+ nameOnOpen,
+ descriptionOnOpen,
+ isPublic,
+ avatarPointerOnMount,
+ avatarPath,
+ setNewName,
+ setNewDescription,
+ } = useUpdateConversationDetailsDialogInternal(convo);
+
+ if (!convo) {
+ throw new Error('UpdateGroupOrCommunityDetailsDialog corresponding convo not found');
+ }
function closeDialog() {
dispatch(updateConversationDetailsModal(null));
@@ -343,7 +376,7 @@ export function UpdateConversationDetailsDialog(props: WithConvoId) {
}
/>
)}
-
+
);
diff --git a/ts/components/dialog/UpdateGroupMembersDialog.tsx b/ts/components/dialog/UpdateGroupMembersDialog.tsx
index 61133fd939..0cd58816da 100644
--- a/ts/components/dialog/UpdateGroupMembersDialog.tsx
+++ b/ts/components/dialog/UpdateGroupMembersDialog.tsx
@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react';
-import { useDispatch } from 'react-redux';
-
import { PubkeyType } from 'libsession_util_nodejs';
+import { getAppDispatch } from '../../state/dispatch';
+
import { ToastUtils } from '../../session/utils';
import { updateGroupMembersModal } from '../../state/ducks/modalDialog';
@@ -60,20 +60,34 @@ function useSortedListOfMembers(convoId: string) {
return sortedMembers;
}
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useContactsToInviteToInternal() {
+ return useContactsToInviteTo('manage-group-members');
+}
+
const useFilteredSortedListOfMembers = (convoId: string) => {
const sortedMembers = useSortedListOfMembers(convoId);
- const { contactsToInvite: globalSearchResults, searchTerm } =
- useContactsToInviteTo('manage-group-members');
-
- return useMemo(
- () =>
- !searchTerm || globalSearchResults === undefined
- ? sortedMembers
- : sortedMembers.filter(m => globalSearchResults.includes(m)),
- [sortedMembers, globalSearchResults, searchTerm]
- );
+
+ const { contactsToInvite: globalSearchResults, searchTerm } = useContactsToInviteToInternal();
+
+ return !searchTerm || globalSearchResults === undefined
+ ? sortedMembers
+ : sortedMembers.filter(m => globalSearchResults.includes(m));
};
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useMembersListDetailsInternal(conversationId: string) {
+ const weAreAdmin = useWeAreAdmin(conversationId);
+ const isV2Group = useSelectedIsGroupV2();
+ const groupAdmins = useGroupAdmins(conversationId);
+
+ return {
+ weAreAdmin,
+ isV2Group,
+ groupAdmins,
+ };
+}
+
const MemberList = (props: {
convoId: string;
selectedMembers: Array;
@@ -81,9 +95,8 @@ const MemberList = (props: {
onUnselect: (m: string) => void;
}) => {
const { onSelect, convoId, onUnselect, selectedMembers } = props;
- const weAreAdmin = useWeAreAdmin(convoId);
- const isV2Group = useSelectedIsGroupV2();
- const groupAdmins = useGroupAdmins(convoId);
+ const { weAreAdmin, isV2Group, groupAdmins } = useMembersListDetailsInternal(convoId);
+
const sortedMembers = useFilteredSortedListOfMembers(convoId);
return (
@@ -126,7 +139,7 @@ export const UpdateGroupMembersDialog = (props: Props) => {
const { addTo, removeFrom, uniqueValues: membersToRemove } = useSet([]);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (isPrivate || isPublic) {
throw new Error('UpdateGroupMembersDialog invalid convoProps');
@@ -250,7 +263,7 @@ export const UpdateGroupMembersDialog = (props: Props) => {
-
+
);
diff --git a/ts/components/dialog/UserProfileModal.tsx b/ts/components/dialog/UserProfileModal.tsx
index 34fdcf6e2c..06322b3653 100644
--- a/ts/components/dialog/UserProfileModal.tsx
+++ b/ts/components/dialog/UserProfileModal.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { ConvoHub } from '../../session/conversations';
import { openConversationWithMessages } from '../../state/ducks/conversations';
@@ -55,7 +55,7 @@ export const UserProfileModal = ({
conversationId,
realSessionId,
}: NonNullable) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [enlargedImage, setEnlargedImage] = useState(false);
const isBlinded = PubKey.isBlinded(conversationId);
@@ -133,7 +133,7 @@ export const UserProfileModal = ({
$alignItems="center"
$flexDirection="column"
$flexGap="var(--margins-md)"
- paddingBlock="0 var(--margins-lg)"
+ $paddingBlock="0 var(--margins-lg)"
style={{ position: 'relative' }}
>
{mode === 'qr' ? (
diff --git a/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx b/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx
index 94becccaa6..6fece9fe05 100644
--- a/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx
+++ b/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx
@@ -1,8 +1,6 @@
-import { useDispatch } from 'react-redux';
-
import { isEmpty } from 'lodash';
-import { useCallback } from 'react';
import useAsyncFn from 'react-use/lib/useAsyncFn';
+import { getAppDispatch } from '../../../state/dispatch';
import { useConversationsNicknameRealNameOrShortenPubkey } from '../../../hooks/useParamSelector';
import { updateBlockOrUnblockModal } from '../../../state/ducks/modalDialog';
import { BlockedNumberController } from '../../../util';
@@ -55,15 +53,15 @@ function useBlockUnblockI18nDescriptionArgs({
}
export const BlockOrUnblockDialog = ({ pubkeys, action, onConfirmed }: NonNullable) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const localizedAction = action === 'block' ? tr('block') : tr('blockUnblock');
const args = useBlockUnblockI18nDescriptionArgs({ action, pubkeys });
- const closeModal = useCallback(() => {
+ const closeModal = () => {
dispatch(updateBlockOrUnblockModal(null));
- }, [dispatch]);
+ };
const [, onConfirm] = useAsyncFn(async () => {
if (action === 'block') {
diff --git a/ts/components/dialog/conversationSettings/ConversationTitleDialog.tsx b/ts/components/dialog/conversationSettings/ConversationTitleDialog.tsx
index 53597f9ff7..3bd7ad61f3 100644
--- a/ts/components/dialog/conversationSettings/ConversationTitleDialog.tsx
+++ b/ts/components/dialog/conversationSettings/ConversationTitleDialog.tsx
@@ -49,16 +49,29 @@ function ProBadge({ conversationId }: WithConvoId) {
return ;
}
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useConversationDetailsInternal(conversationId?: string) {
+ const nicknameOrDisplayName = useConversationUsernameWithFallback(true, conversationId);
+ const isCommunity = useIsPublic(conversationId);
+ const isClosedGroup = useIsClosedGroup(conversationId);
+ const isMe = useIsMe(conversationId);
+
+ return {
+ nicknameOrDisplayName,
+ isCommunity,
+ isClosedGroup,
+ isMe,
+ };
+}
+
export const ConversationTitleDialog = ({
conversationId,
editable,
}: WithConvoId & {
editable: boolean;
}) => {
- const nicknameOrDisplayName = useConversationUsernameWithFallback(true, conversationId);
- const isCommunity = useIsPublic(conversationId);
- const isClosedGroup = useIsClosedGroup(conversationId);
- const isMe = useIsMe(conversationId);
+ const { nicknameOrDisplayName, isCommunity, isClosedGroup, isMe } =
+ useConversationDetailsInternal(conversationId);
const onClickCb = useOnTitleClickCb(conversationId, editable);
diff --git a/ts/components/dialog/conversationSettings/conversationSettingsHeader.tsx b/ts/components/dialog/conversationSettings/conversationSettingsHeader.tsx
index 4955cd0e06..59834ff33f 100644
--- a/ts/components/dialog/conversationSettings/conversationSettingsHeader.tsx
+++ b/ts/components/dialog/conversationSettings/conversationSettingsHeader.tsx
@@ -44,7 +44,7 @@ function AccountId({ conversationId }: WithConvoId) {
);
}
-const StyledDescription = styled.div<{ expanded: boolean }>`
+const StyledDescription = styled.div<{ $expanded: boolean }>`
color: var(--text-secondary-color);
font-size: var(--font-display-size-md);
text-align: center;
@@ -55,7 +55,7 @@ const StyledDescription = styled.div<{ expanded: boolean }>`
white-space: pre-wrap;
word-break: break-word;
overflow: hidden;
- -webkit-line-clamp: ${({ expanded }) => (expanded ? 'unset' : '2')};
+ -webkit-line-clamp: ${({ $expanded }) => ($expanded ? 'unset' : '2')};
display: -webkit-box;
-webkit-box-orient: vertical;
// some padding so we always have room to show the ellipsis, if needed
@@ -101,7 +101,7 @@ function Description({ conversationId }: WithConvoId) {
return (
<>
-
+
{description}
{isClamped && (
diff --git a/ts/components/dialog/conversationSettings/pages/conversationSettingsHooks.tsx b/ts/components/dialog/conversationSettings/pages/conversationSettingsHooks.tsx
index eff62a1990..170e870e78 100644
--- a/ts/components/dialog/conversationSettings/pages/conversationSettingsHooks.tsx
+++ b/ts/components/dialog/conversationSettings/pages/conversationSettingsHooks.tsx
@@ -1,5 +1,5 @@
import { noop } from 'lodash';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../../../state/dispatch';
import { tr } from '../../../../localization/localeTools';
import {
ConversationSettingsModalPage,
@@ -25,7 +25,7 @@ export function useTitleFromPage(page: ConversationSettingsModalPage | undefined
}
export function useCloseActionFromPage(props: ConversationSettingsModalState) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const showConvoSettingsCb = useShowConversationSettingsFor(props?.conversationId);
if (!props?.conversationId || !showConvoSettingsCb) {
return noop;
diff --git a/ts/components/dialog/conversationSettings/pages/disappearing-messages/DisappearingMessagesPage.tsx b/ts/components/dialog/conversationSettings/pages/disappearing-messages/DisappearingMessagesPage.tsx
index 667fa84bf6..5999a7411a 100644
--- a/ts/components/dialog/conversationSettings/pages/disappearing-messages/DisappearingMessagesPage.tsx
+++ b/ts/components/dialog/conversationSettings/pages/disappearing-messages/DisappearingMessagesPage.tsx
@@ -1,6 +1,7 @@
-import { useEffect, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useEffect, useState, Dispatch } from 'react';
+import { useSelector } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../../../state/dispatch';
import { setDisappearingMessagesByConvoId } from '../../../../../interactions/conversationInteractions';
import { TimerOptions } from '../../../../../session/disappearing_messages/timerOptions';
import { DisappearingMessageConversationModeType } from '../../../../../session/disappearing_messages/types';
@@ -82,30 +83,116 @@ function useSingleMode(disappearingModeOptions: Record | undefi
return { singleMode };
}
-export const DisappearingMessagesForConversationModal = (props: ConversationSettingsModalState) => {
- const dispatch = useDispatch();
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useDisappearingMessagesForConversationModalInternal(
+ props: ConversationSettingsModalState
+) {
const onClose = useCloseActionFromPage(props);
const title = useTitleFromPage(props?.settingsModalPage);
const selectedConversationKey = useSelectedConversationKey();
const disappearingModeOptions = useSelector(getSelectedConversationExpirationModes);
- const { singleMode } = useSingleMode(disappearingModeOptions);
- const hasOnlyOneMode = !!(singleMode && singleMode.length > 0);
-
const isGroup = useSelectedIsGroupOrCommunity();
const expirationMode = useSelectedConversationDisappearingMode() || 'off';
const expireTimer = useSelectedExpireTimer();
const backAction = useBackActionForPage(props);
+ const isStandalone = useConversationSettingsModalIsStandalone();
+ const showConvoSettingsCb = useShowConversationSettingsFor(selectedConversationKey);
+
+ return {
+ onClose,
+ title,
+ selectedConversationKey,
+ disappearingModeOptions,
+ isGroup,
+ expirationMode,
+ expireTimer,
+ backAction,
+ isStandalone,
+ showConvoSettingsCb,
+ };
+}
+
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useDisappearingMessagesForConversationStateInternal(
+ initialExpirationModeSelected: DisappearingMessageConversationModeType,
+ expireTimer?: number
+) {
const [modeSelected, setModeSelected] = useState(
- hasOnlyOneMode ? singleMode : expirationMode
+ initialExpirationModeSelected
);
-
const [timeSelected, setTimeSelected] = useState(expireTimer || 0);
- const isStandalone = useConversationSettingsModalIsStandalone();
-
const [loading, setLoading] = useState(false);
- const showConvoSettingsCb = useShowConversationSettingsFor(selectedConversationKey);
+ return {
+ modeSelected,
+ setModeSelected,
+ timeSelected,
+ setTimeSelected,
+ loading,
+ setLoading,
+ };
+}
+
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useHandleExpirationTimeChange(
+ setTimeSelected: Dispatch,
+ modeSelected: DisappearingMessageConversationModeType,
+ hasOnlyOneMode: boolean,
+ expireTimer?: number
+) {
+ useEffect(() => {
+ // NOTE loads a time value from the conversation model or the default
+ setTimeSelected(
+ expireTimer !== undefined && expireTimer > -1
+ ? expireTimer
+ : loadDefaultTimeValue(modeSelected, hasOnlyOneMode)
+ );
+ }, [expireTimer, hasOnlyOneMode, modeSelected, setTimeSelected]);
+}
+
+/**
+ * NOTE: [react-compiler] Helper function to handle the async operation with try/catch.
+ * This is extracted outside the component to work around the React Compiler limitation:
+ * "Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement"
+ */
+async function setDisappearingMessagesWithErrorHandling(
+ convoKey: string,
+ mode: DisappearingMessageConversationModeType,
+ time: number
+): Promise<{ success: true } | { success: false; error: unknown }> {
+ try {
+ await setDisappearingMessagesByConvoId(convoKey, mode, time);
+ return { success: true };
+ } catch (error) {
+ return { success: false, error };
+ }
+}
+
+export const DisappearingMessagesForConversationModal = (props: ConversationSettingsModalState) => {
+ const dispatch = getAppDispatch();
+
+ const {
+ onClose,
+ title,
+ selectedConversationKey,
+ disappearingModeOptions,
+ isGroup,
+ expirationMode,
+ expireTimer,
+ backAction,
+ isStandalone,
+ showConvoSettingsCb,
+ } = useDisappearingMessagesForConversationModalInternal(props);
+
+ const { singleMode } = useSingleMode(disappearingModeOptions);
+ const hasOnlyOneMode = !!(singleMode && singleMode.length > 0);
+
+ const { modeSelected, setModeSelected, timeSelected, setTimeSelected, loading, setLoading } =
+ useDisappearingMessagesForConversationStateInternal(
+ hasOnlyOneMode ? singleMode : expirationMode,
+ expireTimer
+ );
function closeOrBackInPage() {
if (isStandalone) {
@@ -121,38 +208,41 @@ export const DisappearingMessagesForConversationModal = (props: ConversationSett
if (!selectedConversationKey) {
return;
}
+
if (hasOnlyOneMode) {
if (singleMode) {
- try {
- await setDisappearingMessagesByConvoId(
- selectedConversationKey,
- timeSelected === 0 ? 'off' : singleMode,
- timeSelected
- );
+ const modeToSet = timeSelected === 0 ? 'off' : singleMode;
+ setLoading(true);
+ const result = await setDisappearingMessagesWithErrorHandling(
+ selectedConversationKey,
+ modeToSet,
+ timeSelected
+ );
+ setLoading(false);
+ if (result.success) {
closeOrBackInPage();
- } finally {
- setLoading(false);
+ } else {
+ throw result.error;
}
}
return;
}
+
setLoading(true);
- try {
- await setDisappearingMessagesByConvoId(selectedConversationKey, modeSelected, timeSelected);
+ const result = await setDisappearingMessagesWithErrorHandling(
+ selectedConversationKey,
+ modeSelected,
+ timeSelected
+ );
+ setLoading(false);
+ if (result.success) {
closeOrBackInPage();
- } finally {
- setLoading(false);
+ } else {
+ throw result.error;
}
};
- useEffect(() => {
- // NOTE loads a time value from the conversation model or the default
- setTimeSelected(
- expireTimer !== undefined && expireTimer > -1
- ? expireTimer
- : loadDefaultTimeValue(modeSelected, hasOnlyOneMode)
- );
- }, [expireTimer, hasOnlyOneMode, modeSelected]);
+ useHandleExpirationTimeChange(setTimeSelected, modeSelected, hasOnlyOneMode, expireTimer);
if (!disappearingModeOptions) {
return null;
@@ -180,7 +270,7 @@ export const DisappearingMessagesForConversationModal = (props: ConversationSett
buttonChildren={
{loading ? (
-
+
) : (
) {
}
export function DebugMenuModal() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [page, setPage] = useState(DEBUG_MENU_PAGE.MAIN);
@@ -215,7 +215,7 @@ export function DebugMenuModal() {
$container={true}
$flexDirection="column"
$alignItems="flex-start"
- padding="var(--margins-sm) 0 var(--margins-xl)"
+ $padding="var(--margins-sm) 0 var(--margins-xl)"
>
{getPage(page, setPage)}
diff --git a/ts/components/dialog/debug/FeatureFlags.tsx b/ts/components/dialog/debug/FeatureFlags.tsx
index 202a7fdb22..1d1fc826cf 100644
--- a/ts/components/dialog/debug/FeatureFlags.tsx
+++ b/ts/components/dialog/debug/FeatureFlags.tsx
@@ -1,18 +1,18 @@
import { isBoolean } from 'lodash';
import { Dispatch, useCallback, useEffect, useMemo, useState } from 'react';
import { clipboard } from 'electron';
-import { useDispatch } from 'react-redux';
import useAsync from 'react-use/lib/useAsync';
import { ProConfig, ProProof } from 'libsession_util_nodejs';
+import { getAppDispatch } from '../../../state/dispatch';
import {
getDataFeatureFlag,
getFeatureFlag,
MockProAccessExpiryOptions,
SessionDataFeatureFlags,
- useDataFeatureFlag,
+ getDataFeatureFlagMemo,
type SessionDataFeatureFlagKeys,
type SessionBooleanFeatureFlagKeys,
- useFeatureFlag,
+ getFeatureFlagMemo,
} from '../../../state/ducks/types/releasedFeaturesReduxTypes';
import { Flex } from '../../basic/Flex';
import { SessionToggle } from '../../basic/SessionToggle';
@@ -38,8 +38,11 @@ import {
defaultProDataFeatureFlags,
} from '../../../state/ducks/types/defaultFeatureFlags';
import { UserConfigWrapperActions } from '../../../webworker/workers/browser/libsession/libsession_worker_userconfig_interface';
-import { useProAccessDetails } from '../../../hooks/useHasPro';
import { isDebugMode } from '../../../shared/env_vars';
+import {
+ useProBackendProDetails,
+ useProBackendRefetch,
+} from '../../../state/selectors/proBackendData';
type FeatureFlagToggleType = {
forceUpdate: () => void;
@@ -261,7 +264,7 @@ export const FlagIntegerInput = ({
visibleWithBooleanFlag,
label,
}: FlagIntegerInputProps) => {
- const currentValue = useDataFeatureFlag(flag);
+ const currentValue = getDataFeatureFlagMemo(flag);
const key = `feature-flag-integer-input-${flag}`;
const [value, setValue] = useState(() => {
const initValue = window.sessionDataFeatureFlags[flag];
@@ -623,8 +626,8 @@ export function FeatureFlagDumper({ forceUpdate }: { forceUpdate: () => void })
}
function MessageProFeatures({ forceUpdate }: { forceUpdate: () => void }) {
- const proIsAvailable = useFeatureFlag('proAvailable');
- const value = useDataFeatureFlag('mockMessageProFeatures') ?? [];
+ const proIsAvailable = getFeatureFlagMemo('proAvailable');
+ const value = getDataFeatureFlagMemo('mockMessageProFeatures') ?? [];
if (!proIsAvailable) {
return null;
@@ -841,7 +844,8 @@ function ProConfigForm({
}
function ProConfigManager({ forceUpdate }: { forceUpdate: () => void }) {
- const { refetch, isFetching } = useProAccessDetails();
+ const { isFetching } = useProBackendProDetails();
+ const refetch = useProBackendRefetch();
const [proConfig, setProConfig] = useState(null);
const getProConfig = useCallback(async () => {
const config = await UserConfigWrapperActions.getProConfig();
@@ -883,9 +887,9 @@ export const ProDebugSection = ({
forceUpdate,
setPage,
}: DebugMenuPageProps & { forceUpdate: () => void }) => {
- const dispatch = useDispatch();
- const mockExpiry = useDataFeatureFlag('mockProAccessExpiry');
- const proAvailable = useFeatureFlag('proAvailable');
+ const dispatch = getAppDispatch();
+ const mockExpiry = getDataFeatureFlagMemo('mockProAccessExpiry');
+ const proAvailable = getFeatureFlagMemo('proAvailable');
const resetPro = useCallback(async () => {
await UserConfigWrapperActions.removeProConfig();
diff --git a/ts/components/dialog/debug/components.tsx b/ts/components/dialog/debug/components.tsx
index 1c04c28e34..361dae1d49 100644
--- a/ts/components/dialog/debug/components.tsx
+++ b/ts/components/dialog/debug/components.tsx
@@ -1,6 +1,5 @@
import useAsync from 'react-use/lib/useAsync';
import { ipcRenderer, shell } from 'electron';
-import { useDispatch } from 'react-redux';
import { useCallback, useState } from 'react';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import useInterval from 'react-use/lib/useInterval';
@@ -9,6 +8,7 @@ import styled from 'styled-components';
import type { ProProof, PubkeyType } from 'libsession_util_nodejs';
import { chunk, toNumber } from 'lodash';
+import { getAppDispatch } from '../../../state/dispatch';
import { Flex } from '../../basic/Flex';
import { SpacerXS } from '../../basic/Text';
import { tr } from '../../../localization/localeTools';
@@ -56,7 +56,7 @@ import {
import { formatRoundedUpTimeUntilTimestamp } from '../../../util/i18n/formatting/generics';
import { LucideIcon } from '../../icon/LucideIcon';
import { LUCIDE_ICONS_UNICODE } from '../../icon/lucide';
-import { useIsProAvailable } from '../../../hooks/useIsProAvailable';
+import { getIsProAvailableMemo } from '../../../hooks/useIsProAvailable';
import { UserConfigWrapperActions } from '../../../webworker/workers/browser/libsession/libsession_worker_userconfig_interface';
type DebugButtonProps = SessionButtonProps & { shiny?: boolean; hide?: boolean };
@@ -177,7 +177,7 @@ const CheckVersionButton = ({ channelToCheck }: { channelToCheck: ReleaseChannel
);
}}
>
-
+
{!loading && !state.loading ? `Check ${channelToCheck} version` : null}
);
@@ -220,7 +220,7 @@ const CheckForUpdatesButton = () => {
void handleCheckForUpdates();
}}
>
-
+
{!state.loading ? 'Check for updates' : null}
);
@@ -314,7 +314,7 @@ export const LoggingDebugSection = ({ forceUpdate }: { forceUpdate: () => void }
};
export const Playgrounds = ({ setPage }: DebugMenuPageProps) => {
- const proAvailable = useIsProAvailable();
+ const proAvailable = getIsProAvailableMemo();
if (!proAvailable) {
return null;
@@ -329,8 +329,8 @@ export const Playgrounds = ({ setPage }: DebugMenuPageProps) => {
};
export const DebugActions = () => {
- const dispatch = useDispatch();
- const proAvailable = useIsProAvailable();
+ const dispatch = getAppDispatch();
+ const proAvailable = getIsProAvailableMemo();
return (
@@ -479,7 +479,7 @@ export const DebugUrlInteractionsSection = () => {
};
export const ExperimentalActions = ({ forceUpdate }: { forceUpdate: () => void }) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
// const refreshedAt = useReleasedFeaturesRefreshedAt();
// const sesh101NotificationAt = useSesh101NotificationAt();
diff --git a/ts/components/dialog/debug/hooks/useDebugInputCommands.tsx b/ts/components/dialog/debug/hooks/useDebugInputCommands.tsx
index 7e9243cdbc..b0042dd874 100644
--- a/ts/components/dialog/debug/hooks/useDebugInputCommands.tsx
+++ b/ts/components/dialog/debug/hooks/useDebugInputCommands.tsx
@@ -1,7 +1,7 @@
import { type Dispatch, useEffect } from 'react';
import { isDevProd } from '../../../../shared/env_vars';
import { Constants } from '../../../../session';
-import { useFeatureFlag } from '../../../../state/ducks/types/releasedFeaturesReduxTypes';
+import { getFeatureFlagMemo } from '../../../../state/ducks/types/releasedFeaturesReduxTypes';
type DebugInputCommandsArgs = {
value: string;
@@ -25,7 +25,7 @@ export function useDebugInputCommands({ value, setValue }: DebugInputCommandsArg
}
// eslint-disable-next-line react-hooks/rules-of-hooks -- Conditional doesn't change at runtime
- const debugInputCommands = useFeatureFlag('debugInputCommands');
+ const debugInputCommands = getFeatureFlagMemo('debugInputCommands');
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
diff --git a/ts/components/dialog/debug/hooks/useReleaseChannel.tsx b/ts/components/dialog/debug/hooks/useReleaseChannel.tsx
index 4bc418db66..0f57bdb00e 100644
--- a/ts/components/dialog/debug/hooks/useReleaseChannel.tsx
+++ b/ts/components/dialog/debug/hooks/useReleaseChannel.tsx
@@ -1,5 +1,5 @@
-import { useDispatch } from 'react-redux';
import useUpdate from 'react-use/lib/useUpdate';
+import { getAppDispatch } from '../../../../state/dispatch';
import { tr } from '../../../../localization/localeTools';
import { Storage } from '../../../../util/storage';
import { updateConfirmModal } from '../../../../state/ducks/modalDialog';
@@ -14,7 +14,7 @@ export const useReleaseChannel = (): {
setReleaseChannel: (channel: ReleaseChannels) => void;
} => {
const releaseChannel = Storage.get('releaseChannel') as ReleaseChannels;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const forceUpdate = useUpdate();
return {
diff --git a/ts/components/dialog/debug/playgrounds/PopoverPlaygroundPage.tsx b/ts/components/dialog/debug/playgrounds/PopoverPlaygroundPage.tsx
index 8b7b58c851..032e8a5d18 100644
--- a/ts/components/dialog/debug/playgrounds/PopoverPlaygroundPage.tsx
+++ b/ts/components/dialog/debug/playgrounds/PopoverPlaygroundPage.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { RefObject, useRef, useState } from 'react';
import styled from 'styled-components';
import useUpdate from 'react-use/lib/useUpdate';
import { SessionTooltip, type TooltipProps, useTriggerPosition } from '../../../SessionTooltip';
@@ -9,12 +9,12 @@ import { SessionButton } from '../../../basic/SessionButton';
import { SpacerXS } from '../../../basic/Text';
import { SimpleSessionInput } from '../../../inputs/SessionInput';
-const StyledPopoverContainer = styled.div<{ marginTop?: number; marginBottom?: number }>`
+const StyledPopoverContainer = styled.div<{ $marginTop?: number; $marginBottom?: number }>`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-self: center;
- margin-top: ${({ marginTop }) => marginTop ?? 0}px;
- margin-bottom: ${({ marginBottom }) => marginBottom ?? 0}px;
+ margin-top: ${({ $marginTop: marginTop }) => marginTop ?? 0}px;
+ margin-bottom: ${({ $marginBottom: marginBottom }) => marginBottom ?? 0}px;
`;
const StyledTrigger = styled.div`
font-size: var(--text-size-xs);
@@ -35,16 +35,17 @@ function PopoverGrid(
const r5 = useRef(null);
const r6 = useRef(null);
- const t1 = useTriggerPosition(r1);
- const t2 = useTriggerPosition(r2);
- const t3 = useTriggerPosition(r3);
- const t4 = useTriggerPosition(r4);
- const t5 = useTriggerPosition(r5);
- const t6 = useTriggerPosition(r6);
+ // FIXME: remove as cast
+ const t1 = useTriggerPosition(r1 as RefObject);
+ const t2 = useTriggerPosition(r2 as RefObject);
+ const t3 = useTriggerPosition(r3 as RefObject);
+ const t4 = useTriggerPosition(r4 as RefObject);
+ const t5 = useTriggerPosition(r5 as RefObject);
+ const t6 = useTriggerPosition(r6 as RefObject);
return (
<>
-
+
Left
-
+
Left
) {
return (
<>
-
+
Left
@@ -108,7 +109,7 @@ function TooltipGrid(props: Omit) {
Right
-
+
Left
diff --git a/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx b/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx
index 2bd3a1bd88..45fb9de762 100644
--- a/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx
+++ b/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx
@@ -8,12 +8,12 @@ import { LUCIDE_ICONS_UNICODE } from '../../../icon/lucide';
import { DebugButton } from '../components';
import { DebugMenuPageProps, DebugMenuSection } from '../DebugMenuModal';
import { CTAVariant } from '../../cta/types';
-import { useIsProAvailable } from '../../../../hooks/useIsProAvailable';
+import { getIsProAvailableMemo } from '../../../../hooks/useIsProAvailable';
export function ProPlaygroundPage(props: DebugMenuPageProps) {
const forceUpdate = useUpdate();
const handleClick = useShowSessionCTACbWithVariant();
- const proAvailable = useIsProAvailable();
+ const proAvailable = getIsProAvailableMemo();
if (!proAvailable) {
return null;
diff --git a/ts/components/dialog/user-settings/pages/AppearanceSettingsPage.tsx b/ts/components/dialog/user-settings/pages/AppearanceSettingsPage.tsx
index 2ca35edcc2..6c1abff20f 100644
--- a/ts/components/dialog/user-settings/pages/AppearanceSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/AppearanceSettingsPage.tsx
@@ -2,10 +2,10 @@ import useUpdate from 'react-use/lib/useUpdate';
import useInterval from 'react-use/lib/useInterval';
import styled from 'styled-components';
-import { useDispatch } from 'react-redux';
import { isFinite, isNumber, range } from 'lodash';
import { contextMenu, Menu } from 'react-contexify';
import { useRef } from 'react';
+import { getAppDispatch } from '../../../../state/dispatch';
import { type UserSettingsModalState } from '../../../../state/ducks/modalDialog';
import {
@@ -61,7 +61,7 @@ const StyledPrimaryColorSwitcherContainer = styled.div`
function PrimaryColorSwitcher() {
const selectedPrimaryColor = usePrimaryColor();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const diameterRadioBorder = 35;
return (
@@ -124,7 +124,7 @@ const StyledThemeName = styled.div`
const Themes = () => {
const themes = getThemeColors();
const selectedTheme = useTheme();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
<>
diff --git a/ts/components/dialog/user-settings/pages/BlockedContactsSettingsPage.tsx b/ts/components/dialog/user-settings/pages/BlockedContactsSettingsPage.tsx
index 16c5058a05..cd6f8b4639 100644
--- a/ts/components/dialog/user-settings/pages/BlockedContactsSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/BlockedContactsSettingsPage.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react';
import useUpdate from 'react-use/lib/useUpdate';
import styled from 'styled-components';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../../../state/dispatch';
import {
updateBlockOrUnblockModal,
@@ -43,7 +43,7 @@ export function BlockedContactsSettingsPage(modalState: UserSettingsModalState)
const backAction = useUserSettingsBackAction(modalState);
const closeAction = useUserSettingsCloseAction(modalState);
const title = useUserSettingsTitle(modalState);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [selectedIds, setSelectedIds] = useState>([]);
async function unBlockThoseUsers() {
diff --git a/ts/components/dialog/user-settings/pages/ConversationSettingsPage.tsx b/ts/components/dialog/user-settings/pages/ConversationSettingsPage.tsx
index 7ad2367e6e..5664140cc7 100644
--- a/ts/components/dialog/user-settings/pages/ConversationSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/ConversationSettingsPage.tsx
@@ -1,5 +1,6 @@
import useUpdate from 'react-use/lib/useUpdate';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
+import { getAppDispatch } from '../../../../state/dispatch';
import {
userSettingsModal,
@@ -16,11 +17,10 @@ import {
import { SettingsKey } from '../../../../data/settings-key';
import { SettingsToggleBasic } from '../components/SettingsToggleBasic';
import { ToastUtils } from '../../../../session/utils';
-import { toggleAudioAutoplay } from '../../../../state/ducks/userConfig';
-import { getAudioAutoplay } from '../../../../state/selectors/userConfig';
import { SettingsChevronBasic } from '../components/SettingsChevronBasic';
import { UserSettingsModalContainer } from '../components/UserSettingsModalContainer';
+import { getAudioAutoplay } from '../../../../state/selectors/settings';
async function toggleCommunitiesPruning() {
try {
@@ -40,7 +40,7 @@ export function ConversationSettingsPage(modalState: UserSettingsModalState) {
const backAction = useUserSettingsBackAction(modalState);
const closeAction = useUserSettingsCloseAction(modalState);
const title = useUserSettingsTitle(modalState);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isOpengroupPruningEnabled = Boolean(
window.getSettingValue(SettingsKey.settingsOpengroupPruning)
@@ -95,7 +95,7 @@ export function ConversationSettingsPage(modalState: UserSettingsModalState) {
baseDataTestId="audio-message-autoplay"
active={audioAutoPlay}
onClick={async () => {
- dispatch(toggleAudioAutoplay());
+ await window.setSettingValue(SettingsKey.audioAutoplay, !audioAutoPlay);
forceUpdate();
}}
text={{ token: 'conversationsAutoplayAudioMessage' }}
diff --git a/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx b/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx
index ecbee53c51..c833bb0eb4 100644
--- a/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx
@@ -1,7 +1,7 @@
import { type RefObject, useRef, useState } from 'react';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import useMount from 'react-use/lib/useMount';
+import { getAppDispatch } from '../../../../state/dispatch';
import { useHotkey } from '../../../../hooks/useHotkey';
import { useOurConversationUsername, useOurAvatarPath } from '../../../../hooks/useParamSelector';
import { UserUtils, ToastUtils } from '../../../../session/utils';
@@ -31,20 +31,20 @@ import { ModalPencilIcon } from '../../shared/ModalPencilButton';
import { ProfileHeader, ProfileName } from '../components';
import type { ProfileDialogModes } from '../ProfileDialogModes';
import { tr } from '../../../../localization/localeTools';
-import { useIsProAvailable } from '../../../../hooks/useIsProAvailable';
+import { getIsProAvailableMemo } from '../../../../hooks/useIsProAvailable';
import { setDebugMode } from '../../../../state/ducks/debug';
import { useHideRecoveryPasswordEnabled } from '../../../../state/selectors/settings';
import { OnionStatusLight } from '../../OnionStatusPathDialog';
import { UserSettingsModalContainer } from '../components/UserSettingsModalContainer';
-import {
- useCurrentUserHasExpiredPro,
- useCurrentUserHasPro,
- useProAccessDetails,
-} from '../../../../hooks/useHasPro';
+import { useCurrentUserHasExpiredPro, useCurrentUserHasPro } from '../../../../hooks/useHasPro';
import { NetworkTime } from '../../../../util/NetworkTime';
import { APP_URL, DURATION_SECONDS } from '../../../../session/constants';
import { getFeatureFlag } from '../../../../state/ducks/types/releasedFeaturesReduxTypes';
import { useUserSettingsCloseAction } from './userSettingsHooks';
+import {
+ useProBackendProDetails,
+ useProBackendRefetch,
+} from '../../../../state/selectors/proBackendData';
const handleKeyQRMode = (mode: ProfileDialogModes, setMode: (mode: ProfileDialogModes) => void) => {
switch (mode) {
@@ -61,7 +61,7 @@ const handleKeyQRMode = (mode: ProfileDialogModes, setMode: (mode: ProfileDialog
const handleKeyCancel = (
mode: ProfileDialogModes,
setMode: (mode: ProfileDialogModes) => void,
- inputRef: RefObject
+ inputRef: RefObject
) => {
switch (mode) {
case 'qr':
@@ -98,9 +98,9 @@ function LucideIconForSettings(props: Omit
@@ -238,7 +238,7 @@ function SettingsSection() {
}
function AdminSection() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const recoveryPasswordHidden = useHideRecoveryPasswordEnabled();
return (
@@ -302,7 +302,7 @@ const StyledSpanSessionInfo = styled.span<{ opacity?: number }>`
const SessionInfo = () => {
const [clickCount, setClickCount] = useState(0);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
@@ -348,9 +348,10 @@ const SessionInfo = () => {
};
export const DefaultSettingPage = (modalState: UserSettingsModalState) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const closeAction = useUserSettingsCloseAction(modalState);
- const { refetch, t } = useProAccessDetails();
+ const { t } = useProBackendProDetails();
+ const refetch = useProBackendRefetch();
const profileName = useOurConversationUsername() || '';
const [enlargedImage, setEnlargedImage] = useState(false);
@@ -397,7 +398,7 @@ export const DefaultSettingPage = (modalState: UserSettingsModalState) => {
$container={true}
$flexDirection="column"
$alignItems="center"
- paddingBlock="var(--margins-md)"
+ $paddingBlock="var(--margins-md)"
$flexGap="var(--margins-md)"
width="100%"
>
diff --git a/ts/components/dialog/user-settings/pages/EditPasswordSettingsPage.tsx b/ts/components/dialog/user-settings/pages/EditPasswordSettingsPage.tsx
index 10a470f371..575801d746 100644
--- a/ts/components/dialog/user-settings/pages/EditPasswordSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/EditPasswordSettingsPage.tsx
@@ -356,7 +356,7 @@ export function EditPasswordSettingsPage(modalState: {
void)
}
function HasPasswordSubSection() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
diff --git a/ts/components/dialog/user-settings/pages/RecoveryPasswordSettingsPage.tsx b/ts/components/dialog/user-settings/pages/RecoveryPasswordSettingsPage.tsx
index 8bc45aea90..4bfd90da34 100644
--- a/ts/components/dialog/user-settings/pages/RecoveryPasswordSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/RecoveryPasswordSettingsPage.tsx
@@ -1,6 +1,6 @@
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { useState } from 'react';
+import { getAppDispatch } from '../../../../state/dispatch';
import {
updateHideRecoveryPasswordModal,
@@ -72,7 +72,7 @@ export function RecoveryPasswordSettingsPage(modalState: UserSettingsModalState)
const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL(qrLogoProps);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const { hasPassword, passwordValid } = usePasswordModal({
onClose: () => {
diff --git a/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx b/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx
index d12e014031..738bf51a7a 100644
--- a/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx
+++ b/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../../../../state/dispatch';
import { ModalBasicHeader } from '../../../../SessionWrapperModal';
import { StakeSection } from './sections/StakeSection';
import { ExtraSmallText, LastRefreshedText } from './components';
@@ -23,7 +23,7 @@ import { ModalBackButton } from '../../../shared/ModalBackButton';
import { UserSettingsModalContainer } from '../../components/UserSettingsModalContainer';
function ReloadButton({ loading }: { loading: boolean }) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
`
type GradientContainerProps = {
height?: string;
width?: string;
- paddingInline?: string;
- paddingBlock?: string;
+ $paddingInline?: string;
+ $paddingBlock?: string;
};
const GradientContainer = styled.div`
position: relative;
${props => props.height && `height: ${props.height};`}
${props => props.width && `width: ${props.width};`}
- ${props => props.paddingInline && `padding-inline: ${props.paddingInline};`}
- ${props => props.paddingBlock && `padding-block: ${props.paddingBlock};`}
+ ${props => props.$paddingInline && `padding-inline: ${props.$paddingInline};`}
+ ${props => props.$paddingBlock && `padding-block: ${props.$paddingBlock};`}
`;
export const BackgroundGradientContainer = ({
@@ -190,8 +190,8 @@ export const BackgroundGradientContainer = ({
noGradient,
height,
width,
- paddingInline,
- paddingBlock,
+ $paddingInline: paddingInline,
+ $paddingBlock: paddingBlock,
}: GradientContainerProps & {
children: ReactNode;
noGradient?: boolean;
@@ -202,8 +202,8 @@ export const BackgroundGradientContainer = ({
{!noGradient ? (
<>
diff --git a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph1.tsx b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph1.tsx
index f87a23fe70..76f54a7054 100644
--- a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph1.tsx
+++ b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph1.tsx
@@ -13,9 +13,9 @@ export const NodeGraph1 = (props: Omit) => (
diff --git a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph2.tsx b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph2.tsx
index dca1293fe2..15d3f45a67 100644
--- a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph2.tsx
+++ b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph2.tsx
@@ -14,9 +14,9 @@ export const NodeGraph2 = (props: NodeGraphProps) => (
@@ -31,9 +31,9 @@ export const NodeGraph2 = (props: NodeGraphProps) => (
diff --git a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph3.tsx b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph3.tsx
index 7d3cf1cc72..033de12f7f 100644
--- a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph3.tsx
+++ b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph3.tsx
@@ -18,9 +18,9 @@ export const NodeGraph3 = (props: NodeGraphProps) => (
@@ -35,9 +35,9 @@ export const NodeGraph3 = (props: NodeGraphProps) => (
diff --git a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph4.tsx b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph4.tsx
index 740a0f05c3..4a81809e47 100644
--- a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph4.tsx
+++ b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph4.tsx
@@ -18,9 +18,9 @@ export const NodeGraph4 = (props: NodeGraphProps) => (
@@ -35,9 +35,9 @@ export const NodeGraph4 = (props: NodeGraphProps) => (
@@ -52,9 +52,9 @@ export const NodeGraph4 = (props: NodeGraphProps) => (
diff --git a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph5.tsx b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph5.tsx
index 26c011c2b7..fce0034a90 100644
--- a/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph5.tsx
+++ b/ts/components/dialog/user-settings/pages/network/nodes/NodeGraph5.tsx
@@ -17,9 +17,9 @@ export const NodeGraph5 = (props: NodeGraphProps) => (
/>
(
(
{
$alignItems="center"
overflowY="hidden"
height="100%"
- maxHeight="64px"
+ $maxHeight="64px"
>
@@ -96,8 +96,8 @@ const NodesStats = ({ style }: { style?: CSSProperties }) => {
$alignItems="center"
overflowY="hidden"
height="100%"
- maxHeight="64px"
- margin="0 0 auto 0"
+ $maxHeight="64px"
+ $margin="0 0 auto 0"
>
@@ -154,8 +154,8 @@ const CurrentPriceBlock = () => {
$flexDirection="row"
$justifyContent="space-between"
$alignItems="flex-start"
- paddingInline={'12px 0'}
- paddingBlock={'var(--margins-md)'}
+ $paddingInline={'12px 0'}
+ $paddingBlock={'var(--margins-md)'}
backgroundColor={
isDarkTheme ? 'var(--background-primary-color)' : 'var(--background-secondary-color)'
}
@@ -227,8 +227,8 @@ const SecuredByBlock = () => {
$alignItems="flex-start"
$flexGrow={1}
width={'100%'}
- paddingInline={'12px 0'}
- paddingBlock={'var(--margins-md)'}
+ $paddingInline={'12px 0'}
+ $paddingBlock={'var(--margins-md)'}
backgroundColor={
isDarkTheme ? 'var(--background-primary-color)' : 'var(--background-secondary-color)'
}
@@ -247,7 +247,7 @@ const SecuredByBlock = () => {
export function NetworkSection() {
const htmlDirection = useHTMLDirection();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const { swarmNodeCount, dataIsStale } = useSecuringNodesCount();
diff --git a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
index ed537586a5..c1b93977e5 100644
--- a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
+++ b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
@@ -1,6 +1,6 @@
import styled from 'styled-components';
import { type ReactNode } from 'react';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../../../../state/dispatch';
import { tr } from '../../../../../localization/localeTools';
import { Localizer } from '../../../../basic/Localizer';
import { ModalBasicHeader } from '../../../../SessionWrapperModal';
@@ -19,16 +19,17 @@ import { showLinkVisitWarningDialog } from '../../../OpenUrlModal';
import { proButtonProps } from '../../../SessionCTA';
import { Flex } from '../../../../basic/Flex';
import type { ProNonOriginatingPageVariant } from '../../../../../types/ReduxTypes';
-import { useCurrentNeverHadPro, useProAccessDetails } from '../../../../../hooks/useHasPro';
+import { useCurrentNeverHadPro } from '../../../../../hooks/useHasPro';
import LIBSESSION_CONSTANTS from '../../../../../session/utils/libsession/libsession_constants';
import { ProPaymentProvider } from '../../../../../session/apis/pro_backend_api/types';
+import { useProBackendProDetails } from '../../../../../state/selectors/proBackendData';
type VariantPageProps = {
variant: ProNonOriginatingPageVariant;
};
function ProStatusTextUpdate() {
- const { data } = useProAccessDetails();
+ const { data } = useProBackendProDetails();
return data.autoRenew ? (
}
@@ -148,7 +149,7 @@ function ProInfoBlockDevice({ textElement }: { textElement: ReactNode }) {
}
function ProInfoBlockDeviceLinked() {
- const { data } = useProAccessDetails();
+ const { data } = useProBackendProDetails();
const hasNeverHadPro = useCurrentNeverHadPro();
return (
}
@@ -221,8 +222,8 @@ function ProInfoBlockLayout({
}
function ProInfoBlockUpgrade() {
- const dispatch = useDispatch();
- const { data } = useProAccessDetails();
+ const dispatch = getAppDispatch();
+ const { data } = useProBackendProDetails();
return (
;
@@ -494,8 +495,8 @@ function ProInfoBlock({ variant }: VariantPageProps) {
}
function ProPageButtonUpdate() {
- const dispatch = useDispatch();
- const { data } = useProAccessDetails();
+ const dispatch = getAppDispatch();
+ const { data } = useProBackendProDetails();
return (
`
+const HeroImageBg = styled.div<{ $noColors?: boolean }>`
padding-top: 55px;
justify-items: center;
@@ -97,7 +100,7 @@ const HeroImageBg = styled.div<{ noColors?: boolean }>`
circle,
color-mix(
in srgb,
- ${props => (props.noColors ? 'var(--disabled-color) 35%' : 'var(--primary-color) 25%')},
+ ${props => (props.$noColors ? 'var(--disabled-color) 35%' : 'var(--primary-color) 25%')},
transparent
)
0%,
@@ -123,11 +126,11 @@ const HeroImageLabelContainer = styled.div`
}
`;
-export const StyledProStatusText = styled.div<{ isError?: boolean }>`
+export const StyledProStatusText = styled.div<{ $isError?: boolean }>`
text-align: center;
line-height: var(--font-size-sm);
font-size: var(--font-size-sm);
- ${props => (props.isError ? 'color: var(--warning-color);' : '')}
+ ${props => (props.$isError ? 'color: var(--warning-color);' : '')}
`;
export const StyledProHeroText = styled.div`
@@ -155,7 +158,7 @@ export function ProHeroImage({
-
+
{heroStatusText ? (
- {heroStatusText}
+ {heroStatusText}
) : null}
{heroStatusText && heroText ? : null}
{heroText ? {heroText} : null}
@@ -183,8 +186,8 @@ export function ProHeroImage({
}
function useBackendErrorDialogButtons() {
- const dispatch = useDispatch();
- const { refetch } = useProAccessDetails();
+ const dispatch = getAppDispatch();
+ const refetch = useProBackendRefetch();
const buttons = useMemo(() => {
return [
@@ -211,15 +214,23 @@ function useBackendErrorDialogButtons() {
return buttons;
}
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useProBackendProDetailsInternal = useProBackendProDetails;
+const useCurrentUserHasProInternal = useCurrentUserHasPro;
+const useCurrentUserHasExpiredProInternal = useCurrentUserHasExpiredPro;
+const useCurrentNeverHadProInternal = useCurrentNeverHadPro;
+const useIsDarkThemeInternal = useIsDarkTheme;
+const usePinnedConversationsCountInternal = usePinnedConversationsCount;
+
function ProNonProContinueButton({ state }: SectionProps) {
const { returnToThisModalAction, centerAlign, afterCloseAction } = state;
- const dispatch = useDispatch();
- const neverHadPro = useCurrentNeverHadPro();
- const { isLoading, isError } = useProAccessDetails();
+ const dispatch = getAppDispatch();
+ const neverHadPro = useCurrentNeverHadProInternal();
+ const { isLoading, isError } = useProBackendProDetailsInternal();
const backendErrorButtons = useBackendErrorDialogButtons();
- const handleClick = useCallback(() => {
+ const handleClick = () => {
dispatch(
isError
? updateLocalizedPopupDialog({
@@ -240,16 +251,7 @@ function ProNonProContinueButton({ state }: SectionProps) {
centerAlign,
})
);
- }, [
- dispatch,
- isLoading,
- isError,
- backendErrorButtons,
- centerAlign,
- neverHadPro,
- returnToThisModalAction,
- afterCloseAction,
- ]);
+ };
return (
`
const proBoxShadow = '0 4px 4px 0 rgba(0, 0, 0, 0.25)';
const proBoxShadowSmall = '0 4px 4px 0 rgba(0, 0, 0, 0.15)';
+function formatProStats(v: number) {
+ return formatNumber(v, {
+ notation: 'compact',
+ compactDisplay: 'short', // Uses 'K', 'M', 'B' etc.
+ }).toLocaleLowerCase();
+}
+
function ProStats() {
- const mockProLongerMessagesSent = useDataFeatureFlag('mockProLongerMessagesSent');
- const mockProPinnedConversations = useDataFeatureFlag('mockProPinnedConversations');
- const mockProBadgesSent = useDataFeatureFlag('mockProBadgesSent');
- const mockProGroupsUpgraded = useDataFeatureFlag('mockProGroupsUpgraded');
+ const mockProLongerMessagesSent = getDataFeatureFlagMemo('mockProLongerMessagesSent');
+ const mockProPinnedConversations = getDataFeatureFlagMemo('mockProPinnedConversations');
+ const mockProBadgesSent = getDataFeatureFlagMemo('mockProBadgesSent');
+ const mockProGroupsUpgraded = getDataFeatureFlagMemo('mockProGroupsUpgraded');
- const pinnedConversations = usePinnedConversationsCount();
+ const pinnedConversations = usePinnedConversationsCountInternal();
const proLongerMessagesSent =
mockProLongerMessagesSent ?? (Storage.get(SettingsKey.proLongerMessagesSent) || 0);
@@ -305,20 +314,11 @@ function ProStats() {
const proPinnedConversations = mockProPinnedConversations ?? (pinnedConversations || 0);
const proGroupsUpgraded = mockProGroupsUpgraded || 0;
- const isDarkTheme = useIsDarkTheme();
+ const isDarkTheme = useIsDarkThemeInternal();
- const formatter = useMemo(
- () =>
- new Intl.NumberFormat(getBrowserLocale(), {
- notation: 'compact',
- compactDisplay: 'short', // Uses 'K', 'M', 'B' etc.
- }),
- []
- );
-
- const proGroupsAvailable = useIsProGroupsAvailable();
+ const proGroupsAvailable = getIsProGroupsAvailableMemo();
- const userHasPro = useCurrentUserHasPro();
+ const userHasPro = useCurrentUserHasProInternal();
if (!userHasPro) {
return null;
}
@@ -362,7 +362,7 @@ function ProStats() {
{tr('proLongerMessagesSent', {
count: proLongerMessagesSent,
- total: formatter.format(proLongerMessagesSent).toLocaleLowerCase(),
+ total: formatProStats(proLongerMessagesSent),
})}
@@ -375,7 +375,7 @@ function ProStats() {
{tr('proPinnedConversations', {
count: proPinnedConversations,
- total: formatter.format(proPinnedConversations).toLocaleLowerCase(),
+ total: formatProStats(proPinnedConversations),
})}
@@ -390,7 +390,7 @@ function ProStats() {
{tr('proBadgesSent', {
count: proBadgesSent,
- total: formatter.format(proBadgesSent).toLocaleLowerCase(),
+ total: formatProStats(proBadgesSent),
})}
@@ -403,7 +403,7 @@ function ProStats() {
{tr('proGroupsUpgraded', {
count: proGroupsUpgraded,
- total: formatter.format(proGroupsUpgraded).toLocaleLowerCase(),
+ total: formatProStats(proGroupsUpgraded),
})}
{
+ const handleUpdateAccessClick = () => {
dispatch(
isError
? updateLocalizedPopupDialog({
@@ -465,7 +465,7 @@ function ProSettings({ state }: SectionProps) {
centerAlign,
})
);
- }, [dispatch, isLoading, isError, backendErrorButtons, centerAlign, returnToThisModalAction]);
+ };
if (state.fromCTA ? !userHasPro : userNeverHadPro) {
return ;
@@ -524,7 +524,7 @@ function ProFeatureItem({
dataTestId: SessionDataTestId;
onClick?: () => Promise;
}) {
- const isDarkTheme = useIsDarkTheme();
+ const isDarkTheme = useIsDarkThemeInternal();
return (
<>
{iconElement}
@@ -583,7 +583,7 @@ function ProFeatureIconElement({
position,
noColor,
}: WithLucideUnicode & WithProFeaturePosition & { noColor?: boolean }) {
- const isDarkTheme = useIsDarkTheme();
+ const isDarkTheme = useIsDarkThemeInternal();
const bgStyle =
position === 0
? 'linear-gradient(135deg, #57C9FA 0%, #C993FF 100%)'
@@ -680,10 +680,10 @@ function getProFeatures(userHasPro: boolean): Array<
}
function ProFeatures({ state }: SectionProps) {
- const dispatch = useDispatch();
- const userHasPro = useCurrentUserHasPro();
- const expiredPro = useCurrentUserHasExpiredPro();
- const proFeatures = useMemo(() => getProFeatures(userHasPro), [userHasPro]);
+ const dispatch = getAppDispatch();
+ const userHasPro = useCurrentUserHasProInternal();
+ const expiredPro = useCurrentUserHasExpiredProInternal();
+ const proFeatures = getProFeatures(userHasPro);
return (
@@ -738,9 +738,9 @@ function ProFeatures({ state }: SectionProps) {
}
function ManageProCurrentAccess({ state }: SectionProps) {
- const dispatch = useDispatch();
- const { data } = useProAccessDetails();
- const userHasPro = useCurrentUserHasPro();
+ const dispatch = getAppDispatch();
+ const { data } = useProBackendProDetailsInternal();
+ const userHasPro = useCurrentUserHasProInternal();
if (!userHasPro) {
return null;
}
@@ -790,17 +790,17 @@ function ManageProCurrentAccess({ state }: SectionProps) {
}
function ManageProAccess({ state }: SectionProps) {
- const dispatch = useDispatch();
- const isDarkTheme = useIsDarkTheme();
- const userHasExpiredPro = useCurrentUserHasExpiredPro();
+ const dispatch = getAppDispatch();
+ const isDarkTheme = useIsDarkThemeInternal();
+ const userHasExpiredPro = useCurrentUserHasExpiredProInternal();
const { returnToThisModalAction, centerAlign } = state;
- const { isLoading, isError } = useProAccessDetails();
+ const { isLoading, isError } = useProBackendProDetailsInternal();
const backendErrorButtons = useBackendErrorDialogButtons();
- const handleClickRenew = useCallback(() => {
+ const handleClickRenew = () => {
dispatch(
isError
? updateLocalizedPopupDialog({
@@ -820,13 +820,7 @@ function ManageProAccess({ state }: SectionProps) {
centerAlign,
})
);
- }, [dispatch, isLoading, isError, backendErrorButtons, centerAlign, returnToThisModalAction]);
-
- /** const handleClickRecover = useCallback(async () => {
- // isLoading state needs to be reset to true as we are hard reloading and need to show that in the UI
- setProBackendIsLoading({ key: 'details', result: true });
- refetch({ callerContext: 'recover' });
- }, [refetch, setProBackendIsLoading]); */
+ };
return (
@@ -864,41 +858,19 @@ function ManageProAccess({ state }: SectionProps) {
: {})}
/>
) : null}
- {/** NOTE: this is being removed but we'll keep the code for now in case we need it back again
- void handleClickRecover()}
- iconElement={
- isLoading ? (
-
- ) : (
-
- )
- }
- rowReverse
- /> */}
);
}
function ManageProPreviousAccess(props: SectionProps) {
- const userHasExpiredPro = useCurrentUserHasExpiredPro();
+ const userHasExpiredPro = useCurrentUserHasExpiredProInternal();
return userHasExpiredPro ? : null;
}
-function ManageProRecoverAccess(_props: SectionProps) {
- return null;
- /** NOTE: keep this for now, if we want to re-add the never had pro revcover button we need this, otherwise we can delete on launch
- * const neverHadPro = useCurrentNeverHadPro();
- * return neverHadPro ? : null;
- */
-}
-
function ProHelp() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
@@ -927,15 +899,48 @@ function ProHelp() {
);
}
+function HeroStatusText({
+ isError,
+ isLoading,
+ isPro,
+}: {
+ isError?: boolean;
+ isLoading?: boolean;
+ isPro?: boolean;
+}) {
+ if (isError) {
+ return (
+
+
+
+
+ );
+ }
+ if (isLoading) {
+ return (
+
+ );
+ }
+ return null;
+}
+
function PageHero({ state }: SectionProps) {
- const dispatch = useDispatch();
- const isPro = useCurrentUserHasPro();
- const proExpired = useCurrentUserHasExpiredPro();
- const { isLoading, isError } = useProAccessDetails();
+ const dispatch = getAppDispatch();
+ const isPro = useCurrentUserHasProInternal();
+ const proExpired = useCurrentUserHasExpiredProInternal();
+ const { isLoading, isError } = useProBackendProDetailsInternal();
const backendErrorButtons = useBackendErrorDialogButtons();
- const handleClick = useCallback(() => {
+ const handleClick = () => {
if (isError) {
dispatch(
updateLocalizedPopupDialog({
@@ -972,38 +977,12 @@ function PageHero({ state }: SectionProps) {
)
);
}
- // Do nothing if not error or loading
- }, [dispatch, isPro, proExpired, isLoading, isError, backendErrorButtons, state.fromCTA]);
-
- const heroStatusText = useMemo(() => {
- if (isError) {
- return (
-
-
-
-
- );
- }
- if (isLoading) {
- return (
-
- );
- }
- return null;
- }, [isLoading, isError, isPro]);
+ };
return (
}
heroText={
isPro || (proExpired && !state.fromCTA) ? null : (
@@ -1022,7 +1001,7 @@ export function ProSettingsPage(modalState: {
centerAlign?: boolean;
afterCloseAction?: () => void;
}) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const backAction = useUserSettingsBackAction(modalState);
const closeAction = useUserSettingsCloseAction(modalState);
@@ -1061,7 +1040,6 @@ export function ProSettingsPage(modalState: {
{!modalState.fromCTA ? : null}
- {!modalState.fromCTA ? : null}
{!modalState.fromCTA ? : null}
{!modalState.fromCTA ? : null}
diff --git a/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx b/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx
index 2a2e155277..64ae50e843 100644
--- a/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx
+++ b/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../../../state/dispatch';
import { tr } from '../../../../localization/localeTools';
import {
userSettingsModal,
@@ -56,7 +56,7 @@ export function useUserSettingsTitle(page: UserSettingsModalState | undefined) {
}
export function useUserSettingsCloseAction(props: UserSettingsModalState) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (!props?.userSettingsPage) {
return null;
}
@@ -94,7 +94,7 @@ export function useUserSettingsCloseAction(props: UserSettingsModalState) {
}
export function useUserSettingsBackAction(modalState: UserSettingsModalState) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (modalState?.overrideBackAction) {
return modalState.overrideBackAction;
diff --git a/ts/components/icon/FileIcon.tsx b/ts/components/icon/FileIcon.tsx
index b413073274..762fa49e45 100644
--- a/ts/components/icon/FileIcon.tsx
+++ b/ts/components/icon/FileIcon.tsx
@@ -1,9 +1,9 @@
import type { CSSProperties, SessionDataTestId } from 'react';
import styled from 'styled-components';
-const FileIconWrapper = styled.img<{ iconColor?: string; iconSize: string }>`
- height: ${({ iconSize }) => iconSize};
- width: ${({ iconSize }) => iconSize};
+const FileIconWrapper = styled.img<{ $iconSize: string }>`
+ height: ${({ $iconSize: iconSize }) => iconSize};
+ width: ${({ $iconSize: iconSize }) => iconSize};
`;
export type FileIconProps = {
@@ -14,5 +14,5 @@ export type FileIconProps = {
};
export const FileIcon = ({ iconSize, dataTestId, src, style }: FileIconProps) => {
- return ;
+ return ;
};
diff --git a/ts/components/inputs/SessionInput.tsx b/ts/components/inputs/SessionInput.tsx
index 2d73de8b29..76a1a70408 100644
--- a/ts/components/inputs/SessionInput.tsx
+++ b/ts/components/inputs/SessionInput.tsx
@@ -1,9 +1,6 @@
import {
ChangeEvent,
SessionDataTestId,
- useCallback,
- useEffect,
- useRef,
useState,
type CSSProperties,
type KeyboardEvent,
@@ -20,12 +17,14 @@ import { SpacerMD } from '../basic/Text';
import { Localizer } from '../basic/Localizer';
import { ShowHideButton, type ShowHideButtonProps } from './ShowHidePasswordButton';
import type { TrArgs } from '../../localization/localeTools';
+import { useUpdateInputValue } from './useUpdateInputValue';
+import { StyledTextAreaContainer } from './SimpleSessionTextarea';
-type TextSizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+export type SessionInputTextSizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
-const StyledSessionInput = styled(Flex)<{
- error: boolean;
- textSize: TextSizes;
+export const StyledSessionInput = styled(Flex)<{
+ $error: boolean;
+ $textSize: SessionInputTextSizes;
}>`
position: relative;
width: 100%;
@@ -50,111 +49,68 @@ const StyledSessionInput = styled(Flex)<{
input::placeholder,
textarea::placeholder {
transition: opacity var(--default-duration) color var(--default-duration);
- ${props => props.error && `color: var(--danger-color); opacity: 1;`}
+ ${props => props.$error && `color: var(--danger-color); opacity: 1;`}
}
${props =>
- props.textSize &&
+ props.$textSize &&
`
${StyledInput} {
- font-size: var(--font-size-${props.textSize});
+ font-size: var(--font-size-${props.$textSize});
}
${StyledTextAreaContainer} {
- font-size: var(--font-size-${props.textSize});
+ font-size: var(--font-size-${props.$textSize});
textarea {
&:placeholder-shown {
- font-size: var(--font-size-${props.textSize});
+ font-size: var(--font-size-${props.$textSize});
}
}
}
`}
`;
-const StyledBorder = styled(AnimatedFlex)<{ shape: 'round' | 'square' | 'none' }>`
+const StyledBorder = styled(AnimatedFlex)<{ $shape: 'round' | 'square' | 'none' }>`
position: relative;
border: 1px solid var(--input-border-color);
border-radius: ${props =>
- props.shape === 'none' ? '0px' : props.shape === 'square' ? '7px' : '13px'};
+ props.$shape === 'none' ? '0px' : props.$shape === 'square' ? '7px' : '13px'};
`;
const StyledInput = styled(motion.input)<{
- error: boolean;
- textSize: TextSizes;
- centerText?: boolean;
- monospaced?: boolean;
- padding?: string;
+ $error: boolean;
+ $textSize: SessionInputTextSizes;
+ $centerText?: boolean;
+ $monospaced?: boolean;
+ $padding?: string;
}>`
outline: 0;
border: none;
width: 100%;
background: transparent;
- color: ${props => (props.error ? 'var(--danger-color)' : 'var(--input-text-color)')};
+ color: ${props => (props.$error ? 'var(--danger-color)' : 'var(--input-text-color)')};
- font-family: ${props => (props.monospaced ? 'var(--font-mono)' : 'var(--font-default)')};
+ font-family: ${props => (props.$monospaced ? 'var(--font-mono)' : 'var(--font-default)')};
line-height: 1.4;
- padding: ${props => (props.padding ? props.padding : 'var(--margins-lg)')};
- ${props => props.centerText && 'text-align: center;'}
- ${props => `font-size: var(--font-size-${props.textSize});`}
+ padding: ${props => (props.$padding ? props.$padding : 'var(--margins-lg)')};
+ ${props => props.$centerText && 'text-align: center;'}
+ ${props => `font-size: var(--font-size-${props.$textSize});`}
&::placeholder {
color: var(--input-text-placeholder-color);
- ${props => props.centerText && 'text-align: center;'}
+ ${props => props.$centerText && 'text-align: center;'}
}
`;
-const StyledTextAreaContainer = styled(motion.div)<{
- error: boolean;
- textSize: TextSizes;
- centerText?: boolean;
- monospaced?: boolean;
- padding?: string;
-}>`
- display: flex;
- align-items: center;
- position: relative;
- line-height: 1;
- height: 100%;
- width: 100%;
- padding: ${props => (props.padding ? props.padding : 'var(--margins-md)')};
-
- background: transparent;
- color: ${props => (props.error ? 'var(--danger-color)' : 'var(--input-text-color)')};
- outline: 0;
-
- font-family: ${props => (props.monospaced ? 'var(--font-mono)' : 'var(--font-default)')};
- ${props => `font-size: var(--font-size-${props.textSize});`}
-
- textarea {
- display: flex;
- height: 100%;
- width: 100%;
- outline: 0;
- border: none;
- background: transparent;
-
- resize: none;
- word-break: break-all;
- user-select: all;
-
- &:placeholder-shown {
- line-height: 1;
- font-family: ${props => (props.monospaced ? 'var(--font-mono)' : 'var(--font-default)')};
- ${props => `font-size: var(--font-size-${props.textSize});`}
- }
-
- &::placeholder {
- color: var(--input-text-placeholder-color);
- }
- }
-`;
-
-function BorderWithErrorState({ hasError, children }: { hasError: boolean } & PropsWithChildren) {
+export function BorderWithErrorState({
+ hasError,
+ children,
+}: { hasError: boolean } & PropsWithChildren) {
const inputShape = 'round';
return (
) => void;
};
-type WithInputRef = { inputRef?: RefObject };
-type WithTextAreaRef = { inputRef?: RefObject };
-
-function useUpdateInputValue(onValueChanged: (val: string) => void, disabled?: boolean) {
- return useCallback(
- (e: ChangeEvent) => {
- if (disabled) {
- return;
- }
- e.preventDefault();
- const val = e.target.value;
-
- onValueChanged(val);
- },
- [disabled, onValueChanged]
- );
-}
+type WithInputRef = { inputRef?: RefObject };
type SimpleSessionInputProps = Pick<
- Props,
+ GenericSessionInputProps,
| 'type'
| 'placeholder'
| 'value'
@@ -281,7 +221,7 @@ type SimpleSessionInputProps = Pick<
| 'centerText'
> &
WithInputRef &
- Required> & {
+ Required> & {
onValueChanged: (str: string) => void;
onEnterPressed: () => void;
providedError: string | TrArgs | undefined;
@@ -289,6 +229,9 @@ type SimpleSessionInputProps = Pick<
buttonEnd?: ReactNode;
};
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useUpdateInputValueInternal = useUpdateInputValue;
+
/**
* A simpler version of the SessionInput component.
* Does not handle CTA, textarea, nor monospaced fonts.
@@ -323,7 +266,7 @@ export const SimpleSessionInput = (props: SimpleSessionInputProps) => {
const hasError = !isEmpty(providedError);
const hasValue = !isEmpty(value);
- const updateInputValue = useUpdateInputValue(onValueChanged, disabled);
+ const updateInputValue = useUpdateInputValueInternal(onValueChanged, disabled);
const paddingInlineEnd = usePaddingForButtonInlineEnd({
hasButtonInlineEnd: !!buttonEnd && hasValue,
@@ -357,9 +300,9 @@ export const SimpleSessionInput = (props: SimpleSessionInputProps) => {
};
const containerProps = {
- error: hasError,
- textSize,
- padding,
+ $error: hasError,
+ $textSize: textSize,
+ $padding: padding,
};
return (
@@ -368,8 +311,8 @@ export const SimpleSessionInput = (props: SimpleSessionInputProps) => {
$flexDirection="column"
$justifyContent="center"
$alignItems="center"
- error={hasError}
- textSize={textSize}
+ $error={hasError}
+ $textSize={textSize}
>
;
};
-/**
- *
- * Also, and just like SimpleSessionInput, error handling and value management is to be done by the parent component.
- * Providing `error` will make the textarea red and the error string displayed below it.
- * This component should only be used for TextArea that does not need remote validations, as the error
- * state is live. For remote validations, use the SessionInput component.
- */
-export const SimpleSessionTextarea = (
- props: Pick<
- Props,
- | 'placeholder'
- | 'value'
- | 'ariaLabel'
- | 'maxLength'
- | 'autoFocus'
- | 'inputDataTestId'
- | 'textSize'
- | 'padding'
- | 'required'
- | 'tabIndex'
- > &
- WithTextAreaRef &
- Required> & {
- onValueChanged: (str: string) => void;
- providedError: string | TrArgs | undefined;
- disabled?: boolean;
- buttonEnd?: ReactNode;
- } & ({ singleLine: false } | { singleLine: true; onEnterPressed: () => void })
-) => {
- const {
- placeholder,
- value,
- ariaLabel,
- maxLength,
- providedError,
- onValueChanged,
- autoFocus,
- inputRef,
- inputDataTestId,
- errorDataTestId,
- textSize = 'sm',
- disabled,
- padding,
- required,
- tabIndex,
- buttonEnd,
- } = props;
- const hasError = !isEmpty(providedError);
- const hasValue = !isEmpty(value);
-
- const ref = useRef(inputRef?.current || null);
-
- const updateInputValue = useUpdateInputValue(onValueChanged, disabled);
-
- const paddingInlineEnd = usePaddingForButtonInlineEnd({
- hasButtonInlineEnd: !!buttonEnd && hasValue,
- });
-
- const inputProps: any = {
- type: 'text',
- placeholder,
- value,
- textSize,
- disabled,
- maxLength,
- padding,
- autoFocus,
- 'data-testid': inputDataTestId,
- required,
- 'aria-required': required,
- tabIndex,
- onChange: updateInputValue,
- style: { paddingInlineEnd, lineHeight: 1.5 },
- };
-
- const containerProps = {
- error: hasError,
- textSize,
- padding,
- };
-
- useEffect(() => {
- const textarea = ref.current;
- if (textarea) {
- // don't ask me why, but we need to reset the height to auto before calculating it here
- textarea.style.height = 'auto';
- // we want 12 lines of text at most
- textarea.style.height = `${Math.min(textarea.scrollHeight, 12 * parseFloat(getComputedStyle(textarea).lineHeight))}px`;
- }
- }, [ref, value]);
-
- return (
-
-
-
-
-
- {buttonEnd}
-
-
- {hasError ? (
- <>
-
-
- >
- ) : null}
-
- );
-};
-
export function ShowHideSessionInput(
props: Pick<
SimpleSessionInputProps,
diff --git a/ts/components/inputs/SimpleSessionTextarea.tsx b/ts/components/inputs/SimpleSessionTextarea.tsx
new file mode 100644
index 0000000000..c58742fa51
--- /dev/null
+++ b/ts/components/inputs/SimpleSessionTextarea.tsx
@@ -0,0 +1,183 @@
+import { useEffect, useRef, type ReactNode } from 'react';
+import { isEmpty } from 'lodash';
+import styled from 'styled-components';
+import { motion } from 'framer-motion';
+import { type TrArgs } from '../../localization/localeTools';
+import {
+ BorderWithErrorState,
+ type SessionInputTextSizes,
+ SimpleErrorItem,
+ StyledSessionInput,
+ type GenericSessionInputProps,
+} from './SessionInput';
+import { useUpdateInputValue } from './useUpdateInputValue';
+import { SpacerMD } from '../basic/Text';
+
+export const StyledTextAreaContainer = styled(motion.div)<{
+ $error: boolean;
+ $textSize: SessionInputTextSizes;
+ $monospaced?: boolean;
+ $padding?: string;
+}>`
+ display: flex;
+ align-items: center;
+ position: relative;
+ line-height: 1;
+ height: 100%;
+ width: 100%;
+ padding: ${props => (props.$padding ? props.$padding : 'var(--margins-md)')};
+
+ background: transparent;
+ color: ${props => (props.$error ? 'var(--danger-color)' : 'var(--input-text-color)')};
+ outline: 0;
+
+ font-family: ${props => (props.$monospaced ? 'var(--font-mono)' : 'var(--font-default)')};
+ ${props => `font-size: var(--font-size-${props.$textSize});`}
+
+ textarea {
+ display: flex;
+ height: 100%;
+ width: 100%;
+ outline: 0;
+ border: none;
+ background: transparent;
+
+ resize: none;
+ word-break: break-all;
+ user-select: all;
+
+ &:placeholder-shown {
+ line-height: 1;
+ font-family: ${props => (props.$monospaced ? 'var(--font-mono)' : 'var(--font-default)')};
+ ${props => `font-size: var(--font-size-${props.$textSize});`}
+ }
+
+ &::placeholder {
+ color: var(--input-text-placeholder-color);
+ }
+ }
+`;
+
+/**
+ *
+ * Also, and just like SimpleSessionInput, error handling and value management is to be done by the parent component.
+ * Providing `error` will make the textarea red and the error string displayed below it.
+ * This component should only be used for TextArea that does not need remote validations, as the error
+ * state is live. For remote validations, use the SessionInput component.
+ */
+export const SimpleSessionTextarea = (
+ props: Pick<
+ GenericSessionInputProps,
+ | 'placeholder'
+ | 'value'
+ | 'ariaLabel'
+ | 'maxLength'
+ | 'autoFocus'
+ | 'inputDataTestId'
+ | 'textSize'
+ | 'padding'
+ | 'required'
+ | 'tabIndex'
+ > &
+ Required> & {
+ onValueChanged: (str: string) => void;
+ providedError: string | TrArgs | undefined;
+ disabled?: boolean;
+ buttonEnd?: ReactNode;
+ } & ({ singleLine: false } | { singleLine: true; onEnterPressed: () => void })
+) => {
+ const {
+ placeholder,
+ value,
+ ariaLabel,
+ maxLength,
+ providedError,
+ onValueChanged,
+ autoFocus,
+ inputDataTestId,
+ errorDataTestId,
+ textSize = 'sm',
+ disabled,
+ padding,
+ required,
+ tabIndex,
+ buttonEnd,
+ } = props;
+ const hasError = !isEmpty(providedError);
+ const hasValue = !isEmpty(value);
+
+ const ref = useRef(null);
+
+ const updateInputValue = useUpdateInputValue(onValueChanged, disabled);
+
+ const paddingInlineEnd = !!buttonEnd && hasValue ? '48px' : undefined;
+
+ const inputProps: any = {
+ type: 'text',
+ placeholder,
+ value,
+ textSize,
+ disabled,
+ maxLength,
+ padding,
+ autoFocus,
+ 'data-testid': inputDataTestId,
+ required,
+ 'aria-required': required,
+ tabIndex,
+ onChange: updateInputValue,
+ style: { paddingInlineEnd, lineHeight: 1.5 },
+ };
+
+ useEffect(() => {
+ const textarea = ref.current;
+ if (textarea) {
+ // don't ask me why, but we need to reset the height to auto before calculating it here
+ textarea.style.height = 'auto';
+ // we want 12 lines of text at most
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 12 * parseFloat(getComputedStyle(textarea).lineHeight))}px`;
+ }
+ }, [ref, value]);
+
+ return (
+
+
+
+
+
+ {buttonEnd}
+
+
+ {hasError ? (
+ <>
+
+
+ >
+ ) : null}
+
+ );
+};
diff --git a/ts/components/inputs/useUpdateInputValue.tsx b/ts/components/inputs/useUpdateInputValue.tsx
new file mode 100644
index 0000000000..f9dfaf52b7
--- /dev/null
+++ b/ts/components/inputs/useUpdateInputValue.tsx
@@ -0,0 +1,16 @@
+import { type ChangeEvent, useCallback } from 'react';
+
+export function useUpdateInputValue(onValueChanged: (val: string) => void, disabled?: boolean) {
+ return useCallback(
+ (e: ChangeEvent) => {
+ if (disabled) {
+ return;
+ }
+ e.preventDefault();
+ const val = e.target.value;
+
+ onValueChanged(val);
+ },
+ [disabled, onValueChanged]
+ );
+}
diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx
index 2f5d8994f9..8e354ef244 100644
--- a/ts/components/leftpane/ActionsPanel.tsx
+++ b/ts/components/leftpane/ActionsPanel.tsx
@@ -1,13 +1,14 @@
import { ipcRenderer } from 'electron';
import { useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import useInterval from 'react-use/lib/useInterval';
import useTimeoutFn from 'react-use/lib/useTimeoutFn';
import useMount from 'react-use/lib/useMount';
import useThrottleFn from 'react-use/lib/useThrottleFn';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import {
getOurPrimaryConversation,
@@ -37,7 +38,6 @@ import { useIsDarkTheme } from '../../state/theme/selectors/theme';
import { switchThemeTo } from '../../themes/switchTheme';
import { getOppositeTheme } from '../../util/theme';
-import { useCheckReleasedFeatures } from '../../hooks/useCheckReleasedFeatures';
import { useDebugMode } from '../../state/selectors/debug';
import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
import { themesArray } from '../../themes/constants/colors';
@@ -47,10 +47,11 @@ import { useZoomShortcuts } from '../../hooks/useZoomingShortcut';
import { OnionStatusLight } from '../dialog/OnionStatusPathDialog';
import { AvatarReupload } from '../../session/utils/job_runners/jobs/AvatarReuploadJob';
import { useDebugMenuModal } from '../../state/selectors/modal';
-import { useFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes';
+import { getFeatureFlagMemo } from '../../state/ducks/types/releasedFeaturesReduxTypes';
import { useDebugKey } from '../../hooks/useDebugKey';
import { UpdateProRevocationList } from '../../session/utils/job_runners/jobs/UpdateProRevocationListJob';
-import { useIsProAvailable } from '../../hooks/useIsProAvailable';
+import { getIsProAvailableMemo } from '../../hooks/useIsProAvailable';
+import { SettingsKey } from '../../data/settings-key';
const StyledContainerAvatar = styled.div`
padding: var(--margins-lg);
@@ -59,7 +60,7 @@ const StyledContainerAvatar = styled.div`
`;
function handleThemeSwitch() {
- const currentTheme = window.Events.getThemeSetting();
+ const currentTheme = window.getSettingValue(SettingsKey.settingsTheme);
let newTheme = getOppositeTheme(currentTheme);
if (isDebugMode()) {
// rotate over the 4 themes
@@ -98,7 +99,7 @@ function useUpdateBadgeCount() {
* Note: a job will only be added if it wasn't fetched recently, so there is no harm in running this every minute.
*/
function usePeriodicFetchRevocationList() {
- const proAvailable = useIsProAvailable();
+ const proAvailable = getIsProAvailableMemo();
useInterval(
() => {
if (!proAvailable) {
@@ -119,7 +120,7 @@ function useDebugThemeSwitch() {
}
function DebugMenuModalButton() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const debugMenuModalState = useDebugMenuModal();
useDebugKey({
@@ -143,19 +144,21 @@ function DebugMenuModalButton() {
);
}
-/**
- * ActionsPanel is the far left banner (not the left pane).
- * The panel with buttons to switch between the message/contact/settings/theme views
- */
-export const ActionsPanel = () => {
- const dispatch = useDispatch();
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useActionsPanelInternal() {
const [startCleanUpMedia, setStartCleanUpMedia] = useState(false);
const ourPrimaryConversation = useSelector(getOurPrimaryConversation);
const showDebugMenu = useDebugMode();
const ourNumber = useSelector(getOurNumber);
const isDarkTheme = useIsDarkTheme();
- const fsTTL30sEnabled = useFeatureFlag('fsTTL30s');
- useDebugThemeSwitch();
+
+ useFetchLatestReleaseFromFileServer();
+ // setup our own shortcuts so that it changes show in the appearance tab too
+ useZoomShortcuts();
+ useInterval(
+ DecryptedAttachmentsManager.cleanUpOldDecryptedMedias,
+ startCleanUpMedia ? cleanUpMediasInterval : null
+ );
// wait for cleanUpMediasInterval and then start cleaning up medias
// this would be way easier to just be able to not trigger a call with the setInterval
@@ -165,17 +168,27 @@ export const ActionsPanel = () => {
return () => clearTimeout(timeout);
});
- useUpdateBadgeCount();
- usePeriodicFetchRevocationList();
- // setup our own shortcuts so that it changes show in the appearance tab too
- useZoomShortcuts();
+ return {
+ ourPrimaryConversation,
+ ourNumber,
+ showDebugMenu,
+ isDarkTheme,
+ };
+}
- useInterval(
- DecryptedAttachmentsManager.cleanUpOldDecryptedMedias,
- startCleanUpMedia ? cleanUpMediasInterval : null
- );
+/**
+ * ActionsPanel is the far left banner (not the left pane).
+ * The panel with buttons to switch between the message/contact/settings/theme views
+ */
+export const ActionsPanel = () => {
+ const dispatch = getAppDispatch();
+ const { ourPrimaryConversation, ourNumber, showDebugMenu, isDarkTheme } =
+ useActionsPanelInternal();
- useFetchLatestReleaseFromFileServer();
+ const fsTTL30sEnabled = getFeatureFlagMemo('fsTTL30s');
+ useDebugThemeSwitch();
+ useUpdateBadgeCount();
+ usePeriodicFetchRevocationList();
useInterval(() => {
if (!ourPrimaryConversation) {
@@ -212,8 +225,6 @@ export const ActionsPanel = () => {
fsTTL30sEnabled ? DURATION.SECONDS * 1 : DURATION.DAYS * 1
);
- useCheckReleasedFeatures();
-
if (!ourPrimaryConversation) {
window?.log?.warn('ActionsPanel: ourPrimaryConversation is not set');
return null;
diff --git a/ts/components/leftpane/LeftPane.tsx b/ts/components/leftpane/LeftPane.tsx
index b2178e690a..07989b9f0a 100644
--- a/ts/components/leftpane/LeftPane.tsx
+++ b/ts/components/leftpane/LeftPane.tsx
@@ -10,7 +10,7 @@ import { useIsRtl } from '../../util/i18n/rtlSupport';
export const leftPaneListWidth = 300; // var(--left-panel-width) without the 80px of the action gutter
-const StyledLeftPane = styled.div<{ isRtl: boolean }>`
+const StyledLeftPane = styled.div<{ $isRtl: boolean }>`
width: ${() => `${leftPaneListWidth}px`};
height: 100%;
display: inline-flex;
@@ -19,7 +19,7 @@ const StyledLeftPane = styled.div<{ isRtl: boolean }>`
flex-shrink: 0;
border-left: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
- direction: ${({ isRtl }) => (isRtl ? 'rtl' : 'ltr')};
+ direction: ${({ $isRtl }) => ($isRtl ? 'rtl' : 'ltr')};
`;
const LeftPaneSection = () => {
@@ -51,7 +51,7 @@ export const LeftPane = () => {
-
+
diff --git a/ts/components/leftpane/LeftPaneMessageSection.tsx b/ts/components/leftpane/LeftPaneMessageSection.tsx
index d18dbf8880..dc688c7825 100644
--- a/ts/components/leftpane/LeftPaneMessageSection.tsx
+++ b/ts/components/leftpane/LeftPaneMessageSection.tsx
@@ -1,8 +1,10 @@
import { isEmpty } from 'lodash';
-import { useSelector, useDispatch } from 'react-redux';
-
+import { useSelector } from 'react-redux';
import { AutoSizer, List, ListRowProps } from 'react-virtualized';
import styled from 'styled-components';
+import type { JSX } from 'react';
+import { getAppDispatch } from '../../state/dispatch';
+
import { SearchResults } from '../search/SearchResults';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
import { MessageRequestsBanner } from './MessageRequestsBanner';
@@ -119,7 +121,7 @@ const ConversationList = () => {
export const LeftPaneMessageSection = () => {
const leftOverlayMode = useLeftOverlayMode();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
diff --git a/ts/components/leftpane/LeftPaneSectionHeader.tsx b/ts/components/leftpane/LeftPaneSectionHeader.tsx
index 120a530dd7..e682a3d4d2 100644
--- a/ts/components/leftpane/LeftPaneSectionHeader.tsx
+++ b/ts/components/leftpane/LeftPaneSectionHeader.tsx
@@ -1,11 +1,13 @@
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { LeftOverlayMode, sectionActions } from '../../state/ducks/section';
-import { disableRecoveryPhrasePrompt } from '../../state/ducks/userConfig';
import { useLeftOverlayMode } from '../../state/selectors/section';
-import { useHideRecoveryPasswordEnabled } from '../../state/selectors/settings';
+import {
+ getShowRecoveryPhrasePrompt,
+ useHideRecoveryPasswordEnabled,
+} from '../../state/selectors/settings';
import { useIsDarkTheme } from '../../state/theme/selectors/theme';
-import { getShowRecoveryPhrasePrompt } from '../../state/selectors/userConfig';
import { isSignWithRecoveryPhrase } from '../../util/storage';
import { Flex } from '../basic/Flex';
import { SessionButton, SessionButtonColor } from '../basic/SessionButton';
@@ -19,6 +21,7 @@ import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
import { SessionLucideIconButton } from '../icon/SessionIconButton';
import { tr } from '../../localization/localeTools';
import { userSettingsModal } from '../../state/ducks/modalDialog';
+import { SettingsKey } from '../../data/settings-key';
const StyledLeftPaneSectionHeader = styled(Flex)`
height: var(--main-view-header-height);
@@ -110,10 +113,10 @@ export const LeftPaneBanner = () => {
const isSignInWithRecoveryPhrase = isSignWithRecoveryPhrase();
const hideRecoveryPassword = useHideRecoveryPasswordEnabled();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
- const showRecoveryPhraseModal = () => {
- dispatch(disableRecoveryPhrasePrompt());
+ const showRecoveryPhraseModal = async () => {
+ await window.setSettingValue(SettingsKey.showRecoveryPhrasePrompt, false);
dispatch(userSettingsModal({ userSettingsPage: 'recovery-password' }));
};
@@ -131,7 +134,7 @@ export const LeftPaneBanner = () => {
width={'100%'}
$flexDirection="column"
$alignItems={'flex-start'}
- padding={'var(--margins-md)'}
+ $padding={'var(--margins-md)'}
>
@@ -161,7 +164,7 @@ export const LeftPaneSectionHeader = () => {
const showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt);
const leftOverlayMode = useLeftOverlayMode();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const goBack = () => {
if (!leftOverlayMode) {
return;
diff --git a/ts/components/leftpane/MessageRequestsBanner.tsx b/ts/components/leftpane/MessageRequestsBanner.tsx
index a8d9dbc68d..e3504b23a4 100644
--- a/ts/components/leftpane/MessageRequestsBanner.tsx
+++ b/ts/components/leftpane/MessageRequestsBanner.tsx
@@ -5,11 +5,11 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { getUnreadConversationRequests } from '../../state/selectors/conversations';
import { useIsSearchingForType } from '../../state/selectors/search';
-import { getHideMessageRequestBanner } from '../../state/selectors/userConfig';
import { MessageRequestBannerContextMenu } from '../menu/MessageRequestBannerContextMenu';
import { Localizer } from '../basic/Localizer';
import { LucideIcon } from '../icon/LucideIcon';
import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
+import { getHideMessageRequestBanner } from '../../state/selectors/settings';
const StyledMessageRequestBanner = styled.div`
// The conversation list item row is set to 64px height
@@ -118,5 +118,11 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => {
};
const Portal = ({ children }: { children: ReactNode }) => {
- return createPortal(children, document.querySelector('.inbox.index') as Element);
+ const container = document.querySelector('.inbox.index');
+
+ if (!container) {
+ return null;
+ }
+
+ return createPortal(children, container);
};
diff --git a/ts/components/leftpane/conversation-list-item/InteractionItem.tsx b/ts/components/leftpane/conversation-list-item/InteractionItem.tsx
index 1156542f11..821bd4833a 100644
--- a/ts/components/leftpane/conversation-list-item/InteractionItem.tsx
+++ b/ts/components/leftpane/conversation-list-item/InteractionItem.tsx
@@ -16,8 +16,8 @@ import { tr } from '../../../localization/localeTools';
import { getStyleForMessageItemText } from './MessageItem';
import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation';
-const StyledInteractionItemText = styled.div<{ isError: boolean }>`
- ${props => props.isError && 'color: var(--danger-color) !important;'}
+const StyledInteractionItemText = styled.div<{ $isError: boolean }>`
+ ${props => props.$isError && 'color: var(--danger-color) !important;'}
`;
type InteractionItemProps = {
@@ -102,7 +102,7 @@ export const InteractionItem = (props: InteractionItemProps) => {
return (
();
+ return { groupNameError, setGroupNameError };
+}
+
export const OverlayClosedGroupV2 = () => {
- const dispatch = useDispatch();
- const us = useOurPkStr();
- const { contactsToInvite, searchTerm } = useContactsToInviteTo('create-group');
- const isCreatingGroup = useIsCreatingGroupFromUIPending();
- const groupName = useSelector((state: StateType) => state.groups.creationGroupName) || '';
+ const dispatch = getAppDispatch();
+ const us = useOurPkStrInternal();
+ const { contactsToInvite, searchTerm } = useContactsToInviteToInternal();
+ const isCreatingGroup = useIsCreatingGroupFromUIPendingInternal();
+ const groupName = useGroupName();
const [inviteAsAdmin, setInviteAsAdmin] = useBoolean(false);
- const [groupNameError, setGroupNameError] = useState();
+ const { groupNameError, setGroupNameError } = useGroupNameError();
- const selectedMemberIds = useSelector(
- (state: StateType) => state.groups.creationMembersSelected || []
- );
+ const selectedMemberIds = useSelectedMemberIds();
- function addMemberToSelection(member: PubkeyType) {
+ const addMemberToSelection = (member: PubkeyType) => {
dispatch(groupInfoActions.addSelectedGroupMember({ memberToAdd: member }));
- }
+ };
- function removeMemberFromSelection(member: PubkeyType) {
+ const removeMemberFromSelection = (member: PubkeyType) => {
dispatch(groupInfoActions.removeSelectedGroupMember({ memberToRemove: member }));
- }
+ };
function closeOverlay() {
dispatch(searchActions.clearSearch());
@@ -78,7 +108,6 @@ export const OverlayClosedGroupV2 = () => {
return;
}
- // Validate groupName and groupMembers length
if (groupName.length === 0) {
ToastUtils.pushToastError('invalidGroupName', tr('groupNameEnterPlease'));
return;
@@ -88,9 +117,6 @@ export const OverlayClosedGroupV2 = () => {
return;
}
- // >= because we add ourself as a member AFTER this. so a 10 member group is already invalid as it will be 11 with us
- // the same is valid with groups count < 1
-
if (selectedMemberIds.length < 1) {
ToastUtils.pushToastError('pickClosedGroupMember', tr('groupCreateErrorNoMembers'));
return;
@@ -99,7 +125,7 @@ export const OverlayClosedGroupV2 = () => {
ToastUtils.pushToastError('closedGroupMaxSize', tStripped('groupAddMemberMaximum'));
return;
}
- // trigger the add through redux.
+
dispatch(
groupInfoActions.initNewGroupInWrapper({
members: concat(selectedMemberIds, [us]),
@@ -113,9 +139,29 @@ export const OverlayClosedGroupV2 = () => {
useKey('Escape', closeOverlay);
const noContactsForClosedGroup = isEmpty(searchTerm) && contactsToInvite.length === 0;
-
const disableCreateButton = isCreatingGroup || (!selectedMemberIds.length && !groupName.length);
+ const rowRenderer = ({ index, key, style }: ListRowProps) => {
+ const memberPubkey = contactsToInvite[index];
+
+ if (!PubKey.is05Pubkey(memberPubkey)) {
+ throw new Error('Invalid member rendered in member list');
+ }
+
+ return (
+
+
+
+ );
+ };
+
return (
{
width={'100%'}
$flexDirection="column"
$alignItems="center"
- padding={'var(--margins-md)'}
+ $padding={'var(--margins-md)'}
>
{
>
)}
-
+
@@ -179,28 +225,25 @@ export const OverlayClosedGroupV2 = () => {
) : searchTerm && !contactsToInvite.length ? (
) : (
- contactsToInvite.map((memberPubkey: string) => {
- if (!PubKey.is05Pubkey(memberPubkey)) {
- throw new Error('Invalid member rendered in member list');
- }
-
- return (
-
+ {({ width, height }) => (
+
- );
- })
+ )}
+
)}
-
+
{
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [groupUrl, setGroupUrl] = useState('');
const [groupUrlError, setGroupUrlError] = useState(undefined);
@@ -96,7 +96,7 @@ export const OverlayCommunity = () => {
$flexDirection={'column'}
$flexGrow={1}
$alignItems={'center'}
- padding={'var(--margins-md)'}
+ $padding={'var(--margins-md)'}
>
{
buttonColor={SessionButtonColor.PrimaryDark}
/>
{!loading ? : null}
-
+
);
diff --git a/ts/components/leftpane/overlay/OverlayInvite.tsx b/ts/components/leftpane/overlay/OverlayInvite.tsx
index a6663ac7fa..25763271b0 100644
--- a/ts/components/leftpane/overlay/OverlayInvite.tsx
+++ b/ts/components/leftpane/overlay/OverlayInvite.tsx
@@ -2,7 +2,7 @@ import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../../state/dispatch';
import { UserUtils } from '../../../session/utils';
import { Flex } from '../../basic/Flex';
import { SpacerLG, SpacerMD, SpacerSM } from '../../basic/Text';
@@ -61,7 +61,7 @@ export const OverlayInvite = () => {
const [idCopied, setIdCopied] = useState(false);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
function closeOverlay() {
dispatch(sectionActions.resetLeftOverlayMode());
@@ -75,7 +75,7 @@ export const OverlayInvite = () => {
$flexDirection={'column'}
$flexGrow={1}
$alignItems={'center'}
- padding={'var(--margins-md)'}
+ $padding={'var(--margins-md)'}
>
{!idCopied ? (
<>
diff --git a/ts/components/leftpane/overlay/OverlayMessage.tsx b/ts/components/leftpane/overlay/OverlayMessage.tsx
index 1b11d17288..19d634117e 100644
--- a/ts/components/leftpane/overlay/OverlayMessage.tsx
+++ b/ts/components/leftpane/overlay/OverlayMessage.tsx
@@ -4,8 +4,8 @@ import styled from 'styled-components';
import { motion } from 'framer-motion';
import { isEmpty } from 'lodash';
-import { useDispatch } from 'react-redux';
import { toASCII } from 'punycode';
+import { getAppDispatch } from '../../../state/dispatch';
import { ConvoHub } from '../../../session/conversations';
@@ -23,7 +23,7 @@ import { SpacerLG, SpacerMD } from '../../basic/Text';
import { HelpDeskButton } from '../../buttons';
import { ConversationTypeEnum } from '../../../models/types';
import { Localizer } from '../../basic/Localizer';
-import { SimpleSessionTextarea } from '../../inputs/SessionInput';
+import { SimpleSessionTextarea } from '../../inputs/SimpleSessionTextarea';
import { tr } from '../../../localization/localeTools';
const StyledDescriptionContainer = styled(motion.div)`
@@ -63,7 +63,7 @@ export const StyledLeftPaneOverlay = styled(Flex)`
`;
export const OverlayMessage = () => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
function closeOverlay() {
dispatch(sectionActions.resetLeftOverlayMode());
@@ -158,7 +158,7 @@ export const OverlayMessage = () => {
$flexDirection={'column'}
$flexGrow={1}
$alignItems={'center'}
- padding={'var(--margins-md)'}
+ $padding={'var(--margins-md)'}
>
{
onEnterPressed={handleMessageButtonClick}
/>
-
+
{!pubkeyOrOnsError && !loading ? (
<>
diff --git a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx
index ae91f325ac..7dea29e0c0 100644
--- a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx
+++ b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx
@@ -1,6 +1,7 @@
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../state/dispatch';
import { declineConversationWithoutConfirm } from '../../../interactions/conversationInteractions';
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { getConversationRequestsIds } from '../../../state/selectors/conversations';
@@ -51,7 +52,7 @@ const StyledLeftPaneOverlay = styled.div`
export const OverlayMessageRequest = () => {
useKey('Escape', closeOverlay);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
function closeOverlay() {
dispatch(sectionActions.resetLeftOverlayMode());
diff --git a/ts/components/leftpane/overlay/SessionJoinableDefaultRooms.tsx b/ts/components/leftpane/overlay/SessionJoinableDefaultRooms.tsx
index 4646bea522..abd3bda3f6 100644
--- a/ts/components/leftpane/overlay/SessionJoinableDefaultRooms.tsx
+++ b/ts/components/leftpane/overlay/SessionJoinableDefaultRooms.tsx
@@ -1,7 +1,8 @@
import { useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../state/dispatch';
import { parseOpenGroupV2 } from '../../../session/apis/open_group_api/opengroupV2/JoinOpenGroupV2';
import {
fileDetailsToURL,
@@ -28,7 +29,7 @@ export type JoinableRoomProps = WithRoomId & {
};
const SessionJoinableRoomAvatar = (props: JoinableRoomProps) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
useEffect(() => {
let isCancelled = false;
@@ -126,8 +127,8 @@ const SessionJoinableRoomRow = (props: JoinableRoomProps) => {
>
@@ -199,10 +200,10 @@ export const SessionJoinableRooms = (props: {
$flexWrap="wrap"
dir={htmlDirection}
width={'100%'}
- margin={'0 0 0 calc(var(--margins-md) * -1)'}
+ $margin={'0 0 0 calc(var(--margins-md) * -1)'}
>
{joinableRooms.inProgress ? (
-
+
) : (
{
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
function closeOverlay() {
dispatch(sectionActions.resetLeftOverlayMode());
diff --git a/ts/components/lightbox/Lightbox.tsx b/ts/components/lightbox/Lightbox.tsx
index 5d25646ddc..483bc91735 100644
--- a/ts/components/lightbox/Lightbox.tsx
+++ b/ts/components/lightbox/Lightbox.tsx
@@ -1,9 +1,9 @@
import { CSSProperties, MouseEvent, MutableRefObject, useRef } from 'react';
import { isUndefined } from 'lodash';
-import { useDispatch } from 'react-redux';
import useUnmount from 'react-use/lib/useUnmount';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { useDisableDrag } from '../../hooks/useDisableDrag';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
import { updateLightBoxOptions } from '../../state/ducks/modalDialog';
@@ -257,7 +257,7 @@ const LightboxObject = ({
export const Lightbox = (props: Props) => {
const renderedRef = useRef(null);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const { caption, contentType, objectURL, onNext, onPrevious, onSave, onClose } = props;
const onObjectClick = (event: any) => {
diff --git a/ts/components/lightbox/LightboxGallery.tsx b/ts/components/lightbox/LightboxGallery.tsx
index c5f756f717..5277757fcc 100644
--- a/ts/components/lightbox/LightboxGallery.tsx
+++ b/ts/components/lightbox/LightboxGallery.tsx
@@ -1,8 +1,8 @@
import { useCallback, useState } from 'react';
-import { useDispatch } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import useMount from 'react-use/lib/useMount';
+import { getAppDispatch } from '../../state/dispatch';
import { Lightbox } from './Lightbox';
import { updateLightBoxOptions } from '../../state/ducks/modalDialog';
@@ -34,7 +34,7 @@ export const LightboxGallery = (props: Props) => {
const [currentIndex, setCurrentIndex] = useState(selectedIndex);
const selectedConversation = useSelectedConversationKey();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
// just run once, when the component is mounted. It's to show the lightbox on the specified index at start.
useMount(() => {
diff --git a/ts/components/loading/bar/SessionProgressBar.tsx b/ts/components/loading/bar/SessionProgressBar.tsx
index 74d1c08a49..eb11bdc078 100644
--- a/ts/components/loading/bar/SessionProgressBar.tsx
+++ b/ts/components/loading/bar/SessionProgressBar.tsx
@@ -59,7 +59,7 @@ export function SessionProgressBar(props: Props) {
width={width}
$flexDirection={'column'}
$alignItems={'flex-start'}
- margin={margin}
+ $margin={margin}
>
{title ? (
<>
diff --git a/ts/components/loading/spinner/SessionSpinner.tsx b/ts/components/loading/spinner/SessionSpinner.tsx
index 22d1f3a675..eb891abfb2 100644
--- a/ts/components/loading/spinner/SessionSpinner.tsx
+++ b/ts/components/loading/spinner/SessionSpinner.tsx
@@ -1,14 +1,14 @@
import { StyledSessionSpinner, type StyledSessionSpinnerProps } from './StyledSessionSpinner';
export const SessionSpinner = (props: StyledSessionSpinnerProps) => {
- const { loading, height, width, color } = props;
+ const { $loading: loading, $height: height, $width: width, $color: color } = props;
return loading ? (
diff --git a/ts/components/loading/spinner/StyledSessionSpinner.tsx b/ts/components/loading/spinner/StyledSessionSpinner.tsx
index 90bb5780a7..788f3c455a 100644
--- a/ts/components/loading/spinner/StyledSessionSpinner.tsx
+++ b/ts/components/loading/spinner/StyledSessionSpinner.tsx
@@ -1,17 +1,17 @@
import styled from 'styled-components';
export type StyledSessionSpinnerProps = {
- loading: boolean;
- height?: string;
- width?: string;
- color?: string;
+ $loading: boolean;
+ $height?: string;
+ $width?: string;
+ $color?: string;
};
export const StyledSessionSpinner = styled.div`
display: inline-block;
position: relative;
- width: ${props => (props.width ? props.width : '80px')};
- height: ${props => (props.height ? props.height : '80px')};
+ width: ${props => (props.$width ? props.$width : '80px')};
+ height: ${props => (props.$height ? props.$height : '80px')};
flex-shrink: 0;
div {
@@ -20,7 +20,7 @@ export const StyledSessionSpinner = styled.div`
width: 13px;
height: 13px;
border-radius: 50%;
- background: ${props => props.color || 'var(--primary-color)'};
+ background: ${props => props.$color || 'var(--primary-color)'};
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
div:nth-child(1) {
diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx
index 2993f8a569..f0fb2cf402 100644
--- a/ts/components/menu/ConversationListItemContextMenu.tsx
+++ b/ts/components/menu/ConversationListItemContextMenu.tsx
@@ -1,5 +1,6 @@
import { Menu } from 'react-contexify';
+import type { JSX } from 'react';
import { useConvoIdFromContext } from '../../contexts/ConvoIdContext';
import { useIsLegacyGroup, useIsPinned } from '../../hooks/useParamSelector';
import { useIsSearchingForType } from '../../state/selectors/search';
diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx
index 4e03441c1d..2bb254cf3c 100644
--- a/ts/components/menu/Menu.tsx
+++ b/ts/components/menu/Menu.tsx
@@ -1,4 +1,5 @@
import { Submenu } from 'react-contexify';
+import type { JSX } from 'react';
import { useConvoIdFromContext } from '../../contexts/ConvoIdContext';
import {
useIsGroupV2,
diff --git a/ts/components/menu/MessageRequestBannerContextMenu.tsx b/ts/components/menu/MessageRequestBannerContextMenu.tsx
index 9784024242..eb6987cc75 100644
--- a/ts/components/menu/MessageRequestBannerContextMenu.tsx
+++ b/ts/components/menu/MessageRequestBannerContextMenu.tsx
@@ -1,25 +1,24 @@
import { Menu } from 'react-contexify';
-import { useDispatch } from 'react-redux';
+import type { JSX } from 'react';
import { SessionContextMenuContainer } from '../SessionContextMenuContainer';
-import { hideMessageRequestBanner } from '../../state/ducks/userConfig';
import { ItemWithDataTestId } from './items/MenuItemWithDataTestId';
import { getMenuAnimation } from './MenuAnimation';
import { tr } from '../../localization/localeTools';
+import { SettingsKey } from '../../data/settings-key';
export type PropsContextConversationItem = {
triggerId: string;
};
+async function hideMessageRequestsBanner() {
+ await window.setSettingValue(SettingsKey.hideMessageRequests, true);
+}
+
const HideBannerMenuItem = (): JSX.Element => {
- const dispatch = useDispatch();
return (
- {
- dispatch(hideMessageRequestBanner());
- }}
- >
+ void hideMessageRequestsBanner()}>
{tr('hide')}
);
diff --git a/ts/components/menu/items/CopyAccountId/CopyAccountIdMenuItem.tsx b/ts/components/menu/items/CopyAccountId/CopyAccountIdMenuItem.tsx
index c6938bbce4..b70c66c525 100644
--- a/ts/components/menu/items/CopyAccountId/CopyAccountIdMenuItem.tsx
+++ b/ts/components/menu/items/CopyAccountId/CopyAccountIdMenuItem.tsx
@@ -1,3 +1,4 @@
+import type { JSX } from 'react';
import { Localizer } from '../../../basic/Localizer';
import { useShowCopyAccountIdCb } from '../../../menuAndSettingsHooks/useCopyAccountId';
import { ItemWithDataTestId } from '../MenuItemWithDataTestId';
diff --git a/ts/components/menu/items/CopyCommunityUrl/CopyCommunityUrlMenuItem.tsx b/ts/components/menu/items/CopyCommunityUrl/CopyCommunityUrlMenuItem.tsx
index ee54207d49..3ab950b18a 100644
--- a/ts/components/menu/items/CopyCommunityUrl/CopyCommunityUrlMenuItem.tsx
+++ b/ts/components/menu/items/CopyCommunityUrl/CopyCommunityUrlMenuItem.tsx
@@ -1,3 +1,4 @@
+import type { JSX } from 'react';
import { Localizer } from '../../../basic/Localizer';
import { useShowCopyCommunityUrlCb } from '../../../menuAndSettingsHooks/useCopyCommunityUrl';
import { ItemWithDataTestId } from '../MenuItemWithDataTestId';
diff --git a/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx b/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx
index 83d092e9dd..0845c3f3b1 100644
--- a/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx
+++ b/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx
@@ -1,4 +1,5 @@
-import { useIsProAvailable } from '../../hooks/useIsProAvailable';
+import { useSelector } from 'react-redux';
+import { getIsProAvailableMemo } from '../../hooks/useIsProAvailable';
import { ConvoHub } from '../../session/conversations';
import {
useIsKickedFromGroup,
@@ -12,7 +13,7 @@ import { Constants } from '../../session';
import { useIsMessageRequestOverlayShown } from '../../state/selectors/section';
import { useCurrentUserHasPro } from '../../hooks/useHasPro';
import { CTAVariant } from '../dialog/cta/types';
-import { usePinnedConversationsCount } from '../../state/selectors/conversations';
+import { getPinnedConversationsCount } from '../../state/selectors/conversations';
function useShowPinUnpin(conversationId: string) {
const isPrivateAndFriend = useIsPrivateAndFriend(conversationId);
@@ -35,18 +36,27 @@ function useShowPinUnpin(conversationId: string) {
return !isMessageRequest && (!isPrivate || (isPrivate && isPrivateAndFriend));
}
-export function useTogglePinConversationHandler(id: string) {
- const conversation = ConvoHub.use().get(id);
- const isPinned = useIsPinned(id);
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function usePinnedConversationCount() {
+ return useSelector(getPinnedConversationsCount);
+}
- const pinnedConversationsCount = usePinnedConversationsCount();
- const isProAvailable = useIsProAvailable();
- const hasPro = useCurrentUserHasPro();
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useHasProInternal = useCurrentUserHasPro;
+const useIsPinnedInternal = useIsPinned;
+const useCTACallbackInternal = useShowSessionCTACbWithVariant;
- const handleShowProDialog = useShowSessionCTACbWithVariant();
+export function useTogglePinConversationHandler(id: string) {
+ const conversation = ConvoHub.use().get(id);
+ const isPinned = useIsPinnedInternal(id);
+ const pinnedConversationsCount = usePinnedConversationCount();
+ const hasPro = useHasProInternal();
+ const handleShowProDialog = useCTACallbackInternal();
const showPinUnpin = useShowPinUnpin(id);
+ const isProAvailable = getIsProAvailableMemo();
+
if (!showPinUnpin) {
return null;
}
diff --git a/ts/components/menuAndSettingsHooks/useAddModerators.ts b/ts/components/menuAndSettingsHooks/useAddModerators.ts
index 68e7008253..ae7252c613 100644
--- a/ts/components/menuAndSettingsHooks/useAddModerators.ts
+++ b/ts/components/menuAndSettingsHooks/useAddModerators.ts
@@ -1,9 +1,9 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useIsPublic, useWeAreAdmin } from '../../hooks/useParamSelector';
import { updateAddModeratorsModal } from '../../state/ducks/modalDialog';
export function useAddModeratorsCb(conversationId: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isPublic = useIsPublic(conversationId);
const weAreAdmin = useWeAreAdmin(conversationId);
// only an admin can add moderators from a community. Another moderator cannot.
diff --git a/ts/components/menuAndSettingsHooks/useBanUser.ts b/ts/components/menuAndSettingsHooks/useBanUser.ts
index b7beb56a69..a1c98a9c75 100644
--- a/ts/components/menuAndSettingsHooks/useBanUser.ts
+++ b/ts/components/menuAndSettingsHooks/useBanUser.ts
@@ -1,10 +1,10 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useIsPublic } from '../../hooks/useParamSelector';
import { updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog';
import { useWeAreCommunityAdminOrModerator } from '../../state/selectors/conversations';
export function useBanUserCb(conversationId?: string, pubkey?: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isPublic = useIsPublic(conversationId);
const weAreCommunityAdminOrModerator = useWeAreCommunityAdminOrModerator(conversationId);
diff --git a/ts/components/menuAndSettingsHooks/useChangeNickname.ts b/ts/components/menuAndSettingsHooks/useChangeNickname.ts
index 6b65f376d3..f088dcd7fc 100644
--- a/ts/components/menuAndSettingsHooks/useChangeNickname.ts
+++ b/ts/components/menuAndSettingsHooks/useChangeNickname.ts
@@ -1,9 +1,9 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useIsMe, useIsPrivate, useIsPrivateAndFriend } from '../../hooks/useParamSelector';
import { changeNickNameModal } from '../../state/ducks/modalDialog';
export function useChangeNickname(conversationId?: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isMe = useIsMe(conversationId);
const isPrivate = useIsPrivate(conversationId);
const isPrivateAndFriend = useIsPrivateAndFriend(conversationId);
diff --git a/ts/components/menuAndSettingsHooks/useClearAllMessages.ts b/ts/components/menuAndSettingsHooks/useClearAllMessages.ts
index 14cf5f8a5e..b522464116 100644
--- a/ts/components/menuAndSettingsHooks/useClearAllMessages.ts
+++ b/ts/components/menuAndSettingsHooks/useClearAllMessages.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { SessionButtonColor } from '../basic/SessionButton';
@@ -17,7 +17,7 @@ import { ToastUtils } from '../../session/utils';
import { groupInfoActions } from '../../state/ducks/metaGroups';
export function useClearAllMessagesCb({ conversationId }: { conversationId: string }) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isKickedFromGroup = useIsKickedFromGroup(conversationId);
const isMe = useIsMe(conversationId);
diff --git a/ts/components/menuAndSettingsHooks/useEditProfilePictureCallback.ts b/ts/components/menuAndSettingsHooks/useEditProfilePictureCallback.ts
index da3cc449dc..15fdd540ec 100644
--- a/ts/components/menuAndSettingsHooks/useEditProfilePictureCallback.ts
+++ b/ts/components/menuAndSettingsHooks/useEditProfilePictureCallback.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useIsGroupV2, useIsMe, useIsPublic, useWeAreAdmin } from '../../hooks/useParamSelector';
import { updateEditProfilePictureModal } from '../../state/ducks/modalDialog';
import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes';
@@ -23,7 +23,7 @@ function useEditProfilePicture({ conversationId }: { conversationId: string }) {
export function useEditProfilePictureCallback({ conversationId }: { conversationId: string }) {
const canEdit = useEditProfilePicture({ conversationId });
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (!canEdit) {
return undefined;
diff --git a/ts/components/menuAndSettingsHooks/useHideNoteToSelf.ts b/ts/components/menuAndSettingsHooks/useHideNoteToSelf.ts
index 7bddaa0e46..c105611c4b 100644
--- a/ts/components/menuAndSettingsHooks/useHideNoteToSelf.ts
+++ b/ts/components/menuAndSettingsHooks/useHideNoteToSelf.ts
@@ -1,7 +1,7 @@
-import { useDispatch } from 'react-redux';
import { useIsHidden, useIsMe } from '../../hooks/useParamSelector';
import { tr } from '../../localization/localeTools';
import { ConvoHub } from '../../session/conversations';
+import { getAppDispatch } from '../../state/dispatch';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { SessionButtonColor } from '../basic/SessionButton';
@@ -14,7 +14,7 @@ function useShowHideNoteToSelf({ conversationId }: { conversationId: string }) {
export function useHideNoteToSelfCb({ conversationId }: { conversationId: string }) {
const showHideNTS = useShowHideNoteToSelf({ conversationId });
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (!showHideNTS) {
return null;
diff --git a/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx b/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx
index 39457beefd..5018accd98 100644
--- a/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx
+++ b/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx
@@ -1,5 +1,5 @@
-import { useDispatch } from 'react-redux';
-import { useIsProAvailable } from '../../hooks/useIsProAvailable';
+import { getAppDispatch } from '../../state/dispatch';
+import { getIsProAvailableMemo } from '../../hooks/useIsProAvailable';
import { ProMessageFeature } from '../../models/proMessageFeature';
import { SessionCTAState, updateSessionCTA } from '../../state/ducks/modalDialog';
import { assertUnreachable } from '../../types/sqlSharedTypes';
@@ -121,9 +121,9 @@ function proFeatureToVariant(proFeature: ProMessageFeature): CTAVariant {
export function useProBadgeOnClickCb(
opts: ProBadgeContext
): ShowTagWithCb | ShowTagNoCb | DoNotShowTag {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const handleShowProInfoModal = useShowSessionCTACbWithVariant();
- const isProAvailable = useIsProAvailable();
+ const isProAvailable = getIsProAvailableMemo();
if (!isProAvailable) {
// if pro is globally disabled, we never show the badge.
diff --git a/ts/components/menuAndSettingsHooks/useRemoveModerators.ts b/ts/components/menuAndSettingsHooks/useRemoveModerators.ts
index b575dd71c4..3637071a53 100644
--- a/ts/components/menuAndSettingsHooks/useRemoveModerators.ts
+++ b/ts/components/menuAndSettingsHooks/useRemoveModerators.ts
@@ -1,9 +1,9 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useIsPublic, useWeAreAdmin } from '../../hooks/useParamSelector';
import { updateRemoveModeratorsModal } from '../../state/ducks/modalDialog';
export function useRemoveModeratorsCb(conversationId: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isPublic = useIsPublic(conversationId);
const weAreAdmin = useWeAreAdmin(conversationId);
// only an admin can remove moderators from a community. Another moderator cannot.
diff --git a/ts/components/menuAndSettingsHooks/useShowAttachments.ts b/ts/components/menuAndSettingsHooks/useShowAttachments.ts
index 6fb5bf9374..1c39282d59 100644
--- a/ts/components/menuAndSettingsHooks/useShowAttachments.ts
+++ b/ts/components/menuAndSettingsHooks/useShowAttachments.ts
@@ -1,10 +1,10 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { updateConversationSettingsModal } from '../../state/ducks/modalDialog';
import { useIsKickedFromGroup } from '../../hooks/useParamSelector';
import { openRightPanel } from '../../state/ducks/conversations';
export function useShowAttachments({ conversationId }: { conversationId: string }) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isKickedFromGroup = useIsKickedFromGroup(conversationId);
diff --git a/ts/components/menuAndSettingsHooks/useShowBlockUnblock.ts b/ts/components/menuAndSettingsHooks/useShowBlockUnblock.ts
index 99d2086551..244dfb8e67 100644
--- a/ts/components/menuAndSettingsHooks/useShowBlockUnblock.ts
+++ b/ts/components/menuAndSettingsHooks/useShowBlockUnblock.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import {
useIsMe,
useIsBlocked,
@@ -14,7 +14,7 @@ export function useShowBlockUnblock(convoId?: string) {
const isBlocked = useIsBlocked(convoId);
const isPrivate = useIsPrivate(convoId);
const isIncomingRequest = useIsIncomingRequest(convoId);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const showBlockUnblock =
convoId && !isMe && isPrivate && !isIncomingRequest && !PubKey.isBlinded(convoId);
diff --git a/ts/components/menuAndSettingsHooks/useShowConversationSettingsFor.ts b/ts/components/menuAndSettingsHooks/useShowConversationSettingsFor.ts
index 9463775332..2875d815e6 100644
--- a/ts/components/menuAndSettingsHooks/useShowConversationSettingsFor.ts
+++ b/ts/components/menuAndSettingsHooks/useShowConversationSettingsFor.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import {
useIsPrivate,
useIsPrivateAndFriend,
@@ -10,7 +10,7 @@ import {
} from '../../state/ducks/modalDialog';
export function useShowConversationSettingsFor(conversationId?: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isPrivate = useIsPrivate(conversationId);
const isPrivateAndFriend = useIsPrivateAndFriend(conversationId);
const isLegacyGroup = useIsLegacyGroup(conversationId);
diff --git a/ts/components/menuAndSettingsHooks/useShowDeletePrivateContact.ts b/ts/components/menuAndSettingsHooks/useShowDeletePrivateContact.ts
index 3fa1b21612..d4e8eb94f6 100644
--- a/ts/components/menuAndSettingsHooks/useShowDeletePrivateContact.ts
+++ b/ts/components/menuAndSettingsHooks/useShowDeletePrivateContact.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import {
useConversationUsernameWithFallback,
useIsIncomingRequest,
@@ -18,10 +18,13 @@ function useShowDeletePrivateContact({ conversationId }: { conversationId: strin
return isPrivate && !isRequest && !isMe;
}
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useConversationUsernameWithFallbackInternal = useConversationUsernameWithFallback;
+
export function useShowDeletePrivateContactCb({ conversationId }: { conversationId: string }) {
const showDeletePrivateContact = useShowDeletePrivateContact({ conversationId });
- const dispatch = useDispatch();
- const name = useConversationUsernameWithFallback(true, conversationId);
+ const dispatch = getAppDispatch();
+ const name = useConversationUsernameWithFallbackInternal(true, conversationId);
if (!showDeletePrivateContact) {
return null;
diff --git a/ts/components/menuAndSettingsHooks/useShowDeletePrivateConversation.ts b/ts/components/menuAndSettingsHooks/useShowDeletePrivateConversation.ts
index f9d8ed372a..a641c66eea 100644
--- a/ts/components/menuAndSettingsHooks/useShowDeletePrivateConversation.ts
+++ b/ts/components/menuAndSettingsHooks/useShowDeletePrivateConversation.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import {
useIsPrivate,
useIsIncomingRequest,
@@ -18,10 +18,13 @@ function useShowDeletePrivateConversation({ conversationId }: { conversationId:
return isPrivate && !isRequest && !isMe;
}
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+const useConversationUsernameWithFallbackInternal = useConversationUsernameWithFallback;
+
export function useShowDeletePrivateConversationCb({ conversationId }: { conversationId: string }) {
const showDeletePrivateConversation = useShowDeletePrivateConversation({ conversationId });
- const dispatch = useDispatch();
- const name = useConversationUsernameWithFallback(true, conversationId);
+ const dispatch = getAppDispatch();
+ const name = useConversationUsernameWithFallbackInternal(true, conversationId);
if (!showDeletePrivateConversation) {
return null;
diff --git a/ts/components/menuAndSettingsHooks/useShowInviteContactToCommunity.ts b/ts/components/menuAndSettingsHooks/useShowInviteContactToCommunity.ts
index 0ddc220197..eccd2cf57e 100644
--- a/ts/components/menuAndSettingsHooks/useShowInviteContactToCommunity.ts
+++ b/ts/components/menuAndSettingsHooks/useShowInviteContactToCommunity.ts
@@ -1,9 +1,9 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { updateInviteContactModal } from '../../state/ducks/modalDialog';
import { useIsPublic } from '../../hooks/useParamSelector';
export function useShowInviteContactToCommunity(conversationId: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isPublic = useIsPublic(conversationId);
const cb = () => dispatch(updateInviteContactModal({ conversationId }));
diff --git a/ts/components/menuAndSettingsHooks/useShowInviteContactToGroup.ts b/ts/components/menuAndSettingsHooks/useShowInviteContactToGroup.ts
index 382ffbbaa9..33b80e53d0 100644
--- a/ts/components/menuAndSettingsHooks/useShowInviteContactToGroup.ts
+++ b/ts/components/menuAndSettingsHooks/useShowInviteContactToGroup.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import {
useIsBlocked,
useIsGroupDestroyed,
@@ -9,7 +9,7 @@ import {
import { updateInviteContactModal } from '../../state/ducks/modalDialog';
export function useShowInviteContactToGroupCb(conversationId: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isGroupV2 = useIsGroupV2(conversationId);
const isBlocked = useIsBlocked(conversationId);
const isKickedFromGroup = useIsKickedFromGroup(conversationId);
diff --git a/ts/components/menuAndSettingsHooks/useShowLeaveCommunity.ts b/ts/components/menuAndSettingsHooks/useShowLeaveCommunity.ts
index 58b48bd1c9..c77de10506 100644
--- a/ts/components/menuAndSettingsHooks/useShowLeaveCommunity.ts
+++ b/ts/components/menuAndSettingsHooks/useShowLeaveCommunity.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useConversationUsernameWithFallback, useIsPublic } from '../../hooks/useParamSelector';
import { tr } from '../../localization/localeTools';
import { ConvoHub } from '../../session/conversations';
@@ -9,7 +9,7 @@ import { leaveGroupOrCommunityByConvoId } from '../../interactions/conversationI
export function useShowLeaveCommunityCb(conversationId?: string) {
const isPublic = useIsPublic(conversationId);
const username = useConversationUsernameWithFallback(true, conversationId) || conversationId;
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
if (!isPublic || !conversationId) {
return null;
diff --git a/ts/components/menuAndSettingsHooks/useShowLeaveGroup.ts b/ts/components/menuAndSettingsHooks/useShowLeaveGroup.ts
index e9a456f308..a34624ec73 100644
--- a/ts/components/menuAndSettingsHooks/useShowLeaveGroup.ts
+++ b/ts/components/menuAndSettingsHooks/useShowLeaveGroup.ts
@@ -37,12 +37,23 @@ export function useShowLeaveGroupCb(conversationId?: string) {
};
}
-export function useShowDeleteGroupCb(conversationId?: string) {
- // Note: useShowLeaveGroupCb and useShowDeleteGroupCb are dependent on each other
- // so I kept them in the same file
+// NOTE: [react-compiler] this convinces the compiler the hook is static
+function useShowDeleteGroupInternal(conversationId?: string) {
const isClosedGroup = useIsClosedGroup(conversationId);
const isMessageRequestShown = useIsMessageRequestOverlayShown();
const username = useConversationUsernameWithFallback(true, conversationId);
+ return {
+ isClosedGroup,
+ isMessageRequestShown,
+ username,
+ };
+}
+
+export function useShowDeleteGroupCb(conversationId?: string) {
+ // Note: useShowLeaveGroupCb and useShowDeleteGroupCb are dependent on each other
+ // so I kept them in the same file
+ const { isClosedGroup, isMessageRequestShown, username } =
+ useShowDeleteGroupInternal(conversationId);
const showLeaveIsOn = useShowLeaveGroupCb(conversationId);
if (!isClosedGroup || isMessageRequestShown || showLeaveIsOn || !conversationId) {
diff --git a/ts/components/menuAndSettingsHooks/useShowNoteToSelf.ts b/ts/components/menuAndSettingsHooks/useShowNoteToSelf.ts
index 0b1c2050f1..8451d4a0f4 100644
--- a/ts/components/menuAndSettingsHooks/useShowNoteToSelf.ts
+++ b/ts/components/menuAndSettingsHooks/useShowNoteToSelf.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useIsHidden, useIsMe } from '../../hooks/useParamSelector';
import { ConvoHub } from '../../session/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
@@ -14,11 +14,7 @@ function useShowNoteToSelf({ conversationId }: { conversationId: string }) {
export function useShowNoteToSelfCb({ conversationId }: { conversationId: string }) {
const showNTS = useShowNoteToSelf({ conversationId });
- const dispatch = useDispatch();
-
- if (!showNTS) {
- return null;
- }
+ const dispatch = getAppDispatch();
const onClickClose = () => {
dispatch(updateConfirmModal(null));
@@ -40,5 +36,10 @@ export function useShowNoteToSelfCb({ conversationId }: { conversationId: string
})
);
};
+
+ if (!showNTS) {
+ return null;
+ }
+
return showConfirmationModal;
}
diff --git a/ts/components/menuAndSettingsHooks/useShowUserDetailsCb.ts b/ts/components/menuAndSettingsHooks/useShowUserDetailsCb.ts
index fc710c46d3..11686f2dbb 100644
--- a/ts/components/menuAndSettingsHooks/useShowUserDetailsCb.ts
+++ b/ts/components/menuAndSettingsHooks/useShowUserDetailsCb.ts
@@ -1,4 +1,4 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import {
findCachedBlindedMatchOrLookItUp,
getCachedNakedKeyFromBlindedNoServerPubkey,
@@ -17,7 +17,7 @@ import { OpenGroupData } from '../../data/opengroups';
* Show the user details modal for a given message in the currently selected conversation.
*/
export function useShowUserDetailsCbFromMessage() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const selectedConvoKey = useSelectedConversationKey();
const isPublic = useIsPublic(selectedConvoKey);
@@ -83,7 +83,7 @@ export function useShowUserDetailsCbFromConversation(
conversationId?: string,
allowForNts?: boolean
) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isPrivate = useIsPrivate(conversationId);
diff --git a/ts/components/menuAndSettingsHooks/useUnbanUser.ts b/ts/components/menuAndSettingsHooks/useUnbanUser.ts
index cd408a73a2..427d6e6b8d 100644
--- a/ts/components/menuAndSettingsHooks/useUnbanUser.ts
+++ b/ts/components/menuAndSettingsHooks/useUnbanUser.ts
@@ -1,10 +1,10 @@
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { useIsPublic } from '../../hooks/useParamSelector';
import { updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog';
import { useWeAreCommunityAdminOrModerator } from '../../state/selectors/conversations';
export function useUnbanUserCb(conversationId?: string, pubkey?: string) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isPublic = useIsPublic(conversationId);
const weAreCommunityAdminOrModerator = useWeAreCommunityAdminOrModerator(conversationId);
diff --git a/ts/components/registration/RegistrationStages.tsx b/ts/components/registration/RegistrationStages.tsx
index 1b6c35e69a..8c13eb202f 100644
--- a/ts/components/registration/RegistrationStages.tsx
+++ b/ts/components/registration/RegistrationStages.tsx
@@ -1,6 +1,6 @@
import { AnimatePresence } from 'framer-motion';
import styled from 'styled-components';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../../state/dispatch';
import { Data } from '../../data/data';
import { ConvoHub } from '../../session/conversations';
import {
@@ -47,7 +47,7 @@ export const RegistrationStages = () => {
const creationStep = useOnboardAccountCreationStep();
const restorationStep = useOnboardAccountRestorationStep();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
diff --git a/ts/components/registration/SessionRegistrationView.tsx b/ts/components/registration/SessionRegistrationView.tsx
index 6745c0db97..3c26b3d5e5 100644
--- a/ts/components/registration/SessionRegistrationView.tsx
+++ b/ts/components/registration/SessionRegistrationView.tsx
@@ -74,7 +74,7 @@ export const SessionRegistrationView = () => {
height="100%"
$flexGrow={1}
>
-
+
diff --git a/ts/components/registration/TermsAndConditions.tsx b/ts/components/registration/TermsAndConditions.tsx
index 8d000cf5c8..5db478fe50 100644
--- a/ts/components/registration/TermsAndConditions.tsx
+++ b/ts/components/registration/TermsAndConditions.tsx
@@ -1,5 +1,5 @@
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { getAppDispatch } from '../../state/dispatch';
import { updateTermsOfServicePrivacyModal } from '../../state/onboarding/ducks/modals';
import { Localizer } from '../basic/Localizer';
@@ -17,7 +17,7 @@ const StyledTermsAndConditions = styled.div`
`;
export const TermsAndConditions = () => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
return (
{
const ourPubkey = useOnboardHexGeneratedPubKey();
const displayName = useDisplayName();
const progress = useProgress();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const recoveryComplete = useCallback(async () => {
await finishRestore(ourPubkey, displayName);
diff --git a/ts/components/registration/stages/CreateAccount.tsx b/ts/components/registration/stages/CreateAccount.tsx
index 0a99a997f9..107b666272 100644
--- a/ts/components/registration/stages/CreateAccount.tsx
+++ b/ts/components/registration/stages/CreateAccount.tsx
@@ -1,7 +1,7 @@
import { isEmpty } from 'lodash';
-import { useDispatch } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import { useState } from 'react';
+import { getAppDispatch } from '../../../state/dispatch';
import { SettingsKey } from '../../../data/settings-key';
import { mnDecode } from '../../../session/crypto/mnemonic';
import { ProfileManager } from '../../../session/profile_manager/ProfileManager';
@@ -61,7 +61,7 @@ export const CreateAccount = () => {
const displayName = useDisplayName();
const displayNameError = useDisplayNameError();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [cannotContinue, setCannotContinue] = useState(true);
@@ -141,7 +141,7 @@ export const CreateAccount = () => {
width="100%"
$flexDirection="column"
$alignItems="flex-start"
- margin={'0 0 0 8px'}
+ $margin={'0 0 0 8px'}
>
{tr('displayNamePick')}
diff --git a/ts/components/registration/stages/RestoreAccount.tsx b/ts/components/registration/stages/RestoreAccount.tsx
index b6f00c9583..a50eb4ae35 100644
--- a/ts/components/registration/stages/RestoreAccount.tsx
+++ b/ts/components/registration/stages/RestoreAccount.tsx
@@ -1,7 +1,7 @@
import { Dispatch } from '@reduxjs/toolkit';
import { isEmpty } from 'lodash';
-import { useDispatch } from 'react-redux';
import { useState } from 'react';
+import { getAppDispatch } from '../../../state/dispatch';
import { ONBOARDING_TIMES } from '../../../session/constants';
import { InvalidWordsError, NotEnoughWordsError } from '../../../session/crypto/mnemonic';
import { ProfileManager } from '../../../session/profile_manager/ProfileManager';
@@ -130,7 +130,7 @@ const showHideButtonDataTestIds = {
} as const;
const RecoveryPhraseInput = ({ onEnterPressed }: { onEnterPressed: () => Promise }) => {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const recoveryPassword = useRecoveryPassword();
const recoveryPasswordError = useRecoveryPasswordError();
@@ -161,7 +161,7 @@ export const RestoreAccount = () => {
const displayNameError = useDisplayNameError();
const progress = useProgress();
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const [cannotContinue, setCannotContinue] = useState(true);
@@ -284,7 +284,7 @@ export const RestoreAccount = () => {
$flexDirection="column"
$justifyContent="flex-start"
$alignItems="flex-start"
- margin={
+ $margin={
step === AccountRestoration.RecoveryPassword || step === AccountRestoration.DisplayName
? '0 0 0 8px'
: '0px'
diff --git a/ts/components/registration/stages/Start.tsx b/ts/components/registration/stages/Start.tsx
index cb2302a473..4989c1ae15 100644
--- a/ts/components/registration/stages/Start.tsx
+++ b/ts/components/registration/stages/Start.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import styled from 'styled-components';
+import { getAppDispatch } from '../../../state/dispatch';
import { sleepFor } from '../../../session/utils/Promise';
import {
AccountCreation,
@@ -20,9 +20,9 @@ import { TermsAndConditions } from '../TermsAndConditions';
import { tr } from '../../../localization/localeTools';
// NOTE we want to prevent the buttons from flashing when the app starts
-const StyledStart = styled.div<{ ready: boolean }>`
+const StyledStart = styled.div<{ $ready: boolean }>`
${props =>
- !props.ready &&
+ !props.$ready &&
`.session-button {
transition: none;
}`}
@@ -31,7 +31,7 @@ const StyledStart = styled.div<{ ready: boolean }>`
export const Start = () => {
const [ready, setReady] = useState(false);
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
useMount(() => {
dispatch(resetOnboardingState());
@@ -42,7 +42,7 @@ export const Start = () => {
});
return (
-
+
{
diff --git a/ts/components/search/SearchResults.tsx b/ts/components/search/SearchResults.tsx
index 67bf4c9643..2c72a6ddfe 100644
--- a/ts/components/search/SearchResults.tsx
+++ b/ts/components/search/SearchResults.tsx
@@ -16,7 +16,7 @@ import { calcContactRowHeight } from '../leftpane/overlay/choose-action/Contacts
import { NoResultsForSearch } from './NoResults';
import { tr } from '../../localization/localeTools';
-const StyledSeparatorSection = styled.div<{ isSubtitle: boolean }>`
+const StyledSeparatorSection = styled.div<{ $isSubtitle: boolean }>`
height: 36px;
line-height: 36px;
letter-spacing: 0;
@@ -25,7 +25,7 @@ const StyledSeparatorSection = styled.div<{ isSubtitle: boolean }>`
font-weight: 400;
${props =>
- props.isSubtitle
+ props.$isSubtitle
? 'color: var(--text-secondary-color); font-size: var(--font-size-sm);'
: 'color: var(--text-primary-color); font-size: var(--font-size-lg);'}
`;
@@ -48,7 +48,7 @@ const SectionHeader = ({
style: CSSProperties;
}) => {
return (
-
+
{title}
);
diff --git a/ts/contexts/MessagesContainerRefContext.tsx b/ts/contexts/MessagesContainerRefContext.tsx
index 84cf81a6f2..a6b1d6cbe6 100644
--- a/ts/contexts/MessagesContainerRefContext.tsx
+++ b/ts/contexts/MessagesContainerRefContext.tsx
@@ -1,6 +1,6 @@
import { createContext, RefObject, useContext } from 'react';
-export const MessagesContainerRefContext = createContext>({
+export const MessagesContainerRefContext = createContext>({
current: null,
});
diff --git a/ts/data/settings-key.ts b/ts/data/settings-key.ts
index 80ef463396..a7cae242b5 100644
--- a/ts/data/settings-key.ts
+++ b/ts/data/settings-key.ts
@@ -60,11 +60,15 @@ export const SettingsKey = {
lastMessageGroupsRegenerated,
proLongerMessagesSent,
proBadgesSent,
+ audioAutoplay: 'audioAutoplay',
+ showRecoveryPhrasePrompt: 'showRecoveryPhrasePrompt',
+ hideMessageRequests: 'hideMessageRequests',
identityKey: 'identityKey',
blocked: 'blocked',
numberId: 'number_id',
localAttachmentEncryptionKey,
spellCheckEnabled: 'spell-check',
+ settingsTheme: 'settingsTheme',
urlInteractions: 'urlInteractions',
proMasterKeyHex: 'proMasterKeyHex',
proRotatingPrivateKeyHex: 'proRotatingPrivateKeyHex',
diff --git a/ts/hooks/useAppFocused.ts b/ts/hooks/useAppFocused.ts
index 202f43cd5c..e4e5c1b36d 100644
--- a/ts/hooks/useAppFocused.ts
+++ b/ts/hooks/useAppFocused.ts
@@ -1,6 +1,7 @@
import { ipcRenderer } from 'electron';
import { useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
+import { getAppDispatch } from '../state/dispatch';
import { getIsAppFocused } from '../state/selectors/section';
import { sectionActions } from '../state/ducks/section';
@@ -9,7 +10,7 @@ import { sectionActions } from '../state/ducks/section';
* It sets up a listener for events from main_node.ts and update the global redux state with the focused state.
*/
export function useAppIsFocused() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const isFocusedFromStore = useSelector(getIsAppFocused);
const ipcCallback = (_event: unknown, isFocused: unknown) => {
diff --git a/ts/hooks/useCheckReleasedFeatures.ts b/ts/hooks/useCheckReleasedFeatures.ts
index 0c4c2c271d..25e2cc88eb 100644
--- a/ts/hooks/useCheckReleasedFeatures.ts
+++ b/ts/hooks/useCheckReleasedFeatures.ts
@@ -1,11 +1,12 @@
import useInterval from 'react-use/lib/useInterval';
-import { useDispatch } from 'react-redux';
+import { getAppDispatch } from '../state/dispatch';
import { releasedFeaturesActions } from '../state/ducks/releasedFeatures';
import { NetworkTime } from '../util/NetworkTime';
import { FEATURE_RELEASE_CHECK_INTERVAL } from '../state/ducks/types/releasedFeaturesReduxTypes';
+// NOTE: this is not used anywhere yet, but we may want it it in the future
export function useCheckReleasedFeatures() {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
useInterval(() => {
const nowFromNetwork = NetworkTime.now();
diff --git a/ts/hooks/useDebouncedSelectAllOnTripleClickHandler.ts b/ts/hooks/useDebouncedSelectAllOnTripleClickHandler.ts
index 34d0521bdd..413c391d9c 100644
--- a/ts/hooks/useDebouncedSelectAllOnTripleClickHandler.ts
+++ b/ts/hooks/useDebouncedSelectAllOnTripleClickHandler.ts
@@ -19,7 +19,7 @@ function selectAllContent(el: HTMLElement) {
}
interface DebouncedSelectAllOnTripleClickHandlerProps {
- elementRef: RefObject;
+ elementRef: RefObject;
onClick?: MouseEventHandler;
}
diff --git a/ts/hooks/useDebouncedSpellcheck.ts b/ts/hooks/useDebouncedSpellcheck.ts
index 94c44882b4..0098a014aa 100644
--- a/ts/hooks/useDebouncedSpellcheck.ts
+++ b/ts/hooks/useDebouncedSpellcheck.ts
@@ -2,7 +2,7 @@ import { type RefObject, useCallback, useEffect } from 'react';
import { debounce } from 'lodash';
interface DebouncedSpellcheckProps {
- elementRef: RefObject;
+ elementRef: RefObject;
delay?: number;
}
diff --git a/ts/hooks/useHasPro.ts b/ts/hooks/useHasPro.ts
index 67e526a202..7ff7b93581 100644
--- a/ts/hooks/useHasPro.ts
+++ b/ts/hooks/useHasPro.ts
@@ -1,42 +1,17 @@
-import { useCallback, useMemo } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { ProOriginatingPlatform } from 'libsession_util_nodejs';
-import useUpdate from 'react-use/lib/useUpdate';
-import {
- getDataFeatureFlag,
- MockProAccessExpiryOptions,
- setDataFeatureFlag,
- useDataFeatureFlag,
- useFeatureFlag,
-} from '../state/ducks/types/releasedFeaturesReduxTypes';
-import { assertUnreachable } from '../types/sqlSharedTypes';
-import { useIsProAvailable } from './useIsProAvailable';
-import {
- formatDateWithLocale,
- formatRoundedUpTimeUntilTimestamp,
-} from '../util/i18n/formatting/generics';
-import LIBSESSION_CONSTANTS from '../session/utils/libsession/libsession_constants';
+import { useSelector } from 'react-redux';
+import { getDataFeatureFlagMemo } from '../state/ducks/types/releasedFeaturesReduxTypes';
+import { getIsProAvailableMemo } from './useIsProAvailable';
import {
+ defaultProAccessDetailsSourceData,
getProBackendCurrentUserStatus,
- useProBackendProDetails,
- useSetProBackendIsError,
- useSetProBackendIsLoading,
} from '../state/selectors/proBackendData';
-import { proBackendDataActions, WithCallerContext } from '../state/ducks/proBackendData';
-import { NetworkTime } from '../util/NetworkTime';
-import {
- getProOriginatingPlatformFromProPaymentProvider,
- ProAccessVariant,
- ProPaymentProvider,
- ProStatus,
-} from '../session/apis/pro_backend_api/types';
-import { sleepFor } from '../session/utils/Promise';
+import { ProStatus } from '../session/apis/pro_backend_api/types';
import { UserUtils } from '../session/utils';
import type { StateType } from '../state/reducer';
export function selectOurProStatus(state: StateType) {
const proBackendCurrentUserStatus = getProBackendCurrentUserStatus(state);
- const mockCurrentStatus = getDataFeatureFlag('mockProCurrentStatus');
+ const mockCurrentStatus = getDataFeatureFlagMemo('mockProCurrentStatus');
return (
mockCurrentStatus ??
@@ -50,14 +25,14 @@ export function selectWeAreProUser(state: StateType) {
}
function useCurrentUserProStatus() {
- return useSelector((state: StateType) => selectOurProStatus(state));
+ return useSelector(selectOurProStatus);
}
/**
* Returns true if pro is available, and the current user has pro (active, not expired)
*/
export function useCurrentUserHasPro() {
- const isProAvailable = useIsProAvailable();
+ const isProAvailable = getIsProAvailableMemo();
const status = useCurrentUserProStatus();
return isProAvailable && status === ProStatus.Active;
@@ -67,7 +42,7 @@ export function useCurrentUserHasPro() {
* Returns true if pro is available, and the current user has expired pro.
*/
export function useCurrentUserHasExpiredPro() {
- const isProAvailable = useIsProAvailable();
+ const isProAvailable = getIsProAvailableMemo();
const status = useCurrentUserProStatus();
return isProAvailable && status === ProStatus.Expired;
@@ -78,7 +53,7 @@ export function useCurrentUserHasExpiredPro() {
* (i.e. the user does not have pro currently and doesn't have an expired pro either)
*/
export function useCurrentNeverHadPro() {
- const isProAvailable = useIsProAvailable();
+ const isProAvailable = getIsProAvailableMemo();
const status = useCurrentUserProStatus();
return isProAvailable && status === ProStatus.NeverBeenPro;
@@ -95,7 +70,7 @@ function useShowProBadgeForOther(convoId?: string) {
}
export function useShowProBadgeFor(convoId?: string) {
- const isProAvailable = useIsProAvailable();
+ const isProAvailable = getIsProAvailableMemo();
// the current user pro badge is always shown if we have a valid pro
const currentUserHasPro = useCurrentUserHasPro();
// the other user pro badge is shown if they have a valid pro proof and pro badge feature enabled
@@ -111,277 +86,3 @@ export function useShowProBadgeFor(convoId?: string) {
return otherUserHasPro;
}
-
-function proAccessVariantToString(variant: ProAccessVariant): string {
- switch (variant) {
- case ProAccessVariant.OneMonth:
- return '1 Month';
- case ProAccessVariant.ThreeMonth:
- return '3 Months';
- case ProAccessVariant.TwelveMonth:
- return '12 Months';
- case ProAccessVariant.Nil:
- return 'N/A';
- default:
- return assertUnreachable(variant, `Unknown pro access variant: ${variant}`);
- }
-}
-
-function useMockProAccessExpiry(): number | null {
- const variant = useDataFeatureFlag('mockProAccessExpiry');
-
- // NOTE: the mock expiry time should be pinned to x - 250ms after "now", the -250ms ensures the string
- // representation rounds up to the expected mock value and prevents render lag from changing the timestamp
- const now = variant !== null ? Date.now() - 250 : 0;
- switch (variant) {
- case MockProAccessExpiryOptions.P7D:
- return now + 7 * 24 * 60 * 60 * 1000;
- case MockProAccessExpiryOptions.P29D:
- return now + 29 * 24 * 60 * 60 * 1000;
- case MockProAccessExpiryOptions.P30D:
- return now + 30 * 24 * 60 * 60 * 1000;
- case MockProAccessExpiryOptions.P30DT1S:
- return now + 30 * 24 * 60 * 61 * 1000;
- case MockProAccessExpiryOptions.P90D:
- return now + 90 * 24 * 60 * 60 * 1000;
- case MockProAccessExpiryOptions.P300D:
- return now + 300 * 24 * 60 * 60 * 1000;
- case MockProAccessExpiryOptions.P365D:
- return now + 365 * 24 * 60 * 60 * 1000;
- case MockProAccessExpiryOptions.P24DT1M:
- return now + 24 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000;
- case MockProAccessExpiryOptions.PT24H1M:
- return now + 24 * 60 * 60 * 1000 + 60 * 60 * 1000;
- case MockProAccessExpiryOptions.PT23H59M:
- return now + 23 * 60 * 60 * 1000 + 59 * 60 * 1000;
- case MockProAccessExpiryOptions.PT33M:
- return now + 33 * 60 * 1000;
- case MockProAccessExpiryOptions.PT1M:
- return now + 1 * 60 * 1000;
- case MockProAccessExpiryOptions.PT10S:
- return now + 10 * 1000;
- default:
- return null;
- }
-}
-
-function getProProviderConstantsWithFallbacks(provider: ProPaymentProvider) {
- const libsessionPaymentProvider = getProOriginatingPlatformFromProPaymentProvider(provider);
- const constants = LIBSESSION_CONSTANTS.LIBSESSION_PRO_PROVIDERS[libsessionPaymentProvider];
-
- if (!constants.store) {
- constants.store = LIBSESSION_CONSTANTS.LIBSESSION_PRO_PROVIDERS.Google.store;
- }
-
- if (!constants.store_other) {
- constants.store_other = LIBSESSION_CONSTANTS.LIBSESSION_PRO_PROVIDERS.Google.store_other;
- }
-
- return constants;
-}
-
-type ProAccessDetailsSourceData = {
- currentStatus: ProStatus;
- autoRenew: boolean;
- inGracePeriod: boolean;
- variant: ProAccessVariant;
- expiryTimeMs: number;
- isPlatformRefundAvailable: boolean;
- provider: ProPaymentProvider;
- isLoading: boolean;
- isError: boolean;
-};
-
-type RequestHook = {
- isLoading: boolean;
- isFetching: boolean;
- isError: boolean;
- refetch: (args?: WithCallerContext) => void;
- data: D;
- t: number;
-};
-
-type ProAccessDetails = {
- currentStatus: ProStatus;
- autoRenew: boolean;
- inGracePeriod: boolean;
- variant: ProAccessVariant;
- variantString: string;
- expiryTimeMs: number;
- expiryTimeDateString: string;
- expiryTimeRelativeString: string;
- isPlatformRefundAvailable: boolean;
- provider: ProPaymentProvider;
- providerConstants: (typeof LIBSESSION_CONSTANTS)['LIBSESSION_PRO_PROVIDERS'][ProOriginatingPlatform];
-};
-
-// These values are used if pro isnt available or if no data is available from the backend.
-const defaultProAccessDetailsSourceData = {
- currentStatus: ProStatus.NeverBeenPro,
- autoRenew: true,
- inGracePeriod: false,
- variant: ProAccessVariant.Nil,
- expiryTimeMs: 0,
- isPlatformRefundAvailable: false,
- provider: ProPaymentProvider.Nil,
- isLoading: false,
- isError: false,
-} satisfies ProAccessDetailsSourceData;
-
-function useMockRecoverAccess() {
- const forceUpdate = useUpdate();
- const mockSuccess = useFeatureFlag('mockProRecoverButtonAlwaysSucceed');
- const mockFail = useFeatureFlag('mockProRecoverButtonAlwaysFail');
-
- const mockRecover = useCallback(() => {
- if (!mockSuccess || mockFail) {
- return;
- }
- setDataFeatureFlag('mockProCurrentStatus', ProStatus.Active);
- setDataFeatureFlag('mockProAccessVariant', ProAccessVariant.OneMonth);
- setDataFeatureFlag('mockProAccessExpiry', MockProAccessExpiryOptions.P7D);
- forceUpdate();
- }, [mockSuccess, mockFail, forceUpdate]);
-
- return {
- mockSuccess,
- mockFail,
- mockRecover,
- };
-}
-
-export function useProAccessDetails(): RequestHook {
- const dispatch = useDispatch();
-
- const setProBackendIsLoading = useSetProBackendIsLoading();
- const setProBackendIsError = useSetProBackendIsError();
-
- const details = useProBackendProDetails();
- const currentUserProStatus = useCurrentUserProStatus();
-
- const mockIsLoading = useFeatureFlag('mockProBackendLoading');
- const mockIsError = useFeatureFlag('mockProBackendError');
- const mockRecoverAccess = useMockRecoverAccess();
-
- const mockVariant = useDataFeatureFlag('mockProAccessVariant');
- const mockPlatform = useDataFeatureFlag('mockProPaymentProvider');
- const mockCancelled = useFeatureFlag('mockCurrentUserHasProCancelled');
- const mockInGracePeriod = useFeatureFlag('mockCurrentUserHasProInGracePeriod');
- const mockIsPlatformRefundAvailable = !useFeatureFlag(
- 'mockCurrentUserHasProPlatformRefundExpired'
- );
- const mockExpiry = useMockProAccessExpiry();
-
- const isLoading = mockIsLoading || details.isLoading;
- const isFetching = mockIsLoading || details.isFetching;
- const isError = mockIsLoading ? false : mockIsError || details.isError;
-
- const t = details.t ?? 0;
-
- const data = useMemo(() => {
- const now = NetworkTime.now();
-
- const expiryTimeMs =
- mockExpiry ??
- details.data?.expiry_unix_ts_ms ??
- defaultProAccessDetailsSourceData.expiryTimeMs;
-
- const latestAccess = details?.data?.items?.[0];
- const provider =
- mockPlatform ?? latestAccess?.payment_provider ?? defaultProAccessDetailsSourceData.provider;
- const variant = mockVariant ?? latestAccess?.plan ?? defaultProAccessDetailsSourceData.variant;
- const isPlatformRefundAvailable =
- mockIsPlatformRefundAvailable ||
- (latestAccess?.platform_refund_expiry_unix_ts_ms &&
- now < latestAccess.platform_refund_expiry_unix_ts_ms) ||
- defaultProAccessDetailsSourceData.isPlatformRefundAvailable;
-
- const autoRenew = mockCancelled
- ? !mockCancelled
- : (details.data?.auto_renewing ?? defaultProAccessDetailsSourceData.autoRenew);
-
- let beginAutoRenew = 0;
- if (details.data) {
- beginAutoRenew = details.data.expiry_unix_ts_ms - details.data.grace_period_duration_ms;
- }
-
- let inGracePeriod = mockInGracePeriod;
- if (beginAutoRenew && !mockInGracePeriod) {
- inGracePeriod = autoRenew && now >= beginAutoRenew && now < expiryTimeMs;
- }
-
- return {
- currentStatus: currentUserProStatus,
- autoRenew,
- inGracePeriod,
- variant,
- variantString: proAccessVariantToString(variant),
- expiryTimeMs,
- expiryTimeDateString: formatDateWithLocale({
- date: new Date(beginAutoRenew),
- formatStr: 'MMM d, yyyy',
- }),
- expiryTimeRelativeString: formatRoundedUpTimeUntilTimestamp(beginAutoRenew),
- isPlatformRefundAvailable,
- provider,
- providerConstants: getProProviderConstantsWithFallbacks(provider),
- };
- }, [
- details.data,
- currentUserProStatus,
- mockVariant,
- mockPlatform,
- mockCancelled,
- mockInGracePeriod,
- mockIsPlatformRefundAvailable,
- mockExpiry,
- ]);
-
- const mockRefetchSuccess = useCallback(async () => {
- if (mockIsLoading) {
- return;
- }
- setProBackendIsLoading({ key: 'details', result: true });
- setProBackendIsError({ key: 'details', result: false });
- await sleepFor(5000);
- mockRecoverAccess.mockRecover();
- setProBackendIsLoading({ key: 'details', result: false });
- }, [setProBackendIsError, setProBackendIsLoading, mockRecoverAccess, mockIsLoading]);
-
- const mockRefetchFail = useCallback(async () => {
- if (mockIsLoading) {
- return;
- }
- setProBackendIsLoading({ key: 'details', result: true });
- setProBackendIsError({ key: 'details', result: false });
- await sleepFor(5000);
- setProBackendIsError({ key: 'details', result: true });
- setProBackendIsLoading({ key: 'details', result: false });
- }, [setProBackendIsError, setProBackendIsLoading, mockIsLoading]);
-
- const refetch = useCallback(
- (args: WithCallerContext = {}) => {
- if (mockIsError || mockRecoverAccess.mockFail) {
- void mockRefetchFail();
- return;
- }
-
- if (mockRecoverAccess.mockSuccess) {
- void mockRefetchSuccess();
- return;
- }
-
- dispatch(proBackendDataActions.refreshGetProDetailsFromProBackend(args) as any);
- },
- [dispatch, mockIsError, mockRecoverAccess, mockRefetchSuccess, mockRefetchFail]
- );
-
- return {
- isLoading,
- isFetching,
- isError,
- refetch,
- data,
- t,
- };
-}
diff --git a/ts/hooks/useIsProAvailable.ts b/ts/hooks/useIsProAvailable.ts
index 83c135e752..6ad56b341a 100644
--- a/ts/hooks/useIsProAvailable.ts
+++ b/ts/hooks/useIsProAvailable.ts
@@ -1,9 +1,9 @@
-import { useFeatureFlag } from '../state/ducks/types/releasedFeaturesReduxTypes';
+import { getFeatureFlagMemo } from '../state/ducks/types/releasedFeaturesReduxTypes';
-export function useIsProAvailable() {
- return !!useFeatureFlag('proAvailable');
+export function getIsProAvailableMemo() {
+ return !!getFeatureFlagMemo('proAvailable');
}
-export function useIsProGroupsAvailable() {
- return !!useFeatureFlag('proGroupsAvailable');
+export function getIsProGroupsAvailableMemo() {
+ return !!getFeatureFlagMemo('proGroupsAvailable');
}
diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts
index 4b62d8b6c9..ba5158108e 100644
--- a/ts/hooks/useParamSelector.ts
+++ b/ts/hooks/useParamSelector.ts
@@ -17,13 +17,17 @@ import {
getMessagePropsByMessageId,
getMessageReactsProps,
} from '../state/selectors/conversations';
-import { useLibGroupAdmins, useLibGroupMembers, useLibGroupName } from '../state/selectors/groups';
+import {
+ selectLibAdminsPubkeys,
+ selectLibGroupName,
+ useLibGroupMembers,
+} from '../state/selectors/groups';
import { isPrivateAndFriend } from '../state/selectors/selectedConversation';
-import { useOurPkStr } from '../state/selectors/user';
+import { getOurNumber } from '../state/selectors/user';
import {
+ getGroupById,
+ getLibGroupKicked,
useLibGroupDestroyed,
- useLibGroupInvitePending,
- useLibGroupKicked,
} from '../state/selectors/userGroups';
import { ConversationInteractionStatus, ConversationInteractionType } from '../interactions/types';
import { tr } from '../localization/localeTools';
@@ -36,6 +40,11 @@ export function useOurAvatarPath() {
return useAvatarPath(UserUtils.getOurPubKeyStrFromCache());
}
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useLibGroupName(convoId?: string): string | undefined {
+ return useSelector((state: StateType) => selectLibGroupName(state, convoId));
+}
+
/**
* Returns the nickname, the groupname or the displayNameInProfile of that conversation if set.
*/
@@ -216,9 +225,14 @@ export function useIsActive(convoId?: string) {
return !!useActiveAt(convoId);
}
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useLibGroupKickedInternal(convoId?: string) {
+ return useSelector((state: StateType) => getLibGroupKicked(state, convoId));
+}
+
export function useIsKickedFromGroup(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
- const libIsKicked = useLibGroupKicked(convoId);
+ const libIsKicked = useLibGroupKickedInternal(convoId);
if (convoId && PubKey.is03Pubkey(convoId)) {
return libIsKicked ?? false;
}
@@ -233,10 +247,27 @@ export function useIsGroupDestroyed(convoId?: string) {
return false;
}
-export function useWeAreAdmin(convoId?: string) {
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useIsAdmin(convoId?: string, pk?: string) {
const groupAdmins = useGroupAdmins(convoId);
- const us = useOurPkStr();
- return Boolean(groupAdmins.includes(us));
+ const isAdmin = Boolean(pk && groupAdmins.includes(pk));
+ return isAdmin;
+}
+
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useOurPkStrInternal() {
+ return useSelector((state: StateType) => getOurNumber(state));
+}
+
+export function useWeAreAdmin(convoId?: string) {
+ const us = useOurPkStrInternal();
+ const isAdmin = useIsAdmin(convoId, us);
+ return isAdmin;
+}
+
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useLibGroupAdmins(convoId?: string): Array {
+ return useSelector((state: StateType) => selectLibAdminsPubkeys(state, convoId));
}
export function useGroupAdmins(convoId?: string) {
@@ -245,7 +276,8 @@ export function useGroupAdmins(convoId?: string) {
const libMembers = useLibGroupAdmins(convoId);
if (convoId && PubKey.is03Pubkey(convoId)) {
- return compact(libMembers?.slice()?.sort()) || [];
+ const members = libMembers?.slice()?.sort() ?? [];
+ return compact(members);
}
return convoProps?.groupAdmins || [];
@@ -281,12 +313,19 @@ export function useIsApproved(convoId?: string) {
return Boolean(convoProps && convoProps.isApproved);
}
+// NOTE: [react-compiler] this has to live here for the hook to be identified as static
+function useLibGroupInvitePendingInternal(convoId?: string) {
+ return useSelector((state: StateType) => getGroupById(state, convoId)?.invitePending ?? false);
+}
+
export function useIsIncomingRequest(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
- const invitePending = useLibGroupInvitePending(convoId) || false;
+ const invitePending = useLibGroupInvitePendingInternal(convoId);
+
if (!convoProps) {
return false;
}
+
return Boolean(
convoProps &&
hasValidIncomingRequestValues({
@@ -450,6 +489,11 @@ export function use05GroupMembers(convoId: string | undefined): Array void;
onClose?: () => void;
}) {
- const dispatch = useDispatch();
+ const dispatch = getAppDispatch();
const hashFromStorage = getPasswordHash();
const [hasPassword] = useState(!!hashFromStorage);
diff --git a/ts/mains/main_renderer.tsx b/ts/mains/main_renderer.tsx
index 66fd3b5625..c54f0d866d 100644
--- a/ts/mains/main_renderer.tsx
+++ b/ts/mains/main_renderer.tsx
@@ -1,12 +1,9 @@
import { toPairs } from 'lodash';
import { createRoot } from 'react-dom/client';
-
import nativeEmojiData from '@emoji-mart/data';
import { ipcRenderer } from 'electron';
-// eslint-disable-next-line import/no-named-default
-
import { isMacOS } from '../OS';
-import { doAppStartUp, SessionInboxView } from '../components/SessionInboxView';
+import { SessionInboxView } from '../components/SessionInboxView';
import { SessionRegistrationView } from '../components/registration/SessionRegistrationView';
import { Data } from '../data/data';
import { OpenGroupData } from '../data/opengroups';
@@ -16,7 +13,6 @@ import { loadKnownBlindedKeys } from '../session/apis/open_group_api/sogsv3/know
import { ConvoHub } from '../session/conversations';
import { DisappearingMessages } from '../session/disappearing_messages';
import { AttachmentDownloads, ToastUtils } from '../session/utils';
-import { getOurPubKeyStrFromCache } from '../session/utils/User';
import { runners } from '../session/utils/job_runners/JobRunner';
import { LibSessionUtil } from '../session/utils/libsession/libsession_utils';
import { switchPrimaryColorTo } from '../themes/switchPrimaryColor';
@@ -30,6 +26,7 @@ import { getOppositeTheme, isThemeMismatched } from '../util/theme';
import { getCrowdinLocale } from '../util/i18n/shared';
import { rtlLocales } from '../localization/constants';
import { SessionEventEmitter } from '../shared/event_emitter';
+import { doAppStartUp } from '../state/startup';
import { getSodiumRenderer } from '../session/crypto';
// Globally disable drag and drop
@@ -69,28 +66,12 @@ window.log.info('Storage fetch');
void Storage.fetch();
-function mapOldThemeToNew(theme: string) {
- switch (theme) {
- case 'dark':
- case 'light':
- return `classic-${theme}`;
- case 'android-dark':
- return 'classic-dark';
- case 'android':
- case 'ios':
- case '':
- return 'classic-dark';
- default:
- return theme;
- }
-}
-
// using __unused as lodash is imported using _
ipcRenderer.on('native-theme-update', (__unused, shouldUseDarkColors) => {
const shouldFollowSystemTheme = window.getSettingValue(SettingsKey.hasFollowSystemThemeEnabled);
if (shouldFollowSystemTheme) {
- const theme = window.Events.getThemeSetting();
+ const theme = window.getSettingValue(SettingsKey.settingsTheme);
if (isThemeMismatched(theme, shouldUseDarkColors)) {
const newTheme = getOppositeTheme(theme);
void switchThemeTo({
@@ -127,23 +108,12 @@ Storage.onready(async () => {
// Update zoom
window.updateZoomFactor();
- // Ensure accounts created prior to 1.0.0-beta8 do have their
- // 'primaryDevicePubKey' defined.
-
- if (Registration.isDone() && !Storage.get('primaryDevicePubKey')) {
- await Storage.put('primaryDevicePubKey', getOurPubKeyStrFromCache());
- }
-
// These make key operations available to IPC handlers created in preload.js
window.Events = {
getPrimaryColorSetting: () => Storage.get('primary-color-setting', 'green'),
setPrimaryColorSetting: async (value: any) => {
await Storage.put('primary-color-setting', value);
},
- getThemeSetting: () => Storage.get('theme-setting', 'classic-dark'),
- setThemeSetting: async (value: any) => {
- await Storage.put('theme-setting', value);
- },
getHideMenuBar: () => Storage.get('hide-menu-bar'),
setHideMenuBar: async (value: boolean) => {
await Storage.put('hide-menu-bar', value);
@@ -179,10 +149,6 @@ Storage.onready(async () => {
await Data.cleanupOrphanedAttachments();
}
- const themeSetting = window.Events.getThemeSetting();
- const newThemeSetting = mapOldThemeToNew(themeSetting);
- await window.Events.setThemeSetting(newThemeSetting);
-
try {
if (Registration.isDone()) {
try {
@@ -273,6 +239,9 @@ async function start() {
const container = document.getElementById('root');
const root = createRoot(container!);
await doAppStartUp();
+ if (!window.inboxStore) {
+ throw new Error('window.inboxStore is not defined in openInbox');
+ }
root.render();
}
@@ -306,10 +275,6 @@ async function start() {
const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1;
- window.setTheme = async newTheme => {
- await window.Events.setThemeSetting(newTheme);
- };
-
window.toggleMenuBar = () => {
const current = window.getSettingValue('hide-menu-bar');
if (current === undefined) {
diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts
index 4d671cf6cf..df481bd479 100644
--- a/ts/receiver/queuedJob.ts
+++ b/ts/receiver/queuedJob.ts
@@ -1,12 +1,10 @@
import _, { isEmpty, isNumber, toNumber } from 'lodash';
import { queueAttachmentDownloads } from './attachments';
-
import { Data } from '../data/data';
import { ConversationModel } from '../models/conversation';
import { MessageModel } from '../models/message';
import { ConvoHub } from '../session/conversations';
import { Quote, type BaseDecodedEnvelope, type SwarmDecodedEnvelope } from './types';
-
import { MessageDirection } from '../models/messageType';
import { ConversationTypeEnum } from '../models/types';
import { SignalService } from '../protobuf';
@@ -18,13 +16,13 @@ import {
lookupQuote,
pushQuotedMessageDetails,
} from '../state/ducks/conversations';
-import { showMessageRequestBannerOutsideRedux } from '../state/ducks/userConfig';
import { selectMemberInviteSentOutsideRedux } from '../state/selectors/groups';
-import { getHideMessageRequestBannerOutsideRedux } from '../state/selectors/userConfig';
import { LinkPreviews } from '../util/linkPreviews';
import { GroupV2Receiver } from './groupv2/handleGroupV2Message';
import { Constants } from '../session';
import { longOrNumberToNumber } from '../types/long/longOrNumberToNumber';
+import { getHideMessageRequestBannerOutsideRedux } from '../state/selectors/settings';
+import { showMessageRequestBannerOutsideRedux } from '../state/ducks/settings';
import { getFeatureFlag } from '../state/ducks/types/releasedFeaturesReduxTypes';
function isMessageModel(
@@ -178,7 +176,7 @@ async function toggleMsgRequestBannerIfNeeded(
isFirstRequestMessage &&
getHideMessageRequestBannerOutsideRedux()
) {
- showMessageRequestBannerOutsideRedux();
+ await showMessageRequestBannerOutsideRedux();
}
// For edge case when messaging a client that's unable to explicitly send request approvals
diff --git a/ts/session/apis/network_api/types.ts b/ts/session/apis/network_api/types.ts
index 68981663de..ebf7cbc818 100644
--- a/ts/session/apis/network_api/types.ts
+++ b/ts/session/apis/network_api/types.ts
@@ -1,11 +1,14 @@
-// NOTE: Dont import the whole package because we would need to setup the providers which we dont need
-import { z } from 'zod';
+import z from '../../../util/zod';
import { SessionBackendBaseResponseSchema } from '../session_backend_server';
// NOTE: this is only needed here for schema validation, but we should move this elsewhere if we use it for other things
const ethereumAddressRegex = /^0x[a-fA-F0-9]{40}$/;
export type EthereumAddress = `0x${string}`;
const isEthereumAddress = (v: string): v is EthereumAddress => ethereumAddressRegex.test(v);
+const EthereumAddressSchema = z
+ .string()
+ .refine(isEthereumAddress, { message: 'Invalid Ethereum address' })
+ .transform(v => v as EthereumAddress);
/**
* Token price info object
@@ -30,7 +33,7 @@ const PriceSchema = z.object({
const TokenSchema = z.object({
staking_requirement: z.number(),
staking_reward_pool: z.number(),
- contract_address: z.string().refine(isEthereumAddress),
+ contract_address: EthereumAddressSchema,
});
/** Network info object
diff --git a/ts/session/apis/pro_backend_api/schemas.ts b/ts/session/apis/pro_backend_api/schemas.ts
index e653bbd80c..2787726205 100644
--- a/ts/session/apis/pro_backend_api/schemas.ts
+++ b/ts/session/apis/pro_backend_api/schemas.ts
@@ -1,5 +1,5 @@
import { base64_variants, from_hex, to_base64 } from 'libsodium-wrappers-sumo';
-import { z } from 'zod';
+import z from '../../../util/zod';
import { ProItemStatus, ProAccessVariant, ProPaymentProvider, ProStatus } from './types';
import { SessionBackendBaseResponseSchema } from '../session_backend_server';
@@ -65,9 +65,9 @@ export const GetProRevocationsResponseSchema = SessionBackendBaseResponseSchema.
export type GetProRevocationsResponseType = z.infer;
const ProDetailsItemSchema = z.object({
- status: z.nativeEnum(ProItemStatus),
- plan: z.nativeEnum(ProAccessVariant),
- payment_provider: z.nativeEnum(ProPaymentProvider),
+ status: z.enum(ProItemStatus),
+ plan: z.enum(ProAccessVariant),
+ payment_provider: z.enum(ProPaymentProvider),
auto_renewing: z.boolean(),
unredeemed_unix_ts_ms: z.number(),
refund_requested_unix_ts_ms: z.number(),
@@ -84,7 +84,7 @@ const ProDetailsItemSchema = z.object({
});
export const ProDetailsResultSchema = z.object({
- status: z.nativeEnum(ProStatus),
+ status: z.enum(ProStatus),
auto_renewing: z.boolean(),
expiry_unix_ts_ms: z.number(),
grace_period_duration_ms: z.number(),
diff --git a/ts/session/apis/session_backend_server.ts b/ts/session/apis/session_backend_server.ts
index a246d4a8f3..349365315c 100644
--- a/ts/session/apis/session_backend_server.ts
+++ b/ts/session/apis/session_backend_server.ts
@@ -1,5 +1,6 @@
import AbortController from 'abort-controller';
-import { z, ZodError } from 'zod';
+import type { ZodError } from 'zod';
+import z, { zodSafeParse } from '../../util/zod';
import { BlindingActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { isOnionV4JSONSnodeResponse, OnionSending } from '../onions/onionSend';
import { fromUInt8ArrayToBase64 } from '../utils/String';
@@ -31,10 +32,6 @@ export type SessionBackendServerApiOptions = Omit<
abortControllerTimeoutMs?: number;
};
-type WithZodSchemaValidation = {
- withZodSchema: S;
-};
-
type HTTPMethod = 'GET' | 'POST';
type SessionBackendServerMakeRequestParams = {
@@ -198,16 +195,16 @@ export default class SessionBackendServerApi {
};
}
- private parseSchema({
+ private parseSchema({
path,
response,
schema,
}: {
path: string;
response: SessionBackendServerApiResponse;
- schema: R;
- }): z.infer | null {
- const result = schema.safeParse(response);
+ schema: z.ZodType;
+ }): T | null {
+ const result = zodSafeParse(schema, response);
if (result.success) {
return result.data;
}
@@ -237,11 +234,12 @@ export default class SessionBackendServerApi {
return response;
}
- public async makeRequestWithSchema({
+ public async makeRequestWithSchema