├── .buildkite
├── commands
│ ├── install_node_dependencies.sh
│ └── package_windows.ps1
├── pipeline.yml
└── shared-pipeline-vars
├── .bundle
└── config
├── .configure
├── .configure-files
└── app_store_connect_fastlane_api_key.p8.enc
├── .editorconfig
├── .gitattributes
├── .githooks
└── pre-commit
├── .github
├── ISSUE_TEMPLATE
│ ├── ---bug-report.md
│ └── ---feature-request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .rubocop.yml
├── .ruby-version
├── .stylelintrc
├── .vscode
└── launch.json
├── .xcode-version
├── CONTRIBUTING.md
├── Gemfile
├── Gemfile.lock
├── LICENSE.md
├── Makefile
├── README.md
├── RELEASE-NOTES.md
├── TESTING-CHECKLIST.md
├── after_sign_hook.js
├── babel.config.js
├── bin
└── deploy.sh
├── config.json
├── desktop
├── app-quit
│ ├── README.md
│ └── index.js
├── app.js
├── config-updater.json
├── config
│ └── index.js
├── context-menu
│ └── index.js
├── detect
│ ├── build
│ │ └── index.js
│ └── platform
│ │ └── index.js
├── env.js
├── evernote-import
│ ├── enml-to-markdown.js
│ ├── index.js
│ └── test
│ │ ├── correct-markdown.txt
│ │ ├── enml-to-markdown.test.ts
│ │ └── mock-enml.txt
├── index.js
├── logger
│ ├── archiver.js
│ ├── index.js
│ ├── namespaces
│ │ └── index.js
│ └── zip-logs.js
├── menus
│ ├── edit-menu.js
│ ├── file-menu.js
│ ├── format-menu.js
│ ├── help-menu.js
│ ├── index.js
│ ├── mac-app-menu.js
│ ├── menu-items.js
│ ├── test
│ │ ├── help-menu.js
│ │ └── utils.js
│ ├── utils.js
│ └── view-menu.js
├── preload.js
└── updater
│ ├── README.md
│ ├── auto-updater
│ ├── README.md
│ └── index.js
│ ├── index.js
│ ├── lib
│ ├── Updater.js
│ └── setup-progress-updates.js
│ └── manual-updater
│ ├── README.md
│ └── index.js
├── docker-compose.yml
├── docs
└── packaging.md
├── electron-builder-appx.json
├── electron-builder.json
├── eslint.config.mjs
├── fastlane
├── .gitignore
├── Fastfile
├── example.env
└── lib
│ └── helpers.rb
├── jest.config.js
├── lib
├── alternate-login-prompt
│ ├── index.tsx
│ └── style.scss
├── analytics
│ ├── README.md
│ ├── index.ts
│ ├── test.ts
│ ├── tracks.ts
│ └── types.ts
├── app-layout
│ ├── index.tsx
│ └── style.scss
├── app.tsx
├── auth
│ ├── index.tsx
│ └── style.scss
├── boot-with-auth.tsx
├── boot-without-auth.tsx
├── boot.ts
├── components
│ ├── boot-warning
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── clipboard-button
│ │ └── index.tsx
│ ├── dev-badge
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── last-sync-time
│ │ └── index.tsx
│ ├── note-preview
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── panel-title
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── progress-bar
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── slider
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── spinner
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── tab-panels
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── tag-chip
│ │ ├── __snapshots__
│ │ │ └── test.tsx.snap
│ │ ├── index.tsx
│ │ ├── style.scss
│ │ └── test.tsx
│ ├── transition-delay-enter
│ │ ├── index.tsx
│ │ └── style.scss
│ └── transition-fade-in-out
│ │ ├── index.tsx
│ │ └── style.scss
├── connection-status
│ ├── index.tsx
│ └── style.scss
├── controls
│ ├── checkbox
│ │ ├── index.tsx
│ │ └── style.scss
│ └── toggle
│ │ ├── index.tsx
│ │ └── style.scss
├── dialog-renderer
│ ├── index.tsx
│ └── style.scss
├── dialog
│ ├── index.tsx
│ └── style.scss
├── dialogs
│ ├── about
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── beta-warning
│ │ └── index.tsx
│ ├── button-group
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── close-window-confirmation
│ │ └── index.tsx
│ ├── import
│ │ ├── dropzone
│ │ │ ├── index.tsx
│ │ │ └── style.scss
│ │ ├── importers.ts
│ │ ├── index.tsx
│ │ ├── source-importer
│ │ │ ├── executor
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.scss
│ │ │ ├── index.tsx
│ │ │ ├── progress
│ │ │ │ ├── bar.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── text.tsx
│ │ │ ├── style.scss
│ │ │ └── utils
│ │ │ │ └── test-importer.ts
│ │ └── style.scss
│ ├── keybindings
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── logout-confirmation
│ │ └── index.tsx
│ ├── radio-settings-group.tsx
│ ├── settings-group.tsx
│ ├── settings
│ │ ├── index.tsx
│ │ ├── panels
│ │ │ ├── account.tsx
│ │ │ ├── display.tsx
│ │ │ └── tools.tsx
│ │ └── style.scss
│ ├── share
│ │ ├── index.tsx
│ │ └── style.scss
│ ├── toggle-settings-group.tsx
│ ├── trash-tag-confirmation
│ │ ├── index.tsx
│ │ └── style.scss
│ └── unsynchronized
│ │ ├── index.tsx
│ │ └── style.scss
├── email-verification
│ ├── index.tsx
│ └── style.scss
├── error-boundary
│ ├── index.tsx
│ └── style.scss
├── global.d.ts
├── icon-button
│ ├── index.tsx
│ └── style.scss
├── icons
│ ├── app-icon
│ │ ├── app-icon.ico
│ │ ├── icon_128x128.png
│ │ ├── icon_128x128@2x.png
│ │ ├── icon_16x16.png
│ │ ├── icon_16x16@2x.png
│ │ ├── icon_256x256.png
│ │ ├── icon_256x256@2x.png
│ │ ├── icon_32x32.png
│ │ ├── icon_32x32@2x.png
│ │ ├── icon_512x512.png
│ │ └── icon_512x512@2x.png
│ ├── arrow-left.tsx
│ ├── arrow-top-right.tsx
│ ├── attention.tsx
│ ├── back.tsx
│ ├── check-list.tsx
│ ├── checkbox-checked.tsx
│ ├── checkbox-unchecked.tsx
│ ├── chevron-right-small.tsx
│ ├── chevron-right.tsx
│ ├── cloud-sync.tsx
│ ├── cloud.tsx
│ ├── connection.tsx
│ ├── cross-small.tsx
│ ├── cross.tsx
│ ├── ellipsis-outline.tsx
│ ├── ellipsis.tsx
│ ├── file-small.tsx
│ ├── help-small.tsx
│ ├── info.tsx
│ ├── mail.tsx
│ ├── menu.tsx
│ ├── new-note.tsx
│ ├── no-connection.tsx
│ ├── notes.tsx
│ ├── pinned-small.tsx
│ ├── pinned.tsx
│ ├── preview-stop.tsx
│ ├── preview.tsx
│ ├── published-small.tsx
│ ├── reorder.tsx
│ ├── revisions.tsx
│ ├── search-small.tsx
│ ├── settings.tsx
│ ├── share.tsx
│ ├── sidebar.tsx
│ ├── simplenote-compact.tsx
│ ├── simplenote.tsx
│ ├── style.scss
│ ├── sync-small.tsx
│ ├── tag.tsx
│ ├── tags.tsx
│ ├── trash.tsx
│ ├── untagged-notes.tsx
│ ├── warning.tsx
│ └── wordpress.tsx
├── index.ejs
├── logging-out.tsx
├── menu-bar
│ ├── index.tsx
│ └── style.scss
├── navigation-bar
│ ├── index.tsx
│ ├── item
│ │ ├── index.tsx
│ │ └── style.scss
│ └── style.scss
├── note-actions
│ ├── index.tsx
│ └── style.scss
├── note-content-editor.tsx
├── note-detail
│ ├── index.tsx
│ ├── render-to-node.ts
│ └── style.scss
├── note-editor
│ ├── index.tsx
│ └── style.scss
├── note-info
│ ├── index.tsx
│ ├── reference.tsx
│ ├── references.tsx
│ └── style.scss
├── note-list
│ ├── decorators.tsx
│ ├── get-note-title-and-preview.test.ts
│ ├── get-note-title-and-preview.ts
│ ├── index.tsx
│ ├── no-notes.tsx
│ ├── note-cell.tsx
│ └── style.scss
├── note-revisions
│ ├── index.tsx
│ └── style.scss
├── note-toolbar
│ ├── index.tsx
│ └── style.scss
├── revision-selector
│ ├── index.tsx
│ └── style.scss
├── search-field
│ ├── index.tsx
│ └── style.scss
├── search-results-bar
│ ├── index.tsx
│ └── style.scss
├── search
│ └── index.ts
├── state
│ ├── action-types.ts
│ ├── actions.ts
│ ├── analytics
│ │ ├── actions.ts
│ │ └── middleware.ts
│ ├── browser
│ │ └── index.ts
│ ├── data
│ │ ├── actions.ts
│ │ ├── middleware.ts
│ │ └── reducer.ts
│ ├── electron
│ │ ├── actions.ts
│ │ └── middleware.ts
│ ├── index.ts
│ ├── persistence.ts
│ ├── selectors.ts
│ ├── settings
│ │ ├── actions.ts
│ │ └── reducer.ts
│ ├── simperium
│ │ ├── actions.ts
│ │ ├── functions
│ │ │ ├── bucket-queue.ts
│ │ │ ├── change-announcer.ts
│ │ │ ├── connection-monitor.ts
│ │ │ ├── in-memory-bucket.ts
│ │ │ ├── in-memory-ghost.ts
│ │ │ ├── note-bucket.ts
│ │ │ ├── note-doctor.ts
│ │ │ ├── preferences-bucket.ts
│ │ │ ├── redux-ghost.ts
│ │ │ ├── tab-close-confirmation.ts
│ │ │ ├── tag-bucket.ts
│ │ │ ├── unconfirmed-changes.ts
│ │ │ └── username-monitor.ts
│ │ ├── middleware.ts
│ │ └── reducer.ts
│ └── ui
│ │ ├── actions.ts
│ │ ├── middleware.ts
│ │ ├── reducer.ts
│ │ └── search-field-middleware.ts
├── tag-email-tooltip
│ ├── index.tsx
│ └── style.scss
├── tag-field
│ ├── index.tsx
│ └── style.scss
├── tag-input
│ ├── index.tsx
│ └── style.scss
├── tag-list
│ ├── index.tsx
│ ├── input.tsx
│ └── style.scss
├── tag-suggestions
│ ├── index.tsx
│ ├── style.scss
│ └── test.tsx
├── types.ts
└── utils
│ ├── crypto-random-string.ts
│ ├── ensure-platform-support.tsx
│ ├── export
│ ├── README.md
│ ├── export-notes.ts
│ ├── index.ts
│ ├── normalize-line-break.ts
│ ├── test
│ │ └── normalize-line-breaks.ts
│ ├── to-zip.ts
│ └── types.ts
│ ├── filter-at-most.ts
│ ├── filter-notes.ts
│ ├── get-note-references.ts
│ ├── import
│ ├── README.MD
│ ├── evernote
│ │ ├── index.ts
│ │ └── test.ts
│ ├── index.ts
│ ├── multiple
│ │ └── index.ts
│ ├── simplenote
│ │ ├── index.ts
│ │ └── test.ts
│ ├── test.ts
│ └── text-files
│ │ └── index.ts
│ ├── is-dev-config
│ ├── index.ts
│ └── test.ts
│ ├── is-email-tag.ts
│ ├── note-scroll-position.ts
│ ├── note-utils.test.ts
│ ├── note-utils.ts
│ ├── platform.ts
│ ├── render-note-to-html.ts
│ ├── sanitize-html.ts
│ ├── tag-hash.ts
│ ├── task-transform.ts
│ ├── test
│ └── is-email-tag.ts
│ ├── url-utils.ts
│ └── validate-password
│ ├── index.ts
│ └── test.ts
├── package-lock.json
├── package.json
├── resources
├── appx
│ ├── Square150x150Logo.png
│ ├── Square150x150Logo.targetsize-150_altform-unplated.png
│ ├── Square44x44Logo.png
│ ├── Square44x44Logo.targetsize-44_altform-unplated.png
│ ├── StoreLogo.png
│ └── Wide310x150Logo.png
├── certificates
│ ├── mac.p12.enc
│ └── win.p12.enc
├── images
│ ├── app-icon.icns
│ ├── dmg-background.png
│ ├── dmg-background@2x.png
│ ├── dmg-icon.icns
│ ├── favicon.ico
│ ├── icon_128x128.png
│ ├── icon_16x16.png
│ ├── icon_256x256.png
│ ├── icon_32x32.png
│ ├── icon_512x512.png
│ └── simplenote.ico
├── macos
│ ├── entitlements.mac.inherit.plist
│ └── entitlements.mac.plist
└── secrets
│ ├── .gitkeep
│ └── config.json.enc
├── scss
├── _components.scss
├── _general.scss
├── _mixins.scss
├── _normalize.scss
├── _scrollbar.scss
├── _variables.scss
├── buttons.scss
├── inputs.scss
├── print.scss
├── style.scss
└── theme.scss
├── setup-tests.js
├── tsconfig.json
├── vip
├── package.json
└── webapp
│ └── index.js
└── webpack.config.js
/.buildkite/commands/install_node_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | echo "--- :npm: Install Node dependencies"
4 | # --legacy-peer-deps is necessary because of react-monaco-editor.
5 | # See README for more details
6 | npm ci --legacy-peer-deps
7 |
--------------------------------------------------------------------------------
/.buildkite/shared-pipeline-vars:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # This file is `source`'d before calling `buildkite-agent pipeline upload`, and can be used
4 | # to set up some variables that will be interpolated in the `.yml` pipeline before uploading it.
5 |
6 | # The ~> modifier is not currently used, but we check for it just in case
7 | XCODE_VERSION=$(sed -E -n 's/^(~> )?(.*)/xcode-\2/p' .xcode-version)
8 | CI_TOOLKIT_PLUGIN_VERSION='5.1.2'
9 | NVM_PLUGIN_VERSION='0.6.0'
10 |
11 | export IMAGE_ID="$XCODE_VERSION"
12 | export CI_TOOLKIT_PLUGIN="automattic/a8c-ci-toolkit#$CI_TOOLKIT_PLUGIN_VERSION"
13 | export NVM_PLUGIN="automattic/nvm#$NVM_PLUGIN_VERSION"
14 |
--------------------------------------------------------------------------------
/.bundle/config:
--------------------------------------------------------------------------------
1 | ---
2 | BUNDLE_PATH: "vendor/bundle"
3 | BUNDLE_JOBS: "3"
4 | BUNDLE_RETRY: "3"
5 |
--------------------------------------------------------------------------------
/.configure:
--------------------------------------------------------------------------------
1 | {
2 | "project_name": "simplenote-electron",
3 | "branch": "trunk",
4 | "pinned_hash": "bbd0130868d58b2e59c909433df83f4ae6721b76",
5 | "files_to_copy": [
6 | {
7 | "file": "iOS/app_store_connect_fastlane_api_key.p8",
8 | "destination": "~/.configure/simplenote-electron/secrets/app_store_connect_api_key.p8",
9 | "encrypt": true
10 | }
11 | ],
12 | "file_dependencies": [
13 |
14 | ]
15 | }
--------------------------------------------------------------------------------
/.configure-files/app_store_connect_fastlane_api_key.p8.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/.configure-files/app_store_connect_fastlane_api_key.p8.enc
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [Makefile]
16 | indent_style = tab
17 | tab_width = 2
18 |
19 | [package.json]
20 | indent_style = space
21 | indent_size = 2
22 |
23 | [*.{diff,md}]
24 | trim_trailing_whitespace = false
25 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Explicitly treat .enc files as binaries to prevent GitHub from occasionally
2 | # reporting changes to these files as "empty" instead of saying "binary file
3 | # not shown"
4 | *.enc binary
5 |
--------------------------------------------------------------------------------
/.githooks/pre-commit:
--------------------------------------------------------------------------------
1 | npx pretty-quick --staged
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41E Bug Report"
3 | about: "Report a bug if something isn't working as expected in the Windows, Linux, or Web Simplenote app."
4 | title: ""
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 |
10 |
11 |
12 |
13 | ### Expected
14 |
15 |
16 | ### Observed
17 |
18 |
19 | ### Reproduced
20 |
26 |
27 | 1.
28 | 2.
29 | 3.
30 |
31 |
32 |
33 | ### Where did you see the bug
34 |
35 | - System Make:
36 | - System Model:
37 | - OS:
38 | - OS version:
39 | - Browser (if applicable):
40 | - Browser version (if applicable):
41 | - Simplenote app version:
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F680 Feature Request"
3 | about: "Suggest a new feature or enhancement to an existing one in the Simplenote Electron app."
4 | title: ""
5 | labels: feature request
6 | assignees: ""
7 | ---
8 |
9 |
10 |
11 |
12 |
13 | ### What
14 |
15 |
16 | ### Why
17 |
18 |
19 | ### How
20 |
21 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Fix
2 |
3 |
8 |
9 | ### Test
10 |
11 |
17 |
18 | 1.
19 | 2.
20 | 3.
21 |
22 | ### Release
23 |
24 |
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | config-local.json
3 | desktop-build/
4 | dist/
5 | logs/
6 | release/
7 | vendor/
8 | *.pvk
9 | *.p12
10 | *.spc
11 | *.tgz
12 | .idea
13 | .DS_Store
14 | dev-app-update.yml
15 | lib/state/data/test_account.json
16 | # Ignore all files in the .configure-files folder apart from the encoded ones
17 | !.configure-files/*.enc
18 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.10.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | dist
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | NewCops: enable
3 |
4 | Metrics/BlockLength:
5 | Exclude: &fastlane
6 | - fastlane/Fastfile
7 |
8 | Metrics/MethodLength:
9 | Max: 30
10 | Exclude: *fastlane
11 |
12 | Layout/LineLength:
13 | Max: 180
14 | Exclude: *fastlane
15 |
16 | Layout/EmptyLines:
17 | Exclude: *fastlane
18 |
19 | Style/AsciiComments:
20 | Exclude: *fastlane
21 |
22 | Metrics/AbcSize:
23 | Exclude: *fastlane
24 |
25 | Metrics/CyclomaticComplexity:
26 | Exclude: *fastlane
27 |
28 | Metrics/PerceivedComplexity:
29 | Exclude: *fastlane
30 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.2.2
2 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "warning",
3 | "extends": ["stylelint-config-standard", "stylelint-config-standard-scss"],
4 | "plugins": ["stylelint-prettier"],
5 | "rules": {
6 | "alpha-value-notation": null,
7 | "color-function-notation": "legacy",
8 | "declaration-property-value-no-unknown": null,
9 | "prettier/prettier": true,
10 | "no-descending-specificity": null,
11 | "selector-class-pattern": [
12 | "^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__([a-z][a-z0-9]*)(-[a-z0-9]+)*)?(--([a-z][a-z0-9]*)(-[a-z0-9]+)*)?)$",
13 | {
14 | "message": "Expected BEM naming convention for class."
15 | }
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Debug Main Process",
6 | "type": "node",
7 | "request": "launch",
8 | "cwd": "${workspaceRoot}",
9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
10 | "windows": {
11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
12 | },
13 | "args": ["."]
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.xcode-version:
--------------------------------------------------------------------------------
1 | 15.4
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | # Normally, we'd let fastlane-plugin-wpmreleasetoolkit transitively fetch Fastlane.
6 | # But there's a bug in the version it resolves by default that we need to fix for while we wait for the plugin to update the resolved version.
7 | #
8 | # 2.219.0 includes a fix for a bug introduced in 2.218.0
9 | # See https://github.com/fastlane/fastlane/issues/21762#issuecomment-1875208663
10 | gem 'fastlane', '~> 2.219'
11 | # This comment avoids typing to switch to a development version for testing.
12 | #
13 | # gem 'fastlane-plugin-wpmreleasetoolkit', git: 'https://github.com/wordpress-mobile/release-toolkit', ref: ''
14 | gem 'fastlane-plugin-wpmreleasetoolkit', '~> 11.0'
15 | # TODO: Remove once Dangermattic is set up
16 | gem 'rubocop', '~> 1.61'
17 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 |
4 | const presets = [
5 | [
6 | '@babel/preset-env',
7 | {
8 | corejs: '3',
9 | include: [
10 | 'transform-class-properties',
11 | 'transform-object-rest-spread',
12 | 'transform-optional-chaining',
13 | ],
14 | useBuiltIns: 'entry',
15 | targets: {
16 | esmodules: true,
17 | },
18 | },
19 | ],
20 | '@babel/preset-react',
21 | '@babel/typescript',
22 | ];
23 | const plugins = [
24 | '@babel/plugin-syntax-dynamic-import',
25 | '@babel/plugin-transform-runtime',
26 | ];
27 | const env = {
28 | development: {
29 | compact: false,
30 | },
31 | test: {
32 | plugins: ['dynamic-import-node'],
33 | },
34 | };
35 |
36 | return {
37 | presets,
38 | plugins,
39 | env,
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "app_id": "history-analyst-dad",
3 | "app_key": "be606bcfa3db4377bf488900281aa1cc",
4 | "development": true,
5 | "wpcc_client_id": "0",
6 | "wpcc_redirect_url": "https://simplenote.com"
7 | }
8 |
--------------------------------------------------------------------------------
/desktop/app-quit/README.md:
--------------------------------------------------------------------------------
1 | App Quit
2 | =========
3 |
4 | Determines whether the app should quit and exit, or quit to background.
5 |
6 | Note that the implementation of quit-to-background is left up to `platform`.
7 |
--------------------------------------------------------------------------------
/desktop/app-quit/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Module variables
5 | */
6 | let quitter = false;
7 |
8 | function AppQuit() {
9 | this.canQuit = false;
10 | }
11 |
12 | AppQuit.prototype.shouldQuitToBackground = function () {
13 | if (this.canQuit) {
14 | this.canQuit = false;
15 | return false;
16 | }
17 |
18 | return true;
19 | };
20 |
21 | AppQuit.prototype.allowQuit = function () {
22 | this.canQuit = true;
23 | };
24 |
25 | if (!quitter) {
26 | quitter = new AppQuit();
27 | }
28 |
29 | module.exports = quitter;
30 |
--------------------------------------------------------------------------------
/desktop/config-updater.json:
--------------------------------------------------------------------------------
1 | {
2 | "updater": {
3 | "downloadUrl": "https://github.com/Automattic/simplenote-electron/releases/latest",
4 | "changelogUrl": "https://github.com/Automattic/simplenote-electron/blob/trunk/RELEASE-NOTES.md",
5 | "apiUrl": "https://api.github.com/repos/automattic/simplenote-electron/releases/latest",
6 | "delay": 2000,
7 | "interval": 600000
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/desktop/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const pkg = require('../../package.json');
4 | let config = require('../config-updater.json');
5 |
6 | // Merge in some details from package.json
7 | config.name = pkg.productName;
8 | config.description = 'Simplenote Desktop';
9 | config.version = pkg.version;
10 | config.author = pkg.author;
11 |
12 | module.exports = config;
13 |
--------------------------------------------------------------------------------
/desktop/context-menu/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * External dependencies
5 | */
6 | const { Menu } = require('electron');
7 |
8 | const { editorCommandSender } = require('../menus/utils');
9 |
10 | module.exports = function (mainWindow) {
11 | mainWindow.webContents.on('context-menu', (event, params) => {
12 | const { editFlags } = params;
13 | Menu.buildFromTemplate([
14 | {
15 | id: 'selectAll',
16 | label: 'Select All',
17 | click: editorCommandSender({ action: 'selectAll' }),
18 | enabled: editFlags.canSelectAll,
19 | },
20 | {
21 | id: 'cut',
22 | label: 'Cut',
23 | role: 'cut',
24 | enabled: editFlags.canCut,
25 | },
26 | {
27 | id: 'copy',
28 | label: 'Copy',
29 | role: 'copy',
30 | enabled: editFlags.canCopy,
31 | },
32 | {
33 | id: 'paste',
34 | label: 'Paste',
35 | role: 'paste',
36 | enabled: editFlags.canPaste,
37 | },
38 | {
39 | type: 'separator',
40 | },
41 | ]).popup({});
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/desktop/detect/build/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Module variables
5 | */
6 | let build = false;
7 |
8 | function Build() {
9 | this.build = false;
10 | }
11 |
12 | // These process properties can either be true or undefined
13 |
14 | Build.prototype.isMAS = function () {
15 | return Boolean(process.mas);
16 | };
17 |
18 | Build.prototype.isWindowsStore = function () {
19 | return Boolean(process.windowsStore);
20 | };
21 |
22 | if (!build) {
23 | build = new Build();
24 | }
25 |
26 | module.exports = build;
27 |
--------------------------------------------------------------------------------
/desktop/detect/platform/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Module variables
5 | */
6 | let platform = false;
7 |
8 | function Platform() {
9 | this.platform = false;
10 | }
11 |
12 | Platform.prototype.isOSX = function () {
13 | return process.platform === 'darwin';
14 | };
15 |
16 | Platform.prototype.isWindows = function () {
17 | return process.platform === 'win32';
18 | };
19 |
20 | Platform.prototype.isLinux = function () {
21 | return process.platform === 'linux';
22 | };
23 |
24 | if (!platform) {
25 | platform = new Platform();
26 | }
27 |
28 | module.exports = platform;
29 |
--------------------------------------------------------------------------------
/desktop/env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | isDev:
3 | process.env.NODE_ENV === 'development' ||
4 | process.defaultApp ||
5 | /node_modules[\\/]electron[\\/]/.test(process.execPath),
6 | };
7 |
--------------------------------------------------------------------------------
/desktop/evernote-import/test/correct-markdown.txt:
--------------------------------------------------------------------------------
1 | (image/png)
2 |
3 | * item 1
4 | * item 2
5 | * item 3
6 |
7 | 1. ordered item 1
8 | 2. ordered item 2
9 |
10 | - [x] task item checked
11 | - [ ] task item unchecked
12 |
13 | This is a [link](https://google.com/)
14 | https://google.com
15 |
16 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
17 |
18 | Lorem **bold** dolor *italic* amet.
19 |
20 | ```
21 | var variable = true;
22 | ```
23 |
24 | Another line.
25 |
26 | (audio/wav)
27 |
--------------------------------------------------------------------------------
/desktop/evernote-import/test/mock-enml.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | -
8 |
item 1
9 |
10 | -
11 |
item 2
12 |
13 | -
14 |
item 3
15 |
16 |
17 |
18 |
19 | -
20 |
ordered item 1
21 |
22 | -
23 |
ordered item 2
24 |
25 |
26 |
27 |
28 |
29 | task item checked
30 |
31 |
32 |
33 | task item unchecked
34 |
35 |
36 |
37 |
38 |
39 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
40 |
41 | Lorem bold dolor italic amet.
42 |
43 |
44 |
var variable = true;
45 |
46 |
47 | Another line.
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/desktop/index.js:
--------------------------------------------------------------------------------
1 | require('./app')();
2 |
--------------------------------------------------------------------------------
/desktop/logger/archiver.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * External dependencies
5 | */
6 | const fs = require('fs');
7 | const JSZip = require('jszip');
8 |
9 | /**
10 | * Internal dependencies
11 | */
12 | const log = require('./')('desktop:lib:archiver');
13 |
14 | module.exports = {
15 | /**
16 | * Compresses `contents` to the archive at `dst`.
17 | *
18 | * @param {String[]} contents Paths to be zipped
19 | * @param {String} dst Path to destination archive
20 | * @param {function():void} onZipped Callback invoked when archive is complete
21 | */
22 | zipContents: (logPath, dst, onZipped) => {
23 | const zip = new JSZip();
24 | zip.file(
25 | 'simplenote.log',
26 | new Promise((resolve, reject) =>
27 | fs.readFile(logPath, (error, data) => {
28 | if (error) {
29 | log.warn('Unexpected error: ', error);
30 | reject(error);
31 | } else {
32 | resolve(data);
33 | }
34 | })
35 | )
36 | );
37 |
38 | const output = fs.createWriteStream(dst);
39 |
40 | // Catch warnings (e.g. stat failures and other non-blocking errors)
41 | output.on('warning', function (err) {
42 | log.warn('Unexpected error: ', err);
43 | });
44 |
45 | output.on('error', function (err) {
46 | throw err;
47 | });
48 |
49 | zip
50 | .generateNodeStream({ type: 'nodebuffer', streamFiles: true })
51 | .pipe(output)
52 | .on('finish', function () {
53 | log.debug('Archive finalized');
54 | if (typeof onZipped === 'function') {
55 | onZipped();
56 | }
57 | });
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/desktop/logger/namespaces/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Credit: Modified from `winston-namespace` by @SetaSouto:
3 | * https://github.com/SetaSouto/winston-namespace
4 | */
5 |
6 | module.exports = {
7 | /**
8 | * Boolean indicating if the object is populated with the environment data.
9 | */
10 | populated: false,
11 | /**
12 | * Populates the private data 'namespaces' as an array with the environment from the NODE_ENV
13 | * environment variable. It splits the data with ',' as separator.
14 | * @private
15 | */
16 | populate: function () {
17 | this.namespaces = [process.env.NODE_ENV === 'production' ? '' : '*'];
18 | this.populated = true;
19 | },
20 | /**
21 | * Checks if the namespace is available to debug. The namespace could be contained in wildcards.
22 | * Ex: 'server:api:controller' would pass the check (return true) if the 'server:api:controller' is in the
23 | * environment variable or if 'server:api:*' or 'server:*' is in the environment variable.
24 | * @param namespace {String} - Namespace to check.
25 | * @returns {boolean} Whether or not the namespace is available.
26 | */
27 | check: function (namespace) {
28 | if (!this.populated) {
29 | this.populate();
30 | }
31 |
32 | if (this.namespaces.includes('*')) {
33 | return true;
34 | }
35 |
36 | if (this.namespaces.includes(namespace)) {
37 | return true;
38 | }
39 |
40 | return namespace
41 | .split(':')
42 | .some((_, level, levels) =>
43 | this.namespaces.includes(levels.slice(0, level).join(':') + ':*')
44 | );
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/desktop/menus/file-menu.js:
--------------------------------------------------------------------------------
1 | const menuItems = require('./menu-items');
2 | const platform = require('../detect/platform');
3 | const { appCommandSender } = require('./utils');
4 |
5 | const buildFileMenu = (isAuthenticated) => {
6 | isAuthenticated = isAuthenticated || false;
7 |
8 | let submenu = [];
9 |
10 | if (isAuthenticated) {
11 | submenu = [
12 | {
13 | label: '&New Note',
14 | accelerator: 'CommandOrControl+Shift+I',
15 | click: appCommandSender({ action: 'newNote' }),
16 | },
17 | { type: 'separator' },
18 | {
19 | label: '&Import Notes…',
20 | click: appCommandSender({
21 | action: 'showDialog',
22 | dialog: 'IMPORT',
23 | }),
24 | },
25 | {
26 | label: '&Export Notes…',
27 | accelerator: 'CommandOrControl+Shift+E',
28 | click: appCommandSender({
29 | action: 'exportNotes',
30 | }),
31 | },
32 | { type: 'separator' },
33 | {
34 | label: '&Print…',
35 | accelerator: 'CommandOrControl+P',
36 | click: appCommandSender({ action: 'printNote' }),
37 | },
38 | ];
39 | }
40 |
41 | const defaultSubmenuAdditions = [
42 | { type: 'separator' },
43 | menuItems.preferences(isAuthenticated),
44 | ...(isAuthenticated ? [{ type: 'separator' }] : []),
45 | { role: 'quit' },
46 | ];
47 |
48 | const fileMenu = {
49 | label: '&File',
50 | submenu: platform.isOSX()
51 | ? submenu
52 | : submenu.concat(defaultSubmenuAdditions),
53 | };
54 |
55 | // we have nothing to show in the File menu on OSX logged out
56 | if (!isAuthenticated && platform.isOSX()) {
57 | fileMenu.visible = false;
58 | }
59 |
60 | return fileMenu;
61 | };
62 |
63 | module.exports = buildFileMenu;
64 |
--------------------------------------------------------------------------------
/desktop/menus/format-menu.js:
--------------------------------------------------------------------------------
1 | const { editorCommandSender } = require('./utils');
2 |
3 | const buildFormatMenu = (isAuthenticated, editMode) => {
4 | isAuthenticated = isAuthenticated || false;
5 | editMode = editMode || false;
6 | const submenu = [
7 | {
8 | label: 'Insert &Checklist',
9 | accelerator: 'CommandOrControl+Shift+C',
10 | click: editorCommandSender({ action: 'insertChecklist' }),
11 | enabled: editMode,
12 | },
13 | ];
14 |
15 | const formatMenu = {
16 | label: 'F&ormat',
17 | submenu,
18 | };
19 |
20 | // we have nothing to show in this menu if not logged in
21 | return isAuthenticated ? formatMenu : null;
22 | };
23 |
24 | module.exports = buildFormatMenu;
25 |
--------------------------------------------------------------------------------
/desktop/menus/help-menu.js:
--------------------------------------------------------------------------------
1 | const { shell } = require('electron');
2 |
3 | const menuItems = require('./menu-items');
4 | const platform = require('../detect/platform');
5 | const build = require('../detect/build');
6 | const log = require('../logger')('desktop:menu:help');
7 | const zipLogs = require('../logger/zip-logs');
8 |
9 | const { appCommandSender } = require('./utils');
10 |
11 | const buildHelpMenu = (mainWindow, isAuthenticated) => {
12 | isAuthenticated = isAuthenticated || false;
13 | const submenu = [
14 | {
15 | label: 'Help && &Support',
16 | accelerator: platform.isLinux() ? 'F1' : null,
17 | click: () => shell.openExternal('https://simplenote.com/help'),
18 | },
19 | {
20 | label: '&Keyboard Shortcuts',
21 | visible: isAuthenticated,
22 | click: appCommandSender({
23 | action: 'showDialog',
24 | dialog: 'KEYBINDINGS',
25 | }),
26 | },
27 | { type: 'separator' },
28 | {
29 | label: 'Advanced',
30 | submenu: [
31 | {
32 | label: 'Debugging Console',
33 | click: (item, focusedWindow) => focusedWindow?.toggleDevTools(),
34 | },
35 | ],
36 | },
37 | {
38 | type: 'separator',
39 | },
40 | {
41 | label: 'Get Application Logs',
42 | click: function () {
43 | log.info("User selected 'Get Application Logs'...");
44 | zipLogs(mainWindow);
45 | },
46 | },
47 | ];
48 |
49 | const defaultSubmenuAdditions = [
50 | { type: 'separator' },
51 | ...(build.isWindowsStore() ? [] : [menuItems.checkForUpdates]),
52 | menuItems.about,
53 | ];
54 |
55 | const helpMenu = {
56 | label: '&Help',
57 | role: 'help',
58 | submenu: platform.isOSX()
59 | ? submenu
60 | : submenu.concat(defaultSubmenuAdditions),
61 | };
62 |
63 | return helpMenu;
64 | };
65 |
66 | module.exports = buildHelpMenu;
67 |
--------------------------------------------------------------------------------
/desktop/menus/index.js:
--------------------------------------------------------------------------------
1 | const platform = require('../detect/platform');
2 |
3 | const buildMacAppMenu = require('./mac-app-menu');
4 | const buildFileMenu = require('./file-menu');
5 | const buildEditMenu = require('./edit-menu');
6 | const buildViewMenu = require('./view-menu');
7 | const buildFormatMenu = require('./format-menu');
8 | const buildHelpMenu = require('./help-menu');
9 |
10 | function createMenuTemplate(args, mainWindow) {
11 | args = args || {};
12 | const settings = args['settings'] || {};
13 | const isAuthenticated = settings && 'accountName' in settings;
14 | const editMode = args['editMode'] || false;
15 | const windowMenu = {
16 | role: 'window',
17 | submenu: [
18 | { role: 'minimize' },
19 | { role: 'close' },
20 | { type: 'separator' },
21 | { role: 'front' },
22 | ],
23 | };
24 |
25 | return [
26 | platform.isOSX() ? buildMacAppMenu(isAuthenticated) : null,
27 | buildFileMenu(isAuthenticated),
28 | buildEditMenu(settings, isAuthenticated, editMode),
29 | buildViewMenu(settings, isAuthenticated),
30 | buildFormatMenu(isAuthenticated, editMode),
31 | platform.isOSX() ? windowMenu : null,
32 | buildHelpMenu(mainWindow, isAuthenticated),
33 | ].filter((menu) => menu !== null);
34 | }
35 |
36 | module.exports = createMenuTemplate;
37 |
--------------------------------------------------------------------------------
/desktop/menus/mac-app-menu.js:
--------------------------------------------------------------------------------
1 | const { app } = require('electron');
2 |
3 | const menuItems = require('./menu-items');
4 | const build = require('../detect/build');
5 |
6 | const buildMacAppMenu = (isAuthenticated) => {
7 | var submenu = [];
8 | isAuthenticated = isAuthenticated || false;
9 |
10 | submenu = [
11 | menuItems.about,
12 | ...(build.isMAS() ? [] : [menuItems.checkForUpdates]),
13 | { type: 'separator' },
14 | menuItems.preferences(isAuthenticated),
15 | ...(isAuthenticated ? [{ type: 'separator' }] : []),
16 | {
17 | role: 'services',
18 | submenu: [],
19 | },
20 | { type: 'separator' },
21 | { role: 'hide' },
22 | { role: 'hideothers' },
23 | { role: 'unhide' },
24 | ...(isAuthenticated
25 | ? [
26 | { type: 'separator' },
27 | menuItems.emptyTrash(isAuthenticated),
28 | menuItems.signout(isAuthenticated),
29 | ]
30 | : []),
31 | { type: 'separator' },
32 | { role: 'quit' },
33 | ];
34 |
35 | const menu = {
36 | label: app.name,
37 | submenu: submenu,
38 | };
39 |
40 | return menu;
41 | };
42 |
43 | module.exports = buildMacAppMenu;
44 |
--------------------------------------------------------------------------------
/desktop/menus/menu-items.js:
--------------------------------------------------------------------------------
1 | const { app } = require('electron');
2 |
3 | const { appCommandSender } = require('./utils');
4 | const updater = require('../updater');
5 | const { autoUpdater } = require('electron-updater');
6 |
7 | const about = {
8 | label: '&About ' + app.name,
9 | click: appCommandSender({
10 | action: 'showDialog',
11 | dialog: 'ABOUT',
12 | }),
13 | };
14 |
15 | const checkForUpdates = {
16 | label: '&Check for Updates…',
17 | enabled: autoUpdater.isUpdaterActive(),
18 | click: updater.pingAndShowProgress.bind(updater),
19 | };
20 |
21 | const emptyTrash = (isAuthenticated) => {
22 | return {
23 | label: '&Empty Trash',
24 | visible: isAuthenticated,
25 | click: appCommandSender({ action: 'emptyTrash' }),
26 | };
27 | };
28 |
29 | const preferences = (isAuthenticated) => {
30 | return {
31 | label: 'P&references…',
32 | visible: isAuthenticated,
33 | accelerator: 'CommandOrControl+,',
34 | click: appCommandSender({
35 | action: 'showDialog',
36 | dialog: 'SETTINGS',
37 | }),
38 | };
39 | };
40 |
41 | const signout = (isAuthenticated) => {
42 | return {
43 | label: '&Sign Out',
44 | visible: isAuthenticated,
45 | click: appCommandSender({
46 | action: 'logout',
47 | }),
48 | };
49 | };
50 |
51 | module.exports = {
52 | about,
53 | checkForUpdates,
54 | emptyTrash,
55 | preferences,
56 | signout,
57 | };
58 |
--------------------------------------------------------------------------------
/desktop/menus/test/help-menu.js:
--------------------------------------------------------------------------------
1 | import helpMenu from '../help-menu';
2 | import '../menu-items';
3 |
4 | jest.mock('../menu-items', () => ({
5 | checkForUpdates: 'checkForUpdates',
6 | }));
7 |
8 | jest.mock('../../detect/platform', () => ({
9 | isLinux: jest.fn().mockReturnValue(false),
10 | isOSX: jest.fn().mockReturnValue(false),
11 | isWindows: jest.fn().mockReturnValue(true),
12 | }));
13 |
14 | jest.mock('../../detect/build', () => ({
15 | isWindowsStore: jest.fn().mockReturnValue(true),
16 | }));
17 |
18 | describe('Help Menu', () => {
19 | it('should not show Check for Updates menu item if Windows Store app', () => {
20 | expect(helpMenu.submenu).not.toEqual(
21 | expect.arrayContaining(['checkForUpdates'])
22 | );
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/desktop/menus/test/utils.js:
--------------------------------------------------------------------------------
1 | import { buildRadioGroup } from '../utils';
2 |
3 | describe('Menu Utils', () => {
4 | describe('buildRadioGroup', () => {
5 | const menuItems = [
6 | {
7 | id: 'myValue',
8 | passThroughProp: 'passThroughValue',
9 | },
10 | {
11 | id: 'myUnselectedValue',
12 | },
13 | ];
14 | const args = {
15 | action: 'myAction',
16 | propName: 'myProp',
17 | settings: { myProp: 'myValue' },
18 | };
19 | const result = menuItems.map(buildRadioGroup(args));
20 |
21 | test('should pass through any props', () => {
22 | expect(result[0].passThroughProp).toBe('passThroughValue');
23 | });
24 |
25 | test('should set the correct item as "checked"', () => {
26 | expect(result[0].checked).toBe(true);
27 | expect(result[1].checked).toBe(false);
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/desktop/menus/utils.js:
--------------------------------------------------------------------------------
1 | const buildRadioGroup = ({ action, propName, settings }) => {
2 | return (item) => {
3 | const { id, ...props } = item;
4 |
5 | return {
6 | type: 'radio',
7 | checked: id === settings[propName],
8 | click: appCommandSender({
9 | action,
10 | [propName]: id,
11 | }),
12 | ...props,
13 | };
14 | };
15 | };
16 |
17 | const appCommandSender = (arg) => {
18 | return commandSender('appCommand', arg);
19 | };
20 |
21 | const editorCommandSender = (arg) => {
22 | return commandSender('editorCommand', arg);
23 | };
24 |
25 | const commandSender = (commandName, arg) => {
26 | return (item, focusedWindow) => {
27 | if (typeof focusedWindow !== 'undefined') {
28 | focusedWindow.webContents.send(commandName, arg);
29 | }
30 | };
31 | };
32 |
33 | module.exports = {
34 | buildRadioGroup,
35 | appCommandSender,
36 | editorCommandSender,
37 | };
38 |
--------------------------------------------------------------------------------
/desktop/updater/README.md:
--------------------------------------------------------------------------------
1 | Updater
2 | =========
3 |
4 | Uses either the manual or auto updater to check for updates.
5 |
6 | The config `updater.url` is used as the base URL and has the platform, version, and beta setting appended.
7 |
8 | The updater uses:
9 |
10 | - [Auto Updater](auto-updater/README.md)
11 | - [Manual Updater](manual-updater/README.md)
12 |
--------------------------------------------------------------------------------
/desktop/updater/auto-updater/README.md:
--------------------------------------------------------------------------------
1 | Auto Updater
2 | =========
3 |
4 | Handles Squirrel auto-update events. Note that the app must be signed for this to work.
5 |
6 | ## Functions
7 |
8 | `ping()` - checks for an update
9 |
--------------------------------------------------------------------------------
/desktop/updater/auto-updater/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * External Dependencies
5 | */
6 | const { autoUpdater } = require('electron-updater');
7 |
8 | /**
9 | * Internal dependencies
10 | */
11 | const Updater = require('../lib/Updater');
12 | const AppQuit = require('../../app-quit');
13 | const setupProgressUpdates = require('../lib/setup-progress-updates');
14 |
15 | class AutoUpdater extends Updater {
16 | constructor({ changelogUrl, options = {} }) {
17 | super(changelogUrl, options);
18 |
19 | autoUpdater.on('error', this.onError.bind(this));
20 | autoUpdater.on('update-not-available', this.onNotAvailable.bind(this));
21 | autoUpdater.on('update-downloaded', this.onDownloaded.bind(this));
22 |
23 | autoUpdater.autoInstallOnAppQuit = false;
24 | }
25 |
26 | // For non-user-initiated checks.
27 | // Check and download in the background, and only notify the user if
28 | // an update exists and has completed downloading.
29 | ping() {
30 | autoUpdater.checkForUpdates();
31 | }
32 |
33 | // For user-initiated checks.
34 | // Will check and download, displaying progress dialogs.
35 | pingAndShowProgress() {
36 | setupProgressUpdates({ updater: autoUpdater, willAutoDownload: true });
37 | autoUpdater.checkForUpdates();
38 | }
39 |
40 | onConfirm() {
41 | AppQuit.allowQuit();
42 | autoUpdater.quitAndInstall();
43 | }
44 | }
45 |
46 | module.exports = AutoUpdater;
47 |
--------------------------------------------------------------------------------
/desktop/updater/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Internal dependencies
5 | */
6 | const platform = require('../detect/platform');
7 | const config = require('../config');
8 | const AutoUpdater = require('./auto-updater');
9 | const ManualUpdater = require('./manual-updater');
10 |
11 | let updater = false;
12 |
13 | if (platform.isOSX() || platform.isWindows() || process.env.APPIMAGE) {
14 | updater = new AutoUpdater({
15 | changelogUrl: config.updater.changelogUrl,
16 | });
17 | } else {
18 | updater = new ManualUpdater({
19 | downloadUrl: config.updater.downloadUrl,
20 | apiUrl: config.updater.apiUrl,
21 | changelogUrl: config.updater.changelogUrl,
22 | options: {
23 | dialogMessage:
24 | '{name} {newVersion} is now available — you have {currentVersion}. Would you like to download it now?',
25 | confirmLabel: 'Download',
26 | },
27 | });
28 | }
29 |
30 | module.exports = updater;
31 |
--------------------------------------------------------------------------------
/desktop/updater/manual-updater/README.md:
--------------------------------------------------------------------------------
1 | Manual Updater
2 | =========
3 |
4 | Provides a manual updater on platforms where the auto updater does not work.
5 |
6 | If an update is detected then a dialog is shown with a link to the update for download.
7 |
8 | The update is shown once until the app is restarted.
9 |
10 | ## Functions
11 |
12 | `ping()` - checks for an update
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | install:
5 | image: node:8.16.1
6 | command: npm install
7 | volumes:
8 | - $PWD:/app
9 | working_dir: /app
10 |
11 | dev:
12 | image: node:8.16.1
13 | command: npm start
14 | ports:
15 | - "4000:4000"
16 | volumes:
17 | - $PWD:/app
18 | working_dir: /app
19 |
--------------------------------------------------------------------------------
/docs/packaging.md:
--------------------------------------------------------------------------------
1 | # Building a Release
2 |
3 | * `make osx`
4 | * `make linux`
5 | * `make win32`
6 |
7 | # Packaging
8 |
9 | Some builds require further packaging before they can be released:
10 |
11 | * `make package-win32` - Produces a signed `Setup.exe` install wizard
12 | * `make package-osx` - Produces a `DMG` file
13 | * `make package-linux` - Produces a `.deb`, `.rpm` and `.AppImage` file for `x86` and `x64` processors
14 |
15 |
16 | # Requirements
17 |
18 | ## Mac Package
19 |
20 | A Mac build requires the app to be signed. This prevents a security warning being issued when you run the app.
21 |
22 | You can obtain all the appropriate signing certificates from an Apple Developer account.
23 |
24 | Note that you need the certificates installed prior to building.
25 |
26 | ## Windows Package
27 |
28 | The Windows package requires installing a valid certificate, installing the `makensis`, `wine` and `mono` packages, installable from `brew`.
29 |
30 | `brew install mono wine makensis`
31 |
32 | The Windows build doesn't get signed until the packaging stage.
33 |
34 | ## Linux Package
35 |
36 | The Linux package is built using [electron-builder][1] which is a tool that makes it easy to build different package systems. electron-builder should be installed by `npm install`.
37 |
38 | ### Note for creating Linux package on Linux
39 |
40 | 1. Creating all Linux packages requires tool for converting `.icns` file to `.png`. May be installed via `apt` by typing:
41 |
42 | `sudo apt install --no-install-recommends -y icnsutils `
43 |
44 | 2. Creating `.rpm` package requires `rpm` package, installable from 'apt'.
45 |
46 | `sudo apt install --no-install-recommends -y rpm`
47 |
48 | ### Note for creating Linux package on MacOS:
49 |
50 | 1. Creating Linux package on MacOS requires `rpm` package, installable from `brew`.
51 |
52 | `brew install rpm`
53 |
54 | [1]: https://www.electron.build/
55 |
--------------------------------------------------------------------------------
/electron-builder-appx.json:
--------------------------------------------------------------------------------
1 | {
2 | "productName": "Simplenote",
3 | "appId": "com.automattic.simplenote",
4 | "directories": {
5 | "output": "./release",
6 | "buildResources": "./resources"
7 | },
8 | "files": ["desktop", "dist", "shared"],
9 | "win": {
10 | "icon": "resources/images/simplenote.ico",
11 | "target": [
12 | {
13 | "target": "appx",
14 | "arch": ["ia32", "x64"]
15 | }
16 | ]
17 | },
18 | "appx": {
19 | "applicationId": "Simplenote",
20 | "identityName": "22490Automattic.Simplenote",
21 | "publisher": "CN=E2E5A157-746D-4B04-9116-ABE5CB928306",
22 | "publisherDisplayName": "Automattic, Inc.",
23 | "backgroundColor": "transparent",
24 | "showNameOnTiles": true,
25 | "artifactName": "Simplenote-win-store-${version}-${arch}.${ext}"
26 | },
27 | "protocols": [
28 | {
29 | "name": "simplenote",
30 | "schemes": ["simplenote"]
31 | }
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/fastlane/.gitignore:
--------------------------------------------------------------------------------
1 | report.xml
2 | README.md
3 |
--------------------------------------------------------------------------------
/fastlane/example.env:
--------------------------------------------------------------------------------
1 | # You don't need to fill in these values for Fastlane to run.
2 | #
3 | # However, if a lane requires some of these environment values and they are not set here, it will fail.
4 | APP_STORE_CONNECT_API_KEY_KEY_ID=
5 | APP_STORE_CONNECT_API_KEY_ISSUER_ID=
6 | APP_STORE_CONNECT_API_KEY_KEY=
7 | MATCH_PASSWORD=
8 | MATCH_S3_ACCESS_KEY=
9 | MATCH_S3_SECRET_ACCESS_KEY=
10 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ['/setup-tests.js'],
3 | globals: {
4 | config: {
5 | appVersion: 'foo',
6 | },
7 | },
8 | roots: ['desktop', 'lib'],
9 | testEnvironment: 'jsdom',
10 | testRegex: '(/test/.*\\.[jt]sx?)|(test\\.[jt]sx?)$',
11 | };
12 |
--------------------------------------------------------------------------------
/lib/alternate-login-prompt/style.scss:
--------------------------------------------------------------------------------
1 | .alternate-login__overlay {
2 | position: fixed;
3 | inset: 0;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | background: var(--overlay-color);
8 | }
9 |
10 | .alternate-login__dismiss {
11 | display: flex;
12 | height: 60px;
13 | justify-content: flex-end;
14 | align-items: center;
15 | margin: 0 16px;
16 | width: 100%;
17 |
18 | svg {
19 | height: 16px;
20 | width: 16px;
21 | margin: auto 0;
22 | }
23 | }
24 |
25 | .alternate-login__email {
26 | word-break: break-all;
27 | }
28 |
29 | .alternate-login__modal {
30 | align-items: center;
31 | background-color: var(--background-color);
32 | border-radius: 8px;
33 | color: var(--primary-color);
34 | display: flex;
35 | flex-direction: column;
36 | justify-content: center;
37 | margin: 20%;
38 | max-width: 420px;
39 | padding: 0 24px 16px;
40 | font-size: 16px;
41 |
42 | .icon-warning {
43 | color: var(--foreground-color);
44 | }
45 |
46 | p {
47 | margin: 0 26px;
48 | margin-block-start: 0;
49 | }
50 |
51 | p:not(:last-of-type) {
52 | padding-bottom: 20px;
53 | }
54 |
55 | .alternate-login__button-row {
56 | padding-top: 10px;
57 | padding-bottom: 10px;
58 | display: flex;
59 | justify-content: flex-end;
60 | flex-flow: row wrap;
61 | width: 100%;
62 |
63 | a {
64 | padding-right: 12px;
65 | }
66 |
67 | .button-borderless {
68 | color: var(--accent-color);
69 | }
70 | }
71 |
72 | &:focus {
73 | outline: 0;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/analytics/README.md:
--------------------------------------------------------------------------------
1 | Analytics
2 | =========
3 |
4 | This module is a simplified version of the analytics module found in WP-Calypso.
5 |
6 | Original: https://github.com/Automattic/wp-calypso/tree/master/client/analytics
7 |
8 | ## Tracks API
9 |
10 | ### analytics#tracks#recordEvent( eventName, eventProperties )
11 |
12 | Record an event with optional properties:
13 |
14 | ```js
15 | analytics.tracks.recordEvent( 'calypso_checkout_coupon_apply', {
16 | 'coupon_code': 'abc123'
17 | } );
18 | ```
--------------------------------------------------------------------------------
/lib/analytics/types.ts:
--------------------------------------------------------------------------------
1 | import * as T from '../types';
2 |
3 | export type TKQItem =
4 | | Function
5 | | [keyof TracksAPI, ...(string | T.JSONSerializable)[]];
6 |
7 | export type TracksAPI = {
8 | storeContext: (c: object) => void;
9 | identifyUser: (user: string, login: string) => void;
10 | recordEvent: (name: string, props: T.JSONSerializable) => void;
11 | setProperties: (properties: object) => void;
12 | clearIdentity: () => void;
13 | };
14 |
--------------------------------------------------------------------------------
/lib/boot-with-auth.tsx:
--------------------------------------------------------------------------------
1 | if (__TEST__) {
2 | window.testEvents = [];
3 | }
4 |
5 | import React from 'react';
6 | import App from './app';
7 | import { ErrorBoundaryWithAnalytics } from './error-boundary';
8 | import Modal from 'react-modal';
9 | import { makeStore } from './state';
10 | import { render } from 'react-dom';
11 | import { Provider } from 'react-redux';
12 | import { initSimperium } from './state/simperium/middleware';
13 |
14 | import '../scss/style.scss';
15 |
16 | import isDevConfig from './utils/is-dev-config';
17 |
18 | export const bootWithToken = (
19 | logout: () => any,
20 | token: string,
21 | username: string | null
22 | ) => {
23 | Modal.setAppElement('#root');
24 |
25 | makeStore(username, initSimperium(logout, token, username)).then((store) => {
26 | Object.defineProperties(window, {
27 | dispatch: {
28 | get() {
29 | return store.dispatch;
30 | },
31 | },
32 | state: {
33 | get() {
34 | return store.getState();
35 | },
36 | },
37 | });
38 |
39 | window.electron?.send('appStateUpdate', {
40 | settings: store.getState().settings,
41 | editMode: store.getState().ui.editMode,
42 | });
43 |
44 | render(
45 |
46 |
49 |
50 |
51 | ,
52 | document.getElementById('root')
53 | );
54 | });
55 | };
56 |
--------------------------------------------------------------------------------
/lib/components/boot-warning/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import './style';
3 |
4 | const BootWarning: FunctionComponent = ({ children }) => (
5 | {children}
6 | );
7 |
8 | export default BootWarning;
9 |
--------------------------------------------------------------------------------
/lib/components/boot-warning/style.scss:
--------------------------------------------------------------------------------
1 | h3.boot-warning__message {
2 | margin: 50px auto;
3 | width: 50%;
4 | }
5 |
--------------------------------------------------------------------------------
/lib/components/clipboard-button/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import Clipboard from 'clipboard';
3 |
4 | type Props = {
5 | container?: React.RefObject;
6 | linkText: string;
7 | text: string;
8 | };
9 |
10 | function ClipboardButton({ container, linkText, text }: Props) {
11 | const buttonRef = useRef();
12 | const textCallback = useRef();
13 | const successCallback = useRef();
14 |
15 | const onCopy = () => {
16 | setCopied(true);
17 | };
18 |
19 | const [isCopied, setCopied] = useState(false);
20 |
21 | // toggle the `isCopied` flag back to `false` after 4 seconds
22 | useEffect(() => {
23 | if (isCopied) {
24 | const confirmationTimeout = setTimeout(() => setCopied(false), 4000);
25 | return () => clearTimeout(confirmationTimeout);
26 | }
27 | }, [isCopied]);
28 |
29 | // update the callbacks on rerenders that change `text` or `onCopy`
30 | useEffect(() => {
31 | textCallback.current = () => text;
32 | successCallback.current = onCopy;
33 | }, [text, onCopy]);
34 |
35 | // create the `Clipboard` object on mount and destroy on unmount
36 | useEffect(() => {
37 | const clipboard = new Clipboard(buttonRef.current, {
38 | container: container?.current || undefined,
39 | text: () => textCallback.current(),
40 | });
41 | clipboard.on('success', () => successCallback.current());
42 |
43 | return () => clipboard.destroy();
44 | }, []);
45 |
46 | return (
47 |
50 | );
51 | }
52 |
53 | export default ClipboardButton;
54 |
--------------------------------------------------------------------------------
/lib/components/dev-badge/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const DevBadge = (props: React.HTMLProps) => {
4 | return (
5 |
6 | DEV
7 |
8 | );
9 | };
10 |
11 | export default DevBadge;
12 |
--------------------------------------------------------------------------------
/lib/components/dev-badge/style.scss:
--------------------------------------------------------------------------------
1 | .dev-badge {
2 | position: absolute;
3 | opacity: 0.8;
4 | z-index: 1;
5 | right: 12px;
6 | bottom: 12px;
7 | padding: 2px 4px;
8 | background: var(--secondary-color);
9 | color: var(--primary-color);
10 | font-size: 0.75rem;
11 | line-height: 1;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/components/last-sync-time/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import type * as S from '../../state';
5 | import type * as T from '../../types';
6 |
7 | type OwnProps = {
8 | noteId: T.EntityId;
9 | };
10 |
11 | type StateProps = {
12 | lastUpdated?: number;
13 | };
14 |
15 | type Props = StateProps;
16 |
17 | export const LastSyncTime: FunctionComponent = ({ lastUpdated }) =>
18 | 'undefined' !== typeof lastUpdated ? (
19 |
28 | ) : (
29 |
30 | );
31 |
32 | const mapStateToProps: S.MapState = (
33 | state,
34 | { noteId }
35 | ) => ({
36 | lastUpdated: state.simperium.lastSync.get(noteId),
37 | });
38 |
39 | export default connect(mapStateToProps)(LastSyncTime);
40 |
--------------------------------------------------------------------------------
/lib/components/note-preview/style.scss:
--------------------------------------------------------------------------------
1 | .note-preview {
2 | white-space: pre-wrap;
3 | }
4 |
--------------------------------------------------------------------------------
/lib/components/panel-title/index.tsx:
--------------------------------------------------------------------------------
1 | import { createElement, FunctionComponent } from 'react';
2 |
3 | type OwnProps = {
4 | children: string;
5 | headingLevel: 1 | 2 | 3 | 4 | 5 | 6;
6 | };
7 |
8 | const PanelTitle: FunctionComponent = ({
9 | children,
10 | headingLevel = 3,
11 | }) => {
12 | return createElement(
13 | `h${headingLevel}`,
14 | { className: 'panel-title' },
15 | children
16 | );
17 | };
18 |
19 | export default PanelTitle;
20 |
--------------------------------------------------------------------------------
/lib/components/panel-title/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../../scss/variables' as *;
2 |
3 | .panel-title {
4 | color: var(--foreground-color);
5 | margin: 0 0 0.5em;
6 | text-transform: uppercase;
7 | font-size: 90%;
8 | font-weight: $bold;
9 | }
10 |
--------------------------------------------------------------------------------
/lib/components/progress-bar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { LinearProgress } from '@mui/material';
3 |
4 | const ProgressBar: typeof LinearProgress = (props) => {
5 | return (
6 |
13 | );
14 | };
15 |
16 | export default ProgressBar;
17 |
--------------------------------------------------------------------------------
/lib/components/progress-bar/style.scss:
--------------------------------------------------------------------------------
1 | .progress-bar {
2 | background-color: var(--secondary-color);
3 | }
4 |
5 | .progress-bar__bar {
6 | background-color: var(--foreground-color) !important;
7 | }
8 |
--------------------------------------------------------------------------------
/lib/components/slider/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEventHandler, FunctionComponent } from 'react';
2 |
3 | type OwnProps = {
4 | disabled: boolean;
5 | onChange: ChangeEventHandler;
6 | min: number;
7 | max: number;
8 | value: number;
9 | };
10 |
11 | type Props = OwnProps & React.HTMLProps;
12 |
13 | export const Slider: FunctionComponent = ({
14 | 'aria-valuetext': ariaValueText,
15 | disabled,
16 | min,
17 | max,
18 | value,
19 | onChange,
20 | }) => (
21 |
32 | );
33 |
34 | export default Slider;
35 |
--------------------------------------------------------------------------------
/lib/components/spinner/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import { CircularProgress, CircularProgressProps } from '@mui/material';
3 | import classnames from 'classnames';
4 |
5 | const Spinner = ({
6 | isWhite,
7 | ...props
8 | }: CircularProgressProps & { isWhite?: boolean }) => {
9 | return (
10 |
17 | );
18 | };
19 |
20 | export default Spinner;
21 |
--------------------------------------------------------------------------------
/lib/components/spinner/style.scss:
--------------------------------------------------------------------------------
1 | .spinner__circle {
2 | color: var(--secondary-color);
3 |
4 | &.is-white {
5 | color: var(--background-color);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/lib/components/tab-panels/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
4 |
5 | type OwnProps = {
6 | tabNames: string[];
7 | };
8 |
9 | export class TabPanels extends Component {
10 | static propTypes = {
11 | children: PropTypes.node.isRequired,
12 | className: PropTypes.string,
13 | tabNames: PropTypes.arrayOf(PropTypes.string).isRequired,
14 | };
15 |
16 | render() {
17 | const { children, tabNames } = this.props;
18 |
19 | return (
20 |
21 |
22 | {tabNames.map((tabName, key) => (
23 |
24 | {tabName}
25 |
26 | ))}
27 |
28 |
29 |
30 | {React.Children.map(children, (tabPanel, key) => (
31 |
32 | {tabPanel}
33 |
34 | ))}
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default TabPanels;
42 |
--------------------------------------------------------------------------------
/lib/components/tab-panels/style.scss:
--------------------------------------------------------------------------------
1 | .tab-panels__tab-list {
2 | display: flex;
3 | flex-wrap: wrap;
4 | justify-content: center;
5 | margin: 0;
6 | padding: 0;
7 | border-bottom: 1px solid var(--secondary-color);
8 | list-style: none;
9 |
10 | li {
11 | margin: 0 0.5em;
12 | border-bottom: 2px solid;
13 | padding: 0.75em;
14 | color: var(--accent-color);
15 | text-transform: capitalize;
16 | border-radius: 0;
17 |
18 | &.button {
19 | border-bottom-color: var(--secondary-color);
20 |
21 | &.is-active {
22 | color: var(--accent-color);
23 | border-bottom-color: var(--accent-color);
24 | }
25 | }
26 |
27 | &:active {
28 | color: var(--background-color);
29 | }
30 | }
31 | }
32 |
33 | .tab-panels__panel {
34 | height: 30.5em;
35 | overflow: auto;
36 | -webkit-overflow-scrolling: touch;
37 | }
38 |
39 | .tab-panels__column {
40 | max-width: 374px;
41 | margin: 0 auto;
42 | padding: 36px 10px 50px;
43 | }
44 |
--------------------------------------------------------------------------------
/lib/components/tag-chip/__snapshots__/test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TagChip should not introduce visual regressions 1`] = `
4 |
8 | spline
9 |
12 |
27 |
28 |
29 | `;
30 |
--------------------------------------------------------------------------------
/lib/components/tag-chip/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import SmallCrossIcon from '../../icons/cross-small';
3 | import classNames from 'classnames';
4 |
5 | import type * as T from '../../types';
6 |
7 | type OwnProps = {
8 | onSelect?: (event: React.MouseEvent) => any;
9 | selected?: boolean;
10 | interactive?: boolean;
11 | deleted?: boolean;
12 | tagName: T.TagName | undefined;
13 | };
14 |
15 | const TagChip: FunctionComponent = ({
16 | onSelect,
17 | selected = false,
18 | interactive = true,
19 | deleted = false,
20 | tagName,
21 | }) => (
22 |
27 | {tagName}
28 | {interactive && (
29 |
30 |
31 |
32 | )}
33 |
34 | );
35 |
36 | export default TagChip;
37 |
--------------------------------------------------------------------------------
/lib/components/tag-chip/style.scss:
--------------------------------------------------------------------------------
1 | .tag-chip {
2 | color: var(--primary-color);
3 | flex: none;
4 | margin: 2px 8px 6px 0;
5 | padding: 1px 14px 3px;
6 | border-radius: 16px;
7 | line-height: 1.25em;
8 | white-space: nowrap;
9 | background: var(--primary-tag-chip-color);
10 | text-decoration: none;
11 | font-size: 14px;
12 | position: relative;
13 | -webkit-tap-highlight-color: transparent;
14 |
15 | &.selected,
16 | &:hover {
17 | .remove-tag-icon {
18 | display: inline-block;
19 | }
20 | }
21 |
22 | &.interactive {
23 | cursor: pointer;
24 | }
25 |
26 | &.deleted {
27 | background: var(--secondary-tag-chip-color);
28 | }
29 |
30 | .remove-tag-icon {
31 | display: none;
32 | position: absolute;
33 | background-color: var(--foreground-color);
34 | color: var(--background-color);
35 | border-radius: 50%;
36 | top: -8px;
37 | width: 16px;
38 | height: 16px;
39 | line-height: 14px;
40 | font-size: 14px;
41 | text-align: center;
42 |
43 | .icon-cross-small {
44 | height: 14px;
45 | width: 14px;
46 | position: absolute;
47 | left: 1px;
48 | top: 1px;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/components/transition-delay-enter/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FunctionComponent,
3 | useEffect,
4 | useState,
5 | ReactNode,
6 | } from 'react';
7 | import { CSSTransition } from 'react-transition-group';
8 |
9 | type OwnProps = {
10 | children: ReactNode;
11 | delay: number;
12 | };
13 |
14 | /**
15 | * A wrapper to delay the mounting of children.
16 | *
17 | * Useful for progress bars and spinners, that should generally have about a
18 | * 1000 ms delay before displaying to the user.
19 | */
20 | const TransitionDelayEnter: FunctionComponent = ({
21 | children,
22 | delay = 1000,
23 | }) => {
24 | const [shouldRender, setShouldRender] = useState(false);
25 |
26 | useEffect(() => {
27 | const timer = window.setTimeout(() => {
28 | setShouldRender(true);
29 | }, delay);
30 |
31 | return (): void => window.clearTimeout(timer);
32 | }, []);
33 |
34 | return (
35 |
42 | {children}
43 |
44 | );
45 | };
46 |
47 | export default TransitionDelayEnter;
48 |
--------------------------------------------------------------------------------
/lib/components/transition-delay-enter/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../../scss/mixins';
2 |
3 | .transition-delay-enter {
4 | @include mixins.react-transition-fade-in;
5 | }
6 |
--------------------------------------------------------------------------------
/lib/components/transition-fade-in-out/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, ReactNode } from 'react';
2 | import { CSSTransition } from 'react-transition-group';
3 |
4 | type OwnProps = {
5 | children: ReactNode;
6 | shouldMount: boolean;
7 | wrapperClassName?: string;
8 | };
9 |
10 | const TransitionFadeInOut: FunctionComponent = ({
11 | children,
12 | shouldMount,
13 | wrapperClassName = '',
14 | }) => {
15 | return (
16 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | export default TransitionFadeInOut;
29 |
--------------------------------------------------------------------------------
/lib/components/transition-fade-in-out/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../../scss/mixins';
2 |
3 | .transition-fade-in-out {
4 | @include mixins.react-transition-fade-in;
5 | @include mixins.react-transition-fade-out;
6 | }
7 |
--------------------------------------------------------------------------------
/lib/connection-status/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Tooltip } from '@mui/material';
4 | import ConnectionIcon from '../icons/connection';
5 | import NoConnectionIcon from '../icons/no-connection';
6 |
7 | import * as S from '../state';
8 | import * as T from '../types';
9 |
10 | import './style';
11 |
12 | type StateProps = {
13 | connectionStatus: T.ConnectionState;
14 | };
15 |
16 | type Props = StateProps;
17 |
18 | export const ConnectionStatus: FunctionComponent = ({
19 | connectionStatus,
20 | }) => (
21 |
22 |
33 |
34 | {connectionStatus === 'green' ? (
35 |
36 | ) : (
37 |
38 | )}
39 | Server connection
40 |
41 |
42 |
43 | );
44 |
45 | const mapStateToProps: S.MapState = (state) => ({
46 | connectionStatus: state.simperium.connectionStatus,
47 | });
48 |
49 | export default connect(mapStateToProps)(ConnectionStatus);
50 |
--------------------------------------------------------------------------------
/lib/connection-status/style.scss:
--------------------------------------------------------------------------------
1 | .server-connection__label {
2 | margin-left: 6px;
3 | }
4 |
--------------------------------------------------------------------------------
/lib/controls/checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | import CheckedCheckbox from '../../icons/checkbox-checked';
5 | import UncheckedCheckbox from '../../icons/checkbox-unchecked';
6 |
7 | type OwnProps = React.HTMLProps & {
8 | className?: string;
9 | onChange: () => any;
10 | isStandard?: boolean;
11 | };
12 |
13 | function CheckboxControl({
14 | className,
15 | isStandard,
16 | checked,
17 | ...props
18 | }: OwnProps) {
19 | return (
20 |
26 |
27 | {!isStandard ? (
28 |
29 |
30 |
31 | ) : checked ? (
32 |
33 | ) : (
34 |
35 | )}
36 |
37 | );
38 | }
39 |
40 | export default CheckboxControl;
41 |
--------------------------------------------------------------------------------
/lib/controls/checkbox/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../../scss/variables' as *;
2 |
3 | .checkbox-control {
4 | -webkit-touch-callout: none;
5 | width: 16px;
6 | height: 16px;
7 | position: relative;
8 | display: inline-block;
9 | vertical-align: middle;
10 | overflow: hidden;
11 |
12 | &:focus-within {
13 | outline: $focus-outline;
14 | }
15 |
16 | input {
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | width: 100%;
21 | height: 100%;
22 |
23 | /* hide the checkbox */
24 | margin: 0;
25 | padding: 0;
26 | opacity: 0;
27 | border: none;
28 | outline: none;
29 | background: none;
30 | appearance: none;
31 | }
32 |
33 | .checkbox-control-base,
34 | .checkbox-control-checked {
35 | display: block;
36 | position: absolute;
37 | top: 0;
38 | left: 0;
39 | width: 100%;
40 | height: 100%;
41 | border-radius: 50%;
42 | }
43 |
44 | .checkbox-control-base {
45 | pointer-events: none;
46 | border-radius: 50%;
47 | overflow: hidden;
48 | border: 1px solid var(--secondary-color);
49 | }
50 |
51 | .checkbox-control-checked {
52 | background: var(--active-controls-color);
53 | transition: all 0.15s ease;
54 | opacity: 0;
55 | }
56 |
57 | input:checked + .checkbox-control-base {
58 | border: none;
59 |
60 | .checkbox-control-checked {
61 | opacity: 1;
62 | }
63 | }
64 |
65 | &.checkbox-standard {
66 | width: 18px;
67 | height: 18px;
68 |
69 | svg {
70 | vertical-align: top;
71 | fill: var(--foreground-color);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/controls/toggle/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEventHandler, FunctionComponent } from 'react';
2 | import classNames from 'classnames';
3 |
4 | type OwnProps = Partial & {
5 | onChange: (isNowToggled: boolean) => any;
6 | };
7 |
8 | type Props = OwnProps;
9 |
10 | export const ToggleControl: FunctionComponent = ({
11 | className,
12 | onChange,
13 | ...props
14 | }) => {
15 | const onToggle: ChangeEventHandler = ({
16 | currentTarget: { checked },
17 | }) => onChange(checked);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default ToggleControl;
32 |
--------------------------------------------------------------------------------
/lib/dialog-renderer/style.scss:
--------------------------------------------------------------------------------
1 | .dialog-renderer__overlay {
2 | position: fixed;
3 | inset: 0;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | background: var(--overlay-color);
8 | }
9 |
10 | .dialog-renderer__content {
11 | &:focus {
12 | outline: 0;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/dialog/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | import CrossIconSmall from '../icons/cross-small';
6 |
7 | export class Dialog extends Component {
8 | static propTypes = {
9 | children: PropTypes.node.isRequired,
10 | className: PropTypes.string,
11 | closeBtnLabel: PropTypes.string,
12 | hideTitleBar: PropTypes.bool,
13 | title: PropTypes.string,
14 | onDone: PropTypes.func,
15 | };
16 |
17 | render() {
18 | const {
19 | className,
20 | closeBtnLabel = 'Done',
21 | hideTitleBar,
22 | title,
23 | children,
24 | onDone,
25 | } = this.props;
26 |
27 | return (
28 |
29 | {!hideTitleBar && (
30 |
31 |
{title}
32 |
33 | {!!onDone && (
34 |
42 | )}
43 |
44 |
45 | )}
46 |
47 |
{children}
48 |
49 | );
50 | }
51 | }
52 |
53 | export default Dialog;
54 |
--------------------------------------------------------------------------------
/lib/dialog/style.scss:
--------------------------------------------------------------------------------
1 | $dialog-max-height: 420px;
2 | $dialog-title-height: 56px;
3 |
4 | .dialog {
5 | display: flex;
6 | flex-direction: column;
7 | width: calc(100vw - 2rem);
8 | background: var(--background-color);
9 | border-radius: 8px;
10 | max-height: $dialog-max-height;
11 |
12 | @media only screen and (max-height: $dialog-max-height) {
13 | max-height: calc(100vh - 2rem);
14 | }
15 | }
16 |
17 | .dialog-title-bar {
18 | display: flex;
19 | border-bottom: 1px solid var(--secondary-color);
20 | height: $dialog-title-height;
21 |
22 | .button {
23 | border: 0;
24 | }
25 | }
26 |
27 | .dialog-title-side {
28 | display: flex;
29 | flex: none;
30 | width: 3.5em;
31 |
32 | button {
33 | width: 100%;
34 | margin: 0 auto;
35 | }
36 | }
37 |
38 | .dialog-title-text {
39 | color: var(--primary-color);
40 | flex: 1 1 auto;
41 | margin: 15px 0 0 16px;
42 | text-align: left;
43 | font-weight: 600;
44 | font-size: 16px;
45 | }
46 |
47 | .dialog-content {
48 | display: flex;
49 | flex-direction: column;
50 | flex: 1 0 auto;
51 | max-height: $dialog-max-height - $dialog-title-height;
52 | overflow: auto;
53 |
54 | @media only screen and (max-height: $dialog-max-height) {
55 | max-height: calc(100vh - 2rem - #{$dialog-title-height});
56 | }
57 | }
58 |
59 | .dialog-actions {
60 | margin: 1em 0 0;
61 | padding: 0;
62 | list-style: none;
63 |
64 | li + li {
65 | margin-top: 1em;
66 | }
67 |
68 | li:last-child {
69 | margin-top: 2.5em;
70 | }
71 |
72 | button {
73 | width: 100%;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/dialogs/beta-warning/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import SimplenoteLogo from '../../icons/simplenote';
4 | import CrossIcon from '../../icons/cross';
5 | import Dialog from '../../dialog';
6 | import { closeDialog } from '../../state/ui/actions';
7 |
8 | import * as S from '../../state';
9 |
10 | type DispatchProps = {
11 | closeDialog: () => any;
12 | };
13 |
14 | type Props = DispatchProps;
15 |
16 | export class BetaWarning extends Component {
17 | render() {
18 | const { closeDialog } = this.props;
19 |
20 | return (
21 |
22 |
52 |
53 | );
54 | }
55 | }
56 |
57 | const mapDispatchToProps: S.MapDispatch = {
58 | closeDialog,
59 | };
60 |
61 | export default connect(null, mapDispatchToProps)(BetaWarning);
62 |
--------------------------------------------------------------------------------
/lib/dialogs/button-group/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SmallChevronRightIcon from '../../icons/chevron-right-small';
3 | import PropTypes from 'prop-types';
4 |
5 | const ButtonGroup = (props) => {
6 | const { items, onClickItem } = props;
7 |
8 | return (
9 |
10 | {items.map((item) => (
11 | -
12 |
16 |
17 | ))}
18 |
19 | );
20 | };
21 |
22 | ButtonGroup.propTypes = {
23 | items: PropTypes.array.isRequired,
24 | onClickItem: PropTypes.func.isRequired,
25 | };
26 |
27 | export default ButtonGroup;
28 |
--------------------------------------------------------------------------------
/lib/dialogs/button-group/style.scss:
--------------------------------------------------------------------------------
1 | .button-group {
2 | margin: 0;
3 | padding: 0;
4 | list-style: none;
5 | }
6 |
7 | .button-group__item {
8 | color: var(--primary-color);
9 | display: flex;
10 | height: 3em;
11 | margin: 0;
12 | border: 1px solid var(--secondary-color);
13 |
14 | & + & {
15 | border-top-width: 0;
16 | }
17 |
18 | button {
19 | display: flex;
20 | justify-content: space-between;
21 | align-items: center;
22 | width: 100%;
23 | padding: 0 18px;
24 | text-align: left;
25 | font-size: 115%;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/dialogs/close-window-confirmation/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import actions from '../../state/actions';
5 | import UnsynchronizedConfirmation from '../unsynchronized';
6 |
7 | import type * as S from '../../state';
8 |
9 | type DispatchProps = {
10 | reallyCloseWindow: () => any;
11 | };
12 |
13 | type Props = DispatchProps;
14 |
15 | const CloseWindowConfirmation = ({ reallyCloseWindow }: Props) => (
16 |
22 | );
23 |
24 | const mapDispatchToProps: S.MapDispatch = {
25 | reallyCloseWindow: actions.electron.reallyCloseWindow,
26 | };
27 |
28 | export default connect(null, mapDispatchToProps)(CloseWindowConfirmation);
29 |
--------------------------------------------------------------------------------
/lib/dialogs/import/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Suspense } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import Dialog from '../../dialog';
5 | import { isElectron } from '../../utils/platform';
6 | import SourceImporter from './source-importer';
7 | import { closeDialog } from '../../state/ui/actions';
8 |
9 | import * as S from '../../state';
10 |
11 | type DispatchProps = {
12 | closeDialog: () => any;
13 | };
14 |
15 | type Props = DispatchProps;
16 |
17 | class ImportDialog extends Component {
18 | state = {
19 | importStarted: false,
20 | };
21 |
22 | render() {
23 | const { closeDialog } = this.props;
24 | const { importStarted } = this.state;
25 | const source = {
26 | acceptedTypes: isElectron ? '.txt,.md,.json,.enex' : '.txt,.md,.json',
27 | title: `Select the notes you'd like to import.`,
28 | instructions: isElectron
29 | ? 'Accepted file formats: Simplenote (JSON), Text (TXT, MD) and Evernote (ENEX).'
30 | : 'Accepted file formats: Simplenote (JSON) and Text (TXT, MD).',
31 | multiple: true,
32 | };
33 |
34 | return (
35 |
50 | );
51 | }
52 | }
53 |
54 | const mapDispatchToProps: S.MapDispatch = {
55 | closeDialog,
56 | };
57 |
58 | export default connect(null, mapDispatchToProps)(ImportDialog);
59 |
--------------------------------------------------------------------------------
/lib/dialogs/import/source-importer/executor/style.scss:
--------------------------------------------------------------------------------
1 | .source-importer-executor {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | flex: 1 0 auto;
6 |
7 | .import-progress {
8 | padding-left: 16px;
9 | padding-right: 16px;
10 | margin-top: 20px;
11 | }
12 |
13 | p[role='status'] {
14 | margin-top: 8px;
15 | margin-bottom: 5px;
16 | padding-left: 0;
17 | padding-right: 0;
18 | }
19 | }
20 |
21 | .source-importer-executor__options {
22 | flex: 1 0 auto;
23 |
24 | .panel-title {
25 | color: var(--foreground-color);
26 | text-transform: none;
27 | font-weight: normal;
28 | font-size: 14px;
29 | }
30 |
31 | label {
32 | display: flex;
33 | border-top: 1px solid var(--secondary-color);
34 | border-bottom: 1px solid var(--secondary-color);
35 | align-items: center;
36 | font-size: 16px;
37 | height: 28px;
38 | line-height: 28px;
39 | }
40 |
41 | .enable-markdown {
42 | color: var(--primary-color);
43 | flex: 1 1 auto;
44 | line-height: 28px;
45 | }
46 |
47 | .toggle-control {
48 | flex: none;
49 | }
50 | }
51 |
52 | .source-importer-executor__error {
53 | margin-bottom: 0.75em;
54 | color: var(--tertiary-highlight-color);
55 | line-height: 1.3;
56 | }
57 |
58 | .source-importer-executor__button {
59 | height: 56px;
60 | text-align: right;
61 | padding-left: 16px;
62 | padding-right: 16px;
63 |
64 | button {
65 | border-radius: 4px;
66 | padding: 6px 14px;
67 | margin-top: 14px;
68 | line-height: 16px;
69 | font-size: 14px;
70 | border: 0;
71 | font-weight: 400;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/dialogs/import/source-importer/progress/bar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import ProgressBar from '../../../../components/progress-bar';
5 |
6 | class ImportProgressBar extends React.Component {
7 | static propTypes = {
8 | currentValue: PropTypes.number.isRequired,
9 | endValue: PropTypes.number,
10 | isDone: PropTypes.bool.isRequired,
11 | };
12 |
13 | shouldComponentUpdate(nextProps) {
14 | if (!this.props.endValue && this.props.isDone === nextProps.isDone) {
15 | return false;
16 | }
17 | return true;
18 | }
19 |
20 | render() {
21 | const { currentValue, endValue, isDone } = this.props;
22 |
23 | const IndeterminateProgressBar = () => {
24 | return isDone ? (
25 |
26 | ) : (
27 |
28 | );
29 | };
30 |
31 | if (endValue) {
32 | return (
33 |
37 | );
38 | } else {
39 | return ;
40 | }
41 | }
42 | }
43 |
44 | export default ImportProgressBar;
45 |
--------------------------------------------------------------------------------
/lib/dialogs/import/source-importer/progress/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 |
3 | import ImportProgressBar from './bar';
4 | import ImportProgressText from './text';
5 |
6 | type OwnProps = {
7 | currentValue?: number;
8 | endValue?: number;
9 | isDone: boolean;
10 | };
11 |
12 | const ImportProgress: FunctionComponent = ({
13 | currentValue = 0,
14 | endValue = 0,
15 | isDone,
16 | }) => {
17 | return (
18 |
26 | );
27 | };
28 |
29 | export default ImportProgress;
30 |
--------------------------------------------------------------------------------
/lib/dialogs/import/source-importer/progress/text.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const ImportProgressText = (props) => {
5 | const { currentValue, isDone } = props;
6 |
7 | const unit = currentValue === 1 ? 'note' : 'notes';
8 | let text;
9 |
10 | if (isDone) {
11 | text = `Done! ${currentValue} ${unit} imported.`;
12 | } else {
13 | text = currentValue
14 | ? `${currentValue} ${unit} imported...`
15 | : 'Importing...';
16 | }
17 |
18 | return (
19 |
20 | {text}
21 |
22 | );
23 | };
24 |
25 | ImportProgressText.propTypes = {
26 | currentValue: PropTypes.number.isRequired,
27 | isDone: PropTypes.bool.isRequired,
28 | };
29 |
30 | export default ImportProgressText;
31 |
--------------------------------------------------------------------------------
/lib/dialogs/import/source-importer/style.scss:
--------------------------------------------------------------------------------
1 | .source-importer,
2 | .source-importer__executor-wrapper {
3 | display: flex;
4 | flex-direction: column;
5 | flex: 1 0 auto;
6 | }
7 |
8 | .source-importer {
9 | .dialog-buttons {
10 | height: 56px;
11 | margin: 0 16px;
12 | text-align: right;
13 |
14 | button.disabled {
15 | border-radius: 4px;
16 | color: var(--primary-button-fg-color);
17 | padding: 6px 14px;
18 | margin-top: 14px;
19 | line-height: 16px;
20 | font-size: 14px;
21 | border: 0;
22 | background-color: var(--tertiary-color);
23 | opacity: 1;
24 | font-weight: 400;
25 | }
26 | }
27 |
28 | .importer-dropzone {
29 | margin: auto 16px 10px;
30 |
31 | .accepted-files-header {
32 | height: 28px;
33 | line-height: 28px;
34 | margin-top: 10px;
35 | font-size: 14px;
36 | }
37 |
38 | &.is-accepted + .dialog-buttons {
39 | display: none;
40 | }
41 |
42 | &.is-accepted {
43 | margin-left: 0;
44 | margin-right: 0;
45 |
46 | .accepted-files-header,
47 | .accepted-files {
48 | padding-left: 16px;
49 | padding-right: 16px;
50 | }
51 | }
52 | }
53 |
54 | p {
55 | padding-left: 16px;
56 | padding-right: 16px;
57 | color: var(--primary-color);
58 | }
59 |
60 | .source-importer-executor {
61 | h3,
62 | label {
63 | padding-left: 16px;
64 | padding-right: 16px;
65 | }
66 | }
67 |
68 | h3 {
69 | height: 28px;
70 | line-height: 28px;
71 | margin-bottom: 0;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/dialogs/import/source-importer/utils/test-importer.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | /**
4 | * A mock event emitter for testing/tweaking the UI.
5 | * @class
6 | */
7 | class TestImporter extends EventEmitter {
8 | constructor(arg) {
9 | super();
10 | console.log(arg); // eslint-disable-line no-console
11 | }
12 |
13 | importNotes(files) {
14 | console.log(files); // eslint-disable-line no-console
15 |
16 | let count = 0;
17 | const counter = window.setInterval(() => {
18 | this.emit('status', 'progress', ++count);
19 | }, 10);
20 | window.setTimeout(() => {
21 | window.clearInterval(counter);
22 | // Swap commented-out lines to test success/error events
23 | // this.emit('status', 'error', 'Error processing data.');
24 | this.emit('status', 'complete', count);
25 | }, 1000);
26 | }
27 | }
28 |
29 | export default TestImporter;
30 |
--------------------------------------------------------------------------------
/lib/dialogs/import/style.scss:
--------------------------------------------------------------------------------
1 | .import {
2 | max-width: 360px;
3 | max-height: 420px;
4 |
5 | .dialog-title-text {
6 | text-align: left;
7 | margin-left: 16px;
8 | }
9 |
10 | .dialog-title-side:first-child {
11 | display: none;
12 | }
13 | }
14 |
15 | .import__inner,
16 | .import__source-importer-wrapper {
17 | display: flex;
18 | flex-direction: column;
19 | flex: 1 0 auto;
20 | }
21 |
22 | .import__inner {
23 | width: 100%;
24 | overflow-y: auto;
25 | max-height: 360px;
26 | }
27 |
28 | .import__placeholder {
29 | display: flex;
30 | justify-content: center;
31 | align-items: center;
32 | flex: 1 0 auto;
33 | }
34 |
--------------------------------------------------------------------------------
/lib/dialogs/keybindings/style.scss:
--------------------------------------------------------------------------------
1 | .keybindings {
2 | .dialog {
3 | max-width: 500px;
4 | margin: auto;
5 | max-height: calc(100vh - 2rem);
6 | }
7 |
8 | .dialog-content {
9 | color: var(--primary-color);
10 | padding: 10px 20px 20px;
11 | max-height: calc(100vh - 2rem - 56px);
12 | }
13 |
14 | ul {
15 | padding-left: 0;
16 | }
17 |
18 | li {
19 | margin-bottom: 6px;
20 | list-style: none;
21 | }
22 |
23 | kbd {
24 | border: 1px solid var(--primary-color);
25 | border-radius: 5px;
26 | margin: 4px;
27 | padding: 0 5px;
28 | font-size: 12px;
29 | min-width: 8px;
30 | text-align: center;
31 | display: inline-block;
32 | box-shadow: 0 1px 1px gray;
33 | }
34 | }
35 |
36 | .keybindings__key-item {
37 | display: flex;
38 | align-items: baseline;
39 | }
40 |
41 | .keybindings__key-list {
42 | min-width: 180px;
43 | display: inline-block;
44 | text-align: right;
45 | }
46 |
47 | .keybindings__key-description {
48 | display: inline-block;
49 | margin-left: 4px;
50 | }
51 |
52 | .keybindings__sections {
53 | position: relative;
54 | max-height: 480px;
55 | overflow-y: auto;
56 |
57 | section {
58 | margin-right: 2em;
59 |
60 | h1 {
61 | font-size: 18px;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/dialogs/logout-confirmation/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import actions from '../../state/actions';
5 | import UnsynchronizedConfirmation from '../unsynchronized';
6 |
7 | import type * as S from '../../state';
8 |
9 | type DispatchProps = {
10 | reallyLogOut: () => any;
11 | };
12 |
13 | type Props = DispatchProps;
14 |
15 | const LogoutConfirmation = ({ reallyLogOut }: Props) => (
16 |
22 | );
23 |
24 | const mapDispatchToProps: S.MapDispatch = {
25 | reallyLogOut: actions.ui.reallyLogOut,
26 | };
27 |
28 | export default connect(null, mapDispatchToProps)(LogoutConfirmation);
29 |
--------------------------------------------------------------------------------
/lib/dialogs/radio-settings-group.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import CheckboxControl from '../controls/checkbox';
5 |
6 | const RadioGroup = ({ groupSlug, slug, isEnabled, onChange }) => (
7 | onChange(slug)}
14 | />
15 | );
16 |
17 | RadioGroup.propTypes = {
18 | groupSlug: PropTypes.string,
19 | slug: PropTypes.string,
20 | isEnabled: PropTypes.bool.isRequired,
21 | onChange: PropTypes.func.isRequired,
22 | };
23 |
24 | export default RadioGroup;
25 |
--------------------------------------------------------------------------------
/lib/dialogs/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import Dialog from '../../dialog';
5 | import TabPanels from '../../components/tab-panels';
6 |
7 | import AccountPanel from './panels/account';
8 | import DisplayPanel from './panels/display';
9 | import ToolsPanel from './panels/tools';
10 |
11 | import { closeDialog } from '../../state/ui/actions';
12 |
13 | const settingTabs = ['account', 'display', 'tools'];
14 |
15 | type DispatchProps = {
16 | closeDialog: () => any;
17 | };
18 |
19 | type Props = DispatchProps;
20 |
21 | export const SettingsDialog: FunctionComponent = ({ closeDialog }) => (
22 |
29 | );
30 |
31 | export default connect(null, { closeDialog })(SettingsDialog);
32 |
--------------------------------------------------------------------------------
/lib/dialogs/settings/style.scss:
--------------------------------------------------------------------------------
1 | .settings {
2 | max-width: 630px;
3 |
4 | .button-borderless {
5 | color: var(--accent-color);
6 | }
7 |
8 | &.dialog {
9 | max-height: calc(100vh - 2rem);
10 |
11 | .dialog-content {
12 | max-height: calc(100vh - 2rem - 56px);
13 | }
14 | }
15 |
16 | input[type='radio'] {
17 | cursor: pointer;
18 | }
19 |
20 | .settings-account-name {
21 | font-size: 115%;
22 | text-align: center;
23 | width: 100%;
24 | }
25 |
26 | .settings-writing {
27 | padding-top: 48px;
28 | }
29 | }
30 |
31 | .settings-group {
32 | margin-bottom: 50px;
33 |
34 | &:last-child {
35 | margin-bottom: 0;
36 | }
37 |
38 | p {
39 | line-height: normal;
40 | color: var(--settings-fg-color);
41 | }
42 | }
43 |
44 | .settings-items {
45 | border: 1px solid var(--secondary-color);
46 | color: var(--primary-color);
47 | }
48 |
49 | .settings-item {
50 | display: flex;
51 | align-items: center;
52 | height: 3em;
53 | padding-left: 18px;
54 | padding-right: 18px;
55 | border-bottom: 1px solid var(--secondary-color);
56 |
57 | &:last-child {
58 | border-bottom: none;
59 | }
60 |
61 | .settings-item-label {
62 | flex: 1 1 auto;
63 | align-items: center;
64 | font-size: 115%;
65 | }
66 |
67 | .settings-item-control {
68 | flex: none;
69 | }
70 |
71 | .settings-item-text-input {
72 | display: block;
73 | width: 100%;
74 | height: 100%;
75 | padding: 0.25em 0.15em;
76 | border: none;
77 | line-height: 1.5em;
78 | font-size: 115%;
79 |
80 | &:focus {
81 | position: relative;
82 | z-index: 1;
83 | outline: none;
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/lib/dialogs/share/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../../scss/variables' as *;
2 |
3 | .share-collaborators-heading {
4 | border-bottom: 1px solid var(--secondary-color);
5 | }
6 |
7 | .share-collaborators {
8 | margin: 0;
9 | padding: 0;
10 | list-style: none;
11 | line-height: 1.5em;
12 | }
13 |
14 | .share-collaborator {
15 | display: flex;
16 | align-items: center;
17 | margin: 20px 0;
18 |
19 | .share-collaborator-photo {
20 | flex: none;
21 | margin-right: 10px;
22 | width: 34px;
23 | height: 34px;
24 | border-radius: 50%;
25 | overflow: hidden;
26 | background: var(--secondary-color);
27 | }
28 |
29 | .share-collaborator-name {
30 | color: var(--primary-color);
31 | flex: 1 1 auto;
32 | font-size: 143%;
33 | font-weight: $light;
34 | line-height: 1.5em;
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | white-space: nowrap;
38 | }
39 |
40 | .share-collaborator-remove {
41 | flex: none;
42 | margin-left: 10px;
43 | opacity: 0;
44 | transform: $anim;
45 |
46 | .touch-enabled & {
47 | opacity: 1;
48 | }
49 | }
50 |
51 | &:hover .share-collaborator-remove,
52 | .share-collaborator-remove:focus {
53 | opacity: 1;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/dialogs/toggle-settings-group.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 |
3 | import ToggleControl from '../controls/toggle';
4 |
5 | type OwnProps = {
6 | groupSlug: string;
7 | slug: string;
8 | isEnabled: boolean;
9 | onChange: () => any;
10 | };
11 |
12 | type Props = OwnProps;
13 |
14 | const ToggleGroup: FunctionComponent = ({
15 | groupSlug,
16 | slug,
17 | isEnabled,
18 | onChange,
19 | }) => (
20 |
27 | );
28 |
29 | export default ToggleGroup;
30 |
--------------------------------------------------------------------------------
/lib/dialogs/trash-tag-confirmation/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import actions from '../../state/actions';
5 | import Dialog from '../../dialog';
6 |
7 | import type * as S from '../../state';
8 | import type * as T from '../../types';
9 |
10 | type OwnProps = {
11 | tagName: T.TagName;
12 | };
13 |
14 | type DispatchProps = {
15 | closeDialog: () => any;
16 | trashTag: (tagName: T.TagName) => any;
17 | };
18 |
19 | type Props = OwnProps & DispatchProps;
20 |
21 | const TrashTagConfirmation: FunctionComponent = ({
22 | closeDialog,
23 | tagName,
24 | trashTag,
25 | }) => (
26 |
45 | );
46 |
47 | const mapDispatchToProps: S.MapDispatch = {
48 | trashTag: (tagName) => ({
49 | type: 'TRASH_TAG',
50 | tagName,
51 | }),
52 | closeDialog: actions.ui.closeDialog,
53 | };
54 |
55 | export default connect(null, mapDispatchToProps)(TrashTagConfirmation);
56 |
--------------------------------------------------------------------------------
/lib/dialogs/trash-tag-confirmation/style.scss:
--------------------------------------------------------------------------------
1 | .trash-tag-confirmation {
2 | width: 360px;
3 |
4 | .dialog-title-text {
5 | text-align: left;
6 | padding-left: 16px;
7 | font-size: 16px;
8 | font-weight: 600;
9 | line-height: 56px;
10 | margin: 0;
11 | }
12 |
13 | .dialog-title-side:first-child {
14 | display: none;
15 | }
16 |
17 | .dialog-content {
18 | color: var(--primary-color);
19 | padding: 0 16px;
20 |
21 | .dialog-inner-content {
22 | margin: 10px 0;
23 |
24 | span.tag-name {
25 | word-break: break-all;
26 | }
27 | }
28 | }
29 |
30 | .icon-cross {
31 | width: 16px;
32 | height: 16px;
33 | }
34 |
35 | .dialog-title-bar,
36 | .dialog-buttons,
37 | .dialog-title-text,
38 | .dialog-title-side {
39 | height: 56px;
40 | }
41 |
42 | .dialog-buttons {
43 | text-align: right;
44 | }
45 |
46 | button.delete-tag {
47 | border-radius: 4px;
48 | padding: 6px 14px;
49 | margin-top: 14px;
50 | line-height: 16px;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/email-verification/style.scss:
--------------------------------------------------------------------------------
1 | .email-verification__overlay {
2 | position: fixed;
3 | inset: 0;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | background: var(--overlay-color);
8 | }
9 |
10 | .email-verification__dismiss {
11 | display: flex;
12 | height: 60px;
13 | align-items: center;
14 | justify-content: flex-end;
15 | margin: 0 16px;
16 | width: 100%;
17 |
18 | svg {
19 | height: 16px;
20 | width: 16px;
21 | margin: auto 0;
22 | }
23 | }
24 |
25 | .email-verification__modal {
26 | align-items: center;
27 | background-color: var(--background-color);
28 | border-radius: 8px;
29 | color: var(--primary-color);
30 | display: flex;
31 | flex-direction: column;
32 | justify-content: center;
33 | margin: 20%;
34 | max-width: 420px;
35 | padding: 0 24px 16px;
36 | font-size: 16px;
37 |
38 | .icon-mail,
39 | .icon-warning {
40 | color: var(--foreground-color);
41 | height: 48px;
42 | width: 48px;
43 | }
44 |
45 | p {
46 | margin: 0 26px;
47 | margin-block-start: 0;
48 | }
49 |
50 | p:not(:last-of-type) {
51 | padding-bottom: 20px;
52 | }
53 |
54 | .email-verification__button-row {
55 | padding-top: 10px;
56 | padding-bottom: 10px;
57 | display: flex;
58 | justify-content: flex-end;
59 | flex-flow: row wrap;
60 | width: 100%;
61 |
62 | a {
63 | padding-right: 12px;
64 | }
65 |
66 | .button-borderless {
67 | color: var(--accent-color);
68 | }
69 | }
70 |
71 | &:focus {
72 | outline: 0;
73 | }
74 | }
75 |
76 | .email-verification__email {
77 | word-break: break-all;
78 | }
79 |
--------------------------------------------------------------------------------
/lib/error-boundary/style.scss:
--------------------------------------------------------------------------------
1 | .error-message {
2 | align-items: center;
3 | display: flex;
4 | flex-direction: column;
5 | flex: 1;
6 | height: 100vh;
7 | overflow-y: auto;
8 | padding: 2rem 1rem;
9 | }
10 |
11 | .error-message__content {
12 | align-items: center;
13 | font-size: 16px;
14 | max-width: 306px;
15 | margin: auto 0;
16 | text-align: center;
17 | }
18 |
19 | .error-message__icon {
20 | color: var(--foreground-color);
21 | height: 80px;
22 | margin: 0 auto 0.5rem;
23 | width: 80px;
24 |
25 | .icon-warning {
26 | height: 100%;
27 | width: 100%;
28 | }
29 | }
30 |
31 | .error-message__heading {
32 | font-size: 36px;
33 | margin: 0 0 0.25em;
34 | text-align: center;
35 | }
36 |
37 | .error-message p {
38 | margin: 0 0 1.67em;
39 | }
40 |
41 | .error-message__action {
42 | margin-bottom: 1.67em;
43 | }
44 |
45 | .error-message__footnote {
46 | color: var(--foreground-color);
47 | font-size: 14px;
48 | }
49 |
--------------------------------------------------------------------------------
/lib/global.d.ts:
--------------------------------------------------------------------------------
1 | import { TKQItem, TracksAPI } from './analytics/types';
2 | import { compose } from 'redux';
3 |
4 | import { electronAPI } from './preload';
5 |
6 | import * as S from './state';
7 |
8 | declare global {
9 | const __TEST__: boolean;
10 | const config: {
11 | app_engine_url: string;
12 | app_id: string;
13 | app_key: string;
14 | development: boolean;
15 | is_app_engine: string;
16 | version: string;
17 | wpcc_client_id: string;
18 | wpcc_redirect_url: string;
19 | };
20 |
21 | interface Window {
22 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
23 | analyticsEnabled: boolean;
24 | electron: typeof electronAPI;
25 | location: Location;
26 | testEvents: (string | [string, ...any[]])[];
27 | _tkq: TKQItem[] & { a: unknown };
28 | webConfig?: {
29 | signout?: (callback: () => void) => void;
30 | };
31 | wpcom: {
32 | tracks: TracksAPI;
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/icon-button/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ElementType } from 'react';
2 | import { Tooltip } from '@mui/material';
3 |
4 | type OwnProps = {
5 | disableTooltip: boolean;
6 | icon: ElementType;
7 | title: string;
8 | };
9 |
10 | type Props = OwnProps;
11 |
12 | export const IconButton = ({ icon, title, ...props }: Props) => (
13 |
18 |
19 |
28 |
29 |
30 | );
31 |
32 | export default IconButton;
33 |
--------------------------------------------------------------------------------
/lib/icon-button/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/variables' as *;
2 |
3 | .icon-button {
4 | width: 32px;
5 | height: 32px;
6 | color: var(--foreground-color);
7 |
8 | svg {
9 | transition: $anim-fast;
10 | }
11 |
12 | &:disabled {
13 | opacity: 0.4;
14 | pointer-events: none;
15 | }
16 | }
17 |
18 | .icon-button__tooltip {
19 | position: relative;
20 | top: -8px;
21 | font-size: 14px !important;
22 | }
23 |
--------------------------------------------------------------------------------
/lib/icons/app-icon/app-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/app-icon.ico
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_128x128.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_128x128@2x.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_16x16.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_16x16@2x.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_256x256.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_256x256@2x.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_32x32.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_32x32@2x.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_512x512.png
--------------------------------------------------------------------------------
/lib/icons/app-icon/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/lib/icons/app-icon/icon_512x512@2x.png
--------------------------------------------------------------------------------
/lib/icons/arrow-left.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function LeftArrowIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/arrow-top-right.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function TopRightArrowIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/attention.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function AlertIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/back.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function BackIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/check-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function ChecklistIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/checkbox-checked.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function CheckedCheckbox() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/checkbox-unchecked.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function UncheckedCheckbox() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/chevron-right-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallChevronRightIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/chevron-right.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function ChevronRightIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/cloud-sync.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function CloudSyncIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/cloud.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function CloudIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/connection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function connection() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/cross-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallCrossIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/cross.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function CrossIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/ellipsis-outline.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function EllipsisOutlineIcon() {
4 | return (
5 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/lib/icons/ellipsis.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function EllipsisIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/file-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallFileIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/help-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallHelpIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/info.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function InfoIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/mail.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function MailIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function MenuIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/new-note.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function NewNoteIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/no-connection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function connection() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/notes.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function NotesIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/pinned-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallPinnedIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/pinned.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function PinnedIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/preview-stop.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function PreviewStopIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/preview.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function PreviewIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/published-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallPublishedIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/reorder.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function ReorderIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/revisions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function RevisionsIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/search-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallSearchIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/settings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SettingsIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/share.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function ShareIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SidebarIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/simplenote-compact.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SimplenoteCompactLogo() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/simplenote.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SimplenoteLogo() {
4 | return (
5 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/lib/icons/style.scss:
--------------------------------------------------------------------------------
1 | svg[class^='icon-'] {
2 | fill: currentcolor;
3 | height: 24px;
4 | width: 24px;
5 | }
6 |
7 | svg[class^='icon-'][class$='-small'] {
8 | height: 16px;
9 | width: 16px;
10 | }
11 |
12 | // Color for the Simplenote logo
13 | .logo path {
14 | fill: var(--primary-branding-color);
15 | }
16 |
17 | .logo circle {
18 | fill: var(--background-color);
19 | }
20 |
--------------------------------------------------------------------------------
/lib/icons/sync-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function SmallSyncIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/tag.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function TagIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/tags.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function TagsIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/trash.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type OwnProps = {
4 | onClick: (event: React.MouseEvent) => any;
5 | };
6 |
7 | type Props = OwnProps;
8 |
9 | export default function TrashIcon({ onClick }: Props) {
10 | return (
11 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/lib/icons/untagged-notes.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function UntaggedNotesIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/warning.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function WarningIcon() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/lib/icons/wordpress.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function WordPressLogo() {
4 | return (
5 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/lib/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= htmlWebpackPlugin.options.title %>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/lib/logging-out.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import '../scss/style.scss';
5 |
6 | class LoggingOut extends Component {
7 | systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
8 | ? 'dark'
9 | : 'light';
10 |
11 | componentDidMount() {
12 | document.body.dataset.theme = this.systemTheme;
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
25 | Logging out…
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | export const boot = () => {
33 | render(, document.getElementById('root'));
34 | };
35 |
--------------------------------------------------------------------------------
/lib/menu-bar/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/variables' as *;
2 |
3 | .menu-bar {
4 | height: $toolbar-height;
5 | padding: 0 15px;
6 | border-bottom: 1px solid var(--secondary-color);
7 | display: flex;
8 | justify-content: space-between;
9 | align-items: center;
10 | flex: 0 0 auto;
11 | background-color: var(--background-color);
12 |
13 | .notes-title {
14 | font-size: 16px;
15 | font-weight: 500;
16 | flex: 0 1 auto;
17 | overflow: hidden;
18 | text-overflow: ellipsis;
19 | color: var(--primary-color);
20 | }
21 |
22 | .icon-button {
23 | flex: 0 0 auto;
24 | }
25 |
26 | .icon-button svg.icon-menu {
27 | width: 24px;
28 | height: 24px;
29 | }
30 |
31 | .button {
32 | padding: 0;
33 | width: 32px;
34 | height: 32px;
35 | }
36 |
37 | .icon-button:disabled {
38 | svg {
39 | cursor: default;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/navigation-bar/item/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | export const NavigationBarItem = ({
6 | icon,
7 | isSelected = false,
8 | label,
9 | onClick,
10 | }) => {
11 | const classes = classNames('navigation-bar-item', {
12 | 'is-selected': isSelected,
13 | });
14 |
15 | return (
16 |
17 |
21 |
22 | );
23 | };
24 |
25 | NavigationBarItem.propTypes = {
26 | icon: PropTypes.element.isRequired,
27 | isSelected: PropTypes.bool,
28 | label: PropTypes.string.isRequired,
29 | onClick: PropTypes.func.isRequired,
30 | };
31 |
32 | export default NavigationBarItem;
33 |
--------------------------------------------------------------------------------
/lib/navigation-bar/item/style.scss:
--------------------------------------------------------------------------------
1 | .navigation-bar-item {
2 | padding-left: 16px;
3 | text-overflow: ellipsis;
4 | white-space: nowrap;
5 | text-align: left;
6 |
7 | .button {
8 | border: 0;
9 | border-top: 1px solid var(--secondary-color);
10 | border-radius: 0;
11 | color: var(--primary-color);
12 | font-size: 16px;
13 | font-weight: 400;
14 | height: 44px;
15 | padding: 0;
16 | text-align: left;
17 | width: 100%;
18 |
19 | &:active {
20 | background: none;
21 | }
22 | }
23 |
24 | &:hover:not(.is-selected) {
25 | background: var(--secondary-highlight-color);
26 | }
27 |
28 | &.is-selected {
29 | background-color: var(--highlight-color);
30 |
31 | button {
32 | border-top: none;
33 |
34 | svg[class^='icon-'] {
35 | fill: var(--accent-color);
36 | }
37 | }
38 | }
39 |
40 | .navigation-bar-item__icon {
41 | color: var(--foreground-color);
42 | display: inline-block;
43 | margin-right: 18px;
44 | vertical-align: middle;
45 |
46 | svg {
47 | vertical-align: middle;
48 | position: relative;
49 | top: -0.2em;
50 | }
51 | }
52 | }
53 |
54 | /* this is the last child (Settings) and prevents a double border on the bottom */
55 | .navigation-bar-item:first-child .button {
56 | border-top: none !important;
57 | }
58 |
59 | .navigation-bar-item.is-selected + .navigation-bar-item .button {
60 | border-top: none;
61 | }
62 |
--------------------------------------------------------------------------------
/lib/navigation-bar/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/variables' as *;
2 |
3 | .navigation-bar {
4 | background-color: var(--background-color);
5 | display: flex;
6 | flex-direction: column;
7 | position: absolute;
8 | top: 0;
9 | left: 0;
10 | padding-top: $toolbar-height;
11 | width: $navigation-bar-width;
12 | height: 100%;
13 | border-right: 1px solid var(--secondary-color);
14 | z-index: 2;
15 | transition: transform 200ms ease-in-out;
16 | }
17 |
18 | .navigation-bar__folders {
19 | display: flex;
20 | flex: none;
21 | flex-direction: column;
22 | border-bottom: 1px solid var(--secondary-color);
23 | }
24 |
25 | .navigation-bar__tags {
26 | display: flex;
27 | height: 100%;
28 | flex-direction: column;
29 | flex: 0 1 auto;
30 | overflow: hidden;
31 | min-height: 9em;
32 | padding: 12px 0 0;
33 | }
34 |
35 | .navigation-bar__untagged {
36 | border-top: 1px solid var(--secondary-color);
37 | }
38 |
39 | .navigation-bar__tools {
40 | flex: 1 0 auto;
41 | padding: 10px 0;
42 | border-top: 1px solid var(--secondary-color);
43 | }
44 |
45 | .navigation-bar__footer {
46 | color: var(--foreground-color);
47 | display: flex;
48 | flex: none;
49 | justify-content: flex-start;
50 | align-items: center;
51 | padding: 0 20px 20px;
52 | }
53 |
54 | .navigation-bar__server-connection {
55 | color: var(--foreground-color);
56 | display: flex;
57 | flex: none;
58 | justify-content: flex-start;
59 | align-items: center;
60 | padding: 0 20px;
61 | }
62 |
63 | .navigation-bar__footer-item {
64 | margin-right: 10px;
65 |
66 | &:last-child {
67 | margin-right: 0;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/note-editor/style.scss:
--------------------------------------------------------------------------------
1 | .note-editor {
2 | display: flex;
3 | flex-direction: column;
4 | flex: 1 0 auto;
5 | user-select: text;
6 | background-color: var(--background-color);
7 | }
8 |
--------------------------------------------------------------------------------
/lib/note-info/references.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { getNoteReferences } from '../utils/get-note-references';
5 |
6 | import Reference from './reference';
7 |
8 | import type * as S from '../state';
9 | import type * as T from '../types';
10 |
11 | type StateProps = {
12 | references: T.EntityId[];
13 | };
14 |
15 | type Props = StateProps;
16 |
17 | export const References: FunctionComponent = ({ references }) => {
18 | if (!references.length) {
19 | return null;
20 | }
21 |
22 | return (
23 |
24 |
25 |
Referenced In
26 | {references.map((noteId) => (
27 |
28 | ))}
29 |
30 |
31 | );
32 | };
33 |
34 | const mapStateToProps: S.MapState = (state) => ({
35 | references: getNoteReferences(state),
36 | });
37 |
38 | export default connect(mapStateToProps)(References);
39 |
--------------------------------------------------------------------------------
/lib/note-list/decorators.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { escapeRegExp } from 'lodash';
3 | import replaceToArray from 'string-replace-to-array';
4 |
5 | import { withoutTags } from '../utils/filter-notes';
6 |
7 | export const decorateWith = (decorators, text) =>
8 | decorators.length > 0 && text.length > 0
9 | ? decorators
10 | .reduce((output, { filter, replacer }) => {
11 | const searchText = 'string' === typeof filter && withoutTags(filter);
12 | const pattern =
13 | searchText && searchText.length > 0
14 | ? new RegExp(escapeRegExp(searchText), 'gi')
15 | : filter;
16 |
17 | return replaceToArray(output, pattern, replacer);
18 | }, text)
19 | .map((chunk, key) =>
20 | chunk && 'string' !== typeof chunk
21 | ? React.cloneElement(chunk, { key })
22 | : chunk
23 | )
24 | : text;
25 |
26 | export const makeFilterDecorator = (filter) => ({
27 | filter,
28 | replacer: (match) => {
29 | if (match.length) {
30 | return {match};
31 | }
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/lib/note-list/get-note-title-and-preview.test.ts:
--------------------------------------------------------------------------------
1 | import { clearCache, withCache } from './get-note-title-and-preview';
2 |
3 | describe('getNoteTitleAndPreview', () => {
4 | describe('withCache', () => {
5 | const getKey = (note) => note.mockKey;
6 |
7 | afterEach(() => {
8 | clearCache();
9 | });
10 |
11 | it('should only call getValue() when necessary', () => {
12 | const getValue = jest.fn();
13 |
14 | withCache(getKey, getValue)({ id: 0, mockKey: 'foo' }); // should call
15 | withCache(getKey, getValue)({ id: 0, mockKey: 'foo' }); // shouldn't call
16 | expect(getValue).toHaveBeenCalledTimes(1);
17 | withCache(getKey, getValue)({ id: 0, mockKey: 'bar' }); // should call
18 | expect(getValue).toHaveBeenCalledTimes(2);
19 | });
20 |
21 | it('should return the cached value', () => {
22 | const getValue = jest.fn().mockReturnValueOnce('mock value');
23 |
24 | withCache(getKey, getValue)({ id: 0, mockKey: 'foo' });
25 | const result = withCache(getKey, getValue)({ id: 0, mockKey: 'foo' });
26 | expect(result).toBe('mock value');
27 | });
28 |
29 | it('should return an updated value when cache is invalidated', () => {
30 | const getValue = jest
31 | .fn()
32 | .mockReturnValueOnce('initial value')
33 | .mockReturnValueOnce('changed value');
34 |
35 | withCache(getKey, getValue)({ id: 0, mockKey: 'foo' });
36 | const result = withCache(getKey, getValue)({ id: 0, mockKey: 'bar' });
37 | expect(result).toBe('changed value');
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/lib/note-list/get-note-title-and-preview.ts:
--------------------------------------------------------------------------------
1 | import getNoteExcerpt from '../utils/note-utils';
2 |
3 | /** @type {Map} stores a cache of computed note preview excerpts to prevent re-truncating note content */
4 | const noteCache = new Map();
5 |
6 | /**
7 | * Caches based on note id
8 | *
9 | * @param {Function} getKey Get the key used for invalidating the cache
10 | * @param {Function} getValue Get the value for the cache
11 | * @returns {Object} note title and preview excerpt
12 | */
13 | export const withCache = (getKey, getValue) => (note) => {
14 | let cached = noteCache.get(note.id);
15 | const key = getKey(note);
16 |
17 | if ('undefined' === typeof cached || key !== cached.key) {
18 | const newCacheObj = { key, value: getValue(note) };
19 | noteCache.set(note.id, newCacheObj);
20 | cached = newCacheObj;
21 | }
22 | return cached.value;
23 | };
24 |
25 | export const clearCache = () => noteCache.clear();
26 |
27 | const getNoteTitleAndPreview = withCache(
28 | (note) => note.data.content,
29 | getNoteExcerpt
30 | );
31 |
32 | export default getNoteTitleAndPreview;
33 |
--------------------------------------------------------------------------------
/lib/note-revisions/style.scss:
--------------------------------------------------------------------------------
1 | .note-revisions {
2 | background-color: var(--background-color);
3 | display: flex;
4 | flex-direction: column;
5 | flex: 1 1 auto;
6 | padding-top: 20px;
7 |
8 | .note-revisions-tag-list {
9 | display: flex;
10 | justify-content: flex-start;
11 | flex-wrap: wrap;
12 | line-height: 1.75em;
13 | white-space: nowrap;
14 | overflow: auto;
15 | max-height: calc(2.5 * 1.75em + 16px); // about 2.5 rows
16 | padding: 8px 12px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/search-field/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/variables' as *;
2 |
3 | .search-field {
4 | display: flex;
5 | flex-direction: row;
6 | align-items: center;
7 | width: 100%;
8 | padding: 6px 5px 5px 14px;
9 | border-bottom: 1px solid var(--secondary-color);
10 | height: $toolbar-height;
11 | background-color: var(--background-color);
12 |
13 | input {
14 | appearance: none;
15 | background-color: var(--background-color);
16 | border: 0;
17 | color: var(--primary-color);
18 | font-size: 16px;
19 | min-width: 0; // Firefox
20 | text-overflow: ellipsis !important;
21 | width: 100%;
22 |
23 | &:focus {
24 | outline: none;
25 | }
26 | }
27 |
28 | .icon-button {
29 | align-items: center;
30 | display: flex;
31 | flex: 0 0 auto;
32 | justify-content: center;
33 | }
34 |
35 | .icon-cross-small {
36 | transition: $anim-transition;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/search-results-bar/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/variables' as *;
2 |
3 | .search-results {
4 | color: var(--primary-color);
5 | height: $toolbar-height;
6 | line-height: $toolbar-height;
7 | z-index: 100;
8 | border-top: 1px solid var(--secondary-color);
9 | background-color: var(--background-color);
10 | text-align: center;
11 | user-select: none;
12 |
13 | div {
14 | display: inline-block;
15 | }
16 |
17 | .search-results-next,
18 | .search-results-prev {
19 | float: right;
20 | padding: 0 6px;
21 | width: 42px;
22 | height: 100%;
23 |
24 | svg {
25 | fill: var(--accent-color);
26 | }
27 | }
28 |
29 | .search-results-next {
30 | margin-right: 6px;
31 | }
32 |
33 | .search-results-prev svg {
34 | transform: scaleX(-1);
35 | }
36 |
37 | @media only screen and (max-width: $single-column) {
38 | left: 0;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/state/actions.ts:
--------------------------------------------------------------------------------
1 | import * as analytics from './analytics/actions';
2 | import * as data from './data/actions';
3 | import * as electron from './electron/actions';
4 | import * as settings from './settings/actions';
5 | import * as simperium from './simperium/actions';
6 | import * as ui from './ui/actions';
7 |
8 | export default {
9 | analytics,
10 | data,
11 | electron,
12 | simperium,
13 | settings,
14 | ui,
15 | };
16 |
--------------------------------------------------------------------------------
/lib/state/analytics/actions.ts:
--------------------------------------------------------------------------------
1 | import * as A from '../action-types';
2 | import * as T from '../../types';
3 |
4 | export const withEvent =
5 | (eventName: string, eventProperties?: T.JSONSerializable) => (action) => ({
6 | ...action,
7 | meta: {
8 | ...action.meta,
9 | analytics: [
10 | ...(action.meta?.analytics ?? []),
11 | [eventName, eventProperties],
12 | ],
13 | },
14 | });
15 |
16 | export const recordEvent: A.ActionCreator = (
17 | eventName: string,
18 | eventProperties?: T.JSONSerializable
19 | ) =>
20 | withEvent(
21 | eventName,
22 | eventProperties
23 | )({
24 | type: 'RECORD_EVENT',
25 | });
26 |
--------------------------------------------------------------------------------
/lib/state/browser/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import * as A from '../action-types';
4 | import * as S from '../';
5 |
6 | ///////////////////////////////////////
7 | //// HELPERS
8 | ///////////////////////////////////////
9 |
10 | const getTheme = () =>
11 | window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
12 |
13 | const getWidth = () => window.innerWidth;
14 |
15 | ///////////////////////////////////////
16 | //// REDUCERS
17 | ///////////////////////////////////////
18 |
19 | const systemTheme: A.Reducer<'light' | 'dark'> = (
20 | state = getTheme(),
21 | action
22 | ) => (action.type === 'SYSTEM_THEME_UPDATE' ? action.prefers : state);
23 |
24 | const windowWidth: A.Reducer = (state = getWidth(), action) =>
25 | action.type === 'WINDOW_RESIZE' ? action.innerWidth : state;
26 |
27 | ///////////////////////////////////////
28 | //// COMBINED
29 | ///////////////////////////////////////
30 |
31 | export const reducer = combineReducers({
32 | windowWidth,
33 | systemTheme,
34 | });
35 |
36 | export const middleware: S.Middleware = ({ dispatch }) => {
37 | window.addEventListener('resize', () =>
38 | dispatch({
39 | type: 'WINDOW_RESIZE',
40 | innerWidth: getWidth(),
41 | })
42 | );
43 |
44 | window.matchMedia('(prefers-color-scheme: dark)').addListener(() =>
45 | dispatch({
46 | type: 'SYSTEM_THEME_UPDATE',
47 | prefers: getTheme(),
48 | })
49 | );
50 |
51 | return (next) => next;
52 | };
53 |
--------------------------------------------------------------------------------
/lib/state/electron/actions.ts:
--------------------------------------------------------------------------------
1 | import * as A from '../action-types';
2 |
3 | export const reallyCloseWindow: A.ActionCreator = () => ({
4 | type: 'REALLY_CLOSE_WINDOW',
5 | });
6 |
--------------------------------------------------------------------------------
/lib/state/settings/actions.ts:
--------------------------------------------------------------------------------
1 | import * as A from '../action-types';
2 | import * as T from '../../types';
3 |
4 | export const activateTheme: A.ActionCreator = (theme: T.Theme) => ({
5 | type: 'setTheme',
6 | theme,
7 | });
8 |
9 | export const setNoteDisplay: A.ActionCreator = (
10 | noteDisplay: T.ListDisplayMode
11 | ) => ({
12 | type: 'setNoteDisplay',
13 | noteDisplay,
14 | });
15 |
16 | export const setLineLength: A.ActionCreator = (
17 | lineLength: T.LineLength
18 | ) => ({
19 | type: 'setLineLength',
20 | lineLength,
21 | });
22 |
23 | export const toggleKeyboardShortcuts: A.ActionCreator<
24 | A.ToggleKeyboardShortcuts
25 | > = () => ({
26 | type: 'KEYBOARD_SHORTCUTS_TOGGLE',
27 | });
28 |
29 | export const toggleSortOrder: A.ActionCreator = () => ({
30 | type: 'TOGGLE_SORT_ORDER',
31 | });
32 |
33 | export const setSortType: A.ActionCreator = (
34 | sortType: T.SortType,
35 | sortReversed?: boolean
36 | ) => ({
37 | type: 'setSortType',
38 | sortType,
39 | sortReversed,
40 | });
41 |
42 | export const toggleSortTagsAlpha: A.ActionCreator<
43 | A.ToggleSortTagsAlpha
44 | > = () => ({
45 | type: 'TOGGLE_SORT_TAGS_ALPHA',
46 | });
47 |
48 | export const setAccountName: A.ActionCreator = (
49 | accountName: string
50 | ) => ({
51 | type: 'setAccountName',
52 | accountName,
53 | });
54 |
55 | export const toggleFocusMode: A.ActionCreator = () => ({
56 | type: 'TOGGLE_FOCUS_MODE',
57 | });
58 |
59 | export const toggleSpellCheck: A.ActionCreator = () => ({
60 | type: 'TOGGLE_SPELLCHECK',
61 | });
62 |
63 | export const toggleAutoHideMenuBar: A.ActionCreator<
64 | A.ToggleAutoHideMenuBar
65 | > = () => ({
66 | type: 'TOGGLE_AUTO_HIDE_MENU_BAR',
67 | });
68 |
--------------------------------------------------------------------------------
/lib/state/simperium/actions.ts:
--------------------------------------------------------------------------------
1 | import * as A from '../action-types';
2 | import * as T from '../../types';
3 |
4 | export const remoteNoteUpdate: A.ActionCreator = (
5 | noteId: T.EntityId,
6 | note: T.Note
7 | ) => ({
8 | type: 'REMOTE_NOTE_UPDATE',
9 | noteId,
10 | note,
11 | });
12 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/connection-monitor.ts:
--------------------------------------------------------------------------------
1 | import type { Client } from 'simperium';
2 |
3 | import * as S from '../../';
4 |
5 | export const start = (client: Client, { dispatch, getState }: S.Store) => {
6 | let lastMessageAt = -Infinity;
7 |
8 | client.on('message', () => {
9 | lastMessageAt = Date.now();
10 | if (getState().simperium.connectionStatus !== 'green') {
11 | dispatch({ type: 'CHANGE_CONNECTION_STATUS', status: 'green' });
12 | }
13 | });
14 |
15 | setInterval(() => {
16 | const timeSinceLastMessage = Date.now() - lastMessageAt;
17 | const currentStatus = getState().simperium.connectionStatus;
18 | if (timeSinceLastMessage > 8000 && currentStatus === 'green') {
19 | dispatch({ type: 'CHANGE_CONNECTION_STATUS', status: 'red' });
20 | }
21 | }, 1000);
22 |
23 | window.addEventListener('online', () => {
24 | if (getState().simperium.connectionStatus === 'offline') {
25 | dispatch({ type: 'CHANGE_CONNECTION_STATUS', status: 'red' });
26 | }
27 | });
28 |
29 | window.addEventListener('offline', () => {
30 | dispatch({ type: 'CHANGE_CONNECTION_STATUS', status: 'offline' });
31 | });
32 |
33 | client.on('disconnect', () => {
34 | dispatch({
35 | type: 'CHANGE_CONNECTION_STATUS',
36 | status: navigator.onLine ? 'red' : 'offline',
37 | });
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/in-memory-bucket.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BucketObject,
3 | BucketStore,
4 | EntityCallback,
5 | EntitiesCallback,
6 | } from 'simperium';
7 | import type * as T from '../../../types';
8 |
9 | export class InMemoryBucket implements BucketStore {
10 | entities: Map;
11 |
12 | constructor() {
13 | this.entities = new Map();
14 | }
15 |
16 | get(id: T.EntityId, callback: EntityCallback>) {
17 | callback(null, { id, data: this.entities.get(id) });
18 | }
19 |
20 | find(query: {}, callback: EntitiesCallback>) {
21 | callback(
22 | null,
23 | [...this.entities].map(([id, data]) => ({ id, data }))
24 | );
25 | }
26 |
27 | remove(id: T.EntityId, callback: (error: null) => void) {
28 | this.entities.delete(id);
29 | callback(null);
30 | }
31 |
32 | update(
33 | id: T.EntityId,
34 | data: U,
35 | isIndexing: boolean,
36 | callback: EntityCallback>
37 | ) {
38 | this.entities.set(id, data);
39 | callback(null, { id, data, isIndexing });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/in-memory-ghost.ts:
--------------------------------------------------------------------------------
1 | import { ChangeVersion, EntityId, GhostStore, Ghost } from 'simperium';
2 |
3 | export class InMemoryGhost implements GhostStore {
4 | cv: ChangeVersion;
5 | entities: Map>;
6 |
7 | constructor() {
8 | this.cv = '';
9 | this.entities = new Map();
10 | }
11 |
12 | getChangeVersion(): Promise {
13 | return Promise.resolve(this.cv);
14 | }
15 |
16 | setChangeVersion(version: ChangeVersion): Promise {
17 | this.cv = version;
18 | return Promise.resolve();
19 | }
20 |
21 | get(entityId: EntityId): Promise> {
22 | return Promise.resolve(
23 | this.entities.get(entityId) ?? ({ key: entityId, data: {} } as Ghost)
24 | );
25 | }
26 |
27 | put(entityId: EntityId, version: number, data: U): Promise> {
28 | const ghost = { key: entityId, data, version };
29 | this.entities.set(entityId, ghost);
30 |
31 | return Promise.resolve(ghost);
32 | }
33 |
34 | remove(entityId: EntityId): Promise> {
35 | const ghost = this.entities.get(entityId);
36 | this.entities.delete(entityId);
37 |
38 | return Promise.resolve(ghost);
39 | }
40 |
41 | eachGhost(iterator: (ghost: Ghost) => void) {
42 | this.entities.forEach((ghost) => iterator(ghost));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/note-bucket.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BucketObject,
3 | BucketStore,
4 | EntityCallback,
5 | EntitiesCallback,
6 | } from 'simperium';
7 | import * as S from '../../';
8 | import * as T from '../../../types';
9 |
10 | export class NoteBucket implements BucketStore {
11 | store: S.Store;
12 |
13 | constructor(store: S.Store) {
14 | this.store = store;
15 | }
16 |
17 | get(noteId: T.EntityId, callback: EntityCallback>) {
18 | const note = this.store.getState().data.notes.get(noteId);
19 |
20 | callback(null, { id: noteId, data: note });
21 | }
22 |
23 | find(query: {}, callback: EntitiesCallback>) {
24 | callback(
25 | null,
26 | [...this.store.getState().data.notes.entries()].map(([noteId, note]) => ({
27 | id: noteId,
28 | data: note,
29 | }))
30 | );
31 | }
32 |
33 | remove(noteId: T.EntityId, callback: (error: null) => void) {
34 | this.store.dispatch({
35 | type: 'NOTE_BUCKET_REMOVE',
36 | noteId,
37 | });
38 | callback(null);
39 | }
40 |
41 | update(
42 | noteId: T.EntityId,
43 | note: T.Note,
44 | isIndexing: boolean,
45 | callback: EntityCallback>
46 | ) {
47 | this.store.dispatch({
48 | type: 'NOTE_BUCKET_UPDATE',
49 | noteId,
50 | note,
51 | isIndexing,
52 | });
53 | callback(null, { id: noteId, data: note });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/preferences-bucket.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BucketObject,
3 | BucketStore,
4 | EntityCallback,
5 | EntitiesCallback,
6 | } from 'simperium';
7 | import * as S from '../../';
8 | import * as T from '../../../types';
9 |
10 | export class PreferencesBucket implements BucketStore {
11 | store: S.Store;
12 |
13 | constructor(store: S.Store) {
14 | this.store = store;
15 | }
16 |
17 | get(id: T.EntityId, callback: EntityCallback>) {
18 | const data = this.store.getState().data.preferences.get(id);
19 |
20 | callback(null, { id, data });
21 | }
22 |
23 | find(query: {}, callback: EntitiesCallback>) {
24 | callback(
25 | null,
26 | [...this.store.getState().data.preferences.entries()].map(
27 | ([id, data]) => ({ id, data })
28 | )
29 | );
30 | }
31 |
32 | remove(id: T.EntityId, callback: (error: null) => void) {
33 | this.store.dispatch({
34 | type: 'PREFERENCES_BUCKET_REMOVE',
35 | id,
36 | });
37 | callback(null);
38 | }
39 |
40 | update(
41 | id: T.EntityId,
42 | data: T.Preferences,
43 | isIndexing: boolean,
44 | callback: EntityCallback>
45 | ) {
46 | this.store.dispatch({
47 | type: 'PREFERENCES_BUCKET_UPDATE',
48 | id,
49 | data,
50 | isIndexing,
51 | });
52 | callback(null, { id, data });
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/tab-close-confirmation.ts:
--------------------------------------------------------------------------------
1 | import { getUnconfirmedChanges } from './unconfirmed-changes';
2 |
3 | import type * as S from '../../';
4 |
5 | export const confirmBeforeClosingTab = (store: S.Store) => {
6 | window.addEventListener('beforeunload', (event) => {
7 | const changes = getUnconfirmedChanges(store.getState());
8 | if (changes.notes.length === 0) {
9 | return undefined;
10 | }
11 |
12 | event.preventDefault();
13 |
14 | // this message is hidden by most browsers
15 | // and replaced by a generic message
16 | const message =
17 | 'There are unsynchronized changes - do you want to logout and lose those changes?';
18 | event.returnValue = message;
19 | return message;
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/tag-bucket.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BucketObject,
3 | BucketStore,
4 | EntityCallback,
5 | EntitiesCallback,
6 | } from 'simperium';
7 | import type * as S from '../../';
8 | import type * as T from '../../../types';
9 |
10 | export class TagBucket implements BucketStore {
11 | store: S.Store;
12 |
13 | constructor(store: S.Store) {
14 | this.store = store;
15 | }
16 |
17 | get(tagId: T.EntityId, callback: EntityCallback>) {
18 | const tag = this.store.getState().data.tags.get(tagId);
19 |
20 | callback(null, { id: tagId, data: tag });
21 | }
22 |
23 | find(query: {}, callback: EntitiesCallback>) {
24 | callback(
25 | null,
26 | [...this.store.getState().data.tags.entries()].map(([tagHash, tag]) => ({
27 | id: tagHash,
28 | data: tag,
29 | }))
30 | );
31 | }
32 |
33 | remove(tagId: T.EntityId, callback: (error: null) => void) {
34 | this.store.dispatch({
35 | type: 'TAG_BUCKET_REMOVE',
36 | tagHash: tagId,
37 | });
38 | callback(null);
39 | }
40 |
41 | update(
42 | tagId: T.EntityId,
43 | tag: T.Tag,
44 | isIndexing: boolean,
45 | callback: EntityCallback>
46 | ) {
47 | this.store.dispatch({
48 | type: 'TAG_BUCKET_UPDATE',
49 | tagHash: tagId,
50 | tag,
51 | isIndexing,
52 | });
53 | callback(null, { id: tagId, data: tag });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/unconfirmed-changes.ts:
--------------------------------------------------------------------------------
1 | import { notesAreEqual } from '../../selectors';
2 |
3 | import type * as S from '../../';
4 | import type { EntityId } from 'simperium';
5 |
6 | const getUnconfirmedNotes = (state: S.State): EntityId[] => {
7 | const notes: EntityId[] = [];
8 |
9 | state.data.notes.forEach((note, noteId) => {
10 | const ghost = state.simperium.ghosts[1].get('note')?.get(noteId);
11 |
12 | if (!ghost || !notesAreEqual(note, ghost.data)) {
13 | notes.push(noteId);
14 | }
15 | });
16 |
17 | return notes;
18 | };
19 |
20 | const getUnconfirmedPreferences = (state: S.State): EntityId[] => {
21 | return [];
22 | };
23 |
24 | const getUnconfirmedTags = (state: S.State): EntityId[] => {
25 | return [];
26 | };
27 |
28 | export const getUnconfirmedChanges = (
29 | state: S.State
30 | ): Record<'notes' | 'preferences' | 'tags', EntityId[]> => {
31 | return {
32 | notes: getUnconfirmedNotes(state),
33 | preferences: getUnconfirmedPreferences(state),
34 | tags: getUnconfirmedTags(state),
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/lib/state/simperium/functions/username-monitor.ts:
--------------------------------------------------------------------------------
1 | import type { Client } from 'simperium';
2 |
3 | export const getAccountName = (client: Client): Promise =>
4 | new Promise((resolve) => {
5 | const usernameMonitor = (message: string) => {
6 | if (!message.startsWith('0:auth:')) {
7 | return;
8 | }
9 |
10 | const [prefix, accountName] = message.split('0:auth:');
11 | client.off('message', usernameMonitor);
12 | resolve(accountName);
13 | };
14 |
15 | client.on('message', usernameMonitor);
16 | });
17 |
--------------------------------------------------------------------------------
/lib/state/ui/search-field-middleware.ts:
--------------------------------------------------------------------------------
1 | import * as A from '../action-types';
2 | import * as S from '../';
3 |
4 | const searchFields = new Set();
5 |
6 | export const registerSearchField = (focus: Function) => searchFields.add(focus);
7 |
8 | export const middleware: S.Middleware = () => {
9 | return (next) => (action: A.ActionType) => {
10 | const result = next(action);
11 |
12 | switch (action.type) {
13 | case 'SEARCH':
14 | searchFields.forEach((focus) => focus());
15 | break;
16 |
17 | case 'FOCUS_SEARCH_FIELD':
18 | searchFields.forEach((focus) => focus('select'));
19 | break;
20 | }
21 |
22 | return result;
23 | };
24 | };
25 |
26 | export default middleware;
27 |
--------------------------------------------------------------------------------
/lib/tag-email-tooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { showDialog } from '../state/ui/actions';
5 |
6 | import * as S from '../state';
7 |
8 | type DispatchProps = {
9 | openShareDialog: () => any;
10 | };
11 |
12 | type Props = DispatchProps;
13 |
14 | export const EmailToolTip: FunctionComponent = ({ openShareDialog }) => (
15 |
26 | );
27 |
28 | const mapDispatchToProps: S.MapDispatch = (dispatch) => ({
29 | openShareDialog: () => dispatch(showDialog('SHARE')),
30 | });
31 |
32 | export default connect(null, mapDispatchToProps)(EmailToolTip);
33 |
--------------------------------------------------------------------------------
/lib/tag-email-tooltip/style.scss:
--------------------------------------------------------------------------------
1 | .tag-email-tooltip {
2 | position: absolute;
3 | bottom: 40px;
4 | padding: 5px 0;
5 | max-width: 360px;
6 | margin-left: 20px;
7 | margin-right: 20px;
8 | z-index: 100000;
9 | opacity: 0.99;
10 | }
11 |
12 | .tag-email-tooltip__arrow {
13 | position: absolute;
14 | width: 0;
15 | height: 0;
16 | border: solid transparent;
17 | opacity: 0.75;
18 | bottom: 0;
19 | left: 50px;
20 | margin-left: -5px;
21 | border-width: 5px 5px 0;
22 | border-top-color: #000;
23 | }
24 |
25 | .tag-email-tooltip__inside {
26 | padding: 6px 8px;
27 | color: #fff;
28 | text-align: center;
29 | border-radius: 3px;
30 | background-color: #000;
31 | opacity: 0.75;
32 | }
33 |
--------------------------------------------------------------------------------
/lib/tag-field/style.scss:
--------------------------------------------------------------------------------
1 | .tag-editor {
2 | display: flex;
3 | justify-content: flex-start;
4 | flex-wrap: wrap;
5 | flex: 1 1 auto;
6 | line-height: 1.75em;
7 | white-space: nowrap;
8 | overflow: auto;
9 | max-height: calc(2.5 * 1.75em + 16px); // about 2.5 rows
10 | padding: 8px 12px;
11 | background-color: var(--background-color);
12 |
13 | &:focus {
14 | outline: none;
15 | }
16 |
17 | .hidden-tag {
18 | width: 0;
19 | height: 0;
20 | border: 0;
21 | padding: 0;
22 | opacity: 0;
23 |
24 | &:focus {
25 | outline: none;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/tag-input/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/variables' as *;
2 |
3 | .tag-input {
4 | cursor: text;
5 | position: relative;
6 | display: flex;
7 | flex: 1 0 auto;
8 | overflow: visible;
9 | min-height: 26px;
10 | color: var(--primary-color);
11 |
12 | input {
13 | width: 100%;
14 | height: 1.75em;
15 | min-width: 140px;
16 | outline: none;
17 | font: 14px/1.5 $sans;
18 | color: var(--primary-color);
19 | background: transparent;
20 | }
21 | }
22 |
23 | .tag-input__placeholder {
24 | position: absolute;
25 | display: block;
26 | background: transparent;
27 | user-select: none;
28 | color: var(--placeholder-color);
29 | }
30 |
31 | .tag-input__entry {
32 | min-width: 1px; // Needed for the caret to be visible in Safari
33 |
34 | & b,
35 | & strong {
36 | font-weight: normal;
37 | }
38 |
39 | & i,
40 | & em {
41 | font-style: normal;
42 | }
43 |
44 | &:focus {
45 | outline: none;
46 | }
47 | }
48 |
49 | .tag-input__suggestion {
50 | color: var(--foreground-color);
51 | }
52 |
--------------------------------------------------------------------------------
/lib/tag-suggestions/style.scss:
--------------------------------------------------------------------------------
1 | .tag-suggestions {
2 | overflow-x: hidden;
3 |
4 | .tag-suggestions-list {
5 | list-style-type: none;
6 | padding: 0;
7 | margin: 0;
8 | border-bottom: 1px solid var(--secondary-color);
9 |
10 | .tag-suggestion-row {
11 | height: 36px;
12 | line-height: 36px;
13 | cursor: pointer;
14 | position: relative;
15 | padding: 0;
16 | }
17 |
18 | .tag-suggestion {
19 | border-color: var(--secondary-color);
20 | color: var(--primary-color);
21 | font-size: 16px;
22 | margin-left: 28px;
23 | overflow-x: hidden;
24 | text-overflow: ellipsis;
25 | }
26 | }
27 |
28 | .tag-suggestion-row:last-child .tag-suggestion {
29 | border-bottom: none;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/utils/crypto-random-string.ts:
--------------------------------------------------------------------------------
1 | import randomBytes from 'randombytes';
2 |
3 | // Avoid using the npm `crypto-random-string` module,
4 | // since it requires the whole `crypto` module which will bloat
5 | // our bundle with large unused modules `elliptic` and `bn.js`.
6 | const cryptoRandomString = (len) => {
7 | if (!Number.isFinite(len)) {
8 | throw new TypeError('Expected a finite number');
9 | }
10 |
11 | return randomBytes(Math.ceil(len / 2))
12 | .toString('hex')
13 | .slice(0, len);
14 | };
15 |
16 | export default cryptoRandomString;
17 |
--------------------------------------------------------------------------------
/lib/utils/ensure-platform-support.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import BootWarning from '../components/boot-warning';
5 |
6 | const hasLocalStorage = (): boolean => {
7 | try {
8 | localStorage.setItem('__localStorageSentinel__', 'present');
9 | localStorage.removeItem('__localStorageSentinel__');
10 | return true;
11 | } catch (e) {
12 | return false;
13 | }
14 | };
15 |
16 | const deps = [['localStorage', hasLocalStorage()]] as const;
17 |
18 | const missingDeps = deps.filter(([, hasIt]) => !hasIt).map(([name]) => name);
19 |
20 | if (missingDeps.length) {
21 | render(
22 |
23 |
24 | Simplenote depends on a few web technologies to operate. Please make
25 | sure that you have all of the following enabled in your browser.
26 |
27 |
28 | {deps.map(([name, hasIt]) => (
29 | -
30 | {hasIt ? '✅' : '⚠️'} {name} - {hasIt ? 'working' : 'missing'}
31 |
32 | ))}
33 |
34 |
35 | Many browsers disable some of these features in Private Mode. Simplenote
36 | does not currently support running in Private Mode.
37 |
38 | ,
39 | document.getElementById('root')
40 | );
41 | throw new Error(
42 | `Simplenote is missing required dependencies: ${missingDeps.join(', ')}`
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/lib/utils/export/export-notes.ts:
--------------------------------------------------------------------------------
1 | import { partition, sortBy } from 'lodash';
2 |
3 | import normalizeLineBreak from './normalize-line-break';
4 | import isEmailTag from '../is-email-tag';
5 |
6 | import * as T from '../../types';
7 | import { ExportNote } from './types';
8 |
9 | const exportNotes = (notes: Map) => {
10 | const activeNotes: ExportNote[] = [];
11 | const trashedNotes: ExportNote[] = [];
12 | [...notes.entries()].forEach((notePair) => {
13 | const [id, note] = notePair;
14 | const [collaboratorEmails, tags] = partition(
15 | sortBy(note.tags, (a) => a.toLocaleLowerCase()),
16 | isEmailTag
17 | );
18 | const parsedNote = Object.assign(
19 | {
20 | id,
21 | content: normalizeLineBreak(note.content),
22 | creationDate: new Date(note.creationDate * 1000).toISOString(),
23 | lastModified: new Date(note.modificationDate * 1000).toISOString(),
24 | },
25 | note.systemTags.includes('pinned') && { pinned: true },
26 | note.systemTags.includes('markdown') && { markdown: true },
27 | tags.length && { tags },
28 | note.systemTags.includes('published') &&
29 | note?.publishURL && {
30 | publicURL: `http://simp.ly/p/${note.publishURL}`,
31 | },
32 | note.systemTags.includes('shared') &&
33 | collaboratorEmails.length && { collaboratorEmails }
34 | );
35 | note.deleted ? trashedNotes.push(parsedNote) : activeNotes.push(parsedNote);
36 | });
37 | return Promise.resolve({ activeNotes, trashedNotes });
38 | };
39 |
40 | export default exportNotes;
41 |
--------------------------------------------------------------------------------
/lib/utils/export/index.ts:
--------------------------------------------------------------------------------
1 | import saveAs from 'file-saver';
2 | import { get } from 'lodash';
3 |
4 | import exportNotes from './export-notes';
5 | import exportToZip from './to-zip';
6 |
7 | import * as T from '../../types';
8 |
9 | const filename = 'notes.zip';
10 |
11 | const exportZipArchive = (notes: Map) => {
12 | return exportNotes(notes)
13 | .then(exportToZip)
14 | .then((zip) =>
15 | zip?.generateAsync({
16 | compression: 'DEFLATE',
17 | platform: get(window, 'process.platform', 'DOS'),
18 | type: 'blob',
19 | })
20 | )
21 | .then((blob) => saveAs(blob, filename))
22 | .catch(console.log); // eslint-disable-line no-console
23 | };
24 |
25 | export default exportZipArchive;
26 |
--------------------------------------------------------------------------------
/lib/utils/export/normalize-line-break.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Normalize line break characters to CRLF
3 | * @param content string Content to normalize
4 | */
5 | const normalizeLineBreak = (content: string) =>
6 | content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
7 |
8 | export default normalizeLineBreak;
9 |
--------------------------------------------------------------------------------
/lib/utils/export/test/normalize-line-breaks.ts:
--------------------------------------------------------------------------------
1 | import normalizeLineBreak from '../normalize-line-break';
2 |
3 | describe('Normalize line break', () => {
4 | const text = 'Line 1\nLine 2\r\nLine 3\n\r\nLine 5\r\n\nLine 5';
5 | const normalizedText =
6 | 'Line 1\r\nLine 2\r\nLine 3\r\n\r\nLine 5\r\n\r\nLine 5';
7 |
8 | it('should convert LF to CRLF', () => {
9 | expect(normalizeLineBreak(text)).toBe(normalizedText);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/lib/utils/export/types.ts:
--------------------------------------------------------------------------------
1 | import * as T from '../../types';
2 |
3 | export type ExportNote = {
4 | content: string;
5 | collaboratorEmails: T.TagName[];
6 | creationDate: T.SecondsEpoch;
7 | id: T.EntityId;
8 | markdown?: boolean;
9 | modificationDate: T.SecondsEpoch;
10 | pinned?: boolean;
11 | publicURL?: string;
12 | tags: T.TagName[];
13 | };
14 |
15 | export type GroupedExportNotes = {
16 | activeNotes: ExportNote[];
17 | trashedNotes: ExportNote[];
18 | };
19 |
--------------------------------------------------------------------------------
/lib/utils/filter-at-most.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Return the first maxResults matching items in the list
3 | *
4 | * This is like items.filter(predicate).slice(0,maxResults)
5 | * but it early-aborts as soon as we find our max results.
6 | * If we were filtering thousands of tags, for example, there'd
7 | * be no reason to iterate through all of them and only prune
8 | * the list after computing whether each one matches.
9 | *
10 | * @param items items to filter
11 | * @param predicate filtering function
12 | * @param maxResults maximum number of returned matching items
13 | */
14 | export default function (
15 | items: I[],
16 | predicate: (item: I) => boolean,
17 | maxResults: number
18 | ): I[] {
19 | const results = [];
20 | for (const item of items) {
21 | if (predicate(item)) {
22 | results.push(item);
23 | }
24 |
25 | if (results.length === maxResults) {
26 | break;
27 | }
28 | }
29 | return results;
30 | }
31 |
--------------------------------------------------------------------------------
/lib/utils/filter-notes.ts:
--------------------------------------------------------------------------------
1 | const tagPattern = () => /(?:\btag:)([^\s,]+)/g;
2 |
3 | export const withoutTags = (s: string) => s.replace(tagPattern(), '').trim();
4 |
5 | export const getTerms = (filterText: string): string[] => {
6 | if (!filterText) {
7 | return [];
8 | }
9 |
10 | const literalsPattern = /(?:")((?:"|[^"])+?)(?:")/g;
11 | const boundaryPattern = /[\b\s]/g;
12 |
13 | let match;
14 | let storedLastIndex = 0;
15 | let withoutLiterals = '';
16 |
17 | const filter = withoutTags(filterText);
18 |
19 | const literals = [];
20 | while ((match = literalsPattern.exec(filter)) !== null) {
21 | literals.push(match[0].slice(1, -1));
22 |
23 | // anything in between our last saved index and the current match index is a non-literal term
24 | // ex: for the search string [ "foo" bar "baz" ] we'll save "bar" as a non-literal here when we match "baz"
25 | withoutLiterals += filter.slice(storedLastIndex, match.index);
26 |
27 | // lastIndex is the end of the current match
28 | // -- where in the string to start scanning for the next match on the next loop iteration
29 | storedLastIndex = literalsPattern.lastIndex;
30 | }
31 |
32 | // save any search terms that occur after the last matched literal
33 | // i.e. between our last saved index and the end of the string
34 | if (
35 | (storedLastIndex > 0 || literals.length === 0) &&
36 | storedLastIndex < filter.length
37 | ) {
38 | withoutLiterals += filter.slice(storedLastIndex);
39 | }
40 |
41 | const terms = withoutLiterals
42 | .split(boundaryPattern)
43 | .map((a) => a.trim())
44 | .filter((a) => a);
45 |
46 | return [...literals, ...terms];
47 | };
48 |
--------------------------------------------------------------------------------
/lib/utils/get-note-references.ts:
--------------------------------------------------------------------------------
1 | import { number } from 'prop-types';
2 | import type * as S from '../state';
3 | import type * as T from '../types';
4 |
5 | import getNoteTitleAndPreview from './note-utils';
6 |
7 | const getNoteLink = (id: T.EntityId): string => `simplenote://note/${id}`;
8 |
9 | export const getNoteReferences = (state: S.State): T.EntityId[] => {
10 | const matches = new Set();
11 | if (!state.ui.openedNote) {
12 | return [];
13 | }
14 | const noteLink = getNoteLink(state.ui.openedNote);
15 | state.data.notes.forEach((note, key) => {
16 | if (note.content.includes(noteLink)) {
17 | matches.add(key);
18 | }
19 | });
20 | return [...matches.values()];
21 | };
22 |
23 | type noteReference =
24 | | {
25 | count: number;
26 | noteId: T.EntityId;
27 | modificationDate: T.SecondsEpoch;
28 | title: string;
29 | }
30 | | undefined;
31 |
32 | export const getNoteReference = (
33 | state: S.State,
34 | noteId: T.EntityId
35 | ): noteReference => {
36 | const note = state.data.notes.get(noteId);
37 | if (!note || !state.ui.openedNote) {
38 | return;
39 | }
40 | const regExp = new RegExp(getNoteLink(state.ui.openedNote), 'gi');
41 | const count = note?.content.match(regExp)?.length || 0;
42 | const { title } = getNoteTitleAndPreview(note);
43 | return {
44 | count,
45 | noteId,
46 | modificationDate: note?.modificationDate,
47 | title,
48 | };
49 | };
50 |
--------------------------------------------------------------------------------
/lib/utils/import/README.MD:
--------------------------------------------------------------------------------
1 | # Simplenote Import
2 |
3 | This is the core importer for Simplenote. It expects an object of notes in the same format as the `import` module. It is aggressive about only allowing certain properties, and converts dates into timestamps where appropriate. Importing specific export files (Such as Evernote .enex) should use a separate conversion script and then pass the resulting object to `importNotes()`.
4 |
5 | Example object with allowed properties:
6 |
7 | ```
8 | {
9 | "activeNotes": [
10 | {
11 | "content": "Random thought: How much wood could a woodchuck chuck if a woodchuck could chuck wood?",
12 | "creationDate": "2013-10-16T20:17:41.760Z",
13 | "lastModified": "2018-10-10T22:07:54.128Z",
14 | "pinned": true,
15 | "tags": [
16 | "Reminders"
17 | ]
18 | },
19 | ],
20 | "trashedNotes": [
21 | {
22 | "content": "Hello, world!",
23 | "creationDate": "2016-02-29T23:01:03.115Z",
24 | "lastModified": "2016-02-29T23:01:08.000Z"
25 | }
26 | ]
27 | }
28 | ```
29 |
--------------------------------------------------------------------------------
/lib/utils/import/evernote/test.ts:
--------------------------------------------------------------------------------
1 | import EvernoteImporter from './';
2 |
3 | describe('EvernoteImporter', () => {
4 | describe('getConvertedDate', () => {
5 | let dateNowSpy;
6 | let importer;
7 |
8 | beforeAll(() => {
9 | dateNowSpy = jest
10 | .spyOn(Date, 'now')
11 | .mockImplementation(() => 1542312140788);
12 | });
13 |
14 | beforeEach(() => {
15 | importer = new EvernoteImporter({ noteBucket: {}, tagBucket: {} });
16 | });
17 |
18 | afterAll(() => {
19 | dateNowSpy.mockRestore();
20 | });
21 |
22 | it('should return a Unix timestamp, given an un-delimited ISO string', () => {
23 | const convertedDate = importer.getConvertedDate('20181008T172440Z');
24 | expect(convertedDate).toBe(1539019480);
25 | });
26 |
27 | it('should fall back to the current Unix timestamp', () => {
28 | const fallbackDate = importer.getConvertedDate('broken-timestamp');
29 | expect(fallbackDate).toBe(1542312140.788);
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/lib/utils/import/test.ts:
--------------------------------------------------------------------------------
1 | import CoreImporter from './';
2 |
3 | describe('CoreImporter', () => {
4 | let importer;
5 | const addNote = jest.fn();
6 |
7 | beforeEach(() => {
8 | importer = new CoreImporter(addNote);
9 | importer.emit = jest.fn();
10 | });
11 |
12 | describe('importNote', () => {
13 | it('should call addNote() with a note containing the required properties', () => {
14 | const note = {};
15 | importer.importNote(note);
16 |
17 | const passedNote = addNote.mock.calls[0][0];
18 |
19 | // Conforms to schema
20 | expect(passedNote.publishURL).toBe('');
21 | expect(passedNote.shareURL).toBe('');
22 | expect(passedNote.deleted).toBe(false);
23 | expect(passedNote.tags).toEqual([]);
24 | expect(passedNote.systemTags).toEqual([]);
25 | expect(passedNote.creationDate).toEqual(expect.any(Number));
26 | expect(passedNote.modificationDate).toEqual(expect.any(Number));
27 | expect(passedNote.content).toBe('');
28 | });
29 | });
30 |
31 | describe('importNotes', () => {
32 | it('should emit error when no notes are passed', () => {
33 | importer.importNotes();
34 | expect(importer.emit).toHaveBeenCalledWith(
35 | 'status',
36 | 'error',
37 | 'No notes to import.'
38 | );
39 | });
40 |
41 | it('should emit error when invalid object is passed', () => {
42 | const bogusNotes = { actveNotes: [] };
43 | importer.importNotes(bogusNotes);
44 | expect(importer.emit).toHaveBeenCalledWith(
45 | 'status',
46 | 'error',
47 | 'Invalid import format: No active or trashed notes found.'
48 | );
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/lib/utils/is-dev-config/index.ts:
--------------------------------------------------------------------------------
1 | const isDevConfig = (development: boolean = false) => {
2 | const isDev = Boolean(development);
3 | const whichDB = isDev ? 'Development' : 'Production';
4 | const shouldWarn = process.env.NODE_ENV === 'production' && development;
5 | const consoleMode = shouldWarn ? 'warn' : 'info';
6 | console[consoleMode](`Simperium config: ${whichDB}`); // eslint-disable-line no-console
7 |
8 | return isDev;
9 | };
10 |
11 | export default isDevConfig;
12 |
--------------------------------------------------------------------------------
/lib/utils/is-dev-config/test.ts:
--------------------------------------------------------------------------------
1 | import isDevConfig from './';
2 |
3 | describe('isDevConfig', () => {
4 | const unmockedConsole = global.console;
5 |
6 | beforeEach(() => {
7 | global.console = {
8 | info: jest.fn(),
9 | warn: jest.fn(),
10 | };
11 | });
12 |
13 | afterEach(() => {
14 | global.process.env.NODE_ENV = 'test';
15 | global.console = unmockedConsole;
16 | });
17 |
18 | it('should return a boolean of whether it is given a value or not', () => {
19 | expect(isDevConfig(true)).toBe(true);
20 | expect(isDevConfig()).toBe(false);
21 | });
22 |
23 | it('should console.warn when NODE_ENV is production and Simperium is not', () => {
24 | global.process.env.NODE_ENV = 'production';
25 | global.console.warn = jest.fn();
26 | isDevConfig(true);
27 | expect(global.console.warn).toHaveBeenCalled();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/lib/utils/is-email-tag.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module utils/is-email-tag
3 | */
4 |
5 | import type * as T from '../types';
6 |
7 | /**
8 | * Naively matches what might appear to be an email address
9 | *
10 | * This is not spec-compliant but it does not need to
11 | * be. Users of the app should assume that any tag
12 | * name which has the general appearance of a valid
13 | * email address will be treated as if they are actual
14 | * email addresses and will be used for adding people
15 | * to a note as collaborators.
16 | *
17 | * Three main components are required:
18 | * 1. Mailbox piece _before_ the `@`
19 | * 2. `@`
20 | * 3. Host and domain piece ending in `[some host].[some TLD]`
21 | *
22 | * @type {RegExp}
23 | */
24 | const naiveEmailPattern = /^(?:[^@]+)@(?:.+)(?:\.[^.]{2,})$/;
25 |
26 | /**
27 | * Indicates if a given tag name string
28 | * represents an email for collaboration
29 | *
30 | * @param {String} tagName name of tag which might be an email address
31 | * @returns {Boolean} whether or not the tag is considered an email address
32 | */
33 | export const isEmailTag = (tagName: T.TagName) =>
34 | naiveEmailPattern.test(tagName);
35 |
36 | export default isEmailTag;
37 |
--------------------------------------------------------------------------------
/lib/utils/note-scroll-position.ts:
--------------------------------------------------------------------------------
1 | export type notePositions = {
2 | [key: string]: number;
3 | };
4 |
5 | export const setNotePosition = (noteId: string, position: number) => {
6 | const positions = getAllPositions();
7 | if (positions) {
8 | positions[noteId] = position;
9 | sessionStorage.setItem('note_positions', JSON.stringify(positions));
10 | }
11 | };
12 |
13 | export const getNotePosition = (noteId: string): number => {
14 | const positions = getAllPositions();
15 | if (positions) {
16 | return positions[noteId];
17 | } else {
18 | return 0;
19 | }
20 | };
21 |
22 | export const clearNotePositions = () => {
23 | sessionStorage.removeItem('note_positions');
24 | };
25 |
26 | const getAllPositions = (): notePositions => {
27 | const notePositions = sessionStorage.getItem('note_positions');
28 | let currentSavedPositions: notePositions;
29 | if (notePositions) {
30 | try {
31 | currentSavedPositions = JSON.parse(notePositions);
32 | return currentSavedPositions;
33 | } catch (e) {
34 | return {};
35 | }
36 | } else {
37 | currentSavedPositions = {};
38 | }
39 | return currentSavedPositions;
40 | };
41 |
--------------------------------------------------------------------------------
/lib/utils/platform.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/atom/electron/issues/22
2 | export const isElectron = !!window?.electron;
3 |
4 | export const isMac = isElectron
5 | ? window?.electron?.isMac
6 | : navigator.appVersion.indexOf('Mac') !== -1;
7 |
8 | export const CmdOrCtrl = isElectron && isMac ? 'Cmd' : 'Ctrl';
9 |
10 | export const isSafari = /^((?!chrome|android).)*safari/i.test(
11 | window.navigator.userAgent
12 | );
13 |
14 | export const isLinux = isElectron
15 | ? window?.electron?.isLinux
16 | : navigator.appVersion.indexOf('Linux') !== -1;
17 |
--------------------------------------------------------------------------------
/lib/utils/render-note-to-html.ts:
--------------------------------------------------------------------------------
1 | import { sanitizeHtml } from './sanitize-html';
2 |
3 | const enableCheckboxes = {
4 | type: 'output',
5 | regex: '\n',
12 | replace: '>',
13 | };
14 |
15 | export const renderNoteToHtml = (content: string) => {
16 | return import(/* webpackChunkName: 'showdown' */ 'showdown').then(
17 | ({ default: showdown }) => {
18 | showdown.extension('enableCheckboxes', enableCheckboxes);
19 | showdown.extension('removeLineBreaks', removeLineBreaks);
20 | const markdownConverter = new showdown.Converter({
21 | extensions: ['enableCheckboxes', 'removeLineBreaks'],
22 | });
23 | markdownConverter.setFlavor('github');
24 | markdownConverter.setOption('ghMentions', false);
25 | markdownConverter.setOption('literalMidWordUnderscores', true);
26 | markdownConverter.setOption('simpleLineBreaks', false); // override GFM
27 | markdownConverter.setOption('smoothLivePreview', true);
28 | markdownConverter.setOption('splitAdjacentBlockquotes', true);
29 | markdownConverter.setOption('strikethrough', true); // ~~strikethrough~~
30 | markdownConverter.setOption('tables', true); // table syntax
31 |
32 | const transformedContent = content.replace(
33 | /([ \t\u2000-\u200a]*)\u2022(\s)/gm,
34 | '$1-$2'
35 | ); // normalized bullets
36 |
37 | return sanitizeHtml(markdownConverter.makeHtml(transformedContent));
38 | }
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/lib/utils/tag-hash.ts:
--------------------------------------------------------------------------------
1 | import type * as T from '../types';
2 |
3 | export const MAX_TAG_HASH_LENGTH = 256;
4 |
5 | export const tagHashOf = (tagName: T.TagName): T.TagHash => {
6 | const normalized = tagName.normalize('NFC');
7 | const lowercased = normalized.toLocaleLowerCase('en-US');
8 | const encoded = encodeURIComponent(lowercased);
9 |
10 | return encoded.replace(
11 | /[!'()*\-_~.]/g,
12 | (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()
13 | ) as T.TagHash;
14 | };
15 |
16 | export const tagNameOf = (tagHash: T.TagHash): T.TagName =>
17 | decodeURIComponent(tagHash) as T.TagName;
18 |
19 | export const withTag = (tags: T.TagName[], tag: T.TagName): T.TagName[] => {
20 | const hash = tagHashOf(tag);
21 | const tagAt = tags.findIndex((tagName) => tagHashOf(tagName) === hash);
22 | return tagAt > -1 ? tags : [...tags, tag];
23 | };
24 |
25 | export const withoutTag = (tags: T.TagName[], tag: T.TagName): T.TagName[] => {
26 | const hash = tagHashOf(tag);
27 |
28 | for (const tagName of tags) {
29 | if (tagHashOf(tagName) === hash) {
30 | return tags.filter((tagName) => tagHashOf(tagName) !== hash);
31 | }
32 | }
33 |
34 | return tags;
35 | };
36 |
--------------------------------------------------------------------------------
/lib/utils/task-transform.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Regex to find valid checkboxes within a string
3 | *
4 | * Checkboxes are valid if they:
5 | * - exist as the first non-whitespace on a line
6 | * - take one of the following forms ( - [ ] | - [x] | - [X])
7 | */
8 | export const checkboxRegex: RegExp = /^(\s*)- \[( |x|X)\](\s)/gm;
9 |
10 | export const withCheckboxCharacters = (s: string): string =>
11 | s.replace(
12 | checkboxRegex,
13 | (match, prespace, inside, postspace) =>
14 | prespace + (inside === ' ' ? '\ue000' : '\ue001') + postspace
15 | );
16 |
17 | export const withCheckboxSyntax = (s: string): string =>
18 | s.replace(/\ue000|\ue001/g, (match) =>
19 | match === '\ue000' ? '- [ ]' : '- [x]'
20 | );
21 |
--------------------------------------------------------------------------------
/lib/utils/test/is-email-tag.ts:
--------------------------------------------------------------------------------
1 | import isEmailTag from '../is-email-tag';
2 |
3 | describe('Utils', () => {
4 | describe('#isEmailTag', () => {
5 | expect.extend({
6 | toBeAnEmail(received) {
7 | const pass = isEmailTag(received);
8 |
9 | return {
10 | pass,
11 | message: () =>
12 | `${this.utils.matcherHint(
13 | `${pass ? '.not' : ''}.toBeAnEmail`,
14 | '',
15 | '',
16 | { secondArgument: false }
17 | )}\n\n` +
18 | `Expected value to${pass ? ' not' : ''} be an email address\n` +
19 | ` ${this.utils.printReceived(received)}`,
20 | };
21 | },
22 | });
23 |
24 | it('should not match when no `@` present', () => {
25 | ['test', 'test.at.test.com', 'tester.com'].forEach((tag) =>
26 | expect(tag).not.toBeAnEmail()
27 | );
28 | });
29 |
30 | it('should not match when no domain is present', () => {
31 | ['test@', 'test@host', 'test.com@host.', 'test@host.test.'].forEach(
32 | (tag) => expect(tag).not.toBeAnEmail()
33 | );
34 | });
35 |
36 | it('should require at least a two-letter TLD', () => {
37 | expect('a@b.c').not.toBeAnEmail();
38 | });
39 |
40 | it('should match valid email addresses', () => {
41 | ['test@test.com', 'test@test.server.local.com', 'a@b.io'].forEach((tag) =>
42 | expect(tag).toBeAnEmail()
43 | );
44 | });
45 |
46 | it('should match even with multiple `@`s', () => {
47 | expect('test@inbox@host.domain.com').toBeAnEmail();
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/lib/utils/url-utils.ts:
--------------------------------------------------------------------------------
1 | import { has } from 'lodash';
2 |
3 | const allowedProtocols = ['http:', 'https:', 'mailto:'];
4 |
5 | const state = (() => {
6 | const getBoundElectronOpener = () => {
7 | const shell = window.require('electron').shell;
8 | return shell.openExternal.bind(shell);
9 | };
10 |
11 | const isRunningElectron = window && has(window, 'process.versions.electron');
12 | const openExternalUrl = isRunningElectron
13 | ? getBoundElectronOpener()
14 | : window.open.bind(window);
15 |
16 | return { isRunningElectron, openExternalUrl };
17 | })();
18 |
19 | export const viewExternalUrl = (url: string) => {
20 | try {
21 | const protocol = new URL(url).protocol;
22 |
23 | if (!allowedProtocols.some((allowed) => allowed === protocol)) {
24 | return;
25 | }
26 | } catch (e) {
27 | // Invalid Url
28 | return;
29 | }
30 |
31 | state.openExternalUrl(url);
32 | };
33 |
--------------------------------------------------------------------------------
/lib/utils/validate-password/index.ts:
--------------------------------------------------------------------------------
1 | export const validatePassword = function (password: string, email: string) {
2 | // does not equal username (i.e. email address)
3 | if (password === email) {
4 | return false;
5 | }
6 |
7 | // minimum of 8 characters; no tabs or newlines
8 | // (letters, numbers, special characters and spaces allowed)
9 | const re = /^[^\r\n\t]{8,}$/;
10 | return re.test(password);
11 | };
12 |
--------------------------------------------------------------------------------
/lib/utils/validate-password/test.ts:
--------------------------------------------------------------------------------
1 | import { validatePassword } from './';
2 |
3 | describe('validatePassword', () => {
4 | it('should return false for password that is too short', () => {
5 | expect(validatePassword('foo', 'bar@bang.com')).toBeFalsy();
6 | });
7 |
8 | it('should return false for password that equals email that would otherwise be valid', () => {
9 | expect(validatePassword('bar@bang.com', 'bar@bang.com')).toBeFalsy();
10 | });
11 |
12 | it('should return true for password that is more than 64 characters', () => {
13 | expect(
14 | validatePassword(
15 | 'y93HywVqX6sXbsyZiAAZ*xz8-X6uskm8ZjhLXvTTw2m8YxgvgujuWpEqUqqdT!H41111',
16 | 'bar@bang.com'
17 | )
18 | ).toBeTruthy();
19 | });
20 |
21 | it('should return true for password that is numbers only', () => {
22 | expect(
23 | validatePassword(
24 | '1234567890123456789012345678901234567890123456789012345678901234',
25 | 'bar@bang.com'
26 | )
27 | ).toBeTruthy();
28 | });
29 |
30 | it('should return true for password that contains a space', () => {
31 | expect(
32 | validatePassword(
33 | '12345678901234567890123456789012345678 0123456789012345678901234',
34 | 'bar@bang.com'
35 | )
36 | ).toBeTruthy();
37 | });
38 |
39 | it('should return true for password that is valid', () => {
40 | expect(
41 | validatePassword(
42 | 'y93HywVqX6sXbsyZiAAZ*xz8-X6uskm8ZjhLXvTTw2m8YxgvgujuWpEqUqqdT!H4',
43 | 'bar@bang.com'
44 | )
45 | ).toBeTruthy();
46 | });
47 |
48 | it('should allow all special characters', () => {
49 | expect(
50 | validatePassword('~ !@#$%^&*_-+=`|(){}[]:;"\',.?/', 'bar@bang.com')
51 | ).toBeTruthy();
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/resources/appx/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/appx/Square150x150Logo.png
--------------------------------------------------------------------------------
/resources/appx/Square150x150Logo.targetsize-150_altform-unplated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/appx/Square150x150Logo.targetsize-150_altform-unplated.png
--------------------------------------------------------------------------------
/resources/appx/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/appx/Square44x44Logo.png
--------------------------------------------------------------------------------
/resources/appx/Square44x44Logo.targetsize-44_altform-unplated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/appx/Square44x44Logo.targetsize-44_altform-unplated.png
--------------------------------------------------------------------------------
/resources/appx/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/appx/StoreLogo.png
--------------------------------------------------------------------------------
/resources/appx/Wide310x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/appx/Wide310x150Logo.png
--------------------------------------------------------------------------------
/resources/certificates/mac.p12.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/certificates/mac.p12.enc
--------------------------------------------------------------------------------
/resources/certificates/win.p12.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/certificates/win.p12.enc
--------------------------------------------------------------------------------
/resources/images/app-icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/app-icon.icns
--------------------------------------------------------------------------------
/resources/images/dmg-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/dmg-background.png
--------------------------------------------------------------------------------
/resources/images/dmg-background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/dmg-background@2x.png
--------------------------------------------------------------------------------
/resources/images/dmg-icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/dmg-icon.icns
--------------------------------------------------------------------------------
/resources/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/favicon.ico
--------------------------------------------------------------------------------
/resources/images/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/icon_128x128.png
--------------------------------------------------------------------------------
/resources/images/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/icon_16x16.png
--------------------------------------------------------------------------------
/resources/images/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/icon_256x256.png
--------------------------------------------------------------------------------
/resources/images/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/icon_32x32.png
--------------------------------------------------------------------------------
/resources/images/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/icon_512x512.png
--------------------------------------------------------------------------------
/resources/images/simplenote.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/images/simplenote.ico
--------------------------------------------------------------------------------
/resources/macos/entitlements.mac.inherit.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.allow-unsigned-executable-memory
8 |
9 | com.apple.security.cs.disable-executable-page-protection
10 |
11 | com.apple.security.cs.disable-library-validation
12 |
13 | com.apple.security.cs.allow-dyld-environment-variables
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/resources/macos/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.allow-unsigned-executable-memory
8 |
9 | com.apple.security.cs.disable-executable-page-protection
10 |
11 | com.apple.security.cs.disable-library-validation
12 |
13 | com.apple.security.cs.allow-dyld-environment-variables
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/resources/secrets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/secrets/.gitkeep
--------------------------------------------------------------------------------
/resources/secrets/config.json.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Automattic/simplenote-electron/6e0565dea8956b317c54f462ad4fae22d1e60709/resources/secrets/config.json.enc
--------------------------------------------------------------------------------
/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | @use 'variables' as *;
2 |
3 | @mixin react-transition-fade-in {
4 | &-enter {
5 | opacity: 0;
6 | }
7 |
8 | &-enter-done {
9 | opacity: 1;
10 | transition: opacity 0.2s ease-out;
11 | }
12 | }
13 |
14 | @mixin react-transition-fade-out {
15 | &-exit {
16 | opacity: 1;
17 | }
18 |
19 | &-exit-active {
20 | opacity: 0;
21 | transition: opacity 0.2s ease-out;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/scss/_scrollbar.scss:
--------------------------------------------------------------------------------
1 | // Turn on custom scrollbar
2 | ::-webkit-scrollbar {
3 | width: 14px;
4 | background: transparent;
5 | }
6 |
7 | ::-webkit-scrollbar-corner {
8 | background: transparent;
9 | }
10 |
11 | ::-webkit-scrollbar-thumb {
12 | background: var(--secondary-color);
13 | border-radius: 100px;
14 | border: 4px solid var(--background-color);
15 | min-height: 24px;
16 | }
17 |
18 | // hover effect for scrollbar 'thumb'
19 | ::-webkit-scrollbar-thumb:hover {
20 | background-color: var(--foreground-color);
21 | }
22 |
23 | ::-webkit-scrollbar-thumb:active {
24 | background: var(--tertiary-accent-color);
25 | border-radius: 100px;
26 | }
27 |
28 | // Styles for Firefox not a lot of options we are able to set for custom scrollbars
29 | body {
30 | scrollbar-width: thin;
31 | scrollbar-color: var(--tertiary-accent-color) transparent;
32 | }
33 |
--------------------------------------------------------------------------------
/scss/inputs.scss:
--------------------------------------------------------------------------------
1 | input,
2 | button,
3 | select,
4 | textarea {
5 | font-family: inherit;
6 | font-size: inherit;
7 | line-height: inherit;
8 | }
9 |
10 | input {
11 | -webkit-tap-highlight-color: transparent;
12 |
13 | &::placeholder {
14 | color: var(--placeholder-color);
15 | opacity: 1;
16 | }
17 |
18 | &::-ms-clear {
19 | display: none;
20 | }
21 |
22 | // Make sure inputs that are readonly (i.e. tag list) are not selectable
23 | &[readonly] {
24 | user-select: none;
25 | }
26 | }
27 |
28 | .transparent-input {
29 | color: inherit;
30 | background-color: transparent;
31 | }
32 |
--------------------------------------------------------------------------------
/scss/print.scss:
--------------------------------------------------------------------------------
1 | @use '~@automattic/color-studio/dist/color-variables' as *;
2 |
3 | @media print {
4 | html {
5 | -ms-overflow-style: -ms-autohiding-scrollbar;
6 | }
7 |
8 | body {
9 | background-color: #fff;
10 | }
11 |
12 | .app-layout__note-column {
13 | border: 0;
14 | }
15 |
16 | .dev-badge,
17 | .app-layout__source-column,
18 | .tag-field,
19 | .note-toolbar-wrapper,
20 | .react-monaco-editor-container,
21 | .search-results {
22 | display: none;
23 | }
24 |
25 | /* Firefox only prints the first page if any divs are display: flex */
26 | .app,
27 | .simplenote-app,
28 | .app-layout,
29 | .app-layout__note-column,
30 | .note-editor,
31 | .note-detail,
32 | .note-detail-textarea,
33 | .note-content-editor-shell,
34 | .note-detail-wrapper {
35 | display: block;
36 | overflow: auto;
37 | position: relative;
38 | }
39 |
40 | [class^='note-detail-'] {
41 | max-width: 100%;
42 | width: 100%;
43 | color: $studio-black;
44 | overflow-y: auto !important;
45 | }
46 |
47 | .note-content-plaintext {
48 | display: inherit;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/scss/style.scss:
--------------------------------------------------------------------------------
1 | // Vendor
2 | @use 'normalize';
3 | @use '~@automattic/color-studio/dist/color-variables';
4 |
5 | // Internal
6 | @use 'variables';
7 | @use 'mixins';
8 | @use 'general';
9 | @use 'scrollbar';
10 | @use 'components';
11 |
12 | // TODO: Tie these styles to their own components
13 | @use 'inputs';
14 | @use 'buttons';
15 |
16 | // TODO: Figure out a better way to manage these
17 | @use 'theme';
18 | @use 'print';
19 |
--------------------------------------------------------------------------------
/setup-tests.js:
--------------------------------------------------------------------------------
1 | import mockIndexedDB from 'fake-indexeddb';
2 | global.indexedDB = mockIndexedDB;
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "esnext",
5 | "module": "esnext",
6 | "moduleResolution": "bundler",
7 | "allowJs": true,
8 | "jsx": "react",
9 | "noEmit": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "isolatedModules": true,
13 | "esModuleInterop": true
14 | },
15 | "include": ["lib/**/*"]
16 | }
17 |
--------------------------------------------------------------------------------
/vip/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simplenote",
3 | "description": "Simplenote is an easy way to keep notes, lists, ideas and more. Your notes stay in sync with all your devices for free.",
4 | "version": "1.0.0",
5 | "license": "GPL-2.0",
6 | "scripts": {
7 | "start": "node ./webapp/index.js",
8 | "build": "exit 0"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git://github.com/Automattic/simplenote-electron.git"
13 | },
14 | "dependencies": {
15 | "@automattic/vip-go": "1.1.0",
16 | "express": "4.19.2"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/vip/webapp/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 | const { server } = require('@automattic/vip-go');
4 |
5 | const port = process.env.PORT || 4000;
6 |
7 | app.use(express.static('dist'));
8 |
9 | server(app, { PORT: port }).listen();
10 |
--------------------------------------------------------------------------------