diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..0ef5a63a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,74 @@ +{ "extends": ["eslint-config-airbnb", "prettier"], + "env": { + "browser": true, + "node": true, + "jest": true + }, + "parser": "@typescript-eslint/parser", + "rules": { + "camelcase": 0, + "strict": 0, + "react/no-multi-comp": 0, + "import/default": 0, + "import/no-duplicates": 0, + "import/named": 0, + "import/namespace": 0, + "import/no-unresolved": 0, + "import/no-named-as-default": 2, + "import/prefer-default-export": 0, + "comma-dangle": 0, // not sure why airbnb turned this on. gross! + "indent": [2, 2, {"SwitchCase": 1}], + "no-console": 0, + "no-alert": 0, + "no-shadow": 2, + "arrow-body-style": 0, + "react/prop-types": 0, + "react/jsx-filename-extension": 0, + "react/prefer-stateless-function": 0, + "jsx-a11y/anchor-is-valid": 0, + "jsx-a11y/tabindex-no-positive": 0, + "no-mixed-operators": 0, + "no-plusplus": 0, + "no-underscore-dangle": 0, + "prettier/prettier": "error", + "jsx-a11y/no-autofocus": 0, + "jsx-a11y/label-has-for": 0, + "prefer-destructuring": 0, + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "react/jsx-wrap-multilines": ["error", {"declaration": false, "assignment": false}], + "react/jsx-one-expression-per-line": 0, + "@typescript-eslint/no-unused-vars": 1, + "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*_test.ts"]}], + "lines-between-class-members": 0, + "react/jsx-fragments": 0, + "jsx-a11y/label-has-associated-control": 0, + "no-empty": 0 + }, + "plugins": [ + "react", "react-hooks", "import", "prettier", "@typescript-eslint" + ], + "globals": { + // web + "__DEVELOPMENT__": true, + "__PRODUCTION__": true, + "__DISABLE_SSR__": true, + "__DEVTOOLS__": true, + "__DOMAIN__": true, + "__BASE_URL__": true, + "__BASE_NAME__": true, + "__STRIPE_PUBLIC_KEY__": true, + "__ROOT_URL__": true, + "__CDN_URL__": true, + "socket": true, + "webpackIsomorphicTools": true, + "StripeCheckout": true, + + // browser + "browser": true, + "chrome": true, + __WEB_URL__: true, + __API_ENDPOINT__: true, + __VERSION__: true + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ac44b4f3..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: CI -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-go@v6 - with: - go-version: '>=1.25.0' - - - name: Install dependencies - run: | - make install - - - name: Test cli - run: | - make test-cli - - - name: Test app - run: | - make test-api - - - name: Test e2e - run: | - make test-e2e diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml deleted file mode 100644 index 81a2ea5e..00000000 --- a/.github/workflows/release-cli.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Release CLI - -on: - push: - tags: - - 'cli-v*' - -jobs: - release: - runs-on: ubuntu-22.04 - permissions: - contents: write - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - uses: actions/setup-go@v6 - with: - go-version: '>=1.25.0' - - - name: Extract version from tag - id: version - run: | - TAG=${GITHUB_REF#refs/tags/cli-v} - echo "version=$TAG" >> $GITHUB_OUTPUT - echo "Releasing version: $TAG" - - - name: Install dependencies - run: make install - - - name: Run CLI tests - run: make test-cli - - - name: Run E2E tests - run: make test-e2e - - - name: Build CLI - run: make version=${{ steps.version.outputs.version }} build-cli - - - name: Generate changelog - run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="cli-v${VERSION}" - - # Find previous CLI tag - PREV_TAG=$(git tag --sort=-version:refname | grep "^cli-" | grep -v "^${TAG}$" | head -n 1) - - if [ -z "$PREV_TAG" ]; then - echo "Error: No previous CLI tag found" - echo "This appears to be the first release." - exit 1 - fi - - ./scripts/generate-changelog.sh cli "$TAG" "$PREV_TAG" > /tmp/changelog.txt - cat /tmp/changelog.txt - - - name: Create GitHub release - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="cli-v${VERSION}" - - # Determine if prerelease (version not matching major.minor.patch) - FLAGS="" - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - FLAGS="--prerelease" - fi - - gh release create "$TAG" \ - build/cli/*.tar.gz \ - build/cli/*_checksums.txt \ - $FLAGS \ - --title="$TAG" \ - --notes-file=/tmp/changelog.txt \ - --draft diff --git a/.github/workflows/release-server.yml b/.github/workflows/release-server.yml deleted file mode 100644 index a5f52933..00000000 --- a/.github/workflows/release-server.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: Release Server - -on: - push: - tags: - - 'server-v*' - -jobs: - release: - runs-on: ubuntu-22.04 - permissions: - contents: write - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - uses: actions/setup-go@v6 - with: - go-version: '>=1.25.0' - - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Extract version from tag - id: version - run: | - TAG=${GITHUB_REF#refs/tags/server-v} - echo "version=$TAG" >> $GITHUB_OUTPUT - echo "Releasing version: $TAG" - - - name: Install dependencies - run: make install - - - name: Run tests - run: make test - - - name: Build server - run: make version=${{ steps.version.outputs.version }} build-server - - - name: Generate changelog - run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="server-v${VERSION}" - - # Find previous server tag - PREV_TAG=$(git tag --sort=-version:refname | grep "^server-" | grep -v "^${TAG}$" | head -n 1) - - if [ -z "$PREV_TAG" ]; then - echo "Error: No previous server tag found" - echo "This appears to be the first release." - exit 1 - fi - - ./scripts/generate-changelog.sh server "$TAG" "$PREV_TAG" > /tmp/changelog.txt - cat /tmp/changelog.txt - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Prepare Docker build context - run: | - VERSION="${{ steps.version.outputs.version }}" - cp build/server/dnote_server_${VERSION}_linux_amd64.tar.gz host/docker/ - cp build/server/dnote_server_${VERSION}_linux_arm64.tar.gz host/docker/ - cp build/server/dnote_server_${VERSION}_linux_arm.tar.gz host/docker/ - cp build/server/dnote_server_${VERSION}_linux_386.tar.gz host/docker/ - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: ./host/docker - push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386 - tags: | - dnote/dnote:${{ steps.version.outputs.version }} - dnote/dnote:latest - build-args: | - version=${{ steps.version.outputs.version }} - - - name: Create GitHub release - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="server-v${VERSION}" - - # Determine if prerelease (version not matching major.minor.patch) - FLAGS="" - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - FLAGS="--prerelease" - fi - - gh release create "$TAG" \ - build/server/*.tar.gz \ - build/server/*_checksums.txt \ - $FLAGS \ - --title="$TAG" \ - --notes-file=/tmp/changelog.txt \ - --draft diff --git a/.gitignore b/.gitignore index 2847e75d..fbc8252a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ /vendor /build +/node_modules .vagrant *.log -node_modules -/test -tmp -*.db -/server diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..046cb41c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true +} + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..a3603b9d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,40 @@ +language: go +dist: xenial + +go: + - 1.13 + +env: + - NODE_VERSION=10.15.0 + YARN_VERSION=1.19.1-1 + +before_install: + - sudo apt-get update + - sudo apt-get --yes remove postgresql\* + - sudo apt-get install -y postgresql-11 postgresql-client-11 + - sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf + - sudo service postgresql restart 11 + + # install yarn + - sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg + - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + - sudo apt-get update -qq + - sudo apt-get install -y -qq yarn="$YARN_VERSION" + + - nvm install "$NODE_VERSION" + - nvm use "$NODE_VERSION" + - node --version + - psql -c "CREATE DATABASE dnote_test;" -U postgres + +cache: + yarn: true + +install: + - make install + +script: + - make lint + - make test-cli + - make test-api + - make test-web + - make test-jslib diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..540e1634 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,131 @@ +# CHANAGELOG + +All notable changes to the projects under this repository will be documented in this file. + +* [Server](#server) +* [CLI](#cli) +* [Browser Extensions](#browser-extensions) + +## Server + +The following log documents the history of the server project. + +### Unreleased + +N/A + +### 0.3.2 - 2019-11-20 + +#### Fixed + +- Fix server crash upon landing on a note page (#324). +- Allow to synchronize a large number of records (#321) + +### 0.3.1 - 2019-11-12 + +#### Fixed + +- Fix static files not being embedded in the binary. (#309) +- Fix mobile menu not covering the whole screen. (#308) + +### 0.3.0 - 2019-11-12 + +#### Added + +- Share notes (#300) +- Allow to recover from a missed repetition processing (#305) + +### 0.2.1 - 2019-11-04 + +#### Upgrade Guide + +* Please define the follwoing new environment variables: + + - `WebURL`: the URL to your Dnote server, without the trailing slash. (e.g. `https://my-server.com`) (Please see #290) + - `SmtpPort`: the SMTP port. (e.g. `465`) optional - required *if you want to configure email* + +#### Added + +- Display version number in the settings (#293) +- Allow unsecure database connection in production (#276) + +#### Fixed + +- Allow to customize the app URL in the emails (#290) +- Allow to customize the SMTP port (#292) + +### 0.2.0 - 2019-10-28 + +#### Added + +- Specify spaced repetition rule (#280) + +#### Changed + +- Treat a linebreak as a new line in the preview (#261) +- Allow to have multiple editor states for adding and editing notes (#260) + +#### Fixed + +- Fix jumping focus on editor (#265) + +### 0.1.1 - 2019-09-30 + +#### Fixed + +- Fix asset loading (#257) + + +### 0.1.0 - 2019-09-30 + +#### Added + +- Full-text search (#254) +- Password recovery (#254) +- Embedded notes in the digest emails (#254) + +#### Removed + +- **Breaking Change**: End-to-end encryption was removed. Existing users need to go to `/classic` and follow the automated migration steps. (#254) +- **Breaking Change**: `v1` and `v2` API endpoints were removed, and `v3` API was added as a replacement. + +#### Migration guide + +- In your application, navigate to `/classic` and follow the automated migration steps. + + +## CLI + +The following log documentes the history of the CLI project + +### 0.10.0 - 2019-09-30 + +#### Removed + +- **Breaking Change**: End-to-end encryption was removed. Previous versions will no longer be able to interact with the web API, because `v1` and `v2` endpoints were replaced by a new `v3` endpoint to remove encryption. + +#### Migration guide + +- If you are using Dnote Pro, change the value of `apiEndpoint` in `~/.dnote/dnoterc` to `https://api.getdnote.com`. + +## Browser Extensions + +The following log documentes the history of the browser extensions project + +### [Unreleased] + +N/A + +### 2.0.0 - 2019-10-29 + +- Allow to customize API and web URLs (#285) + +### 1.1.1 - 2019-10-02 + +- Fix failing requests (#263) + +### 1.1.0 - 2019-09-30 + +#### Removed + +- **Breaking Change**: End-to-end encryption was removed. Previous versions will no longer be able to interact with the web API, because `v1` and `v2` endpoints were replaced by a new `v3` endpoint to remove encryption. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0fa33eb..a027c90f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,61 +8,95 @@ Dnote is an open source project. ## Setting up -The CLI and server are single single binary files with SQLite embedded - no databases to install, no containers to run, no VMs required. +Dnote uses [Vagrant](https://github.com/hashicorp/vagrant) to provision a consistent development environment. -**Prerequisites** +*Prerequisites* -* Go 1.25+ ([Download](https://go.dev/dl/)) -* Node.js 18+ ([Download](https://nodejs.org/) - only needed for building frontend assets) +* Vagrant ([Download](https://www.vagrantup.com/downloads.html)) +* VirtualBox ([Download](https://www.virtualbox.org/)) -**Quick Start** +Run the following command from the project root. It starts the virtual machine and bootstraps the project. -1. Clone the repository -2. Install dependencies: - ```bash - make install - ``` -3. Start developing! Run tests: - ```bash - make test - ``` - Or start the dev server: - ```bash - make dev-server - ``` +``` +vagrant up +``` -That's it. You're ready to contribute. +*Workflow* + +* You can make changes to the source code from the host machine. +* Any commands need to be run inside the virtual machine. You can connect to it by running `vagrant ssh`. +* If you want to run tests in a WATCH mode, please do so from the host machine. We cannot watch file changes due to the limitation of file system used in a virtual machine. ## Server -```bash -# Start dev server (runs on localhost:3001) -make dev-server +The server consists of the frontend web application and a web server. -# Run tests +### Development + +* Run `make dev-server` to start a local server. +* You can access the server on `localhost:3000` on your machine. + +### Test + +```bash +# Run tests for the frontend web application +make test-web + +# Run in watch mode +WATCH=true make test-web + +# Run tests for API make test-api -# Run tests in watch mode +# Run in watch mode WATCH=true make test-api ``` + ## Command Line Interface -```bash -# Run tests -make test-cli +### Build -# Build dev version (places in your PATH) +You can build either a development version or a production version: + +``` +# Build a development version for your platform and place it in your `PATH`. make debug=true build-cli -# Build production version for all platforms +# Build a production version for all platforms make version=v0.1.0 build-cli -# Build for a specific platform +# Build a production version for a specific platform # Note: You cannot cross-compile using this method because Dnote uses CGO # and requires the OS specific headers. GOOS=[insert OS] GOARCH=[insert arch] make version=v0.1.0 build-cli +``` -# Debug mode +### Test + +* Run all tests for the command line interface: + +``` +make test-cli +``` + +### Debug + +Run Dnote with `DNOTE_DEBUG=1` to print debugging statements. For instance: + +``` DNOTE_DEBUG=1 dnote sync ``` + +### Release + +* Run `make version=v0.1.0 release-cli` to achieve the following: + * Build for all target platforms, create a git tag, push all tags to the repository + * Create a release on GitHub and [Dnote Homebrew tap](https://github.com/dnote/homebrew-dnote). + +**Note** + +- If a release is not stable, + - disable the homebrew release by commenting out relevant code in the release script. + - mark release as pre-release on GitHub release + diff --git a/LICENSE b/LICENSE index 6b0b1270..0c667b35 100644 --- a/LICENSE +++ b/LICENSE @@ -1,203 +1,8 @@ +Source code in this repository is variously licensed under the GNU Affero General Public +License v3.0 (GNU AGPLv3), and GNU General Public License v3.0 (GNU GPLv3). A copy of each +license can be found in the licenses directory. The Source code for the cli is licensed under +GNU GPLv3. The source code for the server and the web is licensed under GNU AGPLv3. Unless +otherwise noted, source code in a given file is licensed under the GNU AGPLv3. - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - +Unless otherwise noted at the beginning of the file, the copyright belongs to +Monomax Software Pty Ltd. diff --git a/Makefile b/Makefile index c38819c0..32e8fcbc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ -NPM := $(shell command -v npm 2> /dev/null) -GH := $(shell command -v gh 2> /dev/null) +PACKR2 := $(shell command -v packr2 2> /dev/null) +YARN := $(shell command -v yarn 2> /dev/null) +HUB := $(shell command -v hub 2> /dev/null) currentDir = $(shell pwd) serverOutputDir = ${currentDir}/build/server @@ -11,29 +12,40 @@ install: install-go install-js .PHONY: install install-go: +ifndef PACKR2 + @echo "==> installing packr2" + @go get -u github.com/gobuffalo/packr/v2/packr2 +endif + @echo "==> installing go dependencies" @go mod download .PHONY: install-go install-js: -ifndef NPM - $(error npm is not installed) +ifndef YARN + $(error yarn is not installed) endif @echo "==> installing js dependencies" ifeq ($(CI), true) - @(cd ${currentDir}/pkg/server/assets && npm ci --cache $(NPM_CACHE_DIR) --prefer-offline --unsafe-perm=true) + @(cd ${currentDir} && yarn --unsafe-perm=true) else - @(cd ${currentDir}/pkg/server/assets && npm install) + @(cd ${currentDir} && yarn) endif .PHONY: install-js +lint: + @(cd ${currentDir}/web && yarn lint) + @(cd ${currentDir}/jslib && yarn lint) + @(cd ${currentDir}/browser && yarn lint) +.PHONY: lint + ## test -test: test-cli test-api test-e2e +test: test-cli test-api test-web test-jslib .PHONY: test -test-cli: generate-cli-schema +test-cli: @echo "==> running CLI test" @(${currentDir}/scripts/cli/test.sh) .PHONY: test-cli @@ -43,47 +55,57 @@ test-api: @(${currentDir}/scripts/server/test-local.sh) .PHONY: test-api -test-e2e: - @echo "==> running E2E test" - @(${currentDir}/scripts/e2e/test.sh) -.PHONY: test-e2e +test-web: + @echo "==> running web test" + +ifeq ($(WATCH), true) + @(cd ${currentDir}/web && yarn test:watch) +else + @(cd ${currentDir}/web && yarn test) +endif +.PHONY: test-web + +test-jslib: + @echo "==> running jslib test" + +ifeq ($(WATCH), true) + @(cd ${currentDir}/jslib && yarn test:watch) +else + @(cd ${currentDir}/jslib && yarn test) +endif +.PHONY: test-jslib + +test-selfhost: + @echo "==> running a smoke test for self-hosting" + + @${currentDir}/host/smoketest/run_test.sh ${tarballPath} +.PHONY: test-jslib # development dev-server: @echo "==> running dev environment" - @VERSION=master ${currentDir}/scripts/server/dev.sh + @VERSION=master ${currentDir}/scripts/web/dev.sh .PHONY: dev-server -build-server: +## build +build-web: +ifndef version + $(error version is required. Usage: make version=0.1.0 build-web) +endif + @echo "==> building web" + @VERSION=${version} ${currentDir}/scripts/web/build-prod.sh +.PHONY: build-web + +build-server: build-web ifndef version $(error version is required. Usage: make version=0.1.0 build-server) endif - @echo "==> building server assets" - @(cd "${currentDir}/pkg/server/assets/" && ./styles/build.sh) - @(cd "${currentDir}/pkg/server/assets/" && ./js/build.sh) - @echo "==> building server" @${currentDir}/scripts/server/build.sh $(version) .PHONY: build-server -build-server-docker: build-server -ifndef version - $(error version is required. Usage: make version=0.1.0 [platform=linux/amd64] build-server-docker) -endif - - @echo "==> building Docker image" - @(cd ${currentDir}/host/docker && ./build.sh $(version) $(platform)) -.PHONY: build-server-docker - -generate-cli-schema: - @echo "==> generating CLI database schema" - @mkdir -p pkg/cli/database - @touch pkg/cli/database/schema.sql - @go run -tags fts5 ./pkg/cli/database/schema -.PHONY: generate-cli-schema - -build-cli: generate-cli-schema +build-cli: ifeq ($(debug), true) @echo "==> building cli in dev mode" @${currentDir}/scripts/cli/dev.sh @@ -98,7 +120,64 @@ endif endif .PHONY: build-cli +## release +release-cli: clean build-cli +ifndef version + $(error version is required. Usage: make version=0.1.0 release-cli) +endif +ifndef HUB + $(error please install hub) +endif + + if [ ! -d ${cliHomebrewDir} ]; then \ + @echo "homebrew-dnote not found locally. did you clone it?"; \ + @exit 1; \ + fi + + @echo "==> releasing cli" + @${currentDir}/scripts/release.sh cli $(version) ${cliOutputDir} + + @echo "===> releading on Homebrew" + @(cd "${cliHomebrewDir}" && \ + ./release.sh \ + "$(version)" \ + "${shasum -a 256 "${cliOutputDir}/dnote_$(version)_darwin_amd64.tar.gz" | cut -d ' ' -f 1}" \ + ) +.PHONY: release-cli + +release-server: clean build-server +ifndef version + $(error version is required. Usage: make version=0.1.0 release-server) +endif +ifndef HUB + $(error please install hub) +endif + + @echo "==> releasing server" + @${currentDir}/scripts/release.sh server $(version) ${serverOutputDir} + + @echo "==> building and releasing docker image" + @(cd ${currentDir}/host/docker && ./build.sh $(version)) + @(cd ${currentDir}/host/docker && ./release.sh $(version)) +.PHONY: release-server + +# migrations +create-migration: +ifndef filename + $(error filename is required. Usage: make filename=your-filename create-migration) +endif + + @(cd ${currentDir}/pkg/server/database && ./scripts/create-migration.sh $(filename)) +.PHONY: create-migration + clean: @git clean -f @rm -rf build + @rm -rf web/public .PHONY: clean + +clean-dep: + @rm -rf ${currentDir}/web/node_modules + @rm -rf ${currentDir}/jslib/node_modules + @rm -rf ${currentDir}/browser/node_modules +.PHONY: clean-dep diff --git a/README.md b/README.md index 7ab0063f..4f115c65 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,65 @@ ![Dnote](assets/logo.png) ========================= -![Build Status](https://github.com/dnote/dnote/actions/workflows/ci.yml/badge.svg) +Dnote is a simple personal knowledge base. -Dnote is a simple command line notebook. Single binary, no dependencies. Since 2017. +[![Build Status](https://travis-ci.org/dnote/dnote.svg?branch=master)](https://travis-ci.org/dnote/dnote) -Your notes are stored in **one SQLite file** - portable, searchable, and completely under your control. Optional sync between devices via a self-hosted server with REST API access. +## What is Dnote? + +Dnote is a lightweight personal knowledge base. The main design goal is to **keep you focused** by providing a way of swiftly capturing new information **without having to switch environment**. To that end, you can use Dnote as a command line interface, browser extension, web client, or an IDE plugin. + +It also offers a seamless **multi device sync**, and **automated spaced repetition** to retain your memory. + +For more details, see the [download page](https://www.getdnote.com/download) and [features](https://www.getdnote.com/pricing). + +![A demo of Dnote CLI](assets/cli.gif) + +## Quick install + +The quickest way to try Dnote is to install the command line interface. + +### Install with Homebrew + +On macOS, you can install using Homebrew: ```sh -# Add a note (or omit -c to launch your editor) -dnote add linux -c "Check disk usage with df -h" - -# View notes in a book -dnote view linux - -# Full-text search -dnote find "disk usage" - -# Sync notes -dnote sync -``` - -## Installation - -```bash -# Linux, macOS, FreeBSD, Windows -curl -s https://www.getdnote.com/install | sh - -# macOS with Homebrew +brew tap dnote/dnote brew install dnote + +# to upgrade to the latest version +brew upgrade dnote ``` -Or [download binary](https://github.com/dnote/dnote/releases). +### Install with script -## Server (Optional) +You can use the installation script to install the latest version: -Server is a binary with SQLite embedded. No database setup is required. + curl -s https://raw.githubusercontent.com/dnote/dnote/master/pkg/cli/install.sh | sh -If using docker, create a compose.yml: +In some cases, you might need an elevated permission: -```yaml -services: - dnote: - image: dnote/dnote:latest - container_name: dnote - ports: - - 3001:3001 - volumes: - - ./dnote_data:/data - restart: unless-stopped -``` + curl -s https://raw.githubusercontent.com/dnote/dnote/master/pkg/cli/install.sh | sudo sh -Then run: +### Install with tarball -```bash -docker-compose up -d -``` +You can download the binary for your platform manually from the [releases page](https://github.com/dnote/dnote/releases). -Or see the [guide](https://www.getdnote.com/docs/server/manual) for binary installation. +## Personal knowledge base -## Documentation +Dnote is great for building a personal knowledge base because: -See the [Dnote doc](https://www.getdnote.com/docs). +* It is fully open source. +* Your data is stored locally first and in a SQLite format which is [suitable for continued accessibility](https://www.sqlite.org/locrsf.html). +* It provides a way of instantly capturing new lessons without distracting you. +* It automates spaced repetition to help you retain your memory. + +You can read more in the following user stories: + +- [How I Built a Personal Knowledge Base for Myself](https://www.getdnote.com/blog/how-i-built-personal-knowledge-base-for-myself/) +- [I Wrote Down Everything I Learned While Programming for a Month](https://www.getdnote.com/blog/writing-everything-i-learn-coding-for-a-month/) + +## See Also + +- [Homepage](https://www.getdnote.com) +- [Forum](https://forum.getdnote.com) diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index e67df891..8fd5f861 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -1,43 +1,165 @@ -# Self-Hosting Dnote Server +# Installing Dnote Server -Please see the [doc](https://www.getdnote.com/docs/server) for more. +This guide documents the steps for installing the Dnote server on your own machine. If you prefer Docker, please see [the Docker guide](https://github.com/dnote/dnote/blob/master/host/docker/README.md). -## Docker Installation +## Overview -1. Install [Docker](https://docs.docker.com/install/). -2. Install Docker [Compose plugin](https://docs.docker.com/compose/install/linux/). -3. Create a `compose.yml` file with the following content: +Dnote server comes as a single binary file that you can simply download and run. It uses Postgres as the database. -```yaml -services: - dnote: - image: dnote/dnote:latest - container_name: dnote - ports: - - 3001:3001 - volumes: - - ./dnote_data:/data - restart: unless-stopped -``` +## Installation -4. Run the following to download the image and start the container - -``` -docker compose up -d -``` - -Visit http://localhost:3001 in your browser to see Dnote running. - -## Manual Installation - -Download from [releases](https://github.com/dnote/dnote/releases), extract, and run: +1. Install Postgres 11+. +2. Create a `dnote` database by running `createdb dnote` +3. Download the official Dnote server release from the [release page](https://github.com/dnote/dnote/releases). +4. Extract the archive and move the `dnote-server` executable to `/usr/local/bin`. ```bash tar -xzf dnote-server-$version-$os.tar.gz mv ./dnote-server /usr/local/bin -dnote-server start --baseUrl=https://your.server ``` -You're up and running. Database: `~/.local/share/dnote/server.db` (customize with `--dbPath`). Run `dnote-server start --help` for options. +4. Run Dnote -Set `apiEndpoint: https://your.server/api` in `~/.config/dnote/dnoterc` to connect your CLI to the server. +```bash +GO_ENV=PRODUCTION \ +DBHost=localhost \ +DBPort=5432 \ +DBName=dnote \ +DBUser=$user \ +DBPassword=$password \ +WebURL=$webURL \ +SmtpHost=$SmtpHost \ +SmtpPort=$SmtpPort \ +SmtpUsername=$SmtpUsername \ +SmtpPassword=$SmtpPassword \ + dnote-server start +``` + +Replace `$user`, `$password` with the credentials of the Postgres user that owns the `dnote` database. + +Replace `$webURL` with the full URL to your server, without a trailing slash (e.g. `https://your.server`). + +Replace `$SmtpHost`, `SmtpPort`, `$SmtpUsername`, `$SmtpPassword` with actual values, if you would like to receive spaced repetition through email. + +By default, dnote server will run on the port 3000. + +## Configuration + +By now, Dnote is fully functional in your machine. The API, frontend app, and the background tasks are all in the single binary. Let's take a few more steps to configure Dnote. + +### Configure Nginx + +To make it accessible from the Internet, you need to configure Nginx. + +1. Install nginx. +2. Create a new file in `/etc/nginx/sites-enabled/dnote` with the following contents: + +``` +server { + server_name my-dnote-server.com; + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + proxy_pass http://127.0.0.1:3000; + } +} +``` +3. Replace `my-dnote-server.com` with the URL for your server. +4. Reload the nginx configuration by running the following: + +``` +sudo service nginx reload +``` + +Now you can access the Dnote frontend application on `/`, and the API on `/api`. + +### Configure TLS by using LetsEncrypt + +It is recommended to use HTTPS. Obtain a certificate using LetsEncrypt and configure TLS in Nginx. + +In the future versions of the Dnote Server, HTTPS will be required at all times. + +### Run Dnote As a Daemon + +We can use `systemd` to run Dnote in the background as a Daemon, and automatically start it on system reboot. + +1. Create a new file at `/etc/systemd/system/dnote.service` with the following content: + +``` +[Unit] +Description=Starts the dnote server +Requires=network.target +After=network.target + +[Service] +Type=simple +User=$user +Restart=always +RestartSec=3 +WorkingDirectory=/home/$user +ExecStart=/usr/local/bin/dnote-server start +Environment=GO_ENV=PRODUCTION +Environment=DBHost=localhost +Environment=DBPort=5432 +Environment=DBName=dnote +Environment=WebURL=$WebURL +Environment=DBUser=$DBUser +Environment=DBPassword=$DBPassword +Environment=SmtpHost= +Environment=SmtpPort= +Environment=SmtpUsername= +Environment=SmtpPassword= + +[Install] +WantedBy=multi-user.target +``` + +Replace `$user`, `$WebURL`, `$DBUser`, and `$DBPassword` with the actual values. + +Optionally, if you would like to send spaced repetitions throught email, populate `SmtpHost`, `SmtpPort`, `SmtpUsername`, and `SmtpPassword`. + +2. Reload the change by running `sudo systemctl daemon-reload`. +3. Enable the Daemon by running `sudo systemctl enable dnote`.` +4. Start the Daemon by running `sudo systemctl start dnote` + +### Enable Pro version + +After signing up with an account, enable the pro version to access all features. + +Log into the `dnote` Postgres database and execute the following query: + +```sql +UPDATE users SET cloud = true FROM accounts WHERE accounts.user_id = users.id AND accounts.email = '$yourEmail'; +``` + +Replace `$yourEmail` with the email you used to create the account. + +### Configure clients + +Let's configure Dnote clients to connect to the self-hosted web API endpoint. + +#### CLI + +We need to modify the configuration file for the CLI. It should have been generated at `~/.dnote/dnoterc` upon running the CLI for the first time. + +The following is an example configuration: + +```yaml +editor: nvim +apiEndpoint: https://api.getdnote.com +``` + +Simply change the value for `apiEndpoint` to a full URL to the self-hosted instance, followed by '/api', and save the configuration file. + +e.g. + +```yaml +editor: nvim +apiEndpoint: my-dnote-server.com/api +``` + +#### Browser extension + +Navigate into the 'Settings' tab and set the values for 'API URL', and 'Web URL'. diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 00000000..65dd3db1 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,20 @@ +# -*- mode: ruby -*- + +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/bionic64" + config.vm.synced_folder '.', '/go/src/github.com/dnote/dnote' + config.vm.network "forwarded_port", guest: 3000, host: 3000 + config.vm.network "forwarded_port", guest: 8080, host: 8080 + config.vm.network "forwarded_port", guest: 5432, host: 5433 + + config.vm.provision 'shell', path: './scripts/vagrant/install_utils.sh' + config.vm.provision 'shell', path: './scripts/vagrant/install_go.sh', privileged: false + config.vm.provision 'shell', path: './scripts/vagrant/install_node.sh', privileged: false + config.vm.provision 'shell', path: './scripts/vagrant/install_postgres.sh', privileged: false + config.vm.provision 'shell', path: './scripts/vagrant/bootstrap.sh', privileged: false + + config.vm.provider "virtualbox" do |v| + v.memory = 4000 + v.cpus = 2 + end +end diff --git a/assets/cli.gif b/assets/cli.gif new file mode 100644 index 00000000..8925e131 Binary files /dev/null and b/assets/cli.gif differ diff --git a/browser/.gitignore b/browser/.gitignore new file mode 100644 index 00000000..b6da8558 --- /dev/null +++ b/browser/.gitignore @@ -0,0 +1,5 @@ +/dist +/package +/node_modules +.DS_Store +extension.tar.gz diff --git a/browser/CONTRIBUTING.md b/browser/CONTRIBUTING.md new file mode 100644 index 00000000..d16106f8 --- /dev/null +++ b/browser/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Contributing + +Use the following commands to set up, build, and release. + +## Set up + +* Run `npm install-js` from the monorepo root. + +## Developing locally + +* `npm run watch:firefox` +* `npm run watch:chrome` + +## Releasing + +* Set a new version in `package.json` +* Run `./scripts/build_prod.sh` + * A gulp task `manifest` will copy the version from `package.json` to `manifest.json` diff --git a/browser/NOTE_TO_REVIEWER.md b/browser/NOTE_TO_REVIEWER.md new file mode 100644 index 00000000..5ba50d8c --- /dev/null +++ b/browser/NOTE_TO_REVIEWER.md @@ -0,0 +1,18 @@ +# Note to reviewer + +This document contains instructions about how to reproduce the final build of this extension. + +All releases are tagged and pushed to [the GitHub repository](https://github.com/dnote/dnote). + +## Steps + +To reproduce the obfuscated code for Firefox, please follow the steps below. + +1. From the monorepo project root, run `make install-js` to install dependencies +2. Run `./scripts/build_prod.sh` to build for Firefox and Chrome. + +The obfuscated code will be under `/dist/firefox` and `/dist/chrome`. + +## Further questions + +Please contact sung@dnote.io diff --git a/browser/README.md b/browser/README.md new file mode 100644 index 00000000..3dece9af --- /dev/null +++ b/browser/README.md @@ -0,0 +1,28 @@ +# Dnote Browser Extension + +Dnote browser extension for Chrome and Firefox. Capture new information without opening a new tab or leaving your browser. + +![Dnote browser demo](assets/demo.gif) + +## Installation + +1. Install the extension + +* Firefox - https://addons.mozilla.org/addon/dnote +* Chrome - https://chrome.google.com/webstore/detail/dnote/mcfbfmihbijfaambfbbfcdcfibcjcahi + +2. Login with your API key from https://dnote.io + +## Overview + +We learn many things while reading technical articles, or browsing StackOverflow. Unless we write them down we forget most of them exponentially. + +This extension integrates seamlessly with [Dnote CLI](https://github.com/dnote/dnote/cli) and requires [Dnote Cloud](https://www.getdnote.com/pricing) account. + +## Hotkeys + +Write new notes without even moving your hands to the mouse. + +* **Ctrl + d** - Open the extension (**Ctrl + Shift + v** on Firefox on Linux). +* **Shift + Enter** - Save the current note +* **b** - Open the saved note in the browser diff --git a/browser/assets/demo.gif b/browser/assets/demo.gif new file mode 100644 index 00000000..b122a441 Binary files /dev/null and b/browser/assets/demo.gif differ diff --git a/browser/gulpfile.js b/browser/gulpfile.js new file mode 100644 index 00000000..69953670 --- /dev/null +++ b/browser/gulpfile.js @@ -0,0 +1,100 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +const gulp = require('gulp'); +const del = require('del'); +const replace = require('gulp-replace'); +const gulpif = require('gulp-if'); +const imagemin = require('gulp-imagemin'); +const livereload = require('gulp-livereload'); +const zip = require('gulp-zip'); + +const target = process.env.TARGET; + +gulp.task('manifest', () => { + const pkg = require('./package.json'); + + return gulp + .src(`manifests/${target}/manifest.json`) + .pipe(replace('__VERSION__', pkg.version)) + .pipe(gulp.dest(`dist/${target}`)); +}); + +gulp.task('styles', () => { + return gulp.src('src/styles/*.css').pipe(gulp.dest(`dist/${target}/styles`)); +}); + +gulp.task( + 'html', + gulp.series('styles', () => { + return gulp.src('src/*.html').pipe(gulp.dest(`dist/${target}`)); + }) +); + +gulp.task('images', () => { + return gulp + .src('src/images/**/*') + .pipe( + gulpif( + gulpif.isFile, + imagemin({ + progressive: true, + interlaced: true, + svgoPlugins: [{ cleanupIDs: false }] + }) + ) + ) + .pipe(gulp.dest(`dist/${target}/images`)); +}); + +gulp.task( + 'clean', + del.bind(null, ['.tmp', `dist/${target}`, `package/${target}`]) +); + +gulp.task( + 'watch', + gulp.series('manifest', 'html', 'styles', 'images', () => { + livereload.listen(); + + gulp + .watch([ + 'src/*.html', + 'src/scripts/**/*', + 'src/images/**/*', + 'src/styles/**/*' + ]) + .on('change', livereload.reload); + + gulp.watch('src/*.html', gulp.parallel('html')); + gulp.watch('manifests/**/*.json', gulp.parallel('manifest')); + }) +); + +gulp.task('package', function() { + const manifest = require(`./dist/${target}/manifest.json`); + + return gulp + .src(`dist/${target}/**`) + .pipe(zip('dnote-' + manifest.version + '.zip')) + .pipe(gulp.dest(`package/${target}`)); +}); + +gulp.task('build', gulp.series('manifest', gulp.parallel('html', 'images'))); + +gulp.task('default', gulp.series('clean', 'build')); diff --git a/browser/manifests/chrome/manifest.json b/browser/manifests/chrome/manifest.json new file mode 100644 index 00000000..4b95c5dc --- /dev/null +++ b/browser/manifests/chrome/manifest.json @@ -0,0 +1,31 @@ +{ + "name": "Dnote", + "version": "__VERSION__", + "description": "Capture your microlessons without leaving the browser.", + "icons": { + "16": "images/iconx16.png", + "48": "images/iconx48.png", + "128": "images/iconx128.png" + }, + "manifest_version": 2, + "browser_action": { + "default_icon": { + "16": "images/iconx16.png", + "32": "images/iconx32.png" + }, + "default_popup": "popup.html" + }, + "background": { + "scripts": [] + }, + "content_scripts": [], + "permissions": ["storage"], + "commands": { + "_execute_browser_action": { + "suggested_key": { + "default": "Ctrl+D", + "mac": "MacCtrl+D" + } + } + } +} diff --git a/browser/manifests/firefox/manifest.json b/browser/manifests/firefox/manifest.json new file mode 100644 index 00000000..8175011f --- /dev/null +++ b/browser/manifests/firefox/manifest.json @@ -0,0 +1,38 @@ +{ + "name": "Dnote", + "version": "__VERSION__", + "description": "Capture your microlessons without leaving the browser.", + "applications": { + "gecko": { + "id": "sung@dnote.io", + "strict_min_version": "42.0" + } + }, + "icons": { + "16": "images/iconx16.png", + "48": "images/iconx48.png", + "128": "images/iconx128.png" + }, + "manifest_version": 2, + "browser_action": { + "default_icon": { + "16": "images/iconx16.png", + "32": "images/iconx32.png" + }, + "default_popup": "popup.html" + }, + "background": { + "scripts": [] + }, + "content_scripts": [], + "permissions": ["storage"], + "commands": { + "_execute_browser_action": { + "suggested_key": { + "default": "Ctrl+D", + "linux": "Ctrl+Shift+V", + "mac": "MacCtrl+D" + } + } + } +} diff --git a/browser/package.json b/browser/package.json new file mode 100644 index 00000000..a3400f96 --- /dev/null +++ b/browser/package.json @@ -0,0 +1,46 @@ +{ + "name": "dnote-extension", + "description": "Dnote browser extension for Chrome and Firefox", + "scripts": { + "clean": "TARGET=firefox gulp clean && TARGET=chrome gulp clean", + "build:chrome": "TARGET=chrome NODE_ENV=production concurrently webpack \"gulp build\"", + "build:firefox": "TARGET=firefox NODE_ENV=production concurrently webpack \"gulp build\"", + "package:chrome": "TARGET=chrome NODE_ENV=production gulp package", + "package:firefox": "TARGET=firefox NODE_ENV=production gulp package", + "watch:chrome": "TARGET=chrome NODE_ENV=development concurrently \"webpack --watch\" \"gulp watch\" ", + "watch:firefox": "TARGET=firefox NODE_ENV=development concurrently \"webpack --watch\" \"gulp watch\" ", + "lint": "eslint ./src --ext .ts,.tsx,.js" + }, + "author": "Monomax Software Pty Ltd", + "license": "GPL-3.0-or-later", + "version": "2.0.0", + "dependencies": { + "classnames": "^2.2.5", + "lodash": "^4.17.15", + "qs": "^6.9.0", + "react": "^16.11.0", + "react-dom": "^16.11.0", + "react-redux": "^7.0.0", + "react-select": "^3.0.8", + "redux": "^4.0.4", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.2.0" + }, + "devDependencies": { + "@babel/preset-env": "^7.7.4", + "@types/react": "^16.9.11", + "@types/react-dom": "^16.9.3", + "concurrently": "^5.0.0", + "del": "^5.0.0", + "gulp": "^4.0.0", + "gulp-if": "^3.0.0", + "gulp-imagemin": "^6.1.1", + "gulp-livereload": "^4.0.2", + "gulp-replace": "^1.0.0", + "gulp-zip": "^5.0.1", + "ts-loader": "^6.2.1", + "typescript": "^3.6.4", + "webpack": "^4.41.2", + "webpack-cli": "^3.3.9" + } +} diff --git a/browser/scripts/build_prod.sh b/browser/scripts/build_prod.sh new file mode 100755 index 00000000..be34dfc2 --- /dev/null +++ b/browser/scripts/build_prod.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# build_prod.sh builds distributable archive for the addon +# remember to bump version in package.json +set -eux + +# clean +yarn clean + +# chrome +yarn build:chrome +yarn package:chrome +# firefox +yarn build:firefox +yarn package:firefox diff --git a/browser/scripts/zip.sh b/browser/scripts/zip.sh new file mode 100755 index 00000000..03cb6acc --- /dev/null +++ b/browser/scripts/zip.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +tar --exclude='./node_modules' --exclude='./package' --exclude='./dist' -zcvf extension.tar.gz * .eslintrc diff --git a/browser/src/browser.d.ts b/browser/src/browser.d.ts new file mode 100644 index 00000000..37ccb184 --- /dev/null +++ b/browser/src/browser.d.ts @@ -0,0 +1,21 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +// browser.d.ts +declare let browser: any; +declare let chrome: any; diff --git a/browser/src/global.d.ts b/browser/src/global.d.ts new file mode 100644 index 00000000..21c3c131 --- /dev/null +++ b/browser/src/global.d.ts @@ -0,0 +1,24 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +// global.d.ts + +// defined by webpack-define-plugin +declare let __API_ENDPOINT__: string; +declare let __WEB_URL__: string; +declare let __VERSION__: string; diff --git a/browser/src/images/close.svg b/browser/src/images/close.svg new file mode 100644 index 00000000..1e675311 --- /dev/null +++ b/browser/src/images/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/src/images/hamberger-menu.svg b/browser/src/images/hamberger-menu.svg new file mode 100644 index 00000000..1dd2b78f --- /dev/null +++ b/browser/src/images/hamberger-menu.svg @@ -0,0 +1 @@ + diff --git a/browser/src/images/iconx128.png b/browser/src/images/iconx128.png new file mode 100644 index 00000000..011a9d8f Binary files /dev/null and b/browser/src/images/iconx128.png differ diff --git a/browser/src/images/iconx16.png b/browser/src/images/iconx16.png new file mode 100644 index 00000000..07ef9d85 Binary files /dev/null and b/browser/src/images/iconx16.png differ diff --git a/browser/src/images/iconx32.png b/browser/src/images/iconx32.png new file mode 100644 index 00000000..ea23c53e Binary files /dev/null and b/browser/src/images/iconx32.png differ diff --git a/browser/src/images/iconx48.png b/browser/src/images/iconx48.png new file mode 100644 index 00000000..1d27086f Binary files /dev/null and b/browser/src/images/iconx48.png differ diff --git a/browser/src/images/iconx96.png b/browser/src/images/iconx96.png new file mode 100644 index 00000000..a7508afd Binary files /dev/null and b/browser/src/images/iconx96.png differ diff --git a/browser/src/images/logo-circle.png b/browser/src/images/logo-circle.png new file mode 100644 index 00000000..54022b6f Binary files /dev/null and b/browser/src/images/logo-circle.png differ diff --git a/browser/src/popup.html b/browser/src/popup.html new file mode 100644 index 00000000..861f45d1 --- /dev/null +++ b/browser/src/popup.html @@ -0,0 +1,15 @@ + + + + Dnote browser extension + + + + + + +
+ + + + diff --git a/browser/src/scripts/components/App.tsx b/browser/src/scripts/components/App.tsx new file mode 100644 index 00000000..49743c50 --- /dev/null +++ b/browser/src/scripts/components/App.tsx @@ -0,0 +1,118 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +import React, { useState, useEffect } from 'react'; + +import initServices from '../utils/services'; +import { logout } from '../store/auth/actions'; +import { AuthState } from '../store/auth/types'; +import { useSelector, useDispatch } from '../store/hooks'; +import Header from './Header'; +import Home from './Home'; +import Menu from './Menu'; +import Success from './Success'; +import Settings from './Settings'; +import Composer from './Composer'; + +interface Props {} + +function renderRoutes(path: string, isLoggedIn: boolean) { + switch (path) { + case '/success': + return ; + case '/': { + if (isLoggedIn) { + return ; + } + + return ; + } + case '/settings': { + return ; + } + default: + return
Not found
; + } +} + +// useCheckSessionValid ensures that the current session is valid +function useCheckSessionValid(auth: AuthState) { + const dispatch = useDispatch(); + + useEffect(() => { + // if session is expired, clear it + const now = Math.round(new Date().getTime() / 1000); + if (auth.sessionKey && auth.sessionKeyExpiry < now) { + dispatch(logout()); + } + }, [dispatch, auth.sessionKey, auth.sessionKeyExpiry]); +} + +const App: React.FunctionComponent = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [errMsg, setErrMsg] = useState(''); + + const dispatch = useDispatch(); + const { path, auth, settings } = useSelector(state => ({ + path: state.location.path, + auth: state.auth, + settings: state.settings + })); + + useCheckSessionValid(auth); + + const isLoggedIn = Boolean(auth.sessionKey); + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; + + const handleLogout = async (done?: Function) => { + try { + await initServices(settings.apiUrl).users.signout(); + dispatch(logout()); + + if (done) { + done(); + } + } catch (e) { + setErrMsg(e.message); + } + }; + + return ( +
+
+ + {isMenuOpen && ( + + )} + +
+ {errMsg &&
{errMsg}
} + + {renderRoutes(path, isLoggedIn)} +
+
+ ); +}; + +export default App; diff --git a/browser/src/scripts/components/BookIcon.tsx b/browser/src/scripts/components/BookIcon.tsx new file mode 100644 index 00000000..6c693ad7 --- /dev/null +++ b/browser/src/scripts/components/BookIcon.tsx @@ -0,0 +1,46 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +import React from 'react'; + +const Icon = ({ fill, width, height, className }) => { + const h = `${height}px`; + const w = `${width}px`; + + return ( + + + + + + ); +}; + +Icon.defaultProps = { + fill: '#000', + width: 32, + height: 32 +}; + +export default Icon; diff --git a/browser/src/scripts/components/BookSelector.tsx b/browser/src/scripts/components/BookSelector.tsx new file mode 100644 index 00000000..75f6b3a4 --- /dev/null +++ b/browser/src/scripts/components/BookSelector.tsx @@ -0,0 +1,117 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +import React from 'react'; +import CreatableSelect from 'react-select/creatable'; +import cloneDeep from 'lodash/cloneDeep'; +import { useSelector, useDispatch } from '../store/hooks'; +import { updateBook, resetBook } from '../store/composer/actions'; + +interface Props { + selectorRef: React.Dispatch; + onAfterChange: () => void; +} + +function useCurrentOptions(options) { + const currentValue = useSelector(state => state.composer.bookUUID); + + for (let i = 0; i < options.length; i++) { + const option = options[i]; + + if (option.value === currentValue) { + return option; + } + } + + return null; +} + +function useOptions() { + const { books, composer } = useSelector(state => ({ + books: state.books, + composer: state.composer + })); + + const opts = books.items.map(book => ({ + label: book.label, + value: book.uuid + })); + + if (composer.bookLabel !== '' && composer.bookUUID === '') { + opts.push({ + label: composer.bookLabel, + value: '' + }); + } + + // clone the array so as not to mutate Redux state manually + // e.g. react-select mutates options prop internally upon adding a new option + return cloneDeep(opts); +} + +const BookSelector: React.FunctionComponent = ({ + selectorRef, + onAfterChange +}) => { + const dispatch = useDispatch(); + const { books } = useSelector(state => ({ + books: state.books + })); + const options = useOptions(); + const currentOption = useCurrentOptions(options); + + let placeholder: string; + if (books.isFetched) { + placeholder = 'Choose a book'; + } else { + placeholder = 'Loading books...'; + } + + return ( + { + selectorRef(el); + }} + multi={false} + isClearable + placeholder={placeholder} + options={options} + value={currentOption} + onChange={(option, meta) => { + if (meta.action === 'clear') { + dispatch(resetBook()); + } else { + let uuid: string; + if (meta.action === 'create-option') { + uuid = ''; + } else { + uuid = option.value; + } + + dispatch(updateBook({ uuid, label: option.label })); + } + + onAfterChange(); + }} + formatCreateLabel={label => `Add a new book ${label}`} + isDisabled={!books.isFetched} + /> + ); +}; + +export default BookSelector; diff --git a/browser/src/scripts/components/CloseIcon.tsx b/browser/src/scripts/components/CloseIcon.tsx new file mode 100644 index 00000000..91859122 --- /dev/null +++ b/browser/src/scripts/components/CloseIcon.tsx @@ -0,0 +1,29 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +import React from 'react'; + +export default () => ( + + + + +); diff --git a/browser/src/scripts/components/Composer.tsx b/browser/src/scripts/components/Composer.tsx new file mode 100644 index 00000000..d9b40fd5 --- /dev/null +++ b/browser/src/scripts/components/Composer.tsx @@ -0,0 +1,239 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import classnames from 'classnames'; + +import { KEYCODE_ENTER } from 'jslib/helpers/keyboard'; +import initServices from '../utils/services'; +import BookSelector from './BookSelector'; +import Flash from './Flash'; +import { useSelector, useDispatch } from '../store/hooks'; +import { updateContent, resetComposer } from '../store/composer/actions'; +import { fetchBooks } from '../store/books/actions'; +import { navigate } from '../store/location/actions'; + +interface Props {} + +// focusBookSelectorInput focuses on the input element of the book selector. +// It needs to traverse the tree returned by the ref API of the 'react-select' library, +// and to guard against possible breaking changes, if the path does not exist, it noops. +function focusBookSelectorInput(bookSelectorRef) { + return ( + bookSelectorRef.select && + bookSelectorRef.select.select && + bookSelectorRef.select.select.inputRef && + bookSelectorRef.select.select.inputRef.focus() + ); +} + +function useFetchData() { + const dispatch = useDispatch(); + + const { books } = useSelector(state => ({ + books: state.books + })); + + useEffect(() => { + if (!books.isFetched) { + dispatch(fetchBooks()); + } + }, [dispatch, books.isFetched]); +} + +function useInitFocus(contentRef, bookSelectorRef) { + const { composer, books } = useSelector(state => ({ + composer: state.composer, + books: state.books + })); + + useEffect(() => { + if (!books.isFetched) { + return () => null; + } + + if (bookSelectorRef && contentRef) { + if (composer.bookLabel === '') { + focusBookSelectorInput(bookSelectorRef); + } else { + contentRef.focus(); + } + } + + return () => null; + }, [contentRef, bookSelectorRef, books.isFetched, composer.bookLabel]); +} + +const Composer: React.FunctionComponent = () => { + useFetchData(); + const [contentFocused, setContentFocused] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [errMsg, setErrMsg] = useState(''); + const dispatch = useDispatch(); + const [contentRef, setContentEl] = useState(null); + const [bookSelectorRef, setBookSelectorEl] = useState(null); + + const { composer, settings, auth } = useSelector(state => ({ + composer: state.composer, + settings: state.settings, + auth: state.auth + })); + + const handleSubmit = useCallback( + async e => { + e.preventDefault(); + + const services = initServices(settings.apiUrl); + + setSubmitting(true); + + try { + let bookUUID; + if (composer.bookUUID === '') { + const resp = await services.books.create( + { + name: composer.bookLabel + }, + { + headers: { + Authorization: `Bearer ${auth.sessionKey}` + } + } + ); + + bookUUID = resp.book.uuid; + } else { + bookUUID = composer.bookUUID; + } + + const resp = await services.notes.create( + { + book_uuid: bookUUID, + content: composer.content + }, + { + headers: { + Authorization: `Bearer ${auth.sessionKey}` + } + } + ); + + // clear the composer state + setErrMsg(''); + setSubmitting(false); + + dispatch(resetComposer()); + + // navigate + dispatch( + navigate('/success', { + bookName: composer.bookLabel, + noteUUID: resp.result.uuid + }) + ); + } catch (err) { + setErrMsg(err.message); + setSubmitting(false); + } + }, + [ + settings.apiUrl, + composer.bookUUID, + composer.content, + composer.bookLabel, + auth.sessionKey, + dispatch + ] + ); + + useEffect(() => { + const handleSubmitShortcut = e => { + // Shift + Enter + if (e.shiftKey && e.keyCode === KEYCODE_ENTER) { + handleSubmit(e); + } + }; + + window.addEventListener('keydown', handleSubmitShortcut); + + return () => { + window.removeEventListener('keydown', handleSubmitShortcut); + }; + }, [composer, handleSubmit]); + + let submitBtnText: string; + if (submitting) { + submitBtnText = 'Saving...'; + } else { + submitBtnText = 'Save'; + } + + useInitFocus(contentRef, bookSelectorRef); + + return ( +
+ + +
+ { + contentRef.focus(); + }} + /> + +
+