Compare commits

..

9 Commits
main ... 5.5.x

Author SHA1 Message Date
Scott Nonnenberg
5577335522 v5.5.0
Some checks failed
Benchmark / linux (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / macos (push) Has been cancelled
CI / linux (push) Has been cancelled
CI / windows (push) Has been cancelled
2021-06-15 17:37:51 -07:00
Scott Nonnenberg
b551fbaff2 Update strings for v5.5.0 2021-06-15 17:24:28 -07:00
automated-signal
dc498a7884
Ensure we always set expiration timer
Co-authored-by: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>
2021-06-15 17:14:32 -07:00
automated-signal
6505b58d52
Ensure that typing indicators are always sent with online = true
Co-authored-by: Scott Nonnenberg <scott@signal.org>
2021-06-15 17:14:16 -07:00
automated-signal
c18d1c402e
Fix syncing of "Off" setting for default timer
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2021-06-15 16:08:21 -07:00
automated-signal
942fe75407
Log conversation job wait time and duration
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2021-06-15 10:07:32 -07:00
automated-signal
0d05b760c1
Wrap all websocket errors, reconnect earlier
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2021-06-15 10:07:16 -07:00
automated-signal
9b7cc59519
On right-click link in message bubble, show menu to copy
Co-authored-by: Jordan Rose <jrose@signal.org>
2021-06-14 13:22:36 -07:00
automated-signal
60aaf4e451
Improve menu bar on macOS
Co-authored-by: Jordan Rose <jrose@signal.org>
2021-06-14 13:22:17 -07:00
5473 changed files with 688910 additions and 1268360 deletions

12
.aptly.conf Normal file
View File

@ -0,0 +1,12 @@
{
"S3PublishEndpoints": {
"signal-desktop-apt": {
"region": "us-east-1",
"bucket": "updates.signal.org",
"prefix": "desktop/apt",
"acl": "public-read",
"plusWorkaround": false,
"disableMultiDel": false
}
}
}

19
.babelrc.js Normal file
View File

@ -0,0 +1,19 @@
// Copyright 2019-2021 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-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
// 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;

34
.eslintignore Normal file
View File

@ -0,0 +1,34 @@
build/**
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
sticker-creator/dist/**
# Third-party files
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js
js/libphonenumber-util.js
libtextsecure/test/blanket_mocha.js
test/blanket_mocha.js
# TypeScript generated files
ts/**/*.js
sticker-creator/**/*.js
!sticker-creator/preload.js
**/*.d.ts
.eslintrc.js
webpack.config.ts
preload.bundle.*

196
.eslintrc.js Normal file
View File

@ -0,0 +1,196 @@
// Copyright 2018-2021 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',
},
],
// 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'],
'no-shadow': 'off',
'no-useless-constructor': 'off',
// 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',
// useful for unused or internal fields
'no-underscore-dangle': 'off',
// useful for unused parameters
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// 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',
// 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',
'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
// Upgrade from a warning
'@typescript-eslint/explicit-module-boundary-types': '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: 'ForOfStatement',
message:
'iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.',
},
{
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.',
},
],
curly: 'error',
};
module.exports = {
root: true,
settings: {
react: {
version: 'detect',
},
'import/core-modules': ['electron'],
},
extends: ['airbnb-base', 'prettier'],
plugins: ['mocha', 'more'],
overrides: [
{
files: ['ts/**/*.ts', 'ts/**/*.tsx'],
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,
},
{
files: ['sticker-creator/**/*.ts', 'sticker-creator/**/*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './sticker-creator/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,
},
{
files: ['**/*.stories.tsx', 'ts/build/**', 'ts/test-*/**'],
rules: {
...rules,
'import/no-extraneous-dependencies': 'off',
'react/no-array-index-key': 'off',
},
},
],
rules,
};

82
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,82 @@
---
name: 🛠️ Bug report
about: Create a report about a technical issue
title: ''
labels: ''
assignees: ''
---
<!--
Our bug tracker is ONLY for bugs. It is not for feature requests, questions, or comments.
Please fill out this template with all the information you have. We can't do much without
both the logs and a detailed description of what you've encountered. Please do your best!
Please note that this tracker is only for bugs. Please try these locations if you have a question or comment:
https://community.signalusers.org/
http://support.signal.org/
support@signal.org
Lastly, be sure to preview your issue before saving. Thanks!
-->
- [ ] I have searched open and closed issues for duplicates
<!--
You can search all issues here:
https://github.com/signalapp/Signal-Desktop/issues?utf8=%E2%9C%93&q=is%3Aissue
Replace [ ] with [X] once you've searched
-->
---
### Bug Description
<!-- Give an overall summary of the issue. -->
### Steps to Reproduce
<!-- Using bullet points, list the steps that reproduce the bug. -->
1. step one
2. step two
3. step three
Actual Result:
<!-- Describe the details of the buggy behaviour. -->
Expected Result:
<!-- Describe in detail what the correct behavior should be. -->
### Screenshots
<!--
How to take screenshots on all OSes: https://www.take-a-screenshot.org/
You can drag and drop images into this text box.
-->
### Platform Info
Signal Version:
<!-- You can see Signal's version number at Help -> About or File -> About Signal Desktop -->
Operating System:
<!-- Instructions for finding your OS version are here: http://whatsmyos.com/ -->
Linked Device Version:
<!-- Android: Settings -> Advanced, iOS: Settings -> General -> About -->
### Link to Debug Log
<!--
Immediately after the bug has happened, submit a debug log via View -> Debug Log, then copy that URL here.
In most cases, a log from your other devices is also useful:
Android: https://support.signal.org/hc/en-us/articles/360007318591#android_debug
iOS: https://support.signal.org/hc/en-us/articles/360007318591#ios_debug
-->

View File

@ -1,115 +0,0 @@
name: 🛠️ Bug report
description: Create a report about a technical issue
body:
- type: markdown
attributes:
value: |
Our bug tracker is ONLY for bugs. It is not for feature requests, questions, or comments.
Please try these locations if you have a question or comment:
https://community.signalusers.org/
http://support.signal.org/
support@signal.org
Please fill out this template with all the information you have. We can't do much without
both the logs and a detailed description of what you've encountered. Please do your best!
- type: checkboxes
id: supported-version-and-no-duplicates
attributes:
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.
required: true
- label: I am using Signal-Desktop as provided by the Signal team, not a 3rd-party package.
required: true
- type: markdown
attributes:
value: |
If you're using a 3rd-party package, please report it to them first.
For Flatpak: https://github.com/flathub/org.signal.Signal/issues
For Snap: https://github.com/snapcrafters/signal-desktop/issues
For Arch Linux: https://aur.archlinux.org/packages/signal-desktop-beta/
etc.
- type: textarea
id: description-of-bug
attributes:
label: Overall summary
description: A clear and concise description of what the problem is that made you submit this report.
placeholder:
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: List the steps to reproduce the issue you are encountering.
placeholder: |
1. Go to any chat.
2. Do thing A.
3. Do thing B.
4. Things look off because...
validations:
required: true
- type: textarea
id: expected-result
attributes:
label: Expected result
description: Describe in detail what the correct behavior should be.
placeholder:
validations:
required: true
- type: textarea
id: actual-result
attributes:
label: Actual result
description: Describe the details of the buggy behavior. Please include all error messages!
placeholder:
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: "How to take screenshots on all Operating Systems: https://www.take-a-screenshot.org/"
placeholder: You can drag and drop images into this text box.
validations:
required: false
- type: input
id: signal-version
attributes:
label: Signal version
description: You can see Signal's version number on the About screen (available from the File, Help, or Signal menu depending on your OS)
placeholder:
validations:
required: true
- type: input
id: os
attributes:
label: Operating system
description: "Instructions for finding your OS version are here: http://whatsmyos.com/"
placeholder: Windows 11
validations:
required: true
- type: input
id: primary-device
attributes:
label: Version of Signal on your phone
description: "Settings->Help"
placeholder:
validations:
required: false
- type: textarea
id: debug-log
attributes:
label: Link to debug log
description: |
Immediately after the bug has happened, submit a debug log via View->Debug Log, then copy that URL here.
In most cases, a log from your other devices is also useful:
Android: https://support.signal.org/hc/en-us/articles/360007318591#android_debug
iOS: https://support.signal.org/hc/en-us/articles/360007318591#ios_debug
placeholder:
validations:
required: false

View File

@ -1,6 +1,3 @@
# Copyright 2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
blank_issues_enabled: false
contact_links:
- name: ✨ Feature request
@ -16,7 +13,7 @@ contact_links:
url: https://community.signalusers.org/c/support/
about: Feel free to ask anything
- name: 📖 Contribution instructions
url: https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md
url: https://github.com/signalapp/Signal-Desktop/blob/development/CONTRIBUTING.md
about: Want to contribute to Signal Desktop? Start here.
- name: ❓ Other issue?
url: https://community.signalusers.org/

View File

@ -9,15 +9,15 @@ Remember, you can preview this before saving it.
### First time contributor checklist:
- [ ] I have read the [README](https://github.com/signalapp/Signal-Desktop/blob/main/README.md) and [Contributor Guidelines](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md)
- [ ] I have read the [README](https://github.com/signalapp/Signal-Desktop/blob/master/README.md) and [Contributor Guidelines](https://github.com/signalapp/Signal-Desktop/blob/master/CONTRIBUTING.md)
- [ ] I have signed the [Contributor Licence Agreement](https://signal.org/cla/)
### Contributor checklist:
- [ ] My contribution is **not** related to translations.
- [ ] My contribution is **not** related to translations. _Please submit translation changes via our [Signal Desktop Transifex project](https://www.transifex.com/signalapp/signal-desktop/)._
- [ ] 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))
- [ ] My changes are [rebased](https://medium.freecodecamp.org/git-rebase-and-the-golden-rule-explained-70715eccc372) on the latest [`development`](https://github.com/signalapp/Signal-Desktop/tree/development) branch
- [ ] A `yarn ready` run passes successfully ([more about tests here](https://github.com/signalapp/Signal-Desktop/blob/master/CONTRIBUTING.md#tests))
- [ ] My changes are ready to be shipped to users
### Description

View File

@ -1,21 +0,0 @@
# Copyright 2024 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
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:
- "/"
schedule:
interval: daily
allow:
- dependency-name: electron

70
.github/stale.yml vendored
View File

@ -1,70 +0,0 @@
# Copyright 2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- Accessibility
- Bug
- Regression
- "Don't mark stale"
- "Feature Request"
- "Good starter task"
- "Upstream Change Needed"
- "PR: Needs Review"
- "PR: Ready to Merge"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been closed due to inactivity.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 5
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed

View File

@ -1,3 +1,4 @@
# Copyright 2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
@ -6,7 +7,7 @@ on:
issue_comment:
types: [created]
pull_request:
types: [opened, labeled, unlabeled, closed]
types: [closed]
jobs:
backport:
@ -15,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v2
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
repository: signalapp/Signal-Backport-Action-Private

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

@ -0,0 +1,91 @@
# Copyright 2020-2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Benchmark
on: push
jobs:
linux:
runs-on: ubuntu-latest
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
steps:
- name: Get system specs
run: lsb_release -a
- name: Get other system specs
run: uname -a
- name: Clone Desktop repo
uses: actions/checkout@v2
- name: Clone Mock-Server repo
uses: actions/checkout@v2
with:
repository: 'signalapp/Mock-Signal-Server-Private'
path: 'Mock-Server'
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
- name: Setup node.js
uses: actions/setup-node@v2
with:
node-version: '14.16.0'
- name: Install global dependencies
run: npm install -g yarn@1.22.10 ts-node
- name: Install xvfb
run: sudo apt-get install xvfb
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('yarn.lock', 'patches/**') }}
- name: Install Desktop node_modules
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Install Mock-Server node_modules
run: npm ci
working-directory: Mock-Server
- name: Build typescript
run: yarn grunt
- name: Bundle
run: yarn build:webpack
- name: Copy CI configuration
run: cp -rf ./Mock-Server/config/local-development.json ./config/
- name: Setup hosts
run: sudo echo "127.0.0.1 mock.signal.org" | sudo tee -a /etc/hosts
- name: Run benchmarks
run: |
xvfb-run --auto-servernum \
ts-node Mock-Server/scripts/load-test.ts ./node_modules/.bin/electron . | tee benchmark.log || \
(cat /home/runner/.config/Signal-mock/logs/{app,main}.log && exit 1)
timeout-minutes: 10
env:
RUN_COUNT: 10
ELECTRON_ENABLE_STACK_DUMPING: on
- name: Clone benchmark branch
uses: actions/checkout@v2
with:
repository: 'signalapp/Signal-Desktop-Benchmarks-Private'
path: 'benchmark-results'
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
- name: Push benchmark branch
working-directory: benchmark-results
run: |
npm ci
node ./bin/collect.js ../benchmark.log data.json
npm run build
git config --global user.email "no-reply@signal.org"
git config --global user.name "Signal Bot"
git add .
git commit --message "${GITHUB_REF} ${GITHUB_SHA}"
git push --force origin main

View File

@ -1,786 +1,139 @@
# Copyright 2020 Signal Messenger, LLC
# Copyright 2020-2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: CI
on:
push:
branches:
- development
- main
- '[0-9]+.[0-9]+.x'
pull_request:
permissions:
contents: read
on: [push, pull_request]
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
runs-on: ubuntu-latest
steps:
- run: lsb_release -a
- 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
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
node-version: '14.16.0'
- run: npm install -g yarn@1.22.10
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v2
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/**') }}
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('yarn.lock', '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
- name: Install Sticker Creator node_modules
run: pnpm install
working-directory: sticker-creator
- 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
env:
REQUIRE_SIGNAL_LIB_FILES: 1
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- run: yarn generate
- run: yarn lint
- run: yarn lint-deps
# - run: yarn lint-license-comments
- run: git diff --exit-code
macos:
name: MacOS
needs: lint
runs-on: macos-26-arm64
if: github.ref == 'refs/heads/main'
timeout-minutes: 30
runs-on: macos-latest
if: github.ref == 'refs/heads/development' || github.ref == 'refs/heads/master'
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
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
node-version: '14.16.0'
- run: npm install -g yarn@1.22.10
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v2
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/**') }}
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('yarn.lock', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
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
env:
ARTIFACTS_DIR: artifacts/macos
WORKER_COUNT: 4
timeout-minutes: 5
- run: touch noop.sh && chmod +x noop.sh
- run: pnpm 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
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- run: yarn generate
- run: yarn prepare-beta-build
- run: yarn build
- run: yarn test-node
- run: yarn test-electron
- run: yarn grunt test-release:osx
env:
NODE_ENV: production
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
path: artifacts
linux:
name: Linux
needs: lint
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
matrix:
include:
- os: ubuntu-22.04-8-cores
arch: x64
- os: ubuntu-22.04-arm64-4-cores
arch: arm64
runs-on: ubuntu-latest
steps:
- run: lsb_release -a
- 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
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Cache .electron-gyp
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
node-version: '14.16.0'
- run: sudo apt-get install xvfb
- run: npm install -g yarn@1.22.10
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v2
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('yarn.lock', 'patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- run: yarn generate
- run: yarn prepare-beta-build
- run: yarn build
- run: xvfb-run --auto-servernum yarn test-node
- run: xvfb-run --auto-servernum yarn test-electron
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
- name: Create preload cache
run: xvfb-run --auto-servernum pnpm 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
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
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: xvfb-run --auto-servernum pnpm run test-node
- name: Clone backup integration tests
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: 'signalapp/Signal-Message-Backup-Tests'
ref: 'a0f900243210efbedc72f0907c5d2f140385daa4'
path: 'backup-integration-tests'
- run: xvfb-run --auto-servernum pnpm 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 yarn grunt test-release:linux
env:
NODE_ENV: production
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
path: artifacts
windows:
name: Windows
needs: lint
runs-on: windows-latest-8-cores
timeout-minutes: 30
runs-on: windows-latest
steps:
- run: systeminfo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- run: git config --global core.autocrlf false
- run: git config --global core.eol lf
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
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') }}
node-version: '14.16.0'
- run: npm install -g yarn@1.22.10
- run: touch noop.js
- name: Cache Desktop node_modules
id: cache-desktop-modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}-${{ hashFiles('patches/**') }}
- name: Install Desktop node_modules
run: pnpm install
env:
NPM_CONFIG_LOGLEVEL: verbose
NPM_CONFIG_NODE_GYP: ${{ github.workspace }}\noop.js
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- run: yarn generate
- run: node build\grunt.js
- run: yarn 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
- name: Create preload cache
run: pnpm run build:preload-cache
env:
ARTIFACTS_DIR: artifacts/win
- name: Build with NSIS
run: pnpm 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
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: pnpm run test-electron
env:
ARTIFACTS_DIR: artifacts/windows
WORKER_COUNT: 4
timeout-minutes: 5
- run: pnpm run test-release
- run: yarn prepare-beta-build
- run: yarn build
- run: node build\grunt.js test
- run: node build\grunt.js test-release:win
env:
SIGNAL_ENV: production
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
path: artifacts
sticker-creator:
name: Sticker Creator
runs-on: ubuntu-22.04-8-cores
timeout-minutes: 30
defaults:
run:
working-directory: sticker-creator
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'
- name: Install Sticker Creator node_modules
run: pnpm install
- name: Build Sticker Creator
run: pnpm run build
- name: Check Sticker Creator types
run: pnpm run check:types
- name: Check Sticker Creator formatting
run: pnpm run prettier:check
- name: Check Sticker Creator linting
run: pnpm run lint
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
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 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
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 xvfb and libpulse0
run: sudo apt-get install xvfb libpulse0 || (sudo apt-get update && sudo apt-get install xvfb libpulse0)
- 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
- name: Build typescript
run: pnpm run generate
- name: Create preload cache
run: xvfb-run --auto-servernum pnpm run build:preload-cache
- name: Set MAX_CYCLES=2 on main
if: ${{ github.ref == 'refs/heads/main' }}
run: |
echo "MAX_CYCLES=2" >> "$GITHUB_ENV"
- name: Run ${{ matrix.metric }}
run: |
set -o pipefail
xvfb-run --auto-servernum ./node_modules/.bin/tsx \
${{ matrix.script }} | tee benchmark.log
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 }}
- name: Upload benchmark logs on failure
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
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

@ -1,20 +0,0 @@
# Copyright 2023 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Commits Check
on:
push:
branches:
- main
- '[0-9]+.[0-9]+.x'
jobs:
linux:
name: Commit Title Check
runs-on: ubuntu-latest
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
steps:
- uses: signalapp/Signal-Check-Commits-Action-Private@main
with:
commit-pattern: "\\(#\\d{1,}\\)" # i.e. "Example commit message (#1234)"
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-title: Pushed to "${{ github.ref_name }}" with issue references

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

@ -1,26 +0,0 @@
# Copyright 2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Notes
on:
pull_request:
types: [closed]
jobs:
backport:
name: Add notes to merge commits
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
repository: signalapp/Signal-Notes-Action-Private
path: ./.github/actions/note
- name: Run action
uses: ./.github/actions/note
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
git-email: note-bot@signal.org
git-name: Note Bot

View File

@ -1,28 +0,0 @@
# Copyright 2024 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Release Notes
on:
issue_comment:
types: [created]
issues:
types: [opened]
pull_request:
types: [opened, labeled, unlabeled, closed]
jobs:
backport:
name: Update release notes issue
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
repository: signalapp/Signal-Release-Notes-Action-Private
path: ./.github/actions/release-notes
- name: Run action
uses: ./.github/actions/release-notes
with:
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}

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 }}"]}'

28
.github/workflows/snyk.yml vendored Normal file
View File

@ -0,0 +1,28 @@
# Copyright 2020-2021 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
name: Snyk
on:
schedule:
- cron: '0 12 * * *'
jobs:
snyk:
runs-on: ubuntu-latest
if: github.repository != 'signalapp/Signal-Desktop'
steps:
- run: lsb_release -a
- run: uname -a
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.16.0'
- run: npm install -g yarn@1.22.10
- run: npm install -g snyk@1.316.1
- run: yarn install --frozen-lockfile
- run: snyk auth "$SNYK_TOKEN"
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- run: snyk test --show-vulnerable-paths=all

37
.gitignore vendored
View File

@ -1,51 +1,42 @@
node_modules
node_modules_bkp
.sass-cache
coverage/*
build/curve25519_compiled.js
build/compact-locales
build/*.policy
build/emoji-data.json
stylesheets/*.css.map
/dist
.DS_Store
config/local.json
config/local-*
config/local-*.json
*.provisionprofile
release/
/dev-app-update.yml
.nyc_output/
*.sublime*
/sql/
/start.sh
.stylelintcache
.eslintcache
tsconfig.tsbuildinfo
.smartling-source.sh
# generated files
js/components.js
js/util_worker.js
libtextsecure/components.js
libtextsecure/test/test.js
stylesheets/*.css
!stylesheets/tailwind-config.css
/storybook-static/
preload.bundle.*
preload.wrapper.js
bundles/
ts/sql/mainWorker.bundle.js.LICENSE.txt
build/ICUMessageParams.d.ts
test/test.js
# React / TypeScript
build/**/*.js
app/*.js
ts/**/*.js
!ts/windows/main/tsx.js
ts/protobuf/*.d.ts
sticker-creator/**/*.js
# CSS Modules
**/*.scss.d.ts
# Editors
# Sticker Creator
sticker-creator/dist/*
/.idea
/.vscode
/.zed
*.sublime*
*.map
/storybook-static/
preload.bundle.*
ts/sql/mainWorker.bundle.js.LICENSE.txt

View File

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

2
.nvmrc
View File

@ -1 +1 @@
24.15.0
14.16.0

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,48 +2,43 @@
# 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/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
sticker-creator/dist/**
# 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
libtextsecure/test/blanket_mocha.js
test/blanket_mocha.js
# Assets
/images/
/fixtures/
# Version control
**/.git
# Test fixtures
test/fixtures.js
# Github workflows
.github/**
# Managed by Transifex:
# Note: the negate pattern only works because it's targeting the same depth as the
# glob on the previous line.
_locales/**/*.json
!_locales/en/messages.json
# Managed by package manager (`bower` and `yarn`/`npm`):
/bower.json
/package.json
@ -52,8 +47,3 @@ scripts/emoji-datasource/emoji-datasource.json
stylesheets/_intlTelInput.scss
preload.bundle.*
preload.wrapper.js
bundles/**
# Sticker Creator has its own prettier config
sticker-creator/**

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

View File

@ -1,9 +0,0 @@
#!/bin/bash
# Copyright 2022 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
# run this before yarn get-strings/push-strings:
# source .smartling-source.sh
export SMARTLING_USER="your token 'user identifier' here"
export SMARTLING_SECRET="your token secret here"

View File

@ -1,7 +0,0 @@
# Copyright 2022 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
# https://github.com/Smartling/smartling-cli/wiki/examples.md
account_id: '92ff14ad'
project_id: 'ef62d1ebb'

184
.snyk Normal file
View File

@ -0,0 +1,184 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.13.5
ignore: {}
patch:
'npm:moment:20170905':
- bunyan > moment:
patched: '2020-04-30T19:26:25.236Z'
'npm:debug:20170905':
- websocket > debug:
patched: '2020-04-30T19:26:25.236Z'
- ref-array-napi > array-index > debug:
patched: '2020-04-30T19:26:25.236Z'
- babel-template > babel-traverse > debug:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-traverse > debug:
patched: '2020-04-30T19:26:25.236Z'
- zkgroup > ref-array-napi > array-index > debug:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > babel-traverse > debug:
patched: '2020-04-30T19:26:25.236Z'
- array-index > debug:
patched: '2020-04-30T19:26:25.236Z'
SNYK-JS-LODASH-450202:
- babel-template > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-generator > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-template > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-generator > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-template > babel-traverse > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-traverse > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-generator > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-template > babel-traverse > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-traverse > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > babel-traverse > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > babel-traverse > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- lodash:
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/generator > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/generator > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/generator > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/generator > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-function-name > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/generator > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/generator > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-function-name > @babel/helper-get-function-arity > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-function-name > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@emotion/core > @emotion/css > babel-plugin-emotion > @babel/helper-module-imports > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-function-name > @babel/helper-get-function-arity > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- babel-generator > lodash:
patched: '2020-04-30T19:26:25.236Z'
SNYK-JS-LODASH-567746:
- babel-template > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-generator > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-template > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-generator > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-template > babel-traverse > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-traverse > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-generator > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- babel-template > babel-traverse > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-traverse > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > babel-traverse > lodash:
patched: '2020-04-30T19:26:25.236Z'
- istanbul-lib-instrument > babel-template > babel-traverse > babel-types > lodash:
patched: '2020-04-30T19:26:25.236Z'
- lodash:
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/generator > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/generator > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/generator > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/generator > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-function-name > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/generator > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/generator > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/traverse > @babel/helper-function-name > @babel/helper-get-function-arity > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-function-name > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@emotion/core > @emotion/css > babel-plugin-emotion > @babel/helper-module-imports > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- '@babel/core > @babel/helpers > @babel/traverse > @babel/helper-function-name > @babel/helper-get-function-arity > @babel/types > lodash':
patched: '2020-04-30T19:26:25.236Z'
- babel-generator > lodash:
patched: '2020-04-30T19:26:25.236Z'

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);

5
.storybook/addons.js Normal file
View File

@ -0,0 +1,5 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import '@storybook/addon-knobs/register';
import '@storybook/addon-actions/register';

121
.storybook/config.js Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { addDecorator, addParameters, configure } from '@storybook/react';
import { withKnobs, boolean, optionsKnob } from '@storybook/addon-knobs';
import classnames from 'classnames';
import * as styles from './styles.scss';
import messages from '../_locales/en/messages.json';
import { I18n } from '../sticker-creator/util/i18n';
import { ClassyProvider } from '../ts/components/PopperRootContext';
const optionsConfig = {
display: 'inline-radio',
};
const persistKnob = id => knob => {
const value = knob(localStorage.getItem(id));
localStorage.setItem(id, value);
return value;
};
const makeThemeKnob = pane =>
persistKnob(`${pane}-pane-theme`)(localValue =>
optionsKnob(
`${pane} Pane Theme`,
{ Light: '', Dark: classnames('dark-theme', styles.darkTheme) },
localValue || '',
optionsConfig,
`${pane} Pane`
)
);
const makeModeKnob = pane =>
persistKnob(`${pane}-pane-mode`)(localValue =>
optionsKnob(
`${pane} Pane Mode`,
{ Mouse: 'mouse-mode', Keyboard: 'keyboard-mode' },
localValue || 'mouse-mode',
optionsConfig,
`${pane} Pane`
)
);
addDecorator(withKnobs);
addDecorator((storyFn /* , context */) => {
const contents = storyFn();
const firstPaneTheme = makeThemeKnob('First');
const firstPaneMode = makeModeKnob('First');
const secondPane = persistKnob('second-pane-active')(localValue =>
boolean('Second Pane Active', localValue !== 'false', 'Second Pane')
);
const secondPaneTheme = makeThemeKnob('Second');
const secondPaneMode = makeModeKnob('Second');
// Adding it to the body as well so that we can cover modals and other
// components that are rendered outside of this decorator container
if (firstPaneTheme === '') {
document.body.classList.remove('dark-theme');
} else {
document.body.classList.add('dark-theme');
}
if (firstPaneMode === 'mouse-mode') {
document.body.classList.remove('keyboard-mode');
document.body.classList.add('mouse-mode');
} else {
document.body.classList.remove('mouse-mode');
document.body.classList.add('keyboard-mode');
}
return (
<div className={styles.container}>
<ClassyProvider themes={['dark']}>
<div
className={classnames(styles.panel, firstPaneTheme, firstPaneMode)}
>
{contents}
</div>
</ClassyProvider>
{secondPane ? (
<div
className={classnames(styles.panel, secondPaneTheme, secondPaneMode)}
>
{contents}
</div>
) : null}
</div>
);
});
// Hack to enable hooks in stories: https://github.com/storybookjs/storybook/issues/5721#issuecomment-473869398
addDecorator(Story => <Story />);
addDecorator(story => <I18n messages={messages}>{story()}</I18n>);
addParameters({
axe: {
disabledRules: ['html-has-lang'],
},
});
configure(() => {
// Load main app stories
const tsComponentsContext = require.context(
'../ts/components',
true,
/\.stories.tsx?$/
);
tsComponentsContext.keys().forEach(f => tsComponentsContext(f));
// Load sticker creator stories
const stickerCreatorContext = require.context(
'../sticker-creator',
true,
/\.stories\.tsx?$/
);
stickerCreatorContext.keys().forEach(f => stickerCreatorContext(f));
}, module);

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

@ -1,143 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
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 = {
typescript: {
reactDocgen: false,
},
stories: ['../ts/axo/**/*.stories.tsx', '../ts/components/**/*.stories.tsx'],
addons: [
'@storybook/addon-a11y',
'@storybook/addon-actions',
'@storybook/addon-controls',
'@storybook/addon-measure',
'@storybook/addon-toolbars',
'@storybook/addon-viewport',
'@storybook/addon-jest',
// This must be imported last.
'@storybook/addon-interactions',
'@storybook/addon-webpack5-compiler-swc',
],
framework: '@storybook/react-webpack5',
core: {
disableTelemetry: true,
},
features: {},
staticDirs: [
{ from: '../fonts', to: 'fonts' },
{ from: '../images', to: 'images' },
{ from: '../fixtures', to: 'fixtures' },
{
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 = {
type: 'filesystem',
};
// oxlint-disable-next-line no-param-reassign, typescript/no-non-null-assertion
webpackConfig.resolve!.extensionAlias = {
'.js': ['.tsx', '.ts', '.js'],
};
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.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;',
},
},
],
});
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.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')],
},
},
},
],
});
// 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();
};
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.plugins!.push(
new ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
})
);
return webpackConfig;
},
docs: {},
};
export default storybookConfig;

View File

@ -0,0 +1,15 @@
<!-- Copyright 2019-2020 Signal Messenger, LLC -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
<!-- prettier-ignore -->
<link rel="stylesheet" href="../stylesheets/manifest.css" />
<script>
window.log = {
fatal: console.error.bind(console),
error: console.error.bind(console),
warn: console.warn.bind(console),
info: console.info.bind(console),
debug: console.debug.bind(console),
trace: console.trace.bind(console),
};
</script>

View File

@ -1,299 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import '../ts/window.d.ts';
import '@signalapp/quill-cjs/dist/quill.core.css';
import '../stylesheets/manifest.scss';
import '../stylesheets/tailwind-config.css';
import * as styles from './styles.scss';
import messages from '../_locales/en/messages.json';
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 {
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';
setEnvironment(Environment.Development, true);
const i18n = setupI18n('en', messages);
export const globalTypes = {
mode: {
name: 'Mode',
description: 'Application mode',
defaultValue: 'mouse',
toolbar: {
dynamicTitle: true,
icon: 'circlehollow',
items: ['mouse', 'keyboard'],
showName: true,
},
},
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
dynamicTitle: true,
icon: 'circlehollow',
items: ['light', 'dark'],
showName: true,
},
},
direction: {
name: 'Direction',
description: 'Direction of text',
defaultValue: 'auto',
toolbar: {
dynamicTitle: true,
icon: 'circlehollow',
items: ['auto', 'ltr', 'rtl'],
showName: true,
},
},
};
const mockStore: Store<StateType> = createStore(
combineReducers({
calling: (state = {}) => state,
nav: (
state = {
selectedLocation: {
tab: NavTab.Chats,
details: {
conversationId: undefined,
},
},
}
) => state,
conversations: (
state = {
conversationLookup: {},
targetedConversationPanels: {},
}
) => state,
globalModals: (state = {}) => state,
user: (state = {}) => state,
})
);
// oxlint-disable-next-line
const noop = () => {};
window.Whisper ??= {};
window.Whisper.events = {
on: noop,
off: noop,
};
window.SignalContext = {
i18n,
activeWindowService: {
isActive: () => true,
registerForActive: noop,
unregisterForActive: noop,
registerForChange: noop,
unregisterForChange: noop,
},
nativeThemeListener: {
getSystemTheme: () => SystemThemeType.light,
subscribe: noop,
unsubscribe: noop,
update: () => SystemThemeType.light,
},
Settings: {
themeSetting: {
getValue: async () => 'light',
setValue: async () => 'light',
},
waitForChange: () => new Promise(noop),
},
OS: {
getClassName: () => '',
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.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 =
context.globals.theme === 'light' ? ThemeType.light : ThemeType.dark;
const mode = context.globals.mode;
const direction = context.globals.direction ?? 'auto';
window.SignalContext.getResolvedMessagesLocaleDirection = () =>
direction === 'auto' ? 'ltr' : direction;
// Adding it to the body as well so that we can cover modals and other
// components that are rendered outside of this decorator container
if (theme === 'light') {
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme');
} else {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
}
if (mode === 'mouse') {
document.body.classList.remove('keyboard-mode');
document.body.classList.add('mouse-mode');
} else {
document.body.classList.remove('mouse-mode');
document.body.classList.add('keyboard-mode');
}
document.body.classList.add('page-is-visible');
document.documentElement.setAttribute('dir', direction);
return (
<div className={styles.container}>
<StorybookThemeContext.Provider value={theme}>
<Story {...context} />
</StorybookThemeContext.Provider>
</div>
);
};
function withMockStoreProvider(Story, context) {
return (
<Provider store={mockStore}>
<Story {...context} />
</Provider>
);
}
function withScrollLockProvider(Story, context) {
return (
<ScrollerLockContext.Provider
value={createScrollerLock('MockStories', () => null)}
>
<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 = {
axe: {
disabledRules: ['html-has-lang'],
},
};
export const tags = [];

View File

@ -1,23 +1,25 @@
// Copyright 2019 Signal Messenger, LLC
// Copyright 2019-2020 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;
display: flex;
flex-direction: row;
align-items: stretch;
align-content: stretch;
width: 100vw;
height: 100vh;
}
.panel {
flex: 1;
padding: 16px;
height: 100%;
width: 100%;
overflow: auto;
}
.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;

View File

@ -0,0 +1,33 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
module.exports = ({ config }) => {
config.entry.unshift(
'!!style-loader!css-loader!sanitize.css',
'!!style-loader!css-loader!typeface-inter'
);
config.module.rules.unshift(
{
test: /\.[jt]sx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.scss$/,
loaders: [
'style-loader',
'css-loader?modules=true&localsConvention=camelCaseOnly',
'sass-loader',
],
}
);
config.resolve.extensions = ['.tsx', '.ts', '.jsx', '.js'];
config.externals = {
net: 'net',
};
return config;
};

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;

8
.tx/config Normal file
View File

@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
[signal-desktop.messagesjson-electron]
file_filter = _locales/<lang>/messages.json
source_file = _locales/en/messages.json
source_lang = en
type = CHROME

View File

@ -5,6 +5,7 @@ tests
powered-test
# asset directories
docs
doc
website
images
@ -41,10 +42,3 @@ Gruntfile.js
# asset directories
!nyc/node_modules/istanbul-reports/lib/html/assets
# bad matches
!patch-package/node_modules/yaml/dist/doc
# used for build:acknowledgements
!@signalapp/libsignal-client/dist/acknowledgments.md
!@signalapp/ringrtc/dist/acknowledgments.md

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,31 @@
<!-- Copyright 2015 Signal Messenger, LLC -->
<!-- Copyright 2015-2020 Signal Messenger, LLC -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# Contributor Guidelines
## 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, it's a good idea to 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
First, you'll need [Node.js](https://nodejs.org/) which matches our current version.
You can check [`.nvmrc` in the `main` branch](https://github.com/signalapp/Signal-Desktop/blob/main/.nvmrc)
to see what the current version is. If you have [nvm](https://github.com/creationix/nvm)
You can check [`.nvmrc` in the `development` branch](https://github.com/signalapp/Signal-Desktop/blob/development/.nvmrc) to see what the current version is. If you have [nvm](https://github.com/creationix/nvm)
you can just run `nvm use` in the project directory and it will switch to the project's
desired Node.js version. [nvm for windows](https://github.com/coreybutler/nvm-windows) is
still useful, but it doesn't support `.nvmrc` files.
Then you need [`git`](https://git-scm.com/), if you don't have it installed yet.
Then you need [`git`](https://git-scm.com/) and [`git-lfs`](https://github.com/git-lfs/git-lfs/wiki/Installation), if you don't have those yet.
### macOS
@ -66,16 +33,21 @@ Install the [Xcode Command-Line Tools](http://osxdaily.com/2014/02/12/install-co
### Windows
1. Download _Build Tools for Visual Studio 2022 Community Edition_ from [Microsoft's website](https://visualstudio.microsoft.com/vs/community/) and install it, including the "Desktop development with C++" option.
2. Download and install the latest Python 3 release from https://www.python.org/downloads/windows/ (3.6 or later required).
1. **Windows 7 only:**
- Install Microsoft .NET Framework 4.5.1:
https://www.microsoft.com/en-us/download/details.aspx?id=40773
- Install Windows SDK version 8.1: https://developer.microsoft.com/en-us/windows/downloads/sdk-archive
1. Install _Windows Build Tools_: Open the [Command Prompt (`cmd.exe`) as Administrator](<https://technet.microsoft.com/en-us/library/cc947813(v=ws.10).aspx>)
and run: `npm install --vs2015 --global --production --add-python-to-path windows-build-tools`
### Linux
1. Pick your favorite package manager.
1. Install `python` (Python 3.6+)
1. Install `python` (Python 2.7+)
1. Install `gcc`
1. Install `g++`
1. Install `make`
1. Install `git-lfs`
### All platforms
@ -84,11 +56,13 @@ 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!
git-lfs install # Setup Git LFS.
npm install --global yarn # (only if you dont already have `yarn`)
yarn install --frozen-lockfile # Install and build dependencies (this will take a while)
yarn grunt # Generate final JS and CSS assets
yarn build:webpack # Build parts of the app that use webpack (Sticker Creator)
yarn test # A good idea to make sure tests run first
yarn start # Start Signal!
```
You'll need to restart the application regularly to see your changes, as there
@ -98,54 +72,16 @@ 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 `yarn grunt` 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:
the latest built assets when you change a file. Run this in its own terminal instance
while you make changes:
```
pnpm run dev:transpile # recompiles when you change .ts files
pnpm run dev:styles # recompiles when you change .scss files
yarn grunt dev # runs until you stop it, re-generating built assets on file changes
```
#### Known issues
##### `yarn install` prints error 'Could not detect abi for version 30.0.6 and runtime electron'
`yarn install` may print an error like the following, but it can be ignored because the overall operation succeeds.
```
$ ./node_modules/.bin/electron-builder install-app-deps
• electron-builder version=24.6.3
• loaded configuration file=package.json ("build" field)
• rebuilding native dependencies dependencies=@nodert-win10-rs4/windows.data.xml.dom@0.4.4, @nodert-win10-rs4/windows.ui.notifications@0.4.4, @signalapp/better-sqlite3@8.7.1, @signalapp/windows-dummy-keystroke@1.0.0, bufferutil@4.0.7, fs-xattr@0.3.0, mac-screen-capture-permissions@2.0.0, utf-8-validate@5.0.10
platform=linux
arch=x64
• install prebuilt binary name=mac-screen-capture-permissions version=2.0.0 platform=linux arch=x64 napi=
• build native dependency from sources name=mac-screen-capture-permissions
version=2.0.0
platform=linux
arch=x64
napi=
reason=prebuild-install failed with error (run with env DEBUG=electron-builder to get more information)
error=/home/ben/sauce/Signal-Desktop/node_modules/node-abi/index.js:30
throw new Error('Could not detect abi for version ' + target + ' and runtime ' + runtime + '. Updating "node-abi" might help solve this issue if it is a new release of ' + runtime)
^
Error: Could not detect abi for version 30.0.6 and runtime electron. Updating "node-abi" might help solve this issue if it is a new release of electron
at getAbi (/home/ben/sauce/Signal-Desktop/node_modules/node-abi/index.js:30:9)
at module.exports (/home/ben/sauce/Signal-Desktop/node_modules/prebuild-install/rc.js:53:57)
at Object.<anonymous> (/home/ben/sauce/Signal-Desktop/node_modules/prebuild-install/bin.js:8:25)
at Module._compile (node:internal/modules/cjs/loader:1376:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
at Module.load (node:internal/modules/cjs/loader:1207:32)
at Module._load (node:internal/modules/cjs/loader:1023:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
at node:internal/main/run_main_module:28:49
Node.js v20.11.1
```
If you miss the `git-lfs` step, run `yarn cache clean` and remove `node_modules` before trying again.
### webpack
@ -154,7 +90,7 @@ You can run a development server for these parts of the app with the
following command:
```
pnpm run dev
yarn dev
```
In order for the app to make requests to the development server you must set
@ -162,7 +98,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 yarn start
```
## Setting up standalone
@ -184,7 +120,7 @@ empty application. But you can use the information from your production install
Desktop to populate your testing application!
First, exit both production and development apps (In macOS - literally quit the apps).
Second, find your application data in the [appData](https://www.electronjs.org/docs/latest/api/app#appgetpathname) directory:
Second, find your application data:
- macOS: `~/Library/Application Support/Signal`
- Linux: `~/.config/Signal`
@ -216,7 +152,7 @@ Once you have the additional numbers, you can setup additional storage profiles
between them using the `NODE_APP_INSTANCE` environment variable.
For example, to create an 'alice' profile, put a file called `local-alice.json` in the
`/config` subdirectory of your project checkout where you'll find other `.json` config files:
`config` directory:
```
{
@ -227,10 +163,11 @@ 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 yarn run start
```
This changes the `userData` directory from `%appData%/Signal` to `%appData%/Signal-aliceProfile`.
This changes the [userData](https://electron.atom.io/docs/all/#appgetpathname)
directory from `%appData%/Signal` to `%appData%/Signal-aliceProfile`.
# Making changes
@ -243,36 +180,37 @@ 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
command line. You can run the client-side tests in an interactive session with
`NODE_ENV=test pnpm start`.
The easiest way to run all tests at once is `yarn test`.
You can browse tests from the command line with `grunt unit-tests` or in an
interactive session with `NODE_ENV=test yarn run start`.
If you want to run the `libtextsecure` tests, you can run `yarn run test-electron`,
which also runs the unit tests.
To run Node.js tests, you can run `yarn test-server` from the command line. You can get
code coverage numbers for this kind of run via `yarn test-server-coverage`, then display
the report with `yarn open-coverage`.
## 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 `yarn 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.
- Please do not submit pull requests for translation fixes. Anyone can update
the translations in
[Transifex](https://www.transifex.com/projects/p/signal-desktop).
- Never use plain strings right in the source code - pull them from `messages.json`!
You **only** need to modify the default locale
[`_locales/en/messages.json`](_locales/en/messages.json). Other locales are generated
automatically based on that file and then periodically translated.
automatically based on that file and then periodically uploaded to Transifex for
translation.
- [Rebase](https://nathanleclaire.com/blog/2014/09/14/dont-be-scared-of-git-rebase/) your
changes on the latest `main` branch, resolving any conflicts.
changes on the latest `development` 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 our master 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.
@ -284,10 +222,10 @@ More guidelines:
to aid in the review process.
- Provide a well written and nicely formatted commit message. See [this
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:
for some tips on formatting. As far as content, try to include 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.
2. Why this change was made (including git issue # if appropriate)
3. Any relevant technical details or motivations for your implementation
choices that may be helpful to someone reviewing or auditing the commit
history in the future. When in doubt, err on the side of a longer
@ -308,7 +246,7 @@ iOS and Android apps are locked to the production servers. To test all scenarios
staging, your best bet is to pull down the development version of the iOS or Android app,
and register it with one of your extra phone numbers:
First, build Signal for Android or iOS from source, and point its service URL to `chat.staging.signal.org`:
First, build Signal for Android or iOS from source, and point its service URL to `textsecure-service-staging.whispersystems.org`:
**on Android:** Replace the `SIGNAL_URL` value in [build.gradle](https://github.com/signalapp/Signal-Android/blob/master/build.gradle)
@ -320,7 +258,7 @@ and [iOS](https://github.com/signalapp/Signal-iOS/blob/master/BUILDING.md) proje
Then you can set up your development build of Signal Desktop as normal. If you've already
set up as a standalone install, you can switch by opening the DevTools (View -> Toggle
Developer Tools) and entering this into the Console and pressing enter: `window.reduxActions.app.openInstaller();`
Developer Tools) and entering this into the Console and pressing enter: `window.owsDesktopApp.appView.openInstaller();`
## Changing to production
@ -339,26 +277,20 @@ 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
yarn generate
yarn build
```
Then, run the tests using `pnpm run test-release`.
Then, run the tests using `grunt test-release:osx --dir=release`, replacing `osx` with `linux` or `win` depending on your platform.
### Testing MacOS builds
## Translations
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.
To pull the latest translations, follow these steps:
1. Build the app while skipping the custom macOS signing script:
```
pnpm run generate
SKIP_SIGNING_SCRIPT=1 pnpm 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.
1. Download Transifex client:
https://docs.transifex.com/client/installing-the-client
2. Create Transifex account: https://transifex.com
3. Generate API token: https://www.transifex.com/user/settings/api/
4. Create `~/.transifexrc` configuration:
https://docs.transifex.com/client/client-configuration#-transifexrc
5. Run `yarn grunt tx`.

File diff suppressed because it is too large Load Diff

430
Gruntfile.js Normal file
View File

@ -0,0 +1,430 @@
// Copyright 2014-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const { join } = require('path');
const importOnce = require('node-sass-import-once');
const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
const spectron = require('spectron');
const asar = require('asar');
const fs = require('fs');
const assert = require('assert');
const sass = require('node-sass');
const packageJson = require('./package.json');
/* eslint-disable more/no-then, no-console */
module.exports = grunt => {
const bower = grunt.file.readJSON('bower.json');
const components = [];
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const i in bower.concat.app) {
components.push(bower.concat.app[i]);
}
const libtextsecurecomponents = [];
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const i in bower.concat.libtextsecure) {
libtextsecurecomponents.push(bower.concat.libtextsecure[i]);
}
grunt.loadNpmTasks('grunt-sass');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
components: {
src: components,
dest: 'js/components.js',
},
libtextsecurecomponents: {
src: libtextsecurecomponents,
dest: 'libtextsecure/components.js',
},
test: {
src: [
'node_modules/mocha/mocha.js',
'node_modules/chai/chai.js',
'test/_test.js',
],
dest: 'test/test.js',
},
libtextsecuretest: {
src: [
'node_modules/jquery/dist/jquery.js',
'components/mock-socket/dist/mock-socket.js',
'node_modules/mocha/mocha.js',
'node_modules/chai/chai.js',
'libtextsecure/test/_test.js',
],
dest: 'libtextsecure/test/test.js',
},
},
sass: {
options: {
implementation: sass,
sourceMap: true,
importer: importOnce,
},
dev: {
files: {
'stylesheets/manifest.css': 'stylesheets/manifest.scss',
'stylesheets/manifest_bridge.css': 'stylesheets/manifest_bridge.scss',
},
},
},
copy: {
deps: {
files: [
{
src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js',
dest: 'js/Mp3LameEncoder.min.js',
},
{
src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js',
dest: 'js/WebAudioRecorderMp3.js',
},
],
},
},
watch: {
libtextsecure: {
files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'],
tasks: ['concat:libtextsecure'],
},
protobuf: {
files: ['./protos/SignalService.proto'],
tasks: ['exec:build-protobuf'],
},
sass: {
files: ['./stylesheets/*.scss', './stylesheets/**/*.scss'],
tasks: ['sass'],
},
},
exec: {
'tx-pull-new': {
cmd: 'tx pull -a --minimum-perc=80',
},
'tx-pull': {
cmd: 'tx pull',
},
transpile: {
cmd: 'yarn transpile',
},
'build-protobuf': {
cmd: 'yarn build-protobuf',
},
},
'test-release': {
osx: {
archive: `mac/${packageJson.productName}.app/Contents/Resources/app.asar`,
exe: `mac/${packageJson.productName}.app/Contents/MacOS/${packageJson.productName}`,
},
mas: {
archive: 'mas/Signal.app/Contents/Resources/app.asar',
exe: `mas/${packageJson.productName}.app/Contents/MacOS/${packageJson.productName}`,
},
linux: {
archive: 'linux-unpacked/resources/app.asar',
exe: `linux-unpacked/${packageJson.name}`,
},
win: {
archive: 'win-unpacked/resources/app.asar',
exe: `win-unpacked/${packageJson.productName}.exe`,
},
},
gitinfo: {}, // to be populated by grunt gitinfo
});
Object.keys(grunt.config.get('pkg').devDependencies).forEach(key => {
if (/^grunt(?!(-cli)?$)/.test(key)) {
// ignore grunt and grunt-cli
grunt.loadNpmTasks(key);
}
});
// Transifex does not understand placeholders, so this task patches all non-en
// locales with missing placeholders
grunt.registerTask('locale-patch', () => {
const en = grunt.file.readJSON('_locales/en/messages.json');
grunt.file.recurse('_locales', (abspath, rootdir, subdir, filename) => {
if (subdir === 'en' || filename !== 'messages.json') {
return;
}
const messages = grunt.file.readJSON(abspath);
// eslint-disable-next-line no-restricted-syntax
for (const key in messages) {
if (en[key] !== undefined && messages[key] !== undefined) {
if (
en[key].placeholders !== undefined &&
messages[key].placeholders === undefined
) {
messages[key].placeholders = en[key].placeholders;
}
}
}
grunt.file.write(abspath, `${JSON.stringify(messages, null, 4)}\n`);
});
});
grunt.registerTask('getExpireTime', () => {
grunt.task.requires('gitinfo');
const gitinfo = grunt.config.get('gitinfo');
const committed = gitinfo.local.branch.current.lastCommitTime;
const time = Date.parse(committed) + 1000 * 60 * 60 * 24 * 90;
grunt.file.write(
'config/local-production.json',
`${JSON.stringify({ buildExpiration: time })}\n`
);
});
grunt.registerTask('clean-release', () => {
rimraf.sync('release');
mkdirp.sync('release');
});
function runTests(environment, cb) {
let failure;
const { Application } = spectron;
const electronBinary =
process.platform === 'win32' ? 'electron.cmd' : 'electron';
const path = join(__dirname, 'node_modules', '.bin', electronBinary);
const args = [join(__dirname, 'main.js')];
console.log('Starting path', path, 'with args', args);
const app = new Application({
path,
args,
env: {
NODE_ENV: environment,
},
requireName: 'unused',
});
function getMochaResults() {
// eslint-disable-next-line no-undef
return window.mochaResults;
}
app
.start()
.then(() => {
console.log('App started. Now waiting for test results...');
return app.client.waitUntil(
() =>
app.client
.execute(getMochaResults)
.then(data => Boolean(data.value)),
25000,
'Expected to find window.mochaResults set!'
);
})
.then(() => app.client.execute(getMochaResults))
.then(data => {
const results = data.value;
if (!results) {
failure = () => grunt.fail.fatal("Couldn't extract test results.");
return app.client.log('browser');
}
if (results.failures > 0) {
console.error(results.reports);
failure = () =>
grunt.fail.fatal(`Found ${results.failures} failing unit tests.`);
return app.client.log('browser');
}
grunt.log.ok(`${results.passes} tests passed.`);
return null;
})
.then(logs => {
if (logs) {
console.error();
console.error('Because tests failed, printing browser logs:');
console.error(logs);
}
})
.catch(error => {
failure = () =>
grunt.fail.fatal(
`Something went wrong: ${error.message} ${error.stack}`
);
})
.then(() => {
// We need to use the failure variable and this early stop to clean up before
// shutting down. Grunt's fail methods are the only way to set the return value,
// but they shut the process down immediately!
if (failure) {
console.log();
console.log('Main process logs:');
return app.client.getMainProcessLogs().then(logs => {
logs.forEach(log => {
console.log(log);
});
return app.stop();
});
}
return app.stop();
})
.then(() => {
if (failure) {
failure();
}
cb();
})
.catch(error => {
console.error('Second-level error:', error.message, error.stack);
if (failure) {
failure();
}
cb();
});
}
grunt.registerTask(
'unit-tests',
'Run unit tests w/Electron',
function thisNeeded() {
const environment = grunt.option('env') || 'test';
const done = this.async();
runTests(environment, done);
}
);
grunt.registerTask(
'lib-unit-tests',
'Run libtextsecure unit tests w/Electron',
function thisNeeded() {
const environment = grunt.option('env') || 'test-lib';
const done = this.async();
runTests(environment, done);
}
);
grunt.registerMultiTask(
'test-release',
'Test packaged releases',
function thisNeeded() {
const dir = grunt.option('dir') || 'release';
const environment = grunt.option('env') || 'production';
const config = this.data;
const archive = [dir, config.archive].join('/');
const files = [
'config/default.json',
`config/${environment}.json`,
`config/local-${environment}.json`,
];
console.log(this.target, archive);
const releaseFiles = files.concat(config.files || []);
releaseFiles.forEach(fileName => {
console.log(fileName);
try {
asar.statFile(archive, fileName);
return true;
} catch (e) {
console.log(e);
throw new Error(`Missing file ${fileName}`);
}
});
if (config.appUpdateYML) {
const appUpdateYML = [dir, config.appUpdateYML].join('/');
if (fs.existsSync(appUpdateYML)) {
console.log('auto update ok');
} else {
throw new Error(`Missing auto update config ${appUpdateYML}`);
}
}
const done = this.async();
// A simple test to verify a visible window is opened with a title
const { Application } = spectron;
const path = [dir, config.exe].join('/');
console.log('Starting path', path);
const app = new Application({
path,
});
const sleep = millis =>
new Promise(resolve => setTimeout(resolve, millis));
Promise.race([app.start(), sleep(15000)])
.then(() => {
if (!app.isRunning()) {
throw new Error('Application failed to start');
}
return app.client.getWindowCount();
})
.then(count => {
assert.equal(count, 1);
console.log('window opened');
})
.then(() =>
// Verify the window's title
app.client.waitUntil(
async () =>
(await app.client.getTitle()) === packageJson.productName,
{
timeoutMsg: `Expected window title to be ${JSON.stringify(
packageJson.productName
)}`,
}
)
)
.then(() => {
console.log('title ok');
})
.then(() => {
assert(
app.chromeDriver.logLines.indexOf(`NODE_ENV ${environment}`) > -1
);
console.log('environment ok');
})
.then(
() =>
// Successfully completed test
app.stop(),
error =>
// Test failed!
app.stop().then(() => {
grunt.fail.fatal(`Test failed: ${error.message} ${error.stack}`);
})
)
.catch(error => {
console.log('Main process logs:');
app.client.getMainProcessLogs().then(logs => {
logs.forEach(log => {
console.log(log);
});
// Test failed!
grunt.fail.fatal(`Failure! ${error.message} ${error.stack}`);
});
})
.then(done);
}
);
grunt.registerTask('tx', [
'exec:tx-pull-new',
'exec:tx-pull',
'locale-patch',
]);
grunt.registerTask('dev', ['default', 'watch']);
grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']);
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
grunt.registerTask('default', [
'exec:build-protobuf',
'exec:transpile',
'concat',
'copy:deps',
'sass',
'date',
]);
};

42
LICENSE
View File

@ -617,45 +617,3 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@ -1,11 +1,21 @@
<!-- Copyright 2014 Signal Messenger, LLC -->
<!-- Copyright 2014-2020 Signal Messenger, LLC -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# Signal Desktop
Signal Desktop links with Signal on [Android](https://github.com/signalapp/Signal-Android) or [iOS](https://github.com/signalapp/Signal-iOS) and lets you message from your Windows, macOS, and Linux computers.
Signal Desktop is an Electron application that links with Signal
on [Android](https://github.com/signalapp/Signal-Android)
or [iOS](https://github.com/signalapp/Signal-iOS).
[Install the production version](https://signal.org/download/) or help us out by [installing the beta version](https://support.signal.org/hc/articles/360007318471-Signal-Beta).
## Install the production version: https://signal.org/download/
## Install the beta
You can install the beta version of Signal Desktop alongside the production version. The beta uses different data and install locations.
- _Windows:_ First, download [this file](https://updates.signal.org/desktop/beta.yml) and look for the `url` property that specifies the location for the latest beta installer. Download the installer by constructing a final URL that looks like this: `https://updates.signal.org/desktop/<installer location>`. Then run the installer.
- _macOS:_ First, download [this file](https://updates.signal.org/desktop/beta-mac.yml) and look for the `url` property that specifies the location for the latest beta installer. Download the installer by constructing a final URL that looks like this: `https://updates.signal.org/desktop/<package location>`. Then unzip that package and copy the `.app` file into the `/Applications` folder using Finder.
- _Linux:_ Follow the production instructions to set up the APT repository and run `apt install signal-desktop-beta`.
## Got a question?
@ -20,13 +30,20 @@ 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 Translations
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!
Interested in helping to translate Signal? Contribute here:
## Donate to Signal
https://www.transifex.com/projects/p/signal-desktop
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.
## Contributing Code
Please see [CONTRIBUTING.md](https://github.com/signalapp/Signal-Desktop/blob/master/CONTRIBUTING.md)
for setup instructions and guidelines for new contributors. Don't forget to sign the [CLA](https://signal.org/cla/).
## Contributing Funds
You can donate to Signal development through the [Signal Technology Foundation](https://signal.org/donate), an independent 501c3 nonprofit.
## Cryptography Notice
@ -39,6 +56,6 @@ The form and manner of this distribution makes it eligible for export under the
## License
Copyright 2013-2024 Signal Messenger, LLC
Copyright 20132021 Signal, a 501c3 nonprofit
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
Licensed under the AGPLv3: https://opensource.org/licenses/agpl-3.0

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

5602
_locales/af/messages.json Normal file

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,съобщения,обаждане,глас,криптиран,поверителен,сигурно,поверителност,група,видео,чат,истории

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