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" > full-brand-logo full-brand-text 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 ( - - - -