Compare commits

..

29 Commits
main ... 7.41.x

Author SHA1 Message Date
ayumi-signal
a0b72bffa3 7.41.0
Some checks failed
On Release / Create release event in datadog (push) Has been cancelled
Benchmark / linux (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / Sticker Creator (push) Has been cancelled
Commits Check / linux (push) Has been cancelled
Stories / test (push) Has been cancelled
CI / macos (push) Has been cancelled
CI / linux (push) Has been cancelled
CI / windows (push) Has been cancelled
CI / mock-tests (push) Has been cancelled
2025-02-05 11:14:30 -08:00
ayumi-signal
dcad4c1b4e Update strings 2025-02-05 11:14:30 -08:00
ayumi-signal
6a2b5ea3bc Release notes for 7.41.0 2025-02-05 11:09:52 -08:00
ayumi-signal
cf10a5c804 7.41.0-beta.2
Some checks failed
On Release / Create release event in datadog (push) Has been cancelled
2025-02-03 13:15:43 -08:00
ayumi-signal
de8030fb39 Update strings 2025-02-03 13:15:43 -08:00
automated-signal
4e9d7321b6
Avoid mute timeouts with invalid delay values
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2025-02-03 13:10:01 -08:00
automated-signal
9f5115a537
Fix status of in-flight sticker packs after import
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-31 11:25:42 +10:00
automated-signal
0beec3745b
Fix timestamp capping for storage service
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-30 11:14:59 -08:00
Yash
48da36376c 7.41.0-beta.1
Some checks failed
On Release / Create release event in datadog (push) Has been cancelled
2025-01-29 14:14:44 -06:00
Yash
94ae230dbe Update strings 2025-01-29 14:14:32 -06:00
Yash
be5a5b2e93 release notes for 7.41 2025-01-29 14:09:44 -06:00
automated-signal
a580564d59
Wrap link preview description for long words (#9683)
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-29 12:38:33 -06:00
automated-signal
71041f323b
Fix accidental loop during backup import
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-29 10:20:36 -08:00
automated-signal
c1d5a835e0 Enable link & sync in beta
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2025-01-28 21:49:03 -08:00
automated-signal
c72f788d9d
Update better-sqlite3 to 9.0.10
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-28 15:37:55 -08:00
automated-signal
301133970e
Upgrade to libsignal-client v0.65.4
Co-authored-by: Alex Bakon <akonradi@signal.org>
2025-01-28 15:08:04 -08:00
automated-signal
ffbd8d5ea7
Resolve sticker pack references after import
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-28 14:33:43 -08:00
automated-signal
5831a23821
Fix padding of conversation list
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-28 13:17:12 -08:00
automated-signal
e646d25b56
Fix QR-code auto-retry logic
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-28 13:17:02 -08:00
automated-signal
539ebb14dd
Update to latest backup integration tests
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2025-01-28 13:16:52 -08:00
automated-signal
9f5602af05
Disallow conversation model creation during import
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-28 13:16:43 -08:00
automated-signal
55464045a4
removeFromGroup: Don't use removed member for credentials
Co-authored-by: Scott Nonnenberg <scott@signal.org>
2025-01-28 13:16:29 -08:00
automated-signal
3b11533a98
Advertise both link and sync capabilities
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2025-01-28 13:15:44 -08:00
automated-signal
86c57aeffb
Roundtrip group.blocked state
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2025-01-28 13:30:11 -05:00
automated-signal
f8088e4fa6
Update which messages affect chat list presence on import
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2025-01-23 14:24:22 -08:00
automated-signal
457d2e6448
Fix sticker pack download on import
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-23 14:24:12 -08:00
automated-signal
7e0d43fb99
Update link & sync availability 2025-01-23 16:20:44 -05:00
automated-signal
77e5e2c947
Update conversation hero message
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2025-01-23 12:41:56 -08:00
automated-signal
7b6e03aff9
Fix author id for e164-only 1:1 messages
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2025-01-23 12:41:47 -08:00
5720 changed files with 603507 additions and 804393 deletions

21
.babelrc.js Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
module.exports = {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
// Detects the type of file being babel'd (either esmodule or commonjs)
sourceType: 'unambiguous',
plugins: [
'react-hot-loader/babel',
'lodash',
'@babel/plugin-transform-typescript',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
// This plugin converts commonjs to esmodules which is required for
// importing commonjs modules from esmodules in storybook. As a part of
// converting to TypeScript we should use esmodules and can eventually
// remove this plugin
process.env.SIGNAL_ENV === 'storybook' && '@babel/transform-runtime',
].filter(Boolean),
};

View File

@ -1,24 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/** @type {import("@babel/core").TransformOptions} */
const config = {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
// Detects the type of file being babel'd (either esmodule or commonjs)
sourceType: 'unambiguous',
plugins: [
'lodash',
'@babel/plugin-transform-typescript',
// This plugin converts commonjs to esmodules which is required for
// importing commonjs modules from esmodules in storybook. As a part of
// converting to TypeScript we should use esmodules and can eventually
// remove this plugin
process.env.SIGNAL_ENV === 'storybook' &&
import.meta.resolve('@babel/plugin-transform-runtime'),
].filter(plugin => {
return typeof plugin === 'string';
}),
};
export default config;

View File

@ -0,0 +1,58 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
function isReadOnlyDeep(node, scope) {
if (node.type !== 'TSTypeReference') {
return false;
}
let reference = scope.references.find(reference => {
return reference.identifier === node.typeName;
});
let variable = reference.resolved;
if (variable == null) {
return false;
}
let defs = variable.defs;
if (defs.length !== 1) {
return false;
}
let [def] = defs;
return (
def.type === 'ImportBinding' &&
def.parent.type === 'ImportDeclaration' &&
def.parent.source.type === 'Literal' &&
def.parent.source.value === 'type-fest'
);
}
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
hasSuggestions: false,
fixable: false,
schema: [],
},
create(context) {
return {
TSTypeAliasDeclaration(node) {
let scope = context.getScope(node);
if (isReadOnlyDeep(node.typeAnnotation, scope)) {
return;
}
context.report({
node: node.id,
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
});
},
};
},
};

View File

@ -0,0 +1,79 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const rule = require('./type-alias-readonlydeep');
const RuleTester = require('eslint').RuleTester;
// avoid triggering mocha's global leak detection
require('@typescript-eslint/parser');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
});
ruleTester.run('type-alias-readonlydeep', rule, {
valid: [
{
code: `import type { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
{
code: `import { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
],
invalid: [
{
code: `type Foo = {}`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `type Foo = Bar<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `type Foo = ReadonlyDeep<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `interface ReadonlyDeep<T> {}; type Foo = ReadonlyDeep<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `import type { ReadonlyDeep } from "foo"; type Foo = ReadonlyDeep<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
],
});

36
.eslintignore Normal file
View File

@ -0,0 +1,36 @@
components/**
coverage/**
dist/**
release/**
# Github workflows
.github/**
# Generated files
js/curve/*
js/components.js
js/util_worker.js
libtextsecure/components.js
libtextsecure/test/test.js
test/test.js
ts/protobuf/compiled.d.ts
storybook-static/**
build/ICUMessageParams.d.ts
# Third-party files
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js
js/calling-tools/**
# TypeScript generated files
app/**/*.js
ts/**/*.js
.eslintrc.js
webpack.config.ts
preload.bundle.*
preload.wrapper.*
bundles/**
# Sticker Creator has its own eslint config
sticker-creator/**

329
.eslintrc.js Normal file
View File

@ -0,0 +1,329 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// For reference: https://github.com/airbnb/javascript
const rules = {
'comma-dangle': [
'error',
{
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
functions: 'never',
},
],
// No omitting braces, keep on the same line
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
curly: ['error', 'all'],
// Immer support
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsForRegex: ['^draft'],
ignorePropertyModificationsFor: ['acc', 'ctx', 'context'],
},
],
// Always use === and !== except when directly comparing to null
// (which only will equal null or undefined)
eqeqeq: ['error', 'always', { null: 'never' }],
// prevents us from accidentally checking in exclusive tests (`.only`):
'mocha/no-exclusive-tests': 'error',
// encourage consistent use of `async` / `await` instead of `then`
'more/no-then': 'error',
// it helps readability to put public API at top,
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
// useful for unused or internal fields
'no-underscore-dangle': 'off',
// Temp: We have because TypeScript's `allowUnreachableCode` option is on.
'no-unreachable': 'error',
// though we have a logger, we still remap console to log to disk
'no-console': 'error',
// consistently place operators at end of line except ternaries
'operator-linebreak': [
'error',
'after',
{ overrides: { '?': 'ignore', ':': 'ignore' } },
],
quotes: [
'error',
'single',
{ avoidEscape: true, allowTemplateLiterals: false },
],
'no-continue': 'off',
'lines-between-class-members': 'off',
'class-methods-use-this': 'off',
// Prettier overrides:
'arrow-parens': 'off',
'function-paren-newline': 'off',
'max-len': [
'error',
{
// Prettier generally limits line length to 80 but sometimes goes over.
// The `max-len` plugin doesnt let us omit `code` so we set it to a
// high value as a buffer to let Prettier control the line length:
code: 999,
// We still want to limit comments as before:
comments: 90,
ignoreUrls: true,
},
],
'react/jsx-props-no-spreading': 'off',
// Updated to reflect future airbnb standard
// Allows for declaring defaultProps inside a class
'react/static-property-placement': ['error', 'static public field'],
// JIRA: DESKTOP-657
'react/sort-comp': 'off',
// We don't have control over the media we're sharing, so can't require
// captions.
'jsx-a11y/media-has-caption': 'off',
// We prefer named exports
'import/prefer-default-export': 'off',
// Prefer functional components with default params
'react/require-default-props': 'off',
// Empty fragments are used in adapters between backbone and react views.
'react/jsx-no-useless-fragment': [
'error',
{
allowExpressions: true,
},
],
// Our code base has tons of arrow functions passed directly to components.
'react/jsx-no-bind': 'off',
// Does not support forwardRef
'react/no-unused-prop-types': 'off',
// Not useful for us as we have lots of complicated types.
'react/destructuring-assignment': 'off',
'react/function-component-definition': [
'error',
{
namedComponents: 'function-declaration',
unnamedComponents: 'arrow-function',
},
],
'react/display-name': 'error',
'react/jsx-pascal-case': ['error', { allowNamespace: true }],
// Allow returning values from promise executors for brevity.
'no-promise-executor-return': 'off',
// Redux ducks use this a lot
'default-param-last': 'off',
'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
'jsx-a11y/no-static-element-interactions': 'error',
'@typescript-eslint/no-non-null-assertion': ['error'],
'@typescript-eslint/no-empty-interface': ['error'],
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'no-restricted-syntax': [
'error',
{
selector: 'TSInterfaceDeclaration',
message:
'Prefer `type`. Interfaces are mutable and less powerful, so we prefer `type` for simplicity.',
},
// Defaults
{
selector: 'ForInStatement',
message:
'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
},
{
selector: 'LabeledStatement',
message:
'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
},
{
selector: 'WithStatement',
message:
'`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
},
],
'react-hooks/exhaustive-deps': [
'error',
{
additionalHooks: '^(useSpring|useSprings)$',
},
],
};
const typescriptRules = {
...rules,
// Override brace style to enable typescript-specific syntax
'brace-style': 'off',
'@typescript-eslint/brace-style': [
'error',
'1tbs',
{ allowSingleLine: false },
],
'@typescript-eslint/array-type': ['error', { default: 'generic' }],
'no-restricted-imports': 'off',
'@typescript-eslint/no-restricted-imports': [
'error',
{
paths: [
{
name: 'chai',
importNames: ['expect', 'should', 'Should'],
message: 'Please use assert',
allowTypeImports: true,
},
],
},
],
// Overrides recommended by typescript-eslint
// https://github.com/typescript-eslint/typescript-eslint/releases/tag/v4.0.0
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-useless-constructor': ['error'],
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false,
},
],
'@typescript-eslint/no-floating-promises': 'error',
// We allow "void promise", but new call-sites should use `drop(promise)`.
'no-void': ['error', { allowAsStatement: true }],
'no-shadow': 'off',
'no-useless-constructor': 'off',
// useful for unused parameters
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// Upgrade from a warning
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
// Future: Maybe switch to never and always use `satisfies`
'@typescript-eslint/consistent-type-assertions': [
'error',
{
assertionStyle: 'as',
// Future: Maybe switch to allow-as-parameter or never
objectLiteralTypeAssertions: 'allow',
},
],
// Already enforced by TypeScript
'consistent-return': 'off',
// TODO: DESKTOP-4655
'import/no-cycle': 'off',
};
module.exports = {
root: true,
settings: {
react: {
version: 'detect',
},
'import/core-modules': ['electron'],
},
extends: ['airbnb-base', 'prettier'],
plugins: ['mocha', 'more', 'local-rules'],
overrides: [
{
files: [
'ts/**/*.ts',
'ts/**/*.tsx',
'app/**/*.ts',
'build/intl-linter/**/*.ts',
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'airbnb-typescript-prettier',
],
rules: typescriptRules,
},
{
files: [
'**/*.stories.tsx',
'ts/build/**',
'ts/test-*/**',
'build/intl-linter/**/*.ts',
],
rules: {
...typescriptRules,
'import/no-extraneous-dependencies': 'off',
'react/no-array-index-key': 'off',
},
},
{
files: ['ts/state/ducks/**/*.ts'],
rules: {
'local-rules/type-alias-readonlydeep': 'error',
},
},
{
files: ['ts/**/*_test.{ts,tsx}'],
rules: {
'func-names': 'off',
},
},
],
rules: {
...rules,
'import/no-unresolved': 'off',
'import/extensions': 'off',
},
reportUnusedDisableDirectives: true,
};

View File

@ -21,7 +21,7 @@ body:
label: "Using a supported version?"
description: "Search issues here: https://github.com/signalapp/Signal-Desktop/issues"
options:
- label: I have searched open and closed issues for duplicates.
- label: I have searched searched open and closed issues for duplicates.
required: true
- label: I am using Signal-Desktop as provided by the Signal team, not a 3rd-party package.
required: true
@ -97,7 +97,7 @@ body:
id: primary-device
attributes:
label: Version of Signal on your phone
description: "Settings->Help"
description: "Android: Settings->Help, iOS: Settings->General->About"
placeholder:
validations:
required: false

View File

@ -17,7 +17,7 @@ Remember, you can preview this before saving it.
- [ ] My contribution is **not** related to translations.
- [ ] My commits are in nice logical chunks with [good commit messages](http://chris.beams.io/posts/git-commit/)
- [ ] My changes are [rebased](https://medium.com/free-code-camp/git-rebase-and-the-golden-rule-explained-70715eccc372) on the latest [`main`](https://github.com/signalapp/Signal-Desktop/tree/main) branch
- [ ] A `pnpm run ready` run passes successfully ([more about tests here](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md#tests))
- [ ] A `npm run ready` run passes successfully ([more about tests here](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md#tests))
- [ ] My changes are ready to be shipped to users
### Description

View File

@ -3,15 +3,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
groups:
minor-actions-dependencies:
# GitHub Actions: Only group minor and patch updates (we want to carefully review major updates)
update-types: [ minor, patch ]
- package-ecosystem: npm
directories:
- "/"

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
repository: signalapp/Signal-Backport-Action-Private

219
.github/workflows/benchmark.yml vendored Normal file
View File

@ -0,0 +1,219 @@
# Copyright 2020 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Benchmark
on:
push:
branches:
- development
- main
- '[0-9]+.[0-9]+.x'
pull_request:
schedule:
- cron: '0 */12 * * *'
jobs:
linux:
runs-on: ubuntu-22.04-8-cores
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && (!github.event.schedule || github.ref == 'refs/heads/main') }}
timeout-minutes: 30
steps:
- name: Get system specs
run: lsb_release -a
- name: Get other system specs
run: uname -a
- name: Clone Desktop repo
uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install global dependencies
run: npm install -g npm@10.2.5
- name: Install xvfb
run: sudo apt-get install xvfb libpulse0
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
- name: Install Desktop node_modules
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: npm ci
env:
NPM_CONFIG_LOGLEVEL: verbose
- name: Build typescript
run: npm run generate
- name: Bundle
run: npm run build:esbuild:prod
- name: Create preload cache
run: xvfb-run --auto-servernum npm run build:preload-cache
- name: Run startup benchmarks
run: |
set -o pipefail
xvfb-run --auto-servernum node ts/test-mock/benchmarks/startup_bench.js |
tee benchmark-startup.log
timeout-minutes: 10
env:
NODE_ENV: production
RUN_COUNT: 10
ELECTRON_ENABLE_STACK_DUMPING: on
DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/startup
- name: Run send benchmarks
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node ts/test-mock/benchmarks/send_bench.js |
tee benchmark-send.log
timeout-minutes: 10
env:
NODE_ENV: production
RUN_COUNT: 100
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/send
- name: Run group send benchmarks
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node \
ts/test-mock/benchmarks/group_send_bench.js | \
tee benchmark-group-send.log
timeout-minutes: 10
env:
NODE_ENV: production
RUN_COUNT: 100
CONVERSATION_SIZE: 500
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/group-send
- name: Run large group send benchmarks with blocks
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node \
ts/test-mock/benchmarks/group_send_bench.js | \
tee benchmark-large-group-send-with-blocks.log
timeout-minutes: 10
env:
NODE_ENV: production
GROUP_SIZE: 500
CONTACT_COUNT: 500
BLOCKED_COUNT: 10
DISCARD_COUNT: 2
RUN_COUNT: 50
CONVERSATION_SIZE: 500
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/group-send
- name: Run large group send benchmarks with delivery receipts
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node \
ts/test-mock/benchmarks/group_send_bench.js | \
tee benchmark-large-group-send.log
timeout-minutes: 10
env:
NODE_ENV: production
GROUP_SIZE: 500
CONTACT_COUNT: 500
GROUP_DELIVERY_RECEIPTS: 500
DISCARD_COUNT: 2
RUN_COUNT: 20
CONVERSATION_SIZE: 50
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/large-group-send
- name: Run conversation open benchmarks
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node \
ts/test-mock/benchmarks/convo_open_bench.js | \
tee benchmark-convo-open.log
timeout-minutes: 10
env:
NODE_ENV: production
RUN_COUNT: 100
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/convo-open
- name: Run call history search benchmarks
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node \
ts/test-mock/benchmarks/call_history_search_bench.js | \
tee benchmark-call-history-search.log
timeout-minutes: 10
env:
NODE_ENV: production
RUN_COUNT: 100
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/call-history-search
- name: Run backup benchmarks
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node \
ts/test-mock/benchmarks/backup_bench.js | \
tee benchmark-backup.log
timeout-minutes: 10
env:
NODE_ENV: production
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/backup-bench
- name: Upload benchmark logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: logs
path: artifacts
- name: Clone benchmark repo
uses: actions/checkout@v4
with:
repository: 'signalapp/Signal-Desktop-Benchmarks-Private'
path: 'benchmark-results'
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
- name: Build benchmark repo
working-directory: benchmark-results
run: |
npm ci
npm run build
- name: Publish to DataDog
working-directory: benchmark-results
run: |
node ./bin/publish.js ../benchmark-startup.log desktop.ci.performance.startup
node ./bin/publish.js ../benchmark-send.log desktop.ci.performance.send
node ./bin/publish.js ../benchmark-group-send.log desktop.ci.performance.groupSend
node ./bin/publish.js ../benchmark-large-group-send-with-blocks.log desktop.ci.performance.largeGroupSendWithBlocks
node ./bin/publish.js ../benchmark-large-group-send.log desktop.ci.performance.largeGroupSend
node ./bin/publish.js ../benchmark-convo-open.log desktop.ci.performance.convoOpen
node ./bin/publish.js ../benchmark-call-history-search.log desktop.ci.performance.callHistorySearch
node ./bin/publish.js ../benchmark-backup.log desktop.ci.performance.backup
env:
DD_API_KEY: ${{ secrets.DATADOG_API_KEY }}

View File

@ -10,346 +10,292 @@ on:
- '[0-9]+.[0-9]+.x'
pull_request:
permissions:
contents: read
jobs:
audit:
name: Dependencies
runs-on: ubuntu-22.04-8-cores
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: '.nvmrc'
- run: node --test .pnpmfile.mjs
- run: pnpm audit --audit-level=high
- run: pnpm audit signatures
- run: pnpm dedupe --check
lint:
name: Lint
runs-on: ubuntu-22.04-8-cores
timeout-minutes: 30
steps:
- run: lsb_release -a
- run: uname -a
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- run: npm install -g npm@10.2.5
# - name: Setup sccache
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
# - name: Restore sccache
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
# with:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
- name: Restore cached .eslintcache and tsconfig.tsbuildinfo
uses: actions/cache/restore@v4
id: cache-lint
with:
path: |
.eslintcache
tsconfig.tsbuildinfo
key: lint-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**', '.eslintrc.js', '.eslint/**', 'tsconfig.json') }}
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: npm ci
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- name: Install Sticker Creator node_modules
run: pnpm install
working-directory: sticker-creator
- run: npm run generate
- run: npm run lint
- run: npm run lint-deps
- run: npm run lint-license-comments
- name: Install libpulse0
run: sudo apt-get install -y libpulse0 || (sudo apt-get update && sudo apt-get install -y libpulse0)
- run: pnpm run generate
- run: pnpm run build:db-schema --check
- run: pnpm run lint-prettier
- run: pnpm run lint-css
- run: pnpm run check:types
- run: pnpm run oxlint:ci
- run: pnpm run lint-deps
- run: pnpm run lint-license-comments
- run: pnpm run lint-intl
- run: pnpm run lint-knip:all --reporter github-actions
- run: pnpm run lint-knip:prod --reporter github-actions
- name: Check acknowledgments file is up to date
run: pnpm run build:acknowledgments
run: npm run build:acknowledgments
env:
REQUIRE_SIGNAL_LIB_FILES: 1
- run: git diff --exit-code
- name: Update cached .eslintcache and tsconfig.tsbuildinfo
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/main'
with:
path: |
.eslintcache
tsconfig.tsbuildinfo
key: ${{ steps.cache-lint.outputs.cache-primary-key }}
macos:
name: MacOS
needs: lint
runs-on: macos-26-arm64
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
timeout-minutes: 30
steps:
- run: uname -a
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
# - name: Setup sccache
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
# - name: Restore sccache
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
# with:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
- run: npm install -g npm@10.2.5
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: npm ci
env:
# CC: sccache clang
# CXX: sccache clang++
# SCCACHE_GHA_ENABLED: "true"
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- run: pnpm run prepare-beta-build
- run: pnpm run generate
- run: pnpm run test-node
- run: pnpm run test-electron
- run: npm run generate
- run: npm run prepare-beta-build
- run: npm run test-node
- run: npm run test-electron
env:
ARTIFACTS_DIR: artifacts/macos
WORKER_COUNT: 4
timeout-minutes: 5
- run: touch noop.sh && chmod +x noop.sh
- run: pnpm run build
- run: npm run build
env:
# CC: sccache clang
# CXX: sccache clang++
# SCCACHE_GHA_ENABLED: "true"
DISABLE_INSPECT_FUSE: on
SIGN_MACOS_SCRIPT: noop.sh
ARTIFACTS_DIR: artifacts/macos
- name: Upload installer size
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }}
run: |
node scripts/publish-installer-size.mjs macos-arm64
node scripts/publish-installer-size.mjs macos-x64
node scripts/publish-installer-size.mjs macos-universal
- run: pnpm run test-release
node ts/scripts/dd-installer-size.js macos-arm64
node ts/scripts/dd-installer-size.js macos-x64
node ts/scripts/dd-installer-size.js macos-universal
env:
DD_API_KEY: ${{ secrets.DATADOG_API_KEY }}
- name: Rebuild native modules for x64
run: npm run electron:install-app-deps
- run: npm run test-release
env:
NODE_ENV: production
- run: npm run test-eslint
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v4
with:
path: artifacts
linux:
name: Linux
needs: lint
runs-on: ${{ matrix.os }}
runs-on: ubuntu-22.04-8-cores
timeout-minutes: 30
strategy:
matrix:
include:
- os: ubuntu-22.04-8-cores
arch: x64
- os: ubuntu-22.04-arm64-4-cores
arch: arm64
steps:
- run: lsb_release -a
- run: uname -a
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
- run: sudo apt-get install xvfb libpulse0
- run: npm install -g npm@10.2.5
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install xvfb and libpulse0
run: sudo apt-get install xvfb libpulse0 || (sudo apt-get update && sudo apt-get install xvfb libpulse0)
# - name: Setup sccache
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
# - name: Restore sccache
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
# with:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: npm ci
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- run: pnpm run prepare-beta-build
- run: pnpm run generate
- run: npm run generate
- run: npm run prepare-beta-build
- name: Create bundle
run: npm run build:esbuild:prod
- name: Create preload cache
run: xvfb-run --auto-servernum pnpm run build:preload-cache
run: xvfb-run --auto-servernum npm run build:preload-cache
env:
ARTIFACTS_DIR: artifacts/linux
- name: Set Linux build target architecture
run: pnpm run prepare-linux-build deb ${{ matrix.arch }}
- name: Build with packaging .deb file
run: pnpm run build:release --publish=never
run: npm run build:release -- --publish=never
if: github.ref == 'refs/heads/main'
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
DISABLE_INSPECT_FUSE: on
- name: Build without packaging .deb file
run: pnpm run build:release --linux dir
run: npm run build:release -- --linux dir
if: github.ref != 'refs/heads/main'
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
DISABLE_INSPECT_FUSE: on
- name: Upload installer size
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }}
run: node scripts/publish-installer-size.mjs linux-${{ matrix.arch }}
run: node ts/scripts/dd-installer-size.js linux
env:
DD_API_KEY: ${{ secrets.DATADOG_API_KEY }}
- run: xvfb-run --auto-servernum pnpm run test-node
- run: xvfb-run --auto-servernum npm run test-node
- name: Clone backup integration tests
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
with:
repository: 'signalapp/Signal-Message-Backup-Tests'
ref: 'a0f900243210efbedc72f0907c5d2f140385daa4'
ref: '93a7d29527cb33e6bf23bc56fbf74d62cf682001'
path: 'backup-integration-tests'
- run: xvfb-run --auto-servernum pnpm run test-electron
- run: xvfb-run --auto-servernum npm run test-electron
timeout-minutes: 5
env:
ARTIFACTS_DIR: artifacts/linux
LANG: en_US
LANGUAGE: en_US
BACKUP_INTEGRATION_DIR: 'backup-integration-tests/test-cases'
WORKER_COUNT: 8
- run: xvfb-run --auto-servernum pnpm run test-release
- run: xvfb-run --auto-servernum npm run test-release
env:
NODE_ENV: production
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v4
with:
path: artifacts
windows:
name: Windows
needs: lint
runs-on: windows-latest-8-cores
runs-on: windows-2019
timeout-minutes: 30
env:
BUILD_LOCATION: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise\\VC\\Tools\\MSVC\\14.29.30133\\lib\\x86\\store\\references\\"
SDK_LOCATION: "C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\10.0.17134.0"
steps:
- run: systeminfo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- run: git config --global core.autocrlf false
- run: git config --global core.eol lf
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
- run: npm install -g npm@10.2.5 node-gyp@10.0.1
# Set things up so @nodert-win10-rs4 dependencies build properly
- run: dir "$env:BUILD_LOCATION"
- run: dir "$env:SDK_LOCATION"
- run: "copy \"$env:BUILD_LOCATION\\platform.winmd\" \"$env:SDK_LOCATION\""
- run: dir "$env:SDK_LOCATION"
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
- run: touch noop.js
- name: Install Desktop node_modules
run: pnpm install
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: npm ci
env:
CHILD_CONCURRENCY: 1
NPM_CONFIG_LOGLEVEL: verbose
NPM_CONFIG_NODE_GYP: ${{ github.workspace }}\noop.js
- run: npm run generate
- run: npm run test-node
- run: copy package.json temp.json
- run: del package.json
- run: type temp.json | findstr /v certificateSubjectName | findstr /v certificateSha1 > package.json
- run: pnpm run prepare-beta-build
- run: pnpm run generate
- run: pnpm run test-node
- run: npm run prepare-beta-build
- name: Create bundle
run: npm run build:esbuild:prod
- name: Create preload cache
run: pnpm run build:preload-cache
run: npm run build:preload-cache
env:
ARTIFACTS_DIR: artifacts/win
- name: Build with NSIS
run: pnpm run build:release
run: npm run build:release
if: github.ref == 'refs/heads/main'
env:
DISABLE_INSPECT_FUSE: on
- name: Build without NSIS
run: pnpm run build:release --win dir
run: npm run build:release -- --win dir
if: github.ref != 'refs/heads/main'
env:
DISABLE_INSPECT_FUSE: on
- name: Upload installer size
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }}
run: node scripts/publish-installer-size.mjs windows
run: node ts/scripts/dd-installer-size.js windows
env:
DD_API_KEY: ${{ secrets.DATADOG_API_KEY }}
- run: pnpm run test-electron
- run: npm run test-electron
env:
ARTIFACTS_DIR: artifacts/windows
WORKER_COUNT: 4
timeout-minutes: 5
- run: pnpm run test-release
- run: npm run test-release
env:
SIGNAL_ENV: production
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v4
with:
path: artifacts
@ -363,304 +309,34 @@ jobs:
working-directory: sticker-creator
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
- name: Install Sticker Creator node_modules
run: pnpm install
run: npm ci
- name: Build Sticker Creator
run: pnpm run build
run: npm run build
- name: Check Sticker Creator types
run: pnpm run check:types
run: npm run check:types
- name: Check Sticker Creator formatting
run: pnpm run prettier:check
run: npm run prettier:check
- name: Check Sticker Creator linting
run: pnpm run lint
run: npm run lint
- name: Run tests
run: npm test -- --run
mock-tests:
name: Mock Tests
needs: lint
strategy:
fail-fast: false
matrix:
workerIndex: [0, 1, 2, 3]
runs-on: ubuntu-latest-8-cores
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
timeout-minutes: 30
steps:
- name: Get system specs
run: lsb_release -a
- name: Get other system specs
run: uname -a
- name: Clone Desktop repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Update apt
run: sudo apt-get update
- name: Install xvfb and libpulse0
run: sudo apt-get install -y xvfb libpulse0
# - name: Setup sccache
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
# - name: Restore sccache
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
# with:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
- name: Install Desktop node_modules
run: |
pnpm install
./node_modules/.bin/install-electron
sudo chown root node_modules/.pnpm/electron@*/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 node_modules/.pnpm/electron@*/node_modules/electron/dist/chrome-sandbox
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- name: Build typescript
run: pnpm run generate
- name: Create preload cache
run: xvfb-run --auto-servernum pnpm run build:preload-cache
env:
ARTIFACTS_DIR: artifacts/linux
- name: Run mock server tests
run: |
set -o pipefail
xvfb-run --auto-servernum pnpm run test-mock
timeout-minutes: 15
env:
NODE_ENV: production
DEBUG: mock:test:*
ARTIFACTS_DIR: artifacts/mock
WORKER_INDEX: ${{ matrix.workerIndex }}
WORKER_COUNT: 4
- name: Run docker mock server tests
if: ${{ matrix.workerIndex == 0 }}
run: |
set -o pipefail
sudo apt-get install -y pipewire pipewire-pulse wireplumber psmisc pulseaudio-utils
systemctl --user start pipewire.service
systemctl --user start pipewire-pulse.service
xvfb-run --auto-servernum pnpm run test-mock-docker
timeout-minutes: 10
env:
NODE_ENV: production
DEBUG: mock:test:*
ARTIFACTS_DIR: artifacts/mock-docker
- name: Upload mock server test logs on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: logs-${{ matrix.workerIndex }}
path: artifacts
check-min-os-version:
name: Check Min OS Version
needs: lint
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- run: uname -a
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install Desktop node_modules
if: matrix.os != 'windows-latest'
run: pnpm install
env:
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- run: touch noop.js
if: matrix.os == 'windows-latest'
- name: Install Desktop node_modules on Windows
if: matrix.os == 'windows-latest'
run: pnpm install
env:
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: ${{ github.workspace }}\noop.js
- run: pnpm generate:phase-0
- name: Run OS version check
run: |
node scripts/check-min-os-version.mjs
danger:
name: Danger
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: '.nvmrc'
package-manager-cache: false # Avoid cache key clashes
- name: Install danger node_modules
run: pnpm install
working-directory: danger
- name: Run DangerJS
run: pnpm run danger:ci
working-directory: danger
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.AUTOMATED_GITHUB_PAT }}
storybook:
name: Storybook
runs-on: ubuntu-latest-8-cores
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
# - name: Setup sccache
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
# - name: Restore sccache
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
# id: cache-sccache
# with:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- run: pnpm run build:storybook
- run: ./node_modules/.bin/playwright install chromium
- run: ./node_modules/.bin/run-p --race test:storybook:serve test:storybook:test
benchmark:
name: Benchmark
strategy:
matrix:
metric:
- startup
- send
- groupSend
- largeGroupSendWithBlocks
- largeGroupSend
- convoOpen
- callHistorySearch
- backup
include:
- metric: startup
script: ts/test-mock/benchmarks/startup_bench.node.js
runCount: 10
- metric: send
script: ts/test-mock/benchmarks/send_bench.node.js
runCount: 100
- metric: groupSend
script: ts/test-mock/benchmarks/group_send_bench.node.js
runCount: 100
conversationSize: 500
- metric: largeGroupSendWithBlocks
script: ts/test-mock/benchmarks/group_send_bench.node.js
runCount: 50
conversationSize: 500
groupSize: 500
contactCount: 500
blockedCount: 10
discardCount: 2
- metric: largeGroupSend
script: ts/test-mock/benchmarks/group_send_bench.node.js
runCount: 20
conversationSize: 50
groupSize: 500
contactCount: 500
discardCount: 2
- metric: convoOpen
script: ts/test-mock/benchmarks/convo_open_bench.node.js
runCount: 100
- metric: callHistorySearch
script: ts/test-mock/benchmarks/call_history_search_bench.node.js
runCount: 100
- metric: backup
script: ts/test-mock/benchmarks/backup_bench.node.js
runs-on: ubuntu-22.04-8-cores
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
timeout-minutes: 30
@ -672,115 +348,53 @@ jobs:
run: uname -a
- name: Clone Desktop repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
# - name: Setup sccache
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
# - name: Restore sccache
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
# id: cache-sccache
# with:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
- name: Install global dependencies
run: npm install -g npm@10.2.5
- name: Install xvfb
run: sudo apt-get install xvfb libpulse0
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install xvfb and libpulse0
run: sudo apt-get install xvfb libpulse0 || (sudo apt-get update && sudo apt-get install xvfb libpulse0)
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: npm ci
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- name: Build typescript
run: pnpm run generate
run: npm run generate
- name: Bundle
run: npm run build:esbuild:prod
- name: Create preload cache
run: xvfb-run --auto-servernum pnpm run build:preload-cache
run: xvfb-run --auto-servernum npm run build:preload-cache
env:
ARTIFACTS_DIR: artifacts/linux
- name: Set MAX_CYCLES=2 on main
if: ${{ github.ref == 'refs/heads/main' }}
run: |
echo "MAX_CYCLES=2" >> "$GITHUB_ENV"
- name: Run ${{ matrix.metric }}
- name: Run mock server tests
run: |
set -o pipefail
xvfb-run --auto-servernum ./node_modules/.bin/tsx \
${{ matrix.script }} | tee benchmark.log
xvfb-run --auto-servernum npm run test-mock
timeout-minutes: 10
env:
NODE_ENV: production
ELECTRON_ENABLE_STACK_DUMPING: on
DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/${{ matrix.metric }}
GROUP_SIZE: ${{ matrix.groupSize }}
CONTACT_COUNT: ${{ matrix.contactCount }}
BLOCKED_COUNT: ${{ matrix.blockedCount }}
DISCARD_COUNT: ${{ matrix.discardCount }}
RUN_COUNT: ${{ matrix.runCount }}
CONVERSATION_SIZE: ${{ matrix.conversationSize }}
DEBUG: mock:test:*
ARTIFACTS_DIR: artifacts/startup
- name: Upload benchmark logs on failure
- name: Upload mock server test logs on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v4
with:
name: logs
path: artifacts
- name: Clone benchmark repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: 'signalapp/Signal-Desktop-Benchmarks-Private'
path: 'benchmark-results'
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
- name: Build benchmark repo
working-directory: benchmark-results
run: |
pnpm install
pnpm run build
- name: Publish
working-directory: benchmark-results
run: |
node ./bin/publish.js ../benchmark.log desktop.ci.performance.${{ matrix.metric }}
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
auto-merge-ready:
if: ${{ github.event_name == 'pull_request' && github.repository == 'signalapp/Signal-Desktop-Private' }}
name: Auto Merge Ready
needs:
- lint
- linux
- windows
- sticker-creator
- mock-tests
- check-min-os-version
- danger
- storybook
- benchmark
runs-on: ubuntu-latest
steps:
- name: Ok
run: echo ok

View File

@ -9,7 +9,6 @@ on:
- '[0-9]+.[0-9]+.x'
jobs:
linux:
name: Commit Title Check
runs-on: ubuntu-latest
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
steps:

35
.github/workflows/danger.yml vendored Normal file
View File

@ -0,0 +1,35 @@
# Copyright 2020 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Danger
on:
pull_request:
jobs:
danger:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # fetch all history
- name: Setup node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
- run: npm install -g npm@10.2.5
- name: Cache danger node_modules
id: cache-desktop-modules
uses: actions/cache@v3
with:
path: danger/node_modules
key: danger-${{ runner.os }}-${{ hashFiles('danger/package.json', 'danger/package-lock.json') }}
- name: Install danger node_modules
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: cd danger && npm ci
- name: Run DangerJS
run: npm run danger:ci
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.AUTOMATED_GITHUB_PAT }}

View File

@ -1,73 +0,0 @@
# Copyright 2025 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: ICU Book
on:
workflow_dispatch:
push:
tags:
- 'v[0-9]+.[0-9]+.*'
jobs:
build-icu-book:
name: Build ICU Book
runs-on: ubuntu-latest-8-cores
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
# - name: Setup sccache
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
# - name: Restore sccache
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
# id: cache-sccache
# with:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
env:
# CC: sccache gcc
# CXX: sccache g++
# SCCACHE_GHA_ENABLED: "true"
NPM_CONFIG_LOGLEVEL: verbose
# We rebuild in `electron:install-app-deps` that doesn't look at this
# environment variable
NPM_CONFIG_NODE_GYP: echo
- run: pnpm run build:storybook
- run: ./node_modules/.bin/playwright install chromium
- run: ./node_modules/.bin/run-p --race test:storybook:serve test:storybook:test
env:
ARTIFACTS_DIR: stories
- run: pnpm run build:rolldown
- run: node scripts/compile-stories-icu-lookup.mjs stories
- name: Upload test artifacts
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: desktop-test-icu
path: stories
compression-level: 9
- name: Upload release artifacts
if: github.event_name != 'workflow_dispatch'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: desktop-${{ github.ref_name }}-icu
path: stories
compression-level: 9

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
repository: signalapp/Signal-Notes-Action-Private

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
repository: signalapp/Signal-Release-Notes-Action-Private

35
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,35 @@
# Copyright 2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: On Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.*'
jobs:
create-release-event:
name: Create release event in datadog
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
runs-on: ubuntu-latest
steps:
- name: Create event on DataDog
run: |
curl -X POST "https://api.datadoghq.com/api/v1/events" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "DD-API-KEY: ${DD_API_KEY}" \
-d '
{
"title": "Desktop Release ${{ github.ref_name }}",
"text": "A new desktop release ${{ github.ref_name }} was just published",
"source_type_name": "git",
"tags": [
"service:desktop.ci.release",
"env:production",
"version:${{ github.ref_name }}"
]
}
'
env:
DD_API_KEY: ${{ secrets.DATADOG_API_KEY }}

View File

@ -1,109 +0,0 @@
# Copyright 2025 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Reproducible Build Scheduler
on:
workflow_dispatch:
inputs:
force:
type: boolean
description: 'Ignore version cache and reproduce the latest builds'
required: true
default: false
schedule:
- cron: '0 12 * * *'
jobs:
linux:
strategy:
matrix:
package: ['signal-desktop', 'signal-desktop-beta']
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
steps:
- name: Log info
run: |
echo "inputs.force: ${{ inputs.force }}";
echo "matrix.package: ${{ matrix.package }}";
- name: Add signal desktop signing key and apt repo
run: |
wget -O- https://updates.signal.org/desktop/apt/keys.asc | gpg --dearmor > signal-desktop-keyring.gpg
cat signal-desktop-keyring.gpg | sudo tee /usr/share/keyrings/signal-desktop-keyring.gpg > /dev/null
wget -O signal-desktop.sources https://updates.signal.org/static/desktop/apt/signal-desktop.sources
cat signal-desktop.sources | sudo tee /etc/apt/sources.list.d/signal-desktop.sources > /dev/null
sudo apt-get update
- name: Restore previous version file from cache
id: restore-cache-version
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
key: ${{ matrix.package }}-version-git-ref-txt
path: ~/version-git-ref.txt
- name: Get previous version tag
id: previous-version
if: steps.restore-cache-version.outputs.cache-hit == 'true'
run: |
PREVIOUS_VERSION_GIT_TAG=$(cat ~/version-git-ref.txt)
echo "Previous git version tag: $PREVIOUS_VERSION_GIT_TAG"
echo "tag=$PREVIOUS_VERSION_GIT_TAG" >> $GITHUB_OUTPUT
- name: Get latest apt version of package and matching git tag
id: latest-version
run: |
LATEST_VERSION_APT=$(apt-cache policy "${{ matrix.package }}" | grep Candidate | awk '{print $2}')
if [ -z "$LATEST_VERSION_APT" ]; then
echo "Error: Could not get latest version of '${{ matrix.package }}' using apt-cache"
exit 1
fi
echo "Latest apt version of ${{ matrix.package }}: $LATEST_VERSION_APT"
VERSION_GIT_TAG="v$(echo "$LATEST_VERSION_APT" | tr '~' '-')"
echo "Latest git version tag: $VERSION_GIT_TAG"
echo "$VERSION_GIT_TAG" > ~/version-git-ref.txt
echo "tag=$VERSION_GIT_TAG" >> $GITHUB_OUTPUT
BRANCH_PREFIX=$(echo "$VERSION_GIT_TAG" | grep -oE '[0-9]+\.[0-9]+')
echo "git_branch=$BRANCH_PREFIX.x" >> $GITHUB_OUTPUT
- name: Determine if a build is needed
id: should-run
run: |
if ${{ inputs.force || steps.restore-cache-version.outputs.cache-hit != 'true' || steps.previous-version.outputs.tag != steps.latest-version.outputs.tag }}; then
echo "result=true" >> $GITHUB_OUTPUT
else
echo "result=false" >> $GITHUB_OUTPUT
fi
- name: Run workflow Reproducible Build using REST API
if: steps.should-run.outputs.result == 'true'
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/reproducible-builds.yml/dispatches \
-d '{"ref":"main","inputs":{"package":"${{ matrix.package }}","version_tag":"${{ steps.latest-version.outputs.tag }}"}}'
- name: Cache latest version
if: steps.should-run.outputs.result == 'true'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
key: ${{ matrix.package }}-version-git-ref-txt
path: ~/version-git-ref.txt
- name: Open issue on failure
if: ${{ failure() && github.repository == 'signalapp/Signal-Desktop-Private' }}
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/issues \
-d '{"title":"Reproducible build scheduler failed: ${{ steps.latest-version.outputs.tag }}","body":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"},"labels":["${{ steps.latest-version.outputs.git_branch }}"]}'

View File

@ -1,183 +0,0 @@
# Copyright 2024 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Reproducible Builds
on:
workflow_dispatch:
inputs:
package:
description: 'Package name'
required: true
default: 'signal-desktop'
type: choice
options:
- signal-desktop
- signal-desktop-beta
version_tag:
description: 'Version tag (e.g. v1.2.3 or v2.0.0-beta.1)'
required: true
type: string
jobs:
linux:
name: Linux deb
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Get system specs
run: lsb_release -a
- name: Get other system specs
run: uname -a
- name: Get version info
id: app_info
run: |
echo "PACKAGE_NAME=${{ inputs.package }}" >> "$GITHUB_ENV"
echo "git_ref=${{ inputs.version_tag }}" >> $GITHUB_OUTPUT
PARSED_VERSION=$(echo "${{ inputs.version_tag }}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+.*' | tr '-' '~')
echo "PACKAGE_VERSION=$PARSED_VERSION" >> "$GITHUB_ENV"
BRANCH_PREFIX=$(echo "${{ inputs.version_tag }}" | grep -oE '[0-9]+\.[0-9]+')
echo "git_branch=$BRANCH_PREFIX.x" >> $GITHUB_OUTPUT
echo "# Reproducing ${{ inputs.package }} Linux deb" >> $GITHUB_STEP_SUMMARY
echo "## Version: ${{ inputs.version_tag }}" >> $GITHUB_STEP_SUMMARY
- name: Add signal desktop signing key and apt repo
run: |
wget -O- https://updates.signal.org/desktop/apt/keys.asc | gpg --dearmor > signal-desktop-keyring.gpg
cat signal-desktop-keyring.gpg | sudo tee /usr/share/keyrings/signal-desktop-keyring.gpg > /dev/null
wget -O signal-desktop.sources https://updates.signal.org/static/desktop/apt/signal-desktop.sources
cat signal-desktop.sources | sudo tee /etc/apt/sources.list.d/signal-desktop.sources > /dev/null
sudo apt-get update
# Note: For beta versions, the APT version is separated by tilde e.g. v1.2.3~beta.1
# However the download URI has a dash e.g. v1.2.3-beta.1
# Thus after apt-get download we need to use the filename of the actual download
- name: Download latest deb
id: download
run: |
DOWNLOAD_URI=$(apt-get download --print-uris "$PACKAGE_NAME=$PACKAGE_VERSION" | cut -d"'" -f2)
EXPECTED_SHA512=$(apt-get download --print-uris "$PACKAGE_NAME=$PACKAGE_VERSION" | grep -oP 'SHA512:\K\s*\S+')
echo "expected_sha512=$EXPECTED_SHA512" >> $GITHUB_OUTPUT
apt-get download "$PACKAGE_NAME=$PACKAGE_VERSION"
DEB_FILE=$(ls | grep deb | tail -1)
echo "deb_file=$DEB_FILE" >> $GITHUB_OUTPUT
DOWNLOAD_SHA512=$(sha512sum $DEB_FILE | cut -d' ' -f1)
echo "Verifying $DEB_FILE"
echo "Expected SHA512: $EXPECTED_SHA512"
echo "Actual SHA512: $DOWNLOAD_SHA512"
echo "### Download from apt" >> $GITHUB_STEP_SUMMARY
echo "Verifying $DEB_FILE" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Expected SHA512: $EXPECTED_SHA512" >> $GITHUB_STEP_SUMMARY
echo "Actual SHA512: $DOWNLOAD_SHA512" >> $GITHUB_STEP_SUMMARY
if [ "$DOWNLOAD_SHA512" == "$EXPECTED_SHA512" ]; then
echo "✅ Download checksum verification successful"
echo "✅ Download checksum verification successful" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Download checksum verification failed!"
echo "❌ Download checksum verification failed!" >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Clone Desktop git repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ steps.app_info.outputs.git_ref }}
- name: Get node version for docker build arg
id: node_version
run: |
NODE_VERSION=$(cat .nvmrc)
echo "version=$NODE_VERSION" >> $GITHUB_OUTPUT
- name: Set up docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Build docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
env:
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: ./reproducible-builds
file: ./reproducible-builds/Dockerfile
tags: signal-desktop:latest
load: true
push: false
build-args: |
SOURCE_DATE_EPOCH=1
NODE_VERSION=${{ steps.node_version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build Linux deb
id: build
run: |
cd reproducible-builds
# Try 3 times before the step actually fails
(./build.sh public) || \
(echo "Retry 1" && ./build.sh public) || \
(echo "Retry 2" && ./build.sh public)
- name: Get checksum of deb
id: build_checksum
run: |
cd release
BUILT_FILE=$(ls | grep deb | tail -1)
echo "built_file=$BUILT_FILE" >> $GITHUB_OUTPUT
ACTUAL_SHA512=$(sha512sum $BUILT_FILE | cut -d' ' -f1)
echo "actual_sha512=$ACTUAL_SHA512" >> $GITHUB_OUTPUT
env:
SKIP_DOCKER_BUILD: true
- name: Compare checksums
run: |
ACTUAL_SHA512="${{ steps.build_checksum.outputs.actual_sha512 }}"
EXPECTED_SHA512="${{ steps.download.outputs.expected_sha512 }}"
echo "Verifying ${{ steps.download.outputs.deb_file }}"
echo "" >> $GITHUB_STEP_SUMMARY
echo "Expected SHA512: $EXPECTED_SHA512"
echo "Actual SHA512: $ACTUAL_SHA512"
echo "### Build and verify" >> $GITHUB_STEP_SUMMARY
echo "Verifying ${{ steps.download.outputs.deb_file }}" >> $GITHUB_STEP_SUMMARY
echo "Build SHA512: $ACTUAL_SHA512" >> $GITHUB_STEP_SUMMARY
if [ "$ACTUAL_SHA512" == "$EXPECTED_SHA512" ]; then
echo "✅ Build checksum verification successful"
echo "✅ Build checksum verification successful" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Build checksum verification failed!"
echo "❌ Build checksum verification failed!" >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Run diffoscope to find diffs
if: failure()
run: |
apt-get download "$PACKAGE_NAME=$PACKAGE_VERSION"
docker run --rm -w $(pwd) -v $(pwd):$(pwd):ro \
registry.salsa.debian.org/reproducible-builds/diffoscope@sha256:51fa7187f093c4cb75b386e7e4caa38ea5783b4a9204b517dc1c91dada209421 --no-progress --max-text-report-size 5000 --max-report-size 5000 --max-diff-block-lines 100 \
release/${{ steps.build_checksum.outputs.built_file }} \
${{ steps.download.outputs.deb_file }}
- name: Open issue on failure
if: ${{ failure() && github.repository == 'signalapp/Signal-Desktop-Private' }}
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/issues \
-d '{"title":"Reproducible build failed: ${{ inputs.version_tag }}","body":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}","labels":["${{ steps.app_info.outputs.git_branch }}"]}'

32
.github/workflows/stories.yml vendored Normal file
View File

@ -0,0 +1,32 @@
# Copyright 2023 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Stories
on:
push:
branches:
- development
- main
- '[0-9]+.[0-9]+.x'
pull_request:
jobs:
test:
runs-on: ubuntu-latest-8-cores
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
- name: Install global dependencies
run: npm install -g npm@10.2.5
- name: Install Desktop node_modules
run: npm ci
env:
CHILD_CONCURRENCY: 1
NPM_CONFIG_LOGLEVEL: verbose
- run: npm run build:storybook
- run: npx playwright install chromium
- run: ./node_modules/.bin/run-p --race test:storybook:serve test:storybook:test

10
.gitignore vendored
View File

@ -4,8 +4,6 @@ node_modules_bkp
coverage/*
build/curve25519_compiled.js
build/compact-locales
build/*.policy
build/emoji-data.json
stylesheets/*.css.map
/dist
.DS_Store
@ -18,14 +16,17 @@ release/
/sql/
/start.sh
.eslintcache
.stylelintcache
tsconfig.tsbuildinfo
.smartling-source.sh
# generated files
js/components.js
js/util_worker.js
libtextsecure/components.js
stylesheets/*.css
!stylesheets/tailwind-config.css
!stylesheets/webrtc_internals.css
/storybook-static/
preload.bundle.*
preload.wrapper.js
@ -34,10 +35,8 @@ ts/sql/mainWorker.bundle.js.LICENSE.txt
build/ICUMessageParams.d.ts
# React / TypeScript
build/**/*.js
app/*.js
ts/**/*.js
!ts/windows/main/tsx.js
ts/protobuf/*.d.ts
# CSS Modules
@ -46,6 +45,5 @@ ts/protobuf/*.d.ts
# Editors
/.idea
/.vscode
/.zed
*.sublime*
*.map

View File

@ -1,4 +1,3 @@
{
"checkLeaks": true,
"node-option": ["import=tsx"]
"checkLeaks": true
}

1
.npmrc Normal file
View File

@ -0,0 +1 @@
legacy-peer-deps=true

2
.nvmrc
View File

@ -1 +1 @@
24.15.0
20.18.1

View File

@ -1,37 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceArrayBuffer } from './rules/enforceArrayBuffer.mjs';
import { enforceFileSuffix } from './rules/enforceFileSuffix.mjs';
import { enforceLicenseComments } from './rules/enforceLicenseComments.mjs';
import { enforceTw } from './rules/enforceTw.mjs';
import { enforceTypeAliasReadonlyDeep } from './rules/enforceTypeAliasReadonlyDeep.mjs';
import { noDisabledTests } from './rules/noDisabledTests.mjs';
import { noExtraneousDependencies } from './rules/noExtraneousDependencies.mjs';
import { noFocusedTests } from './rules/noFocusedTests.mjs';
import { noForIn } from './rules/noForIn.mjs';
import { noRestrictedPaths } from './rules/noRestrictedPaths.mjs';
import { noThen } from './rules/noThen.mjs';
/** @type {import("@typescript-eslint/utils").TSESLint.Linter.Plugin} */
const plugin = {
meta: {
name: 'signal-desktop',
version: '0.0.0',
},
rules: {
'enforce-array-buffer': enforceArrayBuffer,
'enforce-file-suffix': enforceFileSuffix,
'enforce-license-comments': enforceLicenseComments,
'enforce-tw': enforceTw,
'enforce-type-alias-readonlydeep': enforceTypeAliasReadonlyDeep,
'no-disabled-tests': noDisabledTests,
'no-extraneous-dependencies': noExtraneousDependencies,
'no-focused-tests': noFocusedTests,
'no-for-in': noForIn,
'no-restricted-paths': noRestrictedPaths,
'no-then': noThen,
},
};
export default plugin;

View File

@ -1,48 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
export const enforceArrayBuffer = ESLintUtils.RuleCreator.withoutDocs({
name: 'enforce-array-buffer',
meta: {
type: 'problem',
fixable: 'code',
messages: {
shouldUseArrayBuffer: `Should be {{replacement}}`,
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
TSTypeReference(node) {
if (node.typeName.type !== 'Identifier') {
return;
}
let replacement;
if (node.typeName.name === 'Uint8Array') {
replacement = 'Uint8Array<ArrayBuffer>';
} else if (node.typeName.name === 'Buffer') {
replacement = 'Buffer<ArrayBuffer>';
} else {
return;
}
if (node.typeArguments != null) {
return;
}
context.report({
node,
messageId: 'shouldUseArrayBuffer',
data: { replacement },
fix(fixer) {
return [fixer.replaceTextRange(node.range, replacement)];
},
});
},
};
},
});

View File

@ -1,64 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceArrayBuffer } from './enforceArrayBuffer.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('enforce-array-buffer', enforceArrayBuffer, {
valid: [
{ code: 'type T = number;' },
{ code: 'type T = Uint16Array;' },
{ code: 'type T = Uint8Array<ArrayBuffer>;' },
{ code: 'type T = Uint8Array<SharedArrayBuffer>;' },
{ code: 'type T = Uint8Array<ArrayBufferLike>;' },
{ code: 'type T = Uint8Array<U>;' },
{ code: 'function f(): Uint8Array<ArrayBuffer> {}' },
{ code: 'function f(p: Uint8Array<ArrayBuffer>) {}' },
{ code: 'let v: Uint8Array<ArrayBuffer>;' },
{ code: 'let v = new Uint8Array();' },
{ code: 'let v = new Uint8Array<ArrayBuffer>();' },
{ code: 'let v = Uint8Array.of();' },
{ code: 'let v = Uint8Array.from();' },
{ code: 'let v: { p: Uint8Array<ArrayBuffer> };' },
{ code: 'type T = Buffer<ArrayBuffer>;' },
{ code: 'type T = Buffer<SharedArrayBuffer>;' },
{ code: 'type T = Buffer<ArrayBufferLike>;' },
{ code: 'type T = Buffer<U>;' },
{ code: 'let v = new Buffer();' },
{ code: 'let v = Buffer.from();' },
],
invalid: [
{
code: `type T = Uint8Array`,
output: `type T = Uint8Array<ArrayBuffer>`,
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `function f(): Uint8Array {}`,
output: `function f(): Uint8Array<ArrayBuffer> {}`,
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `function f(p: Uint8Array) {}`,
output: `function f(p: Uint8Array<ArrayBuffer>) {}`,
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `let v: Uint8Array;`,
output: `let v: Uint8Array<ArrayBuffer>;`,
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `let v: { p: Uint8Array };`,
output: `let v: { p: Uint8Array<ArrayBuffer> };`,
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `type T = Buffer`,
output: `type T = Buffer<ArrayBuffer>`,
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
],
});

View File

@ -1,654 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { getReferenceType } from './utils/getReferenceType.mjs';
import { isStringLiteral } from './utils/astUtils.mjs';
import { assert } from './utils/assert.mjs';
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
* @typedef {import("@typescript-eslint/utils").TSESTree.ImportDeclaration} ImportDeclaration
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportAllDeclaration} ExportAllDeclaration
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportNamedDeclaration} ExportNamedDeclaration
* @typedef {import("@typescript-eslint/utils").TSESTree.ImportClause} ImportClause
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportSpecifier} ExportSpecifier
*/
/**
* @typedef {'std' | 'node' | 'dom' | 'preload' | 'main'} Suffix
*/
const ELECTRON_MAIN_MODULES = new Set([
'app',
'autoUpdater',
'BaseWindow',
'BrowserView',
'BrowserWindow',
'contentTracing',
'desktopCapturer',
'dialog',
'globalShortcut',
'inAppPurchase',
'ipcMain',
'Menu',
'MenuItem',
'MessageChannelMain',
'MessagePortMain',
'nativeTheme',
'net',
'netLog',
'Notification',
'powerMonitor',
'powerSaveBlocker',
'process',
'protocol',
'pushNotifications',
'safeStorage',
'screen',
'session',
'ShareMenu',
'shell',
'systemPreferences',
'TouchBar',
'Tray',
'utilityProcess',
'webContents',
'WebContentsView',
'webFrameMain',
'View',
]);
const ELECTRON_RENDERER_MODULES = new Set([
'contextBridge',
'ipcRenderer',
'webFrame',
'webUtils',
]);
const ELECTRON_SHARED_MODULES = new Set([
'clipboard',
'crashReporter',
'nativeImage',
]);
// Packages that use Node.js APIs (file system, etc)
const NODE_PACKAGES = new Set([
'@electron/asar',
'@indutny/dicer',
'@indutny/mac-screen-share',
'@indutny/range-finder',
'@indutny/simple-windows-notifications',
'@signalapp/libsignal-client',
'@signalapp/mute-state-change',
'@signalapp/ringrtc',
'@signalapp/sqlcipher',
'@signalapp/windows-ucv',
'cirbuf',
'config',
'dashdash',
'encoding',
'fast-glob',
'fs-extra',
'fs-xattr',
'got',
'growing-file',
'http-proxy-agent',
'https-proxy-agent',
'node-fetch',
'read-last-lines',
'socks-proxy-agent',
'split2',
'write-file-atomic',
// Dev dependencies
'@electron/fuses',
'@electron/notarize',
'@electron/symbolicate-mac',
'@indutny/parallel-prettier',
'@indutny/rezip-electron',
'@napi-rs/canvas',
'@signalapp/mock-server',
'@tailwindcss/cli',
'@tailwindcss/postcss',
'better-blockmap',
'chokidar-cli',
'cross-env',
'electron-builder',
'electron-mocha',
'endanger',
'enhanced-resolve',
'enquirer',
'execa',
'http-server',
'json-to-ast',
'log-symbols',
'node-gyp',
'node-gyp-build',
'npm-run-all',
'p-limit',
'pe-library',
'pixelmatch',
'playwright',
'postcss-loader',
'prettier',
'prettier-plugin-tailwindcss',
'react-devtools',
'react-devtools-core',
'resolve-url-loader',
'rolldown',
'sass',
'sass-loader',
'style-loader',
'stylelint',
'stylelint-config-css-modules',
'stylelint-config-recommended-scss',
'stylelint-use-logical-spec',
'svgo',
'synckit',
'tailwindcss',
'tsx',
'typescript',
'wait-on',
'webpack',
'webpack-cli',
'webpack-dev-server',
]);
// Packages that use DOM APIs
const DOM_PACKAGES = new Set([
'@popperjs/core',
'@react-aria/focus',
'@react-aria/interactions',
'@react-aria/utils',
'@react-spring/web',
'@tanstack/react-virtual',
'blob-util',
'blueimp-load-image',
'dom-accessibility-api',
'fabric',
'radix-ui',
'react-aria',
'react-aria-components',
'react-blurhash',
'react-popper',
'react-virtualized',
// Note that: react-dom/server is categorized separately
'react-dom',
// Dev dependencies
'@storybook/addon-a11y',
'@storybook/addon-actions',
'@storybook/addon-controls',
'@storybook/addon-interactions',
'@storybook/addon-jest',
'@storybook/addon-measure',
'@storybook/addon-toolbars',
'@storybook/addon-viewport',
'@storybook/addon-webpack5-compiler-swc',
'@storybook/react',
'@storybook/react-webpack5',
'@storybook/test',
'@storybook/test-runner',
'@storybook/types',
'storybook',
]);
// Packages that can run in both browser/node
const STD_PACKAGES = new Set([
'@babel/core',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
'@babel/preset-react',
'@babel/preset-typescript',
'@formatjs/fast-memoize',
'@formatjs/icu-messageformat-parser',
'@formatjs/intl',
'@formatjs/intl-localematcher',
'@indutny/sneequals',
'@internationalized/date',
'@react-types/shared',
'@signalapp/minimask',
'@signalapp/parchment-cjs',
'@signalapp/quill-cjs',
'@signalapp/lame',
'@typescript-eslint/eslint-plugin',
'@typescript-eslint/parser',
'axe-core',
'babel-core',
'babel-loader',
'babel-plugin-lodash',
'blurhash',
'buffer',
'card-validator',
'casual',
'chai',
'chai-as-promised',
'changedpi',
'classnames',
'country-codes-list',
'credit-card-type',
'css-loader',
'csv-parse',
'danger',
'debug',
'direction',
'emoji-regex-xs',
'eslint',
'eslint-plugin-better-tailwindcss',
'filesize',
'firstline',
'form-data',
'motion',
'motion/react',
'fuse.js',
'google-libphonenumber',
'heic-convert',
'humanize-duration',
'intl-tel-input',
'js-yaml',
'linkify-it',
'lodash',
'lru-cache',
'memoizee',
'mocha',
'moment',
'mp4box',
'nop',
'normalize-path',
'p-map',
'p-queue',
'p-timeout',
'parsecurrency',
'pino',
'pngjs',
'qrcode-generator',
'react',
'react-intl',
'react-redux',
'redux',
'redux-logger',
'redux-promise-middleware',
'redux-thunk',
'reselect',
'semver',
'sinon',
'tinykeys',
'type-fest',
'url',
'uuid',
'zod',
]);
export const enforceFileSuffix = ESLintUtils.RuleCreator.withoutDocs({
name: 'enforce-file-suffix',
meta: {
type: 'problem',
messages: {
missingFileSuffix: 'Missing file suffix in {{source}} import',
unrecognizedFileSuffix:
'Unrecognized file suffix in {{source}}, expected: node/preload/main/std, found: {{depSuffix}}',
commonJsImportOfElectronNoAllowed:
'CJS import of electron is not allowed',
uncategorizedElectronApi:
'Uncategorized electron API: "{{name}}". ' +
'Please update .oxlint/rules/file-suffix.js and add it to ' +
'ELECTRON_MAIN_MODULES/ELECTRON_RENDERER_MODULES/' +
'ELECTRON_SHARED_MODULES',
unsupportedNamespaceImportForElectron:
'Unsupported namespace import specifier for electron',
unsupportedImportSpecifierForElectron:
'Unsupported import specifier for electron',
uncategorizedDependency:
'Uncategorized dependency "{{moduleName}}". ' +
'Please update .oxlint/rules/file-suffix.js and add it to either ' +
'of NODE_PACKAGES/DOM_PACKAGES/STD_PACKAGES',
missingFileSuffixMustBeOneOf:
'Missing file suffix. Has to be one of: node/preload/main/std',
wrongFileSuffix:
'Invalid suffix {{fileSuffix}}, expected: {{expectedSuffix}}',
invalidImportForSuffix:
'Invalid import/reference for suffix: {{expectedSuffix}}',
invalidRequireCount: 'Invalid require() argument count',
},
schema: [],
defaultOptions: [],
},
create(context) {
const { filename, sourceCode } = context;
/** @type {string} */
let fileSuffix;
/** @type {Node[]} */
const nodeUses = [];
/** @type {Node[]} */
const domUses = [];
/** @type {Node[]} */
const preloadUses = [];
/** @type {Node[]} */
const mainUses = [];
/** @type Record<Suffix, Node[][]> */
const invalidUsesBySuffix = {
std: [nodeUses, domUses, preloadUses, mainUses],
node: [domUses, preloadUses, mainUses],
dom: [nodeUses, preloadUses, mainUses],
preload: [mainUses],
main: [domUses, preloadUses],
};
/**
* @param {Node} node
* @param {string} source
*/
function trackLocalDep(node, source) {
if (!/\.tsx?/.test(source)) {
return;
}
const match = source.match(/\.([^.\/]+)(?:\.stories)?\.tsx?$/);
if (match == null) {
context.report({
node,
messageId: 'missingFileSuffix',
data: { source },
});
return;
}
const [, depSuffix] = match;
if (depSuffix === 'node') {
nodeUses.push(node);
} else if (depSuffix === 'dom') {
domUses.push(node);
} else if (depSuffix === 'preload') {
preloadUses.push(node);
} else if (depSuffix === 'main') {
mainUses.push(node);
} else if (depSuffix === 'std') {
// Ignore
} else {
context.report({
node,
messageId: 'unrecognizedFileSuffix',
data: { source, depSuffix },
});
}
}
/**
* @param {Node} node
* @param {string} source
* @param {Array<ImportClause | ExportSpecifier> | null} specifiers
*/
function processUse(node, source, specifiers) {
if (source.startsWith('.')) {
trackLocalDep(node, source);
return;
}
// Node APIs
if (source.startsWith('node:')) {
nodeUses.push(node);
return;
}
// Electron
if (source === 'electron' && specifiers == null) {
context.report({
node,
messageId: 'commonJsImportOfElectronNoAllowed',
});
return;
} else if (source === 'electron') {
for (const s of specifiers ?? []) {
// We implicitly skip:
// they are used in scripts
if (s.type === 'ImportSpecifier') {
if (s.importKind === 'type') {
continue;
}
/** @type {string} */
let importName;
if (s.imported.type === 'Identifier') {
importName = s.imported.name;
} else {
importName = s.imported.value;
}
if (ELECTRON_MAIN_MODULES.has(importName)) {
mainUses.push(s);
} else if (ELECTRON_RENDERER_MODULES.has(importName)) {
preloadUses.push(s);
} else if (ELECTRON_SHARED_MODULES.has(importName)) {
// no-op
} else {
context.report({
node: s,
messageId: 'uncategorizedElectronApi',
data: { name: importName },
});
}
} else if (s.type === 'ImportNamespaceSpecifier') {
// import * as electron from 'electron';
context.report({
node: s,
messageId: 'unsupportedNamespaceImportForElectron',
});
nodeUses.push(s);
} else if (s.type === 'ImportDefaultSpecifier') {
// import ELECTRON_CLI from 'electron';
nodeUses.push(s);
} else {
context.report({
node: s,
messageId: 'unsupportedImportSpecifierForElectron',
});
}
}
return;
}
const match = source.match(/^([^@/]+|@[^/]+\/[^/]+)/);
if (match == null) {
return;
}
const [, moduleName] = match;
assert(moduleName, 'Missing moduleName');
if (NODE_PACKAGES.has(moduleName)) {
nodeUses.push(node);
} else if (source === 'react-dom/server') {
// no-op
} else if (
DOM_PACKAGES.has(moduleName) ||
source === 'react-dom/client'
) {
domUses.push(node);
} else if (!STD_PACKAGES.has(moduleName)) {
context.report({
node,
messageId: 'uncategorizedDependency',
data: { moduleName },
});
}
}
/**
* @param {ImportDeclaration | ExportAllDeclaration | ExportNamedDeclaration} node
*/
function processESMReference(node) {
/** @type {Array<ImportClause | ExportSpecifier> | null} */
let specifiers;
if (node.type === 'ImportDeclaration') {
if (node.importKind === 'type') {
return;
}
if (node.specifiers.length > 0) {
const allTypes = node.specifiers.every(specifier => {
return (
specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'
);
});
if (allTypes) {
return;
}
}
specifiers = node.specifiers;
} else if (node.type === 'ExportNamedDeclaration') {
specifiers = node.specifiers;
} else {
specifiers = null;
}
if (!node.source) {
return;
}
if (node.source.type !== 'Literal') {
return;
}
const source = node.source.value;
processUse(node, source, specifiers);
}
return {
Program: node => {
if (/\.d\.m?ts$/.test(filename)) {
// Skip types
return;
}
const match = filename.match(
/\.([^.\/]+)(?:\.stories)?\.(?:ts|tsx|js|mjs)$/
);
if (match == null) {
context.report({
node,
messageId: 'missingFileSuffixMustBeOneOf',
});
return;
}
const matchedSuffix = match[1];
assert(matchedSuffix, 'Missing matchedSuffix');
fileSuffix = matchedSuffix;
},
'Program:exit': node => {
if (fileSuffix == null) {
return;
}
/** @type {Suffix} */
let expectedSuffix;
if (mainUses.length > 0) {
expectedSuffix = 'main';
} else if (preloadUses.length > 0) {
expectedSuffix = 'preload';
} else if (nodeUses.length > 0) {
if (domUses.length > 0) {
expectedSuffix = 'preload';
} else {
expectedSuffix = 'node';
}
} else if (domUses.length > 0) {
expectedSuffix = 'dom';
} else {
expectedSuffix = 'std';
}
// All .tsx files should normally be .dom.tsx, but could also be
// .std.tsx.
if (
expectedSuffix === 'std' &&
filename.endsWith('.tsx') &&
fileSuffix !== 'std'
) {
expectedSuffix = 'dom';
}
if (fileSuffix !== expectedSuffix) {
context.report({
node,
messageId: 'wrongFileSuffix',
data: { fileSuffix, expectedSuffix },
});
}
const invalid = invalidUsesBySuffix[expectedSuffix].flat();
for (const use of invalid) {
context.report({
node: use,
messageId: 'invalidImportForSuffix',
data: { expectedSuffix },
});
}
},
ImportDeclaration(node) {
processESMReference(node);
},
ExportAllDeclaration(node) {
processESMReference(node);
},
ExportNamedDeclaration(node) {
processESMReference(node);
},
CallExpression(node) {
if (
node.callee.type !== 'Identifier' ||
node.callee.name !== 'require'
) {
return;
}
const refType = getReferenceType(sourceCode, node.callee);
if (refType !== 'global') {
return;
}
const { arguments: args } = node;
if (args.length !== 1) {
context.report({
node,
messageId: 'invalidRequireCount',
});
return;
}
const [arg] = args;
assert(arg, 'Missing arg');
/** @type {string} */
let source;
if (isStringLiteral(arg)) {
source = arg.value;
} else if (
arg.type === 'TSAsExpression' &&
isStringLiteral(arg.expression)
) {
source = arg.expression.value;
} else {
// Ignore other expressions
return;
}
processUse(node, source, null);
},
Identifier(node) {
if (node.name !== 'window' && node.name !== 'document') {
return;
}
const refType = getReferenceType(sourceCode, node);
if (refType == null) {
// Not part of expression
return;
}
if (refType !== 'global') {
return;
}
domUses.push(node);
},
};
},
});

View File

@ -1,139 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceFileSuffix } from './enforceFileSuffix.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
const ALLOWED_REFERENCES = /* @type {const} */ [
{
fileSuffix: 'std',
requiredLine: '',
depSuffixes: ['std'],
},
{
fileSuffix: 'dom',
requiredLine: 'window.addEventListener();',
depSuffixes: ['std', 'dom'],
},
{
fileSuffix: 'node',
requiredLine: 'require("node:fs");',
depSuffixes: ['std', 'node'],
},
{
fileSuffix: 'preload',
requiredLine: 'import { ipcRenderer } from "electron";',
depSuffixes: ['std', 'node', 'preload'],
},
{
fileSuffix: 'main',
requiredLine: 'import { autoUpdater } from "electron";',
depSuffixes: ['std', 'node', 'main'],
},
];
const DISALLOWED_REFERENCES = /* @type {const} */ [
{ fileSuffix: 'std', depSuffixes: ['dom', 'node', 'preload', 'main'] },
{ fileSuffix: 'dom', depSuffixes: ['node', 'preload', 'main'] },
{ fileSuffix: 'node', depSuffixes: ['preload', 'main'] },
{ fileSuffix: 'preload', depSuffixes: ['main'] },
{ fileSuffix: 'main', depSuffixes: ['dom', 'preload'] },
];
ruleTester.run('file-suffix', enforceFileSuffix, {
valid: [
...ALLOWED_REFERENCES.map(({ fileSuffix, requiredLine, depSuffixes }) => {
return depSuffixes.map(depSuffix => {
/** @type {const} */
return {
name: `importing ${depSuffix} from ${fileSuffix}`,
filename: `a.${fileSuffix}.ts`,
code: `
import { x } from './b.${depSuffix}.ts';
${requiredLine}
`,
languageOptions: {
globals: {
window: 'writable',
require: 'readable',
},
},
};
});
}).flat(),
{
name: 'type import should have no effect',
filename: 'a.std.ts',
code: `import type { ReadonlyDeep } from './b.dom.ts'`,
},
],
invalid: [
...DISALLOWED_REFERENCES.map(({ fileSuffix, depSuffixes }) => {
return depSuffixes.map(depSuffix => {
/** @type {const} */
return {
name: `importing ${depSuffix} from ${fileSuffix}`,
filename: `a.${fileSuffix}.ts`,
code: `import { x } from './b.${depSuffix}.ts'`,
errors: [
{
messageId: 'wrongFileSuffix',
data: { fileSuffix, expectedSuffix: depSuffix },
},
],
};
});
}).flat(),
...['dom', 'node', 'preload', 'main'].map(fileSuffix => {
/** @type {const} */
return {
name: `no ${fileSuffix} imports`,
filename: `a.${fileSuffix}.ts`,
code: '',
errors: [
{
messageId: 'wrongFileSuffix',
data: { fileSuffix, expectedSuffix: 'std' },
},
],
};
}),
// Invalid imports
{
name: 'preload in main',
filename: 'a.main.ts',
code: `
import { autoUpdater } from 'electron';
import './b.preload.ts';
`,
errors: [
{
messageId: 'invalidImportForSuffix',
data: { expectedSuffix: 'main' },
},
],
},
{
name: 'main in preload',
filename: 'a.preload.ts',
code: `
import { ipcRenderer } from 'electron';
import './b.main.ts';
`,
errors: [
{
messageId: 'wrongFileSuffix',
data: { fileSuffix: 'preload', expectedSuffix: 'main' },
},
{
messageId: 'invalidImportForSuffix',
data: { expectedSuffix: 'main' },
},
],
},
],
});

View File

@ -1,77 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
const COMMENT_LINE_1_EXACT = /^ Copyright \d{4} Signal Messenger, LLC$/;
const COMMENT_LINE_2_EXACT = /^ SPDX-License-Identifier: AGPL-3.0-only$/;
const COMMENT_LINE_1_LOOSE = /Copyright (\d{4}) Signal Messenger, LLC/;
const COMMENT_LINE_2_LOOSE = /SPDX-License-Identifier: AGPL-3.0-only/;
export const enforceLicenseComments = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
fixable: 'code',
messages: {
missingLicenseComment: 'Missing license comment',
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
Program(node) {
const comment1 = node.comments?.at(0);
const comment2 = node.comments?.at(1);
if (
comment1?.type === 'Line' &&
comment2?.type === 'Line' &&
COMMENT_LINE_1_EXACT.test(comment1.value) &&
COMMENT_LINE_2_EXACT.test(comment2.value)
) {
return;
}
context.report({
node,
messageId: 'missingLicenseComment',
fix(fixer) {
let year = null;
const remove = [];
for (const comment of node.comments ?? []) {
const match1 = comment.value.match(COMMENT_LINE_1_LOOSE);
const match2 = comment.value.match(COMMENT_LINE_2_LOOSE);
if (match1 != null) {
year = match1[1];
}
if (match1 != null || match2 != null) {
remove.push(comment);
}
}
year ??= new Date().getFullYear().toString();
const insert =
`// Copyright ${year} Signal Messenger, LLC\n` +
'// SPDX-License-Identifier: AGPL-3.0-only\n';
return [
fixer.replaceTextRange([0, 0], insert),
...remove.map(comment => {
return fixer.replaceTextRange(
[comment.range[0], comment.range[1]],
''
);
}),
];
},
});
},
};
},
});

View File

@ -1,146 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { createSyncFn } from 'synckit';
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
*/
const worker = createSyncFn(import.meta.resolve('./enforceTw.worker.mjs'));
export const enforceTw = ESLintUtils.RuleCreator.withoutDocs({
name: 'enforce-tw',
meta: {
type: 'problem',
messages: {
needsTw: 'Tailwind classes must be wrapped with tw()',
},
schema: [],
defaultOptions: [],
},
create(context) {
/**
* @param {string} input
* @param {Node} node
*/
function check(input, node) {
if (typeof input !== 'string') {
throw new Error(`Unexpected input ${input} for node type ${node.type}`);
}
const tailwindClasses = worker(input.split(/\s+/));
for (const tailwindClass of tailwindClasses) {
const index = input.indexOf(tailwindClass) + 1;
const length = tailwindClass.length;
context.report({
node,
loc: {
start: {
line: node.loc.start.line,
column: node.loc.start.column + index,
},
end: {
line: node.loc.end.line,
column: node.loc.start.column + index + length,
},
},
messageId: 'needsTw',
});
}
}
/**
* @param {Node} node
*/
function traverse(node) {
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
check(node.value, node);
}
// ignore other literals
} else if (node.type === 'TemplateLiteral') {
for (const element of node.quasis) {
traverse(element);
}
for (const expression of node.expressions) {
traverse(expression);
}
} else if (node.type === 'TemplateElement') {
if (node.value.cooked != null) {
check(node.value.cooked, node);
}
} else if (node.type === 'JSXExpressionContainer') {
traverse(node.expression);
} else if (node.type === 'ConditionalExpression') {
// ignore node.test
traverse(node.consequent);
traverse(node.alternate);
} else if (node.type === 'LogicalExpression') {
if (node.operator === '||' || node.operator === '??') {
traverse(node.left);
}
traverse(node.right);
} else if (node.type === 'BinaryExpression') {
if (node.operator === '+') {
traverse(node.left);
traverse(node.right);
} else {
throw new Error(`Unexpected binary operator: ${node.operator}`);
}
} else if (node.type === 'ObjectExpression') {
for (const prop of node.properties) {
traverse(prop);
}
} else if (node.type === 'Property') {
if (node.key.type === 'Identifier') {
if (!node.computed) {
check(node.key.name, node.key);
}
// ignore computed
} else if (node.key.type === 'Literal') {
traverse(node.key);
} else if (node.key.type === 'TemplateLiteral') {
traverse(node.key);
} else if (node.key.type === 'CallExpression') {
// ignore
} else {
throw new Error(`Unexpected property key type: ${node.key.type}`);
}
} else if (node.type === 'ArrayExpression') {
for (const element of node.elements) {
if (element != null) {
traverse(element);
}
}
} else if (node.type === 'Identifier') {
// ignore
} else if (node.type === 'CallExpression') {
// ignore
} else if (node.type === 'MemberExpression') {
// ignore
} else {
throw new Error(`Unexpected traverse node type: ${node.type}`);
}
}
return {
CallExpression(node) {
if (node.callee.type !== 'Identifier') return;
if (node.callee.name !== 'classNames') return;
for (const arg of node.arguments) {
traverse(arg);
}
},
JSXAttribute(node) {
if (node.name.type !== 'JSXIdentifier') return;
if (node.name.name !== 'className') return;
if (node.value != null) {
traverse(node.value);
}
},
};
},
});

View File

@ -1,69 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceTw } from './enforceTw.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
});
ruleTester.run('enforce-tw', enforceTw, {
valid: [
{ code: `classNames("foo")` },
{ code: `<div className="foo"/>` },
{ code: `tw("flex")` },
],
invalid: [
{
code: `classNames("flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `<div className="flex"/>`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `<div className={"flex"}/>`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames("foo", "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond ? "foo" : "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond ? "flex" : "foo")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond && "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond || "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond ?? "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames("foo" + "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames("flex" + "foo")`,
errors: [{ messageId: 'needsTw' }],
},
],
});

View File

@ -1,57 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { runAsWorker } from 'synckit';
import enhancedResolve from 'enhanced-resolve';
import * as tailwind from 'tailwindcss';
import path from 'node:path';
import fs from 'node:fs';
const rootDir = path.join(import.meta.dirname, '../..');
const tailwindCssPath = path.join(rootDir, 'stylesheets/tailwind-config.css');
async function loadDesignSystem() {
const tailwindCss = fs.readFileSync(tailwindCssPath, 'utf-8');
const resolver = enhancedResolve.create.sync({
conditionNames: ['style'],
extensions: ['.css'],
mainFields: ['style'],
});
const designSystem = await tailwind.__unstable__loadDesignSystem(
tailwindCss,
{
base: path.dirname(tailwindCssPath),
async loadStylesheet(id, base) {
const resolved = resolver(base, id);
if (!resolved) {
return { path: '', base: '', content: '' };
}
return {
path: resolved,
base: path.dirname(resolved),
content: fs.readFileSync(resolved, 'utf-8'),
};
},
}
);
return designSystem;
}
let cachedDesignSystem = null;
/**
* @param {Array<string>} classNames
*/
async function worker(classNames) {
cachedDesignSystem ??= await loadDesignSystem();
const designSystem = cachedDesignSystem;
const css = designSystem.candidatesToCss(classNames);
const tailwindClassNames = classNames.filter((_, index) => {
return css.at(index) != null;
});
return tailwindClassNames;
}
runAsWorker(worker);

View File

@ -1,75 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { assert } from './utils/assert.mjs';
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
* @typedef {import("@typescript-eslint/utils").TSESLint.Scope.Scope} Scope
*/
/**
* @param {Node} node
* @param {Scope} scope
*/
function isReadOnlyDeep(node, scope) {
if (node.type !== 'TSTypeReference') {
return false;
}
const reference = scope.references.find(ref => {
return ref.identifier === node.typeName;
});
const variable = reference?.resolved;
if (variable == null) {
return false;
}
const defs = variable.defs;
if (defs.length !== 1) {
return false;
}
const [def] = defs;
assert(def, 'Missing def');
return (
def.type === 'ImportBinding' &&
def.parent.type === 'ImportDeclaration' &&
def.parent.source.type === 'Literal' &&
def.parent.source.value === 'type-fest'
);
}
export const enforceTypeAliasReadonlyDeep = ESLintUtils.RuleCreator.withoutDocs(
{
name: 'enforce-type-alias-readonlydeep',
meta: {
type: 'problem',
messages: {
needsReadonlyDeep:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
TSTypeAliasDeclaration(node) {
const scope = context.sourceCode.getScope(node);
if (isReadOnlyDeep(node.typeAnnotation, scope)) {
return;
}
context.report({
node: node.id,
messageId: 'needsReadonlyDeep',
});
},
};
},
}
);

View File

@ -1,40 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceTypeAliasReadonlyDeep } from './enforceTypeAliasReadonlyDeep.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('type-alias-readonlydeep', enforceTypeAliasReadonlyDeep, {
valid: [
{
code: `import type { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
{
code: `import { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
],
invalid: [
{
code: `type Foo = {}`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `type Foo = Bar<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `type Foo = ReadonlyDeep<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `interface ReadonlyDeep<T> {}; type Foo = ReadonlyDeep<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `import type { ReadonlyDeep } from "foo"; type Foo = ReadonlyDeep<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
],
});

View File

@ -1,22 +0,0 @@
{
"dependencies": {
"prod-dep": "0.0.0",
"@scoped/prod-dep": "0.0.0"
},
"devDependencies": {
"dev-dep": "0.0.0",
"@scoped/dev-dep": "0.0.0"
},
"peerDependencies": {
"peer-dep": "0.0.0",
"@scoped/peer-dep": "0.0.0"
},
"optionalDependencies": {
"optional-dep": "0.0.0",
"@scoped/optional-dep": "0.0.0"
},
"bundledDependencies": [
"bundled-dep",
"@scoped/bundled-dep"
]
}

View File

@ -1,3 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export {};

View File

@ -1,3 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export {};

View File

@ -1,4 +0,0 @@
{
"include": ["./client/**", "./server/**"],
"compilerOptions": {}
}

View File

@ -1,64 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { getReferenceType } from './utils/getReferenceType.mjs';
import { isPropertyAccess } from './utils/astUtils.mjs';
export const noDisabledTests = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-disabled-tests',
meta: {
type: 'problem',
hasSuggestions: true,
messages: {
unexpectedDisabledTest: 'Unexpected disabled test',
removeSkip: 'Remove .skip()',
},
schema: [],
defaultOptions: [],
},
create(context) {
const { sourceCode } = context;
return {
MemberExpression(node) {
if (node.object.type !== 'Identifier') {
return;
}
let replacement;
if (node.object.name === 'describe') {
replacement = 'describe';
} else if (node.object.name === 'it') {
replacement = 'it';
} else if (node.object.name === 'test') {
replacement = 'test';
} else {
return;
}
if (!isPropertyAccess(node, 'skip')) {
return;
}
const refType = getReferenceType(sourceCode, node.object);
if (refType != null && refType !== 'global') {
return;
}
context.report({
node,
messageId: 'unexpectedDisabledTest',
suggest: [
{
messageId: 'removeSkip',
fix(fixer) {
return [fixer.replaceTextRange(node.range, replacement)];
},
},
],
});
},
};
},
});

View File

@ -1,56 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noDisabledTests } from './noDisabledTests.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('no-disabled-tests', noDisabledTests, {
valid: [
{ code: 'describe(() => {});' },
{ code: 'it(() => {});' },
{ code: 'test(() => {});' },
{ code: 'describe.only(() => {});' },
{ code: 'it.only(() => {});' },
{ code: 'test.only(() => {});' },
{ code: 'let describe; describe.skip(() => {});' },
{ code: 'x.describe.skip(() => {});' },
],
invalid: [
{
code: `describe.skip(() => {});`,
suggestion: `describe(() => {});`,
},
{
code: `it.skip(() => {});`,
suggestion: `it(() => {});`,
},
{
code: `test.skip(() => {});`,
suggestion: `test(() => {});`,
},
{
code: `describe['skip'](() => {});`,
suggestion: `describe(() => {});`,
},
{
code: `it['skip'](() => {});`,
suggestion: `it(() => {});`,
},
{
code: `test['skip'](() => {});`,
suggestion: `test(() => {});`,
},
].map(opts => {
return {
code: opts.code,
errors: [
{
messageId: 'unexpectedDisabledTest',
suggestions: [{ messageId: 'removeSkip', output: opts.suggestion }],
},
],
};
}),
});

View File

@ -1,202 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { readFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { isBuiltin, findPackageJSON } from 'node:module';
import { createImportSourceVisitor } from './utils/createImportSourceVisitor.mjs';
/**
* @param value {unknown}
* @returns {value is Record<string, unknown>}
*/
function isObject(value) {
return typeof value === 'object' && value != null;
}
/**
* @param deps {unknown}
*/
function getDepsKeys(deps) {
return new Set(isObject(deps) ? Object.keys(deps) : null);
}
/**
* @param deps {unknown}
* @returns {Set<string>}
*/
function getBundledDepsKeys(deps) {
return Array.isArray(deps) ? new Set(deps) : getDepsKeys(deps);
}
/**
* @typedef {object} PkgDeps
* @property {Set<string>} dependencies
* @property {Set<string>} devDependencies
* @property {Set<string>} peerDependencies
* @property {Set<string>} optionalDependencies
* @property {Set<string>} bundledDependencies
*/
/** @type {Map<string, PkgDeps>} */
const PKG_DEPS_CACHE = new Map();
/** @param {string} currentFile */
function getPkgDeps(currentFile) {
const currentDir = dirname(currentFile);
const cached = PKG_DEPS_CACHE.get(currentDir);
if (cached != null) {
return cached;
}
const pkgPath = findPackageJSON('.', currentFile);
if (pkgPath == null) {
throw new Error(`Could not resolve package.json from ${currentFile}`);
}
const pkgText = readFileSync(pkgPath, 'utf8');
const pkgJson = JSON.parse(pkgText);
/** @type {PkgDeps} */
const pkgDeps = {
dependencies: getDepsKeys(pkgJson.dependencies),
devDependencies: getDepsKeys(pkgJson.devDependencies),
peerDependencies: getDepsKeys(pkgJson.peerDependencies),
optionalDependencies: getDepsKeys(pkgJson.optionalDependencies),
bundledDependencies: getBundledDepsKeys(pkgJson.bundledDependencies),
};
PKG_DEPS_CACHE.set(currentDir, pkgDeps);
return pkgDeps;
}
/** @param {string} source */
function getPackageNameFromSource(source) {
if (source.startsWith('@')) {
const [scope, name] = source.split('/', 2);
return `${scope}/${name}`;
}
const [name] = source.split('/', 1);
return name;
}
/**
* @typedef {object} Options
* @property {boolean=} devDependencies
* @property {boolean=} peerDependencies
* @property {boolean=} optionalDependencies
* @property {boolean=} bundledDependencies
*/
/** @type {[Options]} */
const defaultOptions = [
{
devDependencies: true,
peerDependencies: true,
optionalDependencies: true,
bundledDependencies: true,
},
];
export const noExtraneousDependencies = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-extraneous-dependencies',
meta: {
type: 'problem',
messages: {
missingFromProjectDeps:
"'{{pkgName}}' should be listed in the project's dependencies",
wrongProjectDeps:
"'{{pkgName}}' should be listed in the project's dependencies, found in {{found}}",
},
schema: [
{
type: 'object',
properties: {
devDependencies: { type: 'boolean' },
peerDependencies: { type: 'boolean' },
optionalDependencies: { type: 'boolean' },
bundledDependencies: { type: 'boolean' },
},
additionalProperties: false,
},
],
defaultOptions,
},
create(context) {
const { sourceCode, options } = context;
const opts = {
devDependencies: options[0]?.devDependencies ?? true,
peerDependencies: options[0]?.peerDependencies ?? true,
optionalDependencies: options[0]?.optionalDependencies ?? true,
bundledDependencies: options[0]?.bundledDependencies ?? true,
};
const pkgDeps = getPkgDeps(context.physicalFilename);
return createImportSourceVisitor(sourceCode, node => {
const source = node.value;
if (
source.startsWith('.') ||
source.startsWith('/') ||
source.trim() === ''
) {
return;
}
if (isBuiltin(source)) {
return;
}
const pkgName = getPackageNameFromSource(source);
/** @type {Array<string>} */
const found = [];
if (pkgDeps.dependencies.has(pkgName)) {
return;
}
if (pkgDeps.devDependencies.has(pkgName)) {
found.push('devDependencies');
if (opts.devDependencies) {
return;
}
}
if (pkgDeps.peerDependencies.has(pkgName)) {
found.push('peerDependencies');
if (opts.peerDependencies) {
return;
}
}
if (pkgDeps.optionalDependencies.has(pkgName)) {
found.push('optionalDependencies');
if (opts.optionalDependencies) {
return;
}
}
if (pkgDeps.bundledDependencies.has(pkgName)) {
found.push('bundledDependencies');
if (opts.bundledDependencies) {
return;
}
}
if (found.length > 0) {
context.report({
node,
messageId: 'wrongProjectDeps',
data: { pkgName, found: found.join(', ') },
});
} else {
context.report({
node,
messageId: 'missingFromProjectDeps',
data: { pkgName },
});
}
});
},
});

View File

@ -1,112 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import path from 'node:path';
import { noExtraneousDependencies } from './noExtraneousDependencies.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
/**
* @typedef {import("./noExtraneousDependencies.mjs").Options} Options
*/
const ruleTester = new RuleTester();
const filename = path.join(
import.meta.dirname,
'fixtures/noExtraneousDependencies/package/foo.js'
);
/** @type {Options} */
const NONE = {
devDependencies: false,
peerDependencies: false,
optionalDependencies: false,
bundledDependencies: false,
};
/**
* @satisfies {Record<string, [Options]>}
*/
const opts = {
none: [NONE],
dev: [{ ...NONE, devDependencies: true }],
peer: [{ ...NONE, peerDependencies: true }],
optional: [{ ...NONE, optionalDependencies: true }],
bundled: [{ ...NONE, bundledDependencies: true }],
};
ruleTester.run('no-extraneous-dependencies', noExtraneousDependencies, {
valid: [
{ filename, code: `import a from "./a";`, options: opts.none },
{ filename, code: `import a from "../a";`, options: opts.none },
{ filename, code: `import a from "path";`, options: opts.none },
{ filename, code: `import a from "node:path";`, options: opts.none },
{ filename, code: `import a from "";`, options: opts.none },
{ filename, code: `import a from "prod-dep";`, options: opts.none },
{ filename, code: `import a from "prod-dep/nested";`, options: opts.none },
{ filename, code: `import a from "@scoped/prod-dep";`, options: opts.none },
{
filename,
code: `import a from "@scoped/prod-dep/nested";`,
options: opts.none,
},
{ filename, code: `import a from "dev-dep";`, options: opts.dev },
{ filename, code: `import a from "peer-dep";`, options: opts.peer },
{
filename,
code: `import a from "optional-dep";`,
options: opts.optional,
},
{ filename, code: `import a from "bundled-dep";`, options: opts.bundled },
],
invalid: [
{
filename,
code: `import a from "dev-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "peer-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "optional-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "bundled-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "dev-dep";`,
options: opts.peer,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "dev-dep";`,
options: opts.optional,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "dev-dep";`,
options: opts.bundled,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "does-not-exist";`,
options: opts.bundled,
errors: [{ messageId: 'missingFromProjectDeps' }],
},
],
});

View File

@ -1,60 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { getReferenceType } from './utils/getReferenceType.mjs';
import { isPropertyAccess } from './utils/astUtils.mjs';
export const noFocusedTests = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-focused-tests',
meta: {
type: 'problem',
hasSuggestions: true,
fixable: 'code',
messages: {
unexpectedFocusedTest: 'Unexpected focused test',
},
schema: [],
},
create(context) {
const { sourceCode } = context;
return {
MemberExpression(node) {
if (node.object.type !== 'Identifier') {
return;
}
let replacement;
if (node.object.name === 'describe') {
replacement = 'describe';
} else if (node.object.name === 'it') {
replacement = 'it';
} else if (node.object.name === 'test') {
replacement = 'test';
} else {
return;
}
if (!isPropertyAccess(node, 'only')) {
return;
}
const refType = getReferenceType(sourceCode, node.object);
if (refType != null && refType !== 'global') {
return;
}
context.report({
node,
messageId: 'unexpectedFocusedTest',
fix(fixer) {
if (node.range == null) {
return null;
}
return [fixer.replaceTextRange(node.range, replacement)];
},
});
},
};
},
});

View File

@ -1,52 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noFocusedTests } from './noFocusedTests.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('no-focused-tests', noFocusedTests, {
valid: [
{ code: 'describe(() => {});' },
{ code: 'it(() => {});' },
{ code: 'test(() => {});' },
{ code: 'describe.skip(() => {});' },
{ code: 'it.skip(() => {});' },
{ code: 'test.skip(() => {});' },
{ code: 'let describe; describe.only(() => {});' },
{ code: 'x.describe.only(() => {});' },
],
invalid: [
{
code: `describe.only(() => {});`,
output: `describe(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `it.only(() => {});`,
output: `it(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `test.only(() => {});`,
output: `test(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `describe['only'](() => {});`,
output: `describe(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `it['only'](() => {});`,
output: `it(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `test['only'](() => {});`,
output: `test(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
],
});

View File

@ -1,25 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
export const noForIn = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-for-in',
meta: {
type: 'problem',
messages: {
preferForOf: 'Prefer for..of loops',
},
schema: [],
},
create(context) {
return {
ForInStatement(node) {
context.report({
node,
messageId: 'preferForOf',
});
},
};
},
});

View File

@ -1,25 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noForIn } from './noForIn.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('no-for-in', noForIn, {
valid: [
{ code: 'for (let a of b) {}' },
{ code: 'for (;;) {}' },
{ code: 'if (a in b) {}' },
],
invalid: [
{
code: `for (let a in b) {}`,
errors: [{ messageId: 'preferForOf' }],
},
{
code: `for (a in b) {}`,
errors: [{ messageId: 'preferForOf' }],
},
],
});

View File

@ -1,264 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { createImportSourceVisitor } from './utils/createImportSourceVisitor.mjs';
import micromatch from 'micromatch';
import isGlob from 'is-glob';
import * as path from 'node:path';
import { assert } from './utils/assert.mjs';
import enhancedResolve from 'enhanced-resolve';
const resolver = enhancedResolve.create.sync({
extensionAlias: {
'.js': ['.ts', '.tsx', '.js'],
},
});
/**
* @param {string} fromDir
* @param {string} moduleName
*/
function resolveFrom(fromDir, moduleName) {
try {
const result = resolver(fromDir, moduleName);
if (result === false) {
return null;
}
return result;
} catch (error) {
return null;
}
}
/**
* @param {string | string[]} input
* @returns {string[]}
*/
function toArray(input) {
return Array.isArray(input) ? input : [input];
}
/**
* @param {string} filePath
* @param {string} target
*/
function containsPath(filePath, target) {
const relative = path.relative(target, filePath);
return relative === '' || !relative.startsWith('..');
}
/**
* @param {string} fileName
* @param {RegExp | string} targetPath
*/
function isMatchingTargetPath(fileName, targetPath) {
return typeof targetPath === 'string'
? containsPath(fileName, targetPath)
: targetPath.test(fileName);
}
/** @type {Map<string, RegExp | string>} */
const REGEX_CACHE = new Map();
/**
* @typedef {object} Zone
* @property {string | string[]=} target
* @property {string | string[]=} from
* @property {string[]=} except
* @property {string=} message
*/
/**
* @typedef {object} Matcher
* @property {(RegExp | string)[]} targetPaths
* @property {(RegExp | string)[]} fromPaths
* @property {(RegExp | string)[] | null} exceptPaths
* @property {string | null} message
*/
/** @type {[Options]} */
const defaultOptions = [{}];
/**
* @typedef {object} Options
* @property {Zone[]=} zones
* @property {string=} basePath
*/
export const noRestrictedPaths = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-restricted-paths',
meta: {
type: 'problem',
messages: {
pathRestrictedNoMessage:
'Unexpected path "{{moduleName}}" imported in restricted zone.',
pathRestrictedWithMessage:
'Unexpected path "{{moduleName}}" imported in restricted zone. {{message}}',
},
schema: [
{
type: 'object',
properties: {
zones: {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
target: {
anyOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
minItems: 1,
},
],
},
from: {
anyOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
minItems: 1,
},
],
},
except: {
type: 'array',
items: {
type: 'string',
},
uniqueItems: true,
},
message: { type: 'string' },
},
additionalProperties: false,
},
},
basePath: { type: 'string' },
},
additionalProperties: false,
},
],
defaultOptions,
},
create(context) {
const { filename, sourceCode } = context;
const zones = context.options[0]?.zones ?? [];
const basePath = context.options[0]?.basePath ?? context.cwd;
const matchers = zones.map(zone => {
assert(zone.target != null, 'Zone missing `target`');
assert(zone.from != null, 'Zone missing `from`');
const zoneTarget = toArray(zone.target);
const zoneFrom = toArray(zone.from);
assert(zoneTarget.length > 0, 'Zone needs at least one `target`');
assert(zoneFrom.length > 0, 'Zone needs at least one `from`');
let zoneExcept = zone.except != null ? toArray(zone.except) : null;
if (zoneExcept?.length === 0) {
zoneExcept = null;
}
let hasGlobPatterns = false;
let hasNonGlobPatterns = false;
/** @param {string} target */
function compilePattern(target) {
const targetPath = path.resolve(basePath, target);
const cached = REGEX_CACHE.get(targetPath);
if (cached != null) {
return cached;
}
/** @type {RegExp | string} */
let result;
if (isGlob(targetPath)) {
hasGlobPatterns = true;
result = micromatch.makeRe(targetPath);
} else {
hasNonGlobPatterns = true;
result = targetPath;
}
if (hasGlobPatterns && hasNonGlobPatterns) {
throw new Error(
'Cannot have both glob and non-glob patterns in the same zone'
);
}
REGEX_CACHE.set(targetPath, result);
return result;
}
/** @type {Matcher} */
const matcher = {
targetPaths: zoneTarget.map(target => compilePattern(target)),
fromPaths: zoneFrom.map(from => compilePattern(from)),
exceptPaths: zoneExcept?.map(except => compilePattern(except)) ?? null,
message: zone.message ?? null,
};
return matcher;
});
const targetMatchers = matchers.filter(matcher => {
return matcher.targetPaths.some(targetPath => {
return isMatchingTargetPath(filename, targetPath);
});
});
if (targetMatchers.length === 0) {
return {};
}
return createImportSourceVisitor(sourceCode, source => {
const dirname = path.dirname(filename);
const moduleName = source.value;
const resolvedPath = resolveFrom(dirname, moduleName);
if (resolvedPath == null) {
return;
}
for (const matcher of targetMatchers) {
const matchesFromPath = matcher.fromPaths.some(fromPath => {
return isMatchingTargetPath(resolvedPath, fromPath);
});
if (!matchesFromPath) {
continue;
}
const matchesExceptPath = matcher.exceptPaths?.some(exceptPath => {
return isMatchingTargetPath(resolvedPath, exceptPath);
});
if (matchesExceptPath) {
continue;
}
if (matcher.message != null) {
context.report({
node: source,
messageId: 'pathRestrictedWithMessage',
data: { moduleName, message: matcher.message },
});
} else {
context.report({
node: source,
messageId: 'pathRestrictedNoMessage',
data: { moduleName },
});
}
}
});
},
});

View File

@ -1,54 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noRestrictedPaths } from './noRestrictedPaths.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
import * as path from 'node:path';
const basePath = path.join(import.meta.dirname, 'fixtures/noRestrictedPaths');
const filename = path.join(basePath, 'client/entry.ts');
/**
* @param {boolean=} withMessage
* @returns {[import("./noRestrictedPaths.mjs").Options]}
*/
function opts(withMessage) {
const message = withMessage ? 'just stop it' : undefined;
return [
{ basePath, zones: [{ target: './client', from: './server', message }] },
];
}
const ruleTester = new RuleTester();
ruleTester.run('no-restricted-paths', noRestrictedPaths, {
valid: [
{ filename, options: opts(), code: `import b from './client.ts';` },
{ filename, options: opts(), code: `import b from './client.js';` },
{ filename, options: opts(), code: `import b from '../client/client.ts';` },
{ filename, options: opts(), code: `import b from './nonexistant';` },
{ filename, options: opts(), code: `import b from 'node:path';` },
{ filename, options: opts(), code: `import b from 'react';` },
{ filename, options: opts(), code: `import b from 'fake-module';` },
],
invalid: [
{
filename,
options: opts(),
code: `import b from '../server/server.ts';`,
errors: [{ messageId: 'pathRestrictedNoMessage' }],
},
{
filename,
options: opts(),
code: `import b from '../server/server.js';`,
errors: [{ messageId: 'pathRestrictedNoMessage' }],
},
{
filename,
options: opts(true),
code: `import b from '../server/server.ts';`,
errors: [{ messageId: 'pathRestrictedWithMessage' }],
},
],
});

View File

@ -1,35 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { isPropertyAccess } from './utils/astUtils.mjs';
export const noThen = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-then',
meta: {
type: 'problem',
messages: {
preferAwait: 'Prefer await instead of .then()',
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
MemberExpression(node) {
if (!isPropertyAccess(node, 'then')) {
return;
}
if (node.parent.type !== 'CallExpression') {
return;
}
context.report({
node: node.property,
messageId: 'preferAwait',
});
},
};
},
});

View File

@ -1,14 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/**
* @param condition {unknown}
* @param message {string}
* @returns {asserts condition}
*/
export function assert(condition, message) {
if (condition == null || condition === false) {
throw new TypeError(message);
}
}

View File

@ -1,36 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
* @typedef {import("@typescript-eslint/utils").TSESTree.Literal} Literal
* @typedef {import("@typescript-eslint/utils").TSESTree.StringLiteral} StringLiteral
* @typedef {import("@typescript-eslint/utils").TSESTree.Identifier} Identifier
* @typedef {import("@typescript-eslint/utils").TSESTree.MemberExpression} MemberExpression
*/
/**
* @param {Node=} node
* @returns {node is StringLiteral}
*/
export function isStringLiteral(node) {
return node?.type === 'Literal' && typeof node.value === 'string';
}
/**
* @param {Node | null | undefined} node
* @param {string} property
* @returns {node is MemberExpression}
*/
export function isPropertyAccess(node, property) {
if (node?.type !== 'MemberExpression') {
return false;
}
if (node.computed) {
return node.property.type === 'Literal' && node.property.value === property;
}
return node.property.type === 'Identifier' && node.property.name === property;
}

View File

@ -1,123 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { getReferenceType } from './getReferenceType.mjs';
import { isStringLiteral } from './astUtils.mjs';
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.StringLiteral} StringLiteral
* @typedef {import("@typescript-eslint/utils").TSESLint.SourceCode} SourceCode
* @typedef {import("@typescript-eslint/utils").TSESLint.RuleListener} RuleListener
*/
/**
* @param {SourceCode} sourceCode
* @param {(source: StringLiteral) => void} visitSource
* @returns {RuleListener}
*/
export function createImportSourceVisitor(sourceCode, visitSource) {
return {
// import ... from '<source>'
ImportDeclaration(node) {
visitSource(node.source);
},
// import('<source>')
ImportExpression(node) {
if (!isStringLiteral(node.source)) {
return;
}
visitSource(node.source);
},
CallExpression(node) {
// require('<source>')
if (node.callee.type === 'Identifier') {
if (node.callee.name !== 'require') {
return;
}
const refType = getReferenceType(sourceCode, node.callee);
if (refType != null && refType !== 'global') {
return;
}
const arg = node.arguments.at(0);
if (!isStringLiteral(arg)) {
return;
}
visitSource(arg);
return;
}
// require.resolve('<source>')
if (node.callee.type === 'MemberExpression') {
const { object, property } = node.callee;
if (object.type !== 'Identifier') {
return;
}
if (object.name !== 'require') {
return;
}
const refType = getReferenceType(sourceCode, object);
if (refType != null && refType !== 'global') {
return;
}
if (property.type !== 'Identifier') {
return;
}
if (property.name !== 'resolve') {
return;
}
const arg = node.arguments.at(0);
if (!isStringLiteral(arg)) {
return;
}
visitSource(arg);
}
},
// import.meta.resolve('<source>')
MetaProperty(node) {
if (node.meta.name !== 'import') {
return;
}
if (node.property.name !== 'meta') {
return;
}
const memberExpression = node.parent;
if (memberExpression.type !== 'MemberExpression') {
return;
}
if (memberExpression.property.type !== 'Identifier') {
return;
}
if (memberExpression.property.name !== 'resolve') {
return;
}
const callExpression = memberExpression.parent;
if (callExpression.type !== 'CallExpression') {
return;
}
const arg = callExpression.arguments.at(0);
if (!isStringLiteral(arg)) {
return;
}
visitSource(arg);
},
// export {...} from '<source>'
ExportNamedDeclaration(node) {
if (node.source == null) {
return;
}
visitSource(node.source);
},
// export * ... from '<source>'
ExportAllDeclaration(node) {
visitSource(node.source);
},
};
}

View File

@ -1,18 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Identifier} Identifier
* @typedef {import("@typescript-eslint/utils").TSESLint.SourceCode} SourceCode
*/
/**
* @param {SourceCode} sourceCode
* @param {Identifier} node
*/
export function getReferenceType(sourceCode, node) {
const scope = sourceCode.getScope(node);
const ref = scope.references.find(r => r.identifier === node);
return ref?.resolved?.scope.type ?? null;
}

View File

@ -1,7 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import * as mocha from 'mocha'
import { RuleTester } from '@typescript-eslint/rule-tester';
RuleTester.afterAll = mocha.after;

File diff suppressed because it is too large Load Diff

View File

@ -1,269 +0,0 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
//
// WARNING: Do not import (or even `import()`) any packages, they won't always be installed.
//
import { execSync } from 'node:child_process';
import { styleText } from 'node:util';
import { test } from 'node:test';
import assert from 'node:assert/strict';
/**
* From @pnpm/pnpmfile/lib/Hooks.d.ts
*
* @typedef {{
* deprecated?: boolean;
* }} PackageSnapshot
*
* @typedef {Record<string, PackageSnapshot>} PackageSnapshots
*
* @typedef {{
* packages?: PackageSnapshots
* }} LockfileObject
*
* @typedef {{
* log: (message: string) => void;
* }} HookContext
*
* @typedef {{
* verifyDepsBeforeRun?: unknown,
* }} Config
*
* @typedef {{
* afterAllResolved?: (
* lockfile: LockfileObject,
* context: HookContext,
* ) => LockfileObject | Promise<LockfileObject>;
* updateConfig?: (config: Config) => Config | Promise<Config>
* }} Hooks
*/
/**
* @param {boolean} condition
* @param {string} message
* @returns {asserts condition}
*/
// function assert(condition, message) {
// if (!condition) {
// throw new TypeError(message);
// }
// }
/**
* @param {string} message
*/
function formatError(message) {
return `${styleText(['bgRed', 'whiteBright'], '[ERROR]')} ${styleText('red', message)}`;
}
/** @type {any} */
let CACHED_WORKSPACE_CONFIG;
async function getWorkspaceConfig() {
if (CACHED_WORKSPACE_CONFIG == null) {
const stdout = execSync('pnpm config list --json', {
encoding: 'utf-8',
env: { PATH: process.env.PATH },
});
const config = JSON.parse(stdout);
CACHED_WORKSPACE_CONFIG = config;
}
return CACHED_WORKSPACE_CONFIG;
}
/**
* Samples:
* - "jest-process-manager@0.4.0"
* - "@jest/process-manager@0.4.0"
* - "jest-process-manager@0.4.0(debug@4.4.3)"
*
* @param {string} packagePath
*/
function parsePackagePath(packagePath) {
const truncateAt = packagePath.indexOf('(');
const packageSpec =
truncateAt === -1 ? packagePath : packagePath.slice(0, truncateAt);
const splitAt = packageSpec.lastIndexOf('@');
const name = packageSpec.slice(0, splitAt);
const version = packageSpec.slice(splitAt + 1);
return { name, version };
}
/**
* @typedef {{
* path: string,
* name: string,
* version: string,
* snapshot: PackageSnapshot,
* }} PackageSnapshotEntry
*/
/**
* @param {LockfileObject} lockfile
* @returns {ReadonlyArray<PackageSnapshotEntry>}
*/
function getPackages(lockfile) {
const { packages = {} } = lockfile;
return Object.keys(packages).map(path => {
const snapshot = packages[path];
const { name, version } = parsePackagePath(path);
return { path, name, version, snapshot };
});
}
/**
* Minimal semver support, only supports exact versions and `||`
* @param {string} version
* @param {string} range
*/
function satisfies(version, range) {
return range.split('||').some(choice => {
return choice.trim() === version;
});
}
/**
* @typedef {(lockfile: LockfileObject, context: HookContext) => Promise<boolean>} CustomCheck
*/
/** @type {CustomCheck} */
async function noDeprecatedPackages(lockfile, context) {
const config = await getWorkspaceConfig();
const packages = getPackages(lockfile);
const deprecated = packages.filter(pkg => {
if (!pkg.snapshot.deprecated) {
return false;
}
const allowed = config.allowedDeprecatedVersions?.[pkg.name];
if (allowed != null && satisfies(pkg.version, allowed)) {
return false;
}
return true;
});
const success = deprecated.length === 0;
if (!success) {
context.log('');
context.log(
formatError(
'Found deprecated packages, to ignore them add this to the pnpm-workspace.yaml file:'
)
);
context.log('');
context.log('allowedDeprecatedVersions:');
for (const pkg of deprecated) {
context.log(` '${pkg.name}': '${pkg.version}'`);
}
context.log('');
}
return success;
}
/** @type {ReadonlyArray<RegExp>} */
const RESTRICTED_DUPLICATE_DEPENDENCIES = [
// /^@signalapp\//,
// /^@indutny\//,
];
/**
* @param {string} name
* @returns {boolean}
*/
function isRestrictedDuplicateDependency(name) {
return RESTRICTED_DUPLICATE_DEPENDENCIES.some(regex => {
return regex.test(name);
});
}
/** @type {CustomCheck} */
async function restrictDuplicateDependencies(lockfile, context) {
const packages = getPackages(lockfile);
/** @type {Map<string, Set<string>>} */
const seen = new Map();
/** @type {Set<string>} */
const duplicates = new Set();
for (const pkg of packages) {
if (!isRestrictedDuplicateDependency(pkg.name)) {
continue;
}
let versions = seen.get(pkg.name);
if (versions != null) {
duplicates.add(pkg.name);
} else {
versions = new Set();
seen.set(pkg.name, versions);
}
versions.add(pkg.version);
}
const success = duplicates.size === 0;
if (!success) {
context.log('');
context.log(formatError('Found duplicate restricted packages:'));
context.log('');
for (const duplicate of duplicates) {
const versions = seen.get(duplicate);
assert(versions != null, `Missing package versions for ${duplicate}`);
context.log(` ${duplicate}: ${Array.from(versions).join(', ')}`);
}
context.log('');
}
return success;
}
/** @type {Hooks} */
export const hooks = {
async afterAllResolved(lockfile, context) {
const results = await Promise.all([
noDeprecatedPackages(lockfile, context),
restrictDuplicateDependencies(lockfile, context),
]);
const hasAnyFailures = results.includes(false);
if (hasAnyFailures) {
context.log(
formatError(
'pnpm install failed because of a custom check in .pnpmfile.mjs'
)
);
context.log('');
process.exit(1);
}
return lockfile;
},
updateConfig(config) {
return {
...config,
verifyDepsBeforeRun:
process.env.CI || process.env.SKIP_VERIFY_DEPS_BEFORE_RUN
? false
: config.verifyDepsBeforeRun,
};
},
};
if (process.env.NODE_TEST_CONTEXT) {
await test('noDeprecatedPackages', async () => {
const pkg = '@scope/pkg-name@1.0.0(@other-scope/other-pkg@2.0.0)';
const success = await noDeprecatedPackages(
{ packages: { [pkg]: { deprecated: true } } },
{ log: () => undefined }
);
assert(!success);
});
}

View File

@ -2,37 +2,37 @@
# supports `.gitignore`: https://github.com/prettier/prettier/issues/2294
# Generated files
build/**/*.js
build/**/*.json
app/**/*.js
config/local-*.json
config/local.json
dist/**
js/components.js
js/util_worker.js
libtextsecure/components.js
libtextsecure/test/test.js
stylesheets/*.css
test/test.js
ts/**/*.js
!ts/**/.eslintrc.js
ts/protobuf/*.d.ts
ts/protobuf/*.js
stylesheets/manifest.css
stylesheets/tailwind.css
ts/util/lint/exceptions.json
storybook-static
build/locale-display-names.json
build/country-display-names.json
build/compact-locales/**/*.json
release/**
pnpm-lock.yaml
# Third-party files
node_modules/**
packages/*/node_modules/**
packages/lame/wrapper.mjs
packages/lame/lame-*/
packages/windows-ucv/dist/**
danger/node_modules/**
sticker-creator/node_modules/**
components/**
scripts/emoji-datasource/emoji-datasource.json
js/curve/**
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js
js/calling-tools/**
# Assets
/images/

8
.prettierrc.js Normal file
View File

@ -0,0 +1,8 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
module.exports = {
singleQuote: true,
arrowParens: 'avoid',
trailingComma: 'es5',
};

View File

@ -1,16 +0,0 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/** @type {import("prettier").Config} */
const config = {
plugins: ['prettier-plugin-tailwindcss'],
singleQuote: true,
arrowParens: 'avoid',
trailingComma: 'es5',
tailwindStylesheet: './stylesheets/tailwind-config.css',
tailwindFunctions: ['tw'],
tailwindAttributes: [],
};
export default config

7
.storybook/StorybookThemeContext.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Context } from 'react';
import type { ThemeType } from '../ts/types/Util';
export const StorybookThemeContext: Context<ThemeType>;

View File

@ -0,0 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createContext } from 'react';
import { ThemeType } from '../ts/types/Util';
export const StorybookThemeContext = createContext(ThemeType.light);

View File

@ -1,7 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createContext } from 'react';
import { ThemeType } from '../ts/types/Util.std.ts';
export const StorybookThemeContext = createContext(ThemeType.light);

View File

@ -1,71 +0,0 @@
<!-- Copyright 2025 Signal Messenger, LLC -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
<!doctype html>
<html>
<head>
<style>
details {
padding-inline-start: 1em;
}
img {
max-width: 800px;
}
</style>
<title>Signal Desktop ICU</title>
</head>
<body>
<input type="search" placeholder="ICU string" id="search" />
<p id="disclaimer"></p>
<section id="results"></section>
<script>
const index = %INDEX%;
const results = document.getElementById('results');
const search = document.getElementById('search');
const disclaimer = document.getElementById('disclaimer');
function onSearch() {
results.replaceChildren();
const reg = new RegExp(search.value, 'i');
const allFiltered = index
.filter(([key, message]) => reg.test(key) || reg.test(message));
const filtered = allFiltered.slice(0, 100);
disclaimer.textContent = filtered.length < allFiltered.length ?
'Showing the first 100 results:' : 'All results:';
for (const [key, message, stories] of filtered) {
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = `${key}: "${message}"`;
details.appendChild(summary);
for (const [storyId, image] of stories) {
const story = document.createElement('details');
details.appendChild(story);
const title = document.createElement('summary');
title.textContent = storyId;
story.appendChild(title);
const img = document.createElement('img');
img.src = `images/${image}`;
img.loading = 'lazy';
story.appendChild(img);
}
results.appendChild(details);
}
}
document.getElementById('search').addEventListener('input', onSearch);
onSearch();
</script>
</body>
</html>

View File

@ -3,20 +3,13 @@
import type { StorybookConfig } from '@storybook/react-webpack5';
import { ProvidePlugin } from 'webpack';
import { builtinModules } from 'node:module';
const EXTERNALS = new Set(builtinModules);
// We have polyfills for these
EXTERNALS.delete('buffer');
EXTERNALS.delete('url');
const storybookConfig: StorybookConfig = {
const config: StorybookConfig = {
typescript: {
reactDocgen: false,
},
stories: ['../ts/axo/**/*.stories.tsx', '../ts/components/**/*.stories.tsx'],
stories: ['../ts/components/**/*.stories.tsx'],
addons: [
'@storybook/addon-a11y',
@ -43,101 +36,80 @@ const storybookConfig: StorybookConfig = {
{ from: '../fonts', to: 'fonts' },
{ from: '../images', to: 'images' },
{ from: '../fixtures', to: 'fixtures' },
{
from: '../node_modules/emoji-datasource-apple/img',
to: 'node_modules/emoji-datasource-apple/img',
},
{
from: '../node_modules/intl-tel-input/build/img',
to: 'node_modules/intl-tel-input/build/img',
},
],
swc() {
return {
jsc: {
transform: {
react: { runtime: 'automatic' },
},
},
};
},
webpackFinal(webpackConfig) {
// oxlint-disable-next-line no-param-reassign
webpackConfig.cache = {
webpackFinal(config) {
config.cache = {
type: 'filesystem',
};
// oxlint-disable-next-line no-param-reassign, typescript/no-non-null-assertion
webpackConfig.resolve!.extensionAlias = {
'.js': ['.tsx', '.ts', '.js'],
};
config.resolve!.extensions = ['.tsx', '.ts', '...'];
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.module!.rules!.unshift({
config.module!.rules!.unshift({
test: /\.scss$/,
use: [
{ loader: require.resolve('style-loader') },
{
loader: require.resolve('css-loader'),
options: { modules: false, url: false },
},
{
loader: require.resolve('sass-loader'),
options: {
additionalData: '$is-storybook: true;',
},
},
{ loader: 'style-loader' },
{ loader: 'css-loader', options: { modules: false, url: false } },
{ loader: 'sass-loader' },
],
});
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.module!.rules!.unshift({
config.module!.rules!.unshift({
test: /\.css$/,
use: [
// prevent storybook defaults from being applied
],
});
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.module!.rules!.push({
test: /tailwind-config\.css$/,
use: [
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
config: false,
plugins: [require.resolve('@tailwindcss/postcss')],
},
},
},
],
});
config.node = { global: true };
// oxlint-disable-next-line no-param-reassign
webpackConfig.node = { global: true };
// oxlint-disable-next-line no-param-reassign
webpackConfig.externals = ({ request }, callback) => {
if (
(request.startsWith('node:') && request !== 'node:buffer') ||
EXTERNALS.has(request)
) {
// Keep Node.js imports unchanged
return callback(null, 'commonjs ' + request);
}
callback();
config.externals = {
net: 'commonjs net',
vm: 'commonjs vm',
fs: 'commonjs fs',
async_hooks: 'commonjs async_hooks',
module: 'commonjs module',
stream: 'commonjs stream',
tls: 'commonjs tls',
dns: 'commonjs dns',
http: 'commonjs http',
https: 'commonjs https',
os: 'commonjs os',
constants: 'commonjs constants',
zlib: 'commonjs zlib',
'@signalapp/libsignal-client': 'commonjs @signalapp/libsignal-client',
'@signalapp/libsignal-client/zkgroup':
'commonjs @signalapp/libsignal-client/zkgroup',
'@signalapp/ringrtc': 'commonjs @signalapp/ringrtc',
'@signalapp/better-sqlite3': 'commonjs @signalapp/better-sqlite3',
electron: 'commonjs electron',
'fs-xattr': 'commonjs fs-xattr',
fsevents: 'commonjs fsevents',
'mac-screen-capture-permissions':
'commonjs mac-screen-capture-permissions',
sass: 'commonjs sass',
bufferutil: 'commonjs bufferutil',
'utf-8-validate': 'commonjs utf-8-validate',
};
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.plugins!.push(
config.plugins!.push(
new ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
})
);
return webpackConfig;
return config;
},
docs: {},
};
export default storybookConfig;
export default config;

View File

@ -3,38 +3,25 @@
import '../ts/window.d.ts';
import '@signalapp/quill-cjs/dist/quill.core.css';
import React from 'react';
import 'sanitize.css';
import '../stylesheets/manifest.scss';
import '../stylesheets/tailwind-config.css';
import * as styles from './styles.scss';
import messages from '../_locales/en/messages.json';
import { StorybookThemeContext } from './StorybookThemeContext';
import { ThemeType } from '../ts/types/Util';
import { setupI18n } from '../ts/util/setupI18n';
import { HourCyclePreference } from '../ts/types/I18N';
import { Provider } from 'react-redux';
import type { Store } from 'redux';
import { combineReducers, createStore } from 'redux';
import { Globals } from '@react-spring/web';
import { StorybookThemeContext } from './StorybookThemeContext.std.ts';
import { SystemThemeType, ThemeType } from '../ts/types/Util.std.ts';
import { setupI18n } from '../ts/util/setupI18n.dom.tsx';
import { HourCyclePreference } from '../ts/types/I18N.std.ts';
import { AppProvider } from '../ts/windows/AppProvider.dom.tsx';
import type { StateType } from '../ts/state/reducer.preload.ts';
import { Store, combineReducers, createStore } from 'redux';
import { StateType } from '../ts/state/reducer';
import {
ScrollerLockContext,
createScrollerLock,
} from '../ts/hooks/useScrollLock.dom.tsx';
import { Environment, setEnvironment } from '../ts/environment.std.ts';
import { parseUnknown } from '../ts/util/schemas.std.ts';
import { LocaleEmojiListSchema } from '../ts/types/emoji.std.ts';
import { FunProvider } from '../ts/components/fun/FunProvider.dom.tsx';
import { MOCK_GIFS_PAGINATED_ONE_PAGE } from '../ts/test-helpers/funPickerMocks.dom.tsx';
import { NavTab } from '../ts/types/Nav.std.ts';
import type { FunEmojiSelection } from '../ts/components/fun/panels/FunPanelEmojis.dom.tsx';
import type { FunGifSelection } from '../ts/components/fun/panels/FunPanelGifs.dom.tsx';
import type { FunStickerSelection } from '../ts/components/fun/panels/FunPanelStickers.dom.tsx';
import { Emoji } from '../ts/axo/emoji.std.ts';
} from '../ts/hooks/useScrollLock';
import { Environment, setEnvironment } from '../ts/environment.ts';
setEnvironment(Environment.Development, true);
@ -79,16 +66,6 @@ export const globalTypes = {
const mockStore: Store<StateType> = createStore(
combineReducers({
calling: (state = {}) => state,
nav: (
state = {
selectedLocation: {
tab: NavTab.Chats,
details: {
conversationId: undefined,
},
},
}
) => state,
conversations: (
state = {
conversationLookup: {},
@ -100,10 +77,10 @@ const mockStore: Store<StateType> = createStore(
})
);
// oxlint-disable-next-line
// eslint-disable-next-line
const noop = () => {};
window.Whisper ??= {};
window.Whisper = window.Whisper || {};
window.Whisper.events = {
on: noop,
off: noop,
@ -121,10 +98,10 @@ window.SignalContext = {
},
nativeThemeListener: {
getSystemTheme: () => SystemThemeType.light,
getSystemTheme: () => 'light',
subscribe: noop,
unsubscribe: noop,
update: () => SystemThemeType.light,
update: () => 'light',
},
Settings: {
themeSetting: {
@ -138,50 +115,19 @@ window.SignalContext = {
platform: '',
release: '',
},
// oxlint-disable-next-line typescript/no-explicit-any
config: {} as any,
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
getPreferredSystemLocales: () => ['en'],
getLocaleOverride: () => null,
getLocaleDisplayNames: () => ({ en: { en: 'English' } }),
getLocalizedEmojiList: async locale => {
const data = await fetch(
`https://updates2.signal.org/static/android/emoji/search/13/${locale}.json`
);
const json: unknown = await data.json();
const result = parseUnknown(LocaleEmojiListSchema, json);
return result;
},
getVersion: () => '7.61.0',
// For test-runner
_skipAnimation: () => {
Globals.assign({
skipAnimation: true,
});
},
_trackICUStrings: () => i18n.trackUsage(),
_stopTrackingICUStrings: () => i18n.stopTrackingUsage(),
};
window.ConversationController ??= {};
window.i18n = i18n;
window.ConversationController = window.ConversationController || {};
window.ConversationController.isSignalConversationId = () => false;
window.ConversationController.onConvoMessageMount = noop;
window.reduxStore = mockStore;
window.Signal = {
Services: {
beforeNavigate: {
registerCallback: () => undefined,
unregisterCallback: () => undefined,
shouldCancelNavigation: () => {
throw new Error('Not implemented');
},
},
},
};
const withGlobalTypesProvider = (Story, context) => {
const theme =
@ -234,61 +180,17 @@ function withMockStoreProvider(Story, context) {
function withScrollLockProvider(Story, context) {
return (
<ScrollerLockContext.Provider
value={createScrollerLock('MockStories', () => null)}
value={createScrollerLock('MockStories', () => {})}
>
<Story {...context} />
</ScrollerLockContext.Provider>
);
}
function withFunProvider(Story, context) {
return (
<FunProvider
i18n={window.SignalContext.i18n}
recentEmojis={[]}
recentStickers={[]}
recentGifs={[]}
emojiSkinToneDefault={Emoji.SkinTone.None}
onEmojiSkinToneDefaultChange={noop}
installedStickerPacks={[]}
showStickerPickerHint={false}
onClearStickerPickerHint={noop}
onOpenCustomizePreferredReactionsModal={noop}
fetchGifsSearch={() => Promise.resolve(MOCK_GIFS_PAGINATED_ONE_PAGE)}
fetchGifsFeatured={() => Promise.resolve(MOCK_GIFS_PAGINATED_ONE_PAGE)}
fetchGif={() => Promise.resolve(new Blob([new Uint8Array(1)]))}
onSelectEmoji={function (emojiSelection: FunEmojiSelection): void {
// oxlint-disable-next-line no-console
console.log('onSelectEmoji', emojiSelection);
}}
onSelectSticker={function (stickerSelection: FunStickerSelection): void {
// oxlint-disable-next-line no-console
console.log('onSelectSticker', stickerSelection);
}}
onSelectGif={function (gifSelection: FunGifSelection): void {
// oxlint-disable-next-line no-console
console.log('onSelectGif', gifSelection);
}}
>
<Story {...context} />
</FunProvider>
);
}
function withAppProvider(Story, context) {
return (
<AppProvider>
<Story {...context} />
</AppProvider>
);
}
export const decorators = [
withAppProvider,
withGlobalTypesProvider,
withMockStoreProvider,
withScrollLockProvider,
withFunProvider,
];
export const parameters = {

View File

@ -1,14 +1,7 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../stylesheets/variables';
#storybook-root {
height: 100%;
}
#storybook-root > div {
height: 100%;
}
@import '../stylesheets/variables';
.container {
align-content: stretch;
@ -18,6 +11,6 @@
}
.dark-theme {
background-color: variables.$color-gray-95;
color: variables.$color-gray-05;
background-color: $color-gray-95;
color: $color-gray-05;
}

View File

@ -1,113 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { mkdir, writeFile, symlink } from 'node:fs/promises';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import {
type TestRunnerConfig,
waitForPageReady,
} from '@storybook/test-runner';
const SECOND = 1000;
const { ARTIFACTS_DIR } = process.env;
const config: TestRunnerConfig = {
async preVisit(page) {
if (!ARTIFACTS_DIR) {
return;
}
await page.evaluate('window.SignalContext._skipAnimation()');
await page.evaluate('window.SignalContext._trackICUStrings()');
},
async postVisit(page, context) {
if (context.hasFailure) {
return;
}
if (!ARTIFACTS_DIR) {
return;
}
await waitForPageReady(page);
const result = await page.evaluate(
'window.SignalContext._stopTrackingICUStrings()'
);
// No strings - no file
if (result.length === 0) {
return;
}
const storeDir = join(ARTIFACTS_DIR, 'images');
await mkdir(storeDir, { recursive: true });
const componentDir = join(ARTIFACTS_DIR, 'components', context.id);
await mkdir(componentDir, { recursive: true });
const saves = new Array<Promise<void>>();
for (const [key, value] of result) {
const locator = page
.getByText(value)
.or(page.getByTitle(value))
.or(page.getByLabel(value));
// oxlint-disable-next-line no-await-in-loop
if (await locator.count()) {
const first = locator.first();
try {
// oxlint-disable-next-line no-await-in-loop
await first.focus({ timeout: SECOND });
} catch {
// Opportunistic
}
try {
// oxlint-disable-next-line no-await-in-loop
if (await first.isVisible()) {
// oxlint-disable-next-line no-await-in-loop
await first.scrollIntoViewIfNeeded({ timeout: SECOND });
}
} catch {
// Opportunistic
}
}
// oxlint-disable-next-line no-await-in-loop
const image = await page.screenshot({
animations: 'disabled',
fullPage: true,
mask: [locator],
// Semi-transparent ultramarine
maskColor: 'rgba(44, 107, 273, 0.3)',
type: 'jpeg',
quality: 95,
});
const digest = createHash('sha256').update(image).digest('hex');
const storeFile = join(storeDir, `${digest}.jpg`);
const targetFile = join(componentDir, `${key.replace(/^icu:/, '')}.jpg`);
saves.push(
(async () => {
try {
await writeFile(storeFile, image, {
// Fail if exists
flags: 'wx',
});
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
}
await symlink(storeFile, targetFile);
})()
);
}
await Promise.all(saves);
},
};
export default config;

47
.stylelintrc.js Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
module.exports = {
extends: [
'stylelint-config-recommended-scss',
'stylelint-config-css-modules',
],
plugins: ['stylelint-use-logical-spec'],
rules: {
// Disabled from recommended set to get stylelint working initially
'block-no-empty': null,
'declaration-block-no-duplicate-properties': null,
'declaration-block-no-shorthand-property-overrides': null,
'font-family-no-missing-generic-family-keyword': null,
'no-duplicate-selectors': null,
'no-descending-specificity': null,
'selector-pseudo-element-no-unknown': null,
'scss/at-import-partial-extension': null,
'scss/comment-no-empty': null,
'scss/no-global-function-names': null,
'scss/operator-no-newline-after': null,
'scss/operator-no-unspaced': null,
'scss/function-no-unknown': null,
'scss/load-partial-extension': null,
'unit-no-unknown': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['placeholder'],
},
],
// RTL
'liberty/use-logical-spec': [
'always',
{
except: [/\btop\b/, /\bbottom\b/, /\bwidth\b/, /\bheight\b/],
},
],
'declaration-property-value-disallowed-list': {
// Use dir="ltr/rtl" instead
direction: ['ltr', 'rtl', 'auto'],
transform: [/translate3d\(/, /translateX\(/, /translate\(/],
translate: [/./],
},
},
};

View File

@ -1,54 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
// @ts-expect-error
import githubActionsFormatter from '@csstools/stylelint-formatter-github';
/** @type {import('stylelint').Config} */
const config = {
formatter: process.env.CI ? githubActionsFormatter : undefined,
extends: [
'stylelint-config-recommended-scss',
'stylelint-config-css-modules',
],
plugins: ['stylelint-use-logical-spec'],
rules: {
// Disabled from recommended set to get stylelint working initially
'at-rule-empty-line-before': null,
'block-no-empty': null,
'declaration-block-no-duplicate-properties': null,
'declaration-block-no-shorthand-property-overrides': null,
'font-family-no-missing-generic-family-keyword': null,
'no-duplicate-selectors': null,
'no-descending-specificity': null,
'selector-pseudo-element-no-unknown': null,
'scss/comment-no-empty': null,
'scss/no-global-function-names': null,
'scss/operator-no-newline-after': null,
'scss/operator-no-unspaced': null,
'scss/function-no-unknown': null,
'scss/load-partial-extension': null,
'unit-no-unknown': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['placeholder'],
},
],
// RTL
'liberty/use-logical-spec': [
'always',
{
except: [/\btop\b/, /\bbottom\b/, /\bwidth\b/, /\bheight\b/],
},
],
'declaration-property-value-disallowed-list': {
// Use dir="ltr/rtl" instead
direction: ['ltr', 'rtl', 'auto'],
transform: [/translate3d\(/, /translateX\(/, /translate\(/],
translate: [/./],
},
},
};
export default config;

File diff suppressed because it is too large Load Diff

View File

@ -5,49 +5,17 @@
## Advice for new contributors
First, there are ways to contribute that don't involve the code at all. It helps to
_start small_. Here's a list of things to consider:
Start small. The PRs most likely to be merged are the ones that make small,
easily reviewed changes with clear and specific intentions. See below for more
[guidelines on pull requests](#pull-requests).
1. Talk about Signal with your friends and family - get them to join you in using it!
1. Join the Beta and test out recently-released features before the general public gets access
1. Find and comment on duplicate GitHub issues, so we can close them
1. Determine and provide workarounds on existing GitHub issues
1. Test Signal Desktop and find reliable, well-defined reproduction steps for existing GitHub issues
1. For a given GitHub issue, test Signal iOS and/or Signal Android to see if their behavior matches Signal Desktop, and provide the details of your analysis.
It's a good idea to gauge interest in your intended work by finding the current issue
for it or creating a new one yourself. You can use also that issue as a place to signal
your intentions and get feedback from the users most likely to appreciate your changes.
If you're ready to spend some time on a GitHub issue, please consider commenting to ask us
if there's interest. That will help ensure we minimize wasted time.
### Getting into the code
Have you spent some good time with Signal Desktop and GitHub issues? You can go deeper
and get into the code itself.
Again, it helps to start small. You don't need to create a PR to contribute!
1. Find the root cause of a GitHub issue, and provide your explanation in the issue, with links to specific places in the code
1. For a particularly difficult investigation, provide your process. Talk about the changes you made, and what happened - this might be useful to others working to fix the issue.
1. Test or review the code for others' pull requests
1. When reporting any issue, also include the specific place in the code where it happens.
### Considering a Pull Request?
If you're getting more comfortable with the code, you can consider assembling a PR. We
have very high standards for the code we put into Signal Desktop, so take special care
in changing the code, adding tests, and crafting the PR summary.
Because this can take a lot of time, it's a good idea to gauge interest in your intended
changes first. Find the current GitHub issue for it or create a new one yourself, then
post about your plans. That way you'll get feedback from the users most likely to
appreciate your changes. And we may tell you not to move forward with changes, because we
have other plans for the issue itself or that area of the code.
Than, once you've spent some time planning your solution, please consider going back
to the issue and talking about your approach. We'd be happy to provide feedback.
The PRs most likely to be merged are the ones that fix issues with real user impact,
make small easily reviewed changes, and have clear and specific intentions. See below for
more [guidelines on pull requests](#pull-requests).
Once you've spent a little bit of time planning your solution, you can go
back to the issue and talk about your approach. We'd be happy to provide feedback. [An
ounce of prevention, as they say!](https://www.goodreads.com/quotes/247269-an-ounce-of-prevention-is-worth-a-pound-of-cure)
## Developer Setup
@ -84,11 +52,10 @@ Now, run these commands in your preferred terminal in a good directory for devel
```
git clone https://github.com/signalapp/Signal-Desktop.git
cd Signal-Desktop
npm install -g pnpm
pnpm install # Install and build dependencies (this will take a while)
pnpm run generate # Generate final JS and CSS assets
pnpm test # A good idea to make sure tests run first
pnpm start # Start Signal!
npm install # Install and build dependencies (this will take a while)
npm run generate # Generate final JS and CSS assets
npm test # A good idea to make sure tests run first
npm start # Start Signal!
```
You'll need to restart the application regularly to see your changes, as there
@ -98,14 +65,14 @@ is no automatic restart mechanism. Alternatively, keep the developer tools open
(Windows & Linux).
Also, note that the assets loaded by the application are not necessarily the same files
youre touching. You may not see your changes until you run `pnpm run generate` on the
youre touching. You may not see your changes until you run `npm run generate` on the
command-line like you did during setup. You can make it easier on yourself by generating
the latest built assets when you change a file. Run each of these in their own terminal
instance while you make changes - they'll run until you stop them:
```
pnpm run dev:transpile # recompiles when you change .ts files
pnpm run dev:styles # recompiles when you change .scss files
npm run dev:transpile # recompiles when you change .ts files
npm run dev:sass # recompiles when you change .scss files
```
#### Known issues
@ -154,7 +121,7 @@ You can run a development server for these parts of the app with the
following command:
```
pnpm run dev
npm run dev
```
In order for the app to make requests to the development server you must set
@ -162,7 +129,7 @@ the `SIGNAL_ENABLE_HTTP` environment variable to a truthy value. On Linux and
macOS, that simply looks like this:
```
SIGNAL_ENABLE_HTTP=1 pnpm start
SIGNAL_ENABLE_HTTP=1 npm start
```
## Setting up standalone
@ -227,7 +194,7 @@ For example, to create an 'alice' profile, put a file called `local-alice.json`
Then you can start up the application a little differently to load the profile:
```
NODE_APP_INSTANCE=alice pnpm start
NODE_APP_INSTANCE=alice npm start
```
This changes the `userData` directory from `%appData%/Signal` to `%appData%/Signal-aliceProfile`.
@ -243,25 +210,15 @@ Please write tests! Our testing framework is
[mocha](http://mochajs.org/) and our assertion library is
[chai](http://chaijs.com/api/assert/).
The easiest way to run all tests at once is `pnpm test`, which will run them on the
The easiest way to run all tests at once is `npm test`, which will run them on the
command line. You can run the client-side tests in an interactive session with
`NODE_ENV=test pnpm start`.
`NODE_ENV=test npm start`.
## Pull requests
So you wanna make a pull request?
So you wanna make a pull request? Please observe the following guidelines.
First, know that it's highly unlikely we'll accept visual changes, new strings, or really
anything that changes the user experience. Please talk to us first if you're planning
something like this! It's possible that we'll give you the okay, but very likely not.
The best changes fix bugs in our implementation of the existing user experience. For
example, it's almost certain that we'll reject anything that adds a new option.
More guidelines:
- Don't forget to sign the [CLA](https://signal.org/cla/).
- Be very sure that your `pnpm run ready` run passes - it's very similar to what our
- First, make sure that your `npm run ready` run passes - it's very similar to what our
Continuous Integration servers do to test the app.
- Please do not submit pull requests for translation fixes.
- Never use plain strings right in the source code - pull them from `messages.json`!
@ -272,7 +229,7 @@ More guidelines:
changes on the latest `main` branch, resolving any conflicts.
This ensures that your changes will merge cleanly when you open your PR.
- Be sure to add and run tests!
- Make sure the diff between the main branch and your branch contains only the
- Make sure the diff between the development branch and your branch contains only the
minimal set of changes needed to implement your feature or bugfix. This will
make it easier for the person reviewing your code to approve the changes.
Please do not submit a PR with commented out code or unfinished features.
@ -286,6 +243,7 @@ More guidelines:
link](http://chris.beams.io/posts/git-commit/)
for some tips on formatting. As far as content, try to include the following in your
summary:
1. What you changed
2. Why this change was made. If there is a relevant [GitHub Issue](https://github.com/signalapp/Signal-Desktop/issues), please include the Issue number.
3. Any relevant technical details or motivations for your implementation
@ -339,26 +297,27 @@ will go to your new development desktop app instead of your phone.
To test changes to the build system, build a release using
```
pnpm run generate
pnpm run build
npm run generate
npm run build
```
Then, run the tests using `pnpm run test-release`.
Then, run the tests using `npm run test-release`.
### Testing MacOS builds
macOS requires apps to be code signed with an Apple certificate. To test development builds
you can ad-hoc sign the packaged app which will let you run it locally.
1. Build the app while skipping the custom macOS signing script:
1. In `package.json` remove the macOS signing script: `"sign": "./ts/scripts/sign-macos.js",`
2. Build the app and ad-hoc sign the app bundle:
```
pnpm run generate
SKIP_SIGNING_SCRIPT=1 pnpm run build
npm run generate
npm run build
cd release
# Pick the desired app bundle: mac, mac-arm64, or mac-universal
cd mac-arm64
codesign --force --deep --sign - Signal.app
```
2. Now you can run the app locally.
3. Now you can run the app locally.

File diff suppressed because it is too large Load Diff

View File

@ -20,13 +20,14 @@ Please search for any [existing issues](https://github.com/signalapp/Signal-Desk
Please use our community forum: https://community.signalusers.org/
## Contributing to the project
## Contributing Code
Please see [CONTRIBUTING.md](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md). There are lots of ways to contribute - many that don't involve code!
Please see [CONTRIBUTING.md](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md)
for setup instructions and guidelines for new contributors. Don't forget to sign the [CLA](https://signal.org/cla/).
## Donate to Signal
## Contributing Funds
You can donate to Signal from inside Signal apps (Desktop, Android, or iOS), or via the web here: [Signal Technology Foundation](https://signal.org/donate). Signal is an independent 501c3 nonprofit.
You can donate to Signal development through the [Signal Technology Foundation](https://signal.org/donate), an independent 501c3 nonprofit.
## Cryptography Notice

View File

@ -1,21 +0,0 @@
Signal is 'n boodskaptoepassing wat privaatheid as kernwaarde het. Dis gratis en maklik om te gebruik, met kragtige end-tot-end-enkriptering wat jou kletse en oproepe heeltemal privaat hou. Signal kan nie jou boodskappe lees of na jou oproepe luister nie, en niemand anders kan ook nie.
• Signal in MacOS is gekoppel aan Signal op jou foon.
• Stuur end-tot-end-geënkripteerde teksboodskappe, stemboodskappe, fotos, videos, GIFs en lêers gratis.
• Bly in verbinding met groepkletse vir tot 1000 deelnemers. Beheer wie boodskappe mag plaas en bestuur groeplede met administrateurtoestemming-instellings.
• Bel jou vriende met klokhelder end-tot-end-geënkripteerde stem- en video-oproepe. Groepoproepe ondersteun tot 75 deelnemers.
• Signal is gebou vir jou privaatheid. Ons weet glad nie wie jy is of met wie jy praat nie. Ons oopbron Signal-protokol beteken dat ons nie jou boodskappe kan lees of na jou oproepe kan luister nie. En niemand anders kan ook nie. Geen agterdeure nie, geen data-insameling nie, geen kompromisse nie.
• Deel foto-, teks- en video-Stories wat ná 24 uur verdwyn. Privaatheidinstellings hou jou in beheer van presies wie elke Storie kan sien.
• Signal is onafhanklik en niewinsgewend; dit is 'n ander soort tegnologie van 'n ander soort organisasie. As 'n 501c3-niewinsgewende organisasie word ons ondersteun deur jou skenkings, nie deur adverteerders of beleggers nie.
• Vir steundiens, vrae of meer inligting, besoek asb. https://support.signal.org/
Om na ons bronkode te kyk, besoek https://github.com/signalapp
Volg ons op X @signalapp en Instagram @signal_app

View File

@ -1 +0,0 @@
signal,boodskap(per),oproep,stem,geënkripteer,privaat,veilig,privaatheid,groep,video,klets,stories

View File

@ -1 +0,0 @@
Sê “hallo” vir privaatheid.

View File

@ -1 +0,0 @@
Signal Private Messenger

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
سيجنال هو تطبيق للمراسلة قائم على مراعاة الخصوصية. هو تطبيق مجاني وسهل الاستخدام، ويُوفِّر تشفيرًا قويًا من طرف لِطرف للحفاظ على السرية التامة لمراسلاتك. لا يمكن لسيجنال قراءة رسائلك أو التنصت على مكالماتك، ولا يُمكن لأي أحد آخر القيام بذلك أيضًا.
• يتصل سيجنال على نظام macOS بسيجنال المُثبَّت على هاتفك.
• أرسِل رسائل نصيّة ورسائل صوتية وصور وفيديوهات وصورة متحركة وملفات مُشفَّرة من طرف لطرف بالمجان.
• ابقَ على اتصال مع دردشات جماعية تجمع أكثر من 1000 شخص. تحكَّم في من يُمكنه النشر والقيام بإدارة أعضاء المجموعة باستخدام إعدادات أذونات المُشرِف.
• أجرِ مُكالمات صوتية ومكالمات فيديو مُشفَّرة بجودة عالية مع أصدقائك. تدعم المكالمات الجماعية ما يصل إلى 75 شخصًا.
صُمِّمَ سيجنال للحفاظ على خصوصيتك. لا نعرف شيئًا عنك أو مع من تتحدث. بروتوكول سيجنال الخاص بنا ذو المصدر المفتوح يعني أنه لا يُمكننا قراءة رسائلك أو الاستماع إلى مكالماتك. ولا يُمكن لأي شخص آخر القيام بذلك. لا أبواب خلفية ولا عملية جمع بيانات ولا مساومات.
• شارِك الصور والرسائل وقِصص الفيديو التي تختفي بعد 24 ساعة. تُتيح لك إعدادات الخصوصية فرصة البقاء مسؤولًا عن من يُمكنه رؤية كل قصة.
• تطبيق سيجنال مُستقل ولا يهدف إلى الربح؛ نوع مختلف من التكنولوجيا من نوع مختلف من المنظمات. وبصفتنا مؤسسة غير ربحية، فإننا نستمد دعمنا من التبرُّعات وليس من الإعلانات ولا المُستثمرين.
• للدعم أو لطرح الأسئلة أو للمزيد من المعلومات، يُرجى زيارة https://support.signal.org/
لإلقاء نظرة على كود المصدر الخاص بنا، قُم بزيارة https://github.com/signalapp
تابعنا على X على العنوان @signalapp وعلى انستغرام على العنوان @signal_app

View File

@ -1 +0,0 @@
سيجنال،رسالة،تطبيق،دردشة،مكالمة،صوت،مشفر،خاص،آمن،خصوصية،مجموعة،فيديو،دردشة،قصص

View File

@ -1 +0,0 @@
مرحبًا بكم في عالم الخصوصية.

View File

@ -1 +0,0 @@
سيجنال - تطبيق مراسلة يحترم الخصوصية

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
Signal məxfiliyə əsaslanan bir mesajlaşma tətbiqidir. Çatlar və zənglərinizin məxfiliyini tamamilə qoruyan güclü tam şifrələmə xüsusiyyəti ilə ondan istifadə pulsuz və asandır. Signal mesajlarınızı oxuya və ya zənglərinizi dinləyə bilmir, digərlərinə də bunu etməyə icazə vermir.
• MacOS ilə işləyən Signal telefonunuzdakı Signal tətbiqi ilə əlaqələndirilir.
• Tam şifrələnmiş mətnləri, səsli mesajları, fotoları, videoları, GIF və faylları pulsuz göndərin.
• 1000 nəfərə qədər iştirakçını dəstəkləyən qrup çatlarına qoşulun. Admin icazəsi parametrləri ilə kimin qrup üzvləri ilə yazı paylaşacağına və onları idarə edəcəyinə nəzarət edin.
• Dostlarınıza tam şifrələnmiş yüksək səs keyfiyyətinə malik audio və video zənglər edin. Qrup zəngləri 75 nəfərə qədər insanın qoşulmasını dəstəkləyir.
• Signal məxfiliyinizi qorumaq üçün yaradılıb. Siz və ya söhbət etdiyiniz şəxslər haqqında heç nə bilmirik. Açıq mənbəli Signal Protokolumuz mesajlarınızı oxuya və zənglərinizi dinləyə bilməyəcəyimizi nəzərdə tutur. Bunu nə biz, nə də başqası edə bilməz. Arxa qapılar, verilənlərin toplanması və kompromislər yoxdur.
• 24 saat sonra yox olacaq foto, mətn və video Hekayələri paylaşın. Məxfilik parametrləri sayəsində hər bir Hekayəni kimin görə biləcəyini dəqiqliklə seçirsiniz.
• Signal müstəqildir və mənfəət məqsədi güdmür, o, fərqli bir təşkilatın fərq yaradan bir texnologiyasıdır. Bir 501c3 qeyri-kommersiya təşkilatı kimi biz, reklamçı və investorların deyil, sizin ianələrinizlə dəstəklənirik.
• Dəstək, sual və əlavə məlumat üçün https://support.signal.org/ keçidinə daxil olun
Mənbə kodumuzu yoxlamaq üçün https://github.com/signalapp keçidinə daxil olun
Bizi X-də @signalapp, Instagram-da isə @signal_app istifadəçi profilləri ilə izləyin

View File

@ -1 +0,0 @@
signal,mesaj,messenger,zəng,səsli,şifrələnmiş,şəxsi,təhlükəsiz,məxfilik,qrup,video,çat,hekayələr

View File

@ -1 +0,0 @@
Məxfiliyə "Salam" verin.

View File

@ -1 +0,0 @@
Signal Gizli Mesajlaşma

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
Signal е приложение за съобщения, изградено около поверителността. То е безплатно и лесно за използване, със силно криптиране от край до край, което поддържа вашите чатове и обаждания напълно поверителни. Signal не може да чете вашите съобщения или да слуша вашите обаждания, а и никой друг не може.
• Signal на MacOS се свързва със Signal на телефона ви.
• Изпращайте криптирани от край до край текстови и гласови съобщения, снимки, видеоклипове, GIF-ове и файлове безплатно.
• Поддържайте връзка с групови чатове до 1000 души. Контролирайте кой може да публикува и управлява членовете на групата с настройките за администраторски разрешения.
• Обаждайте се на приятелите си с кристално чисти криптирани от край до край гласови и видео обаждания. Поддържат се групови разговори до 75 души.
• Signal е създаден за вашата поверителност. Не знаем нищо за вас или хората, с които говорите. Нашият Signal протокол с отворен код означава, че не можем да четем вашите съобщения или да слушаме вашите обаждания. И никой друг не може. Без задни вратички, без събиране на данни, без компромиси.
• Споделяйте снимки, текст и видео истории, които изчезват след 24 часа. Настройките за поверителност ви позволяват да контролирате точно кой може да вижда всяка история.
• Signal е независим и не е с цел печалба; различен вид технология от различен вид организация. Като организация с нестопанска цел, ние се издържаме от вашите дарения, а не от рекламодатели или инвеститори.
За поддръжка, въпроси или повече информация, моля посетете https://support.signal.org/
За да разгледате нашия изходен код, посетете https://github.com/signalapp
Последвайте ни в X @signalapp и Instagram @signal_app

View File

@ -1 +0,0 @@
signal,съобщения,обаждане,глас,криптиран,поверителен,сигурно,поверителност,група,видео,чат,истории

View File

@ -1 +0,0 @@
Поверителността преди всичко.

View File

@ -1 +0,0 @@
Signal - Private Messenger

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
Signal একটি মেসেজিং অ্যাপ যার মূল ভিত্তি হলো গোপনীয়তা। এটি ফ্রি-তে ব্যবহার করা যায় এবং ব্যবহার করা সহজ, এতে রয়েছে শক্তিশালী এন্ড-টু-এন্ড এনক্রিপশন ব্যবস্থা যা আপনার চ্যাট ও কল সম্পূর্ণ গোপন রাখে। Signal আপনার মেসেজ পড়তে বা আপনার কল শুনতে পারে না, এবং অন্য কেউই তা পারে না।
• MacOS-এর Signal আপনার ফোনের Signal-এর সাথে সংযুক্ত হয়।
• ফ্রি-তে এন্ড-টু-এন্ড এনক্রিপ্ট করা টেক্সট, ভয়েস মেসেজ, ছবি, ভিডিও, GIF ও ফাইল পাঠান।
• 1,000 জন পর্যন্ত ব্যক্তি নিয়ে সংগঠিত গ্রুপ চ্যাটের মাধ্যমে সংযুক্ত থাকুন সবার সাথে। অ্যাডমিন অনুমতির সেটিংস সহ গ্রুপ সদস্যদের মধ্যে কে কে পোস্ট করতে পারবেন এবং কে কে নিয়ন্ত্রণ করতে পারবেন তা নিয়ন্ত্রণ করুন।
• আপনার বন্ধুদের সাথে এন্ড-টু-এন্ড এনক্রিপ্ট করা ভয়েস ও ভিডিও কলের মাধ্যমে কল করুন। গ্ৰুপ কলে একসাথে সর্বোচ্চ 75 জন যোগ দিতে পারে।
• Signal আপনার গোপনীয়তা রক্ষা করার জন্য তৈরি করা হয়েছে। আমরা আপনার সম্পর্কে এবং আপনি কার সাথে কথা বলছেন তার সম্পর্কে কিছুই জানি না। আমাদের ওপেন সোর্স Signal Protocol-এর অর্থ হলো আমরা আপনার ম্যাসেজ পড়তে বা আপনার কল থেকে কথা শুনতে পারি না। এটি অন্য আর কেউও পারে না। নেই কোনো অসৎ উদ্দেশ্য, নেই কোনো তথ্য সংগ্রহের চর্চা, নেই কোনো আপোষ।
• ছবি, টেক্সট ও ভিডিও স্টোরি শেয়ার করা যায়, যা 24 ঘন্টা পরে অদৃশ্য হয়ে যায়। গোপনীয়তা সেটিংস আপনাকে প্রত্যেকটি স্টোরি কে দেখতে পাবেন তা নিয়ন্ত্রণ করার সুযোগ দেয়।
• Signal একটি স্বাধীন এবং অলাভজনক উদ্যোগ; একটি ভিন্নধর্মী প্রতিষ্ঠানের প্রচেষ্টায় তৈরি একটি ভিন্নধর্মী প্রযুক্তি। একটি 501c3 অলাভজনক প্রতিষ্ঠান হিসাবে আমরা আপনার দেওয়া ডোনেশনের সমর্থনে পরিচালিত, কোনো বিজ্ঞাপনদাতা বা বিনিয়োগকারীর দ্বারা সমর্থিত নয়।
• এ সংক্রান্ত সহায়তা, প্রশ্ন বা আরো তথ্যের জন্য অনুগ্রহ করে ভিজিট করুন: https://support.signal.org/
আমাদের সোর্স কোড চেক করতে, ভিজিট করুন: https://github.com/signalapp
X-এ @signalapp পেজে এবং Instagram-এ @signal_app পেজে আমাদের ফলো করুন

View File

@ -1 +0,0 @@
signal,মেসেজ,মেসেঞ্জার,কল,ভয়েস,এনক্রিপ্ট করা,ব্যক্তিগত,সুরক্ষিত,গোপনীয়তা,গ্ৰুপ,ভিডিও,চ্যাট,স্টোরি

View File

@ -1 +0,0 @@
গোপনীয়তাকে “হ্যালো” বলুন।

View File

@ -1 +0,0 @@
Signal - প্রাইভেট মেসেঞ্জার

File diff suppressed because it is too large Load Diff

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