mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
78 Commits
master
...
web/auth-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d5f6dd909 | ||
|
|
ea2af9fd98 | ||
|
|
445701c7d5 | ||
|
|
333a4bddd7 | ||
|
|
acafed779c | ||
|
|
3cd2d3dfef | ||
|
|
34f4b96473 | ||
|
|
037d0a6f2e | ||
|
|
701ac02ff1 | ||
|
|
c60fb9e7fd | ||
|
|
657c18ffed | ||
|
|
573c218fc8 | ||
|
|
b38e20b2ea | ||
|
|
fd274c4da4 | ||
|
|
d1f7169e1c | ||
|
|
2f972bf5d0 | ||
|
|
97d530cfe3 | ||
|
|
64c87a4687 | ||
|
|
7132ed60e1 | ||
|
|
3dcc107ae4 | ||
|
|
4faa71419d | ||
|
|
7f7f006f15 | ||
|
|
317f889569 | ||
|
|
0f0d239e11 | ||
|
|
639d51d2e0 | ||
|
|
edb6f16fd6 | ||
|
|
cca26b117e | ||
|
|
d8e096f15c | ||
|
|
f3d798dc1c | ||
|
|
e54a91a2d7 | ||
|
|
a4b6e9e476 | ||
|
|
c6579a0a88 | ||
|
|
90ba685aa6 | ||
|
|
e2b398efdb | ||
|
|
f29d9b9948 | ||
|
|
8daea6ac27 | ||
|
|
967b3ce451 | ||
|
|
f1a05711ba | ||
|
|
1e73a511c1 | ||
|
|
8fee146a32 | ||
|
|
ff3570bae8 | ||
|
|
7ffaab036b | ||
|
|
73d2fe1271 | ||
|
|
b6ace863b3 | ||
|
|
912fa3fe49 | ||
|
|
394c12856b | ||
|
|
1473a70681 | ||
|
|
6e4d5490bc | ||
|
|
1a864743d0 | ||
|
|
e87a9297ce | ||
|
|
72e2010074 | ||
|
|
5d151045cd | ||
|
|
60d7208696 | ||
|
|
882a667269 | ||
|
|
7583e16152 | ||
|
|
02f537e296 | ||
|
|
a462e5a10e | ||
|
|
800c1ccd4d | ||
|
|
33880c682a | ||
|
|
a2cd14ffc8 | ||
|
|
7ba5e72f64 | ||
|
|
0e5b216773 | ||
|
|
ee43a020b2 | ||
|
|
364225b34b | ||
|
|
cb862e7293 | ||
|
|
247a2824e8 | ||
|
|
51dceb9715 | ||
|
|
a0ba1fb1d2 | ||
|
|
7ae1cc85ac | ||
|
|
67d25797d6 | ||
|
|
b3b00fc7cf | ||
|
|
ba253db226 | ||
|
|
9db7795e31 | ||
|
|
526487c37e | ||
|
|
37e7d44587 | ||
|
|
2e09a691e7 | ||
|
|
1c8e43d085 | ||
|
|
6ee2e777c2 |
2
.github/workflows/android.e2e.yml
vendored
2
.github/workflows/android.e2e.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
|
||||
104
.github/workflows/android.preview.build.yml
vendored
104
.github/workflows/android.preview.build.yml
vendored
@@ -1,104 +0,0 @@
|
||||
name: Notesnook Android Preview Build
|
||||
|
||||
# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and
|
||||
# compiled with a read-only GITHUB_TOKEN and NO repository secrets. The release
|
||||
# APK is built without any signing secret (the same as before). Firebase
|
||||
# distribution and the PR comment happen in android.preview.publish.yml, which
|
||||
# runs in the trusted `workflow_run` context and never executes fork code.
|
||||
# Because no secrets are exposed here, the build runs automatically for every
|
||||
# PR (including forks) with no authorization gate.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/android.preview.build.yml"
|
||||
- ".github/workflows/android.preview.publish.yml"
|
||||
|
||||
concurrency:
|
||||
group: android-preview-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
STAGING_BUILD: true
|
||||
|
||||
steps:
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build arm64-v8a apk
|
||||
run: |
|
||||
cd apps/mobile/android
|
||||
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
|
||||
|
||||
- name: Stage build artifact
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/artifact"
|
||||
cp apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk \
|
||||
"$RUNNER_TEMP/artifact/app-preview.apk"
|
||||
{
|
||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||
echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}"
|
||||
} > "$RUNNER_TEMP/artifact/pr-meta.env"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-preview-build
|
||||
path: ${{ runner.temp }}/artifact
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/android/app/build/generated/sourcemaps/**/*.map
|
||||
135
.github/workflows/android.preview.firebase.yml
vendored
Normal file
135
.github/workflows/android.preview.firebase.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
name: Notesnook Android Preview
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/android.preview.firebase.yml"
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
build:
|
||||
needs: authorize
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
STAGING_BUILD: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build arm64-v8a apk
|
||||
run: |
|
||||
cd apps/mobile/android
|
||||
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: saionaro/extract-package-version@master
|
||||
with:
|
||||
path: apps/mobile
|
||||
|
||||
- name: Publish to firebase CLI
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{secrets.FIREBASE_APP_ID}}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- android-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/android/app/build/generated/sourcemaps/**/*.map
|
||||
72
.github/workflows/android.preview.publish.yml
vendored
72
.github/workflows/android.preview.publish.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Notesnook Android Preview Publish
|
||||
|
||||
# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so
|
||||
# it has the base repo's secrets and a write-scoped token. It downloads the APK
|
||||
# the build produced and distributes it + posts the PR comment. It never checks
|
||||
# out or executes fork code.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Notesnook Android Preview Build"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: android-preview-build
|
||||
path: ${{ runner.temp }}/artifact
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Load PR metadata
|
||||
run: cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Publish to Firebase
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: ${{ runner.temp }}/artifact/app-preview.apk
|
||||
releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- android-preview-comment -->';
|
||||
const prNumber = Number(process.env.PR_NUMBER);
|
||||
if (!prNumber) return;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.HEAD_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/core.tests.yml
vendored
2
.github/workflows/core.tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
needs: authorize
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
|
||||
12
.github/workflows/desktop.preview.yml
vendored
12
.github/workflows/desktop.preview.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
macos-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
linux-x64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -210,12 +210,12 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
runs-on: windows-2022
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
25
.github/workflows/desktop.publish.yml
vendored
25
.github/workflows/desktop.publish.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
target_commitish: ${{ github.ref }}
|
||||
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.build-linux
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
|
||||
@@ -127,13 +127,6 @@ jobs:
|
||||
files: ./generated-sources.json
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
- name: Upload generated-sources.json as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: inputs.build-linux
|
||||
with:
|
||||
name: generated-sources
|
||||
path: ./generated-sources.json
|
||||
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
@@ -142,7 +135,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -189,9 +182,7 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
@@ -246,7 +237,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -273,7 +264,6 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
@@ -314,7 +304,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -341,7 +331,6 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
@@ -374,11 +363,11 @@ jobs:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-2022
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
|
||||
115
.github/workflows/desktop.tests.yml
vendored
115
.github/workflows/desktop.tests.yml
vendored
@@ -18,13 +18,12 @@ on:
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
|
||||
jobs:
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
runs-on: macos-15-intel
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -34,10 +33,44 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
- name: Generate desktop build
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
needs: build
|
||||
runs-on: macos-15-intel
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
@@ -54,24 +87,33 @@ jobs:
|
||||
|
||||
test-macos:
|
||||
name: Test macOS
|
||||
needs: build
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
@@ -88,24 +130,39 @@ jobs:
|
||||
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
@@ -122,26 +179,44 @@ jobs:
|
||||
|
||||
test-windows:
|
||||
name: Test for Windows
|
||||
runs-on: windows-2022
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
2
.github/workflows/editor.tests.yml
vendored
2
.github/workflows/editor.tests.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
79
.github/workflows/help.preview.yml
vendored
79
.github/workflows/help.preview.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Notesnook Help PR Preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "docs/help/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/help.preview.yml"
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=help
|
||||
|
||||
- name: Build help
|
||||
run: npm run build:help
|
||||
|
||||
- name: Deploy to Cloudflare
|
||||
id: deploy
|
||||
working-directory: ./docs/help
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DEPLOY_OUT=$(npx --yes wrangler versions upload 2>&1) || { echo "$DEPLOY_OUT"; exit 1; }
|
||||
echo "$DEPLOY_OUT"
|
||||
PREVIEW_URL=$(printf "%s" "$DEPLOY_OUT" | grep -Eo 'https?://[^ ]+' | head -1 || true)
|
||||
if [ -z "$PREVIEW_URL" ]; then
|
||||
echo "WARNING: could not parse preview URL from wrangler output"
|
||||
fi
|
||||
echo "preview_url=$PREVIEW_URL" >> $GITHUB_ENV
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
preview_url: ${{ env.preview_url }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- docs-pages-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**Cloudflare Pages Docs Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
25
.github/workflows/help.publish.yml
vendored
25
.github/workflows/help.publish.yml
vendored
@@ -2,6 +2,11 @@ name: Publish Notesnook Help
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "docs/help/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -10,20 +15,16 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
# VitePress reads git history to show the last updated date per page.
|
||||
fetch-depth: 0
|
||||
toolchain: stable
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
- name: Install docgen
|
||||
run: cargo install --git https://github.com/thecodrr/docgen
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=help
|
||||
|
||||
- name: Build help
|
||||
run: npm run build:help
|
||||
- name: Build site
|
||||
run: docgen build --release
|
||||
working-directory: docs/help
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -31,4 +32,4 @@ jobs:
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Publish on Cloudflare Pages
|
||||
run: npx --yes wrangler deploy
|
||||
run: npx --yes wrangler pages deploy --project-name notesnook-help ./docs/help/site/ --branch main
|
||||
|
||||
152
.github/workflows/ios.preview.build.yml
vendored
152
.github/workflows/ios.preview.build.yml
vendored
@@ -1,152 +0,0 @@
|
||||
name: Notesnook iOS Preview Build
|
||||
|
||||
# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and
|
||||
# compiled with a read-only GITHUB_TOKEN and NO repository secrets. It only
|
||||
# produces an *unsigned* archive. Signing, Firebase distribution and PR
|
||||
# comments happen in ios.preview.publish.yml, which runs in the trusted
|
||||
# `workflow_run` context and never executes fork code. Because no secrets are
|
||||
# exposed here, the build runs automatically for every PR (including forks)
|
||||
# with no authorization gate.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/ios.preview.build.yml"
|
||||
- ".github/workflows/ios.preview.publish.yml"
|
||||
|
||||
# A fork that spams pushes shouldn't queue up macOS builds.
|
||||
concurrency:
|
||||
group: ios-preview-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.1.1"
|
||||
|
||||
- name: Setup iOS Platform
|
||||
run: |
|
||||
# GitHub runs this as `/bin/bash -e`, so `set +e` is required -- without
|
||||
# it the first failure aborts the step before any retry can happen.
|
||||
# Both commands hit "Unable to connect to simulator" (exit 70) when
|
||||
# CoreSimulatorService is wedged, so retry each and bounce it between.
|
||||
set +e
|
||||
DL_DIR="$RUNNER_TEMP/ios-platform"
|
||||
|
||||
retry() {
|
||||
for i in 1 2 3; do
|
||||
"$@" && return 0
|
||||
echo "Attempt $i failed ($*); resetting CoreSimulator..."
|
||||
sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService simdiskimaged 2>/dev/null
|
||||
sleep 20
|
||||
xcrun simctl list runtimes >/dev/null 2>&1
|
||||
done
|
||||
echo "::error::Failed after 3 attempts: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
xcrun simctl list runtimes >/dev/null 2>&1
|
||||
retry xcodebuild -downloadPlatform iOS -exportPath "$DL_DIR"
|
||||
|
||||
# Find the dmg; its name embeds a build number that changes.
|
||||
DMG="$(find "$DL_DIR" -name '*.dmg' | head -n1)"
|
||||
[ -n "$DMG" ] || { echo "::error::No dmg in $DL_DIR"; exit 1; }
|
||||
retry xcodebuild -importPlatform "$DMG"
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npm run tx @notesnook/mobile:build
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@v4
|
||||
id: pods-cache
|
||||
with:
|
||||
path: apps/mobile/ios/Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
|
||||
|
||||
- name: Install Pods
|
||||
run: |
|
||||
cd apps/mobile/ios
|
||||
pod install
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Get marketing version
|
||||
id: marketing-version
|
||||
run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Make staging xcconfig active
|
||||
run: |
|
||||
cd apps/mobile/ios/build-configs
|
||||
./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}}
|
||||
|
||||
# Archive unsigned: this job has no certs/profiles (secrets), and the iOS
|
||||
# device SDK forbids ad-hoc signing, so real signing happens in the trusted
|
||||
# publish workflow (ios.preview.publish.yml). Do NOT pass
|
||||
# CODE_SIGN_ENTITLEMENTS="" -- each target must keep its own entitlements so
|
||||
# the archive records them; blanking them dropped the App Group and crashed
|
||||
# MMKV on launch.
|
||||
- name: Archive (unsigned)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild \
|
||||
-workspace apps/mobile/ios/Notesnook.xcworkspace \
|
||||
-scheme Notesnook \
|
||||
-configuration Release \
|
||||
-sdk iphoneos \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath "$RUNNER_TEMP/Notesnook.xcarchive" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
archive
|
||||
|
||||
- name: Stage build artifact
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/artifact"
|
||||
tar -czf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" \
|
||||
-C "$RUNNER_TEMP" Notesnook.xcarchive
|
||||
# Carry the PR context forward; workflow_run cannot see it reliably for forks.
|
||||
{
|
||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||
echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}"
|
||||
} > "$RUNNER_TEMP/artifact/pr-meta.env"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-preview-build
|
||||
path: ${{ runner.temp }}/artifact
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
173
.github/workflows/ios.preview.firebase.yml
vendored
Normal file
173
.github/workflows/ios.preview.firebase.yml
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
name: Notesnook iOS Preview
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/ios.preview.firebase.yml"
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
build:
|
||||
needs: authorize
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.1.1"
|
||||
|
||||
- name: Setup iOS Platform
|
||||
run: |
|
||||
xcodebuild -downloadPlatform iOS -exportPath ~/Downloads
|
||||
xcodebuild -importPlatform ~/Downloads/iphonesimulator_26.1_23B86.dmg
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npm run tx @notesnook/mobile:build
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@v3
|
||||
id: pods-cache
|
||||
with:
|
||||
path: apps/mobile/ios/Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
|
||||
|
||||
- name: Install Pods
|
||||
run: |
|
||||
cd apps/mobile/ios
|
||||
pod install
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Get marketing version
|
||||
id: marketing-version
|
||||
run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Make staging xcconfig active
|
||||
run: |
|
||||
cd apps/mobile/ios/build-configs
|
||||
./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}}
|
||||
|
||||
- name: Build iOS App
|
||||
uses: ammarahm-ed/ios-build-action@master
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
export-options: apps/mobile/ios/ExportOptionsStaging.plist
|
||||
export-method: "ad-hoc"
|
||||
project-path: apps/mobile/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
|
||||
update-targets: |
|
||||
Notesnook
|
||||
Make Note
|
||||
NotesWidgetExtension
|
||||
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
|
||||
code-signing-identity: Apple Distribution
|
||||
team-id: ${{ secrets.APPLE_TEAM_ID }}
|
||||
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
||||
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
|
||||
output-path: Notesnook.ipa
|
||||
mobileprovision-base64: |
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
|
||||
|
||||
- name: Upload IPA artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-preview-ipa
|
||||
path: Notesnook.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Download IPA artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ios-preview-ipa
|
||||
path: .
|
||||
|
||||
- name: Publish to firebase CLI
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{secrets.FIREBASE_IOS_APP_ID}}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: Notesnook.ipa
|
||||
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- ios-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
222
.github/workflows/ios.preview.publish.yml
vendored
222
.github/workflows/ios.preview.publish.yml
vendored
@@ -1,222 +0,0 @@
|
||||
name: Notesnook iOS Preview Publish
|
||||
|
||||
# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so
|
||||
# it has the base repo's secrets and a write-scoped token. It checks out the
|
||||
# BASE repository (never fork code), downloads the unsigned archive the build
|
||||
# produced, then signs + distributes it and posts the PR comment. Signing is
|
||||
# done directly with `codesign` on the prebuilt archive -- no app build phases
|
||||
# run here, so fork code is never executed in this trusted context.
|
||||
#
|
||||
# Split into two jobs on purpose: signing needs macOS, but the Firebase
|
||||
# distribution action is a Docker container action and only runs on Linux.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Notesnook iOS Preview Build"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
export:
|
||||
# Only publish previews for builds that actually succeeded.
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
pr_number: ${{ steps.meta.outputs.PR_NUMBER }}
|
||||
head_sha: ${{ steps.meta.outputs.HEAD_SHA }}
|
||||
|
||||
steps:
|
||||
- name: Checkout base repo (trusted)
|
||||
uses: actions/checkout@v5
|
||||
# No `ref` -> checks out the default branch, i.e. trusted base code.
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.1.1"
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ios-preview-build
|
||||
path: ${{ runner.temp }}/artifact
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Load PR metadata & extract archive
|
||||
id: meta
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_OUTPUT"
|
||||
tar -xzf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" -C "$RUNNER_TEMP"
|
||||
|
||||
- name: Import signing certificate
|
||||
uses: apple-actions/import-codesign-certs@v3
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
|
||||
- name: Install provisioning profiles
|
||||
env:
|
||||
PROFILE_APP: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
|
||||
PROFILE_SHARE: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
|
||||
PROFILE_WIDGET: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$PROFILE_DIR"
|
||||
i=0
|
||||
for p in "$PROFILE_APP" "$PROFILE_SHARE" "$PROFILE_WIDGET"; do
|
||||
echo "$p" | base64 --decode > "$PROFILE_DIR/preview-$i.mobileprovision"
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
# Sign the unsigned archive by hand. `xcodebuild -exportArchive` re-signs
|
||||
# from the archive's `archived-expanded-entitlements.xcent`, which an
|
||||
# unsigned archive does not contain, so it dropped the App Group and the
|
||||
# app crashed on launch. Instead we sign each bundle directly, taking the
|
||||
# entitlements from each provisioning profile (exactly what Apple
|
||||
# provisioned -- App Group, keychain groups with the right team prefix).
|
||||
# Runs on macOS but executes NO fork build phases, so it stays trusted.
|
||||
- name: Sign IPA
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PROFILES="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
APP="$(find "$RUNNER_TEMP/Notesnook.xcarchive/Products/Applications" -maxdepth 1 -name '*.app' | head -n1)"
|
||||
[ -n "$APP" ] || { echo "::error::No .app in archive"; exit 1; }
|
||||
|
||||
# Single Apple Distribution identity from the imported keychain.
|
||||
IDENTITY="$(security find-identity -v -p codesigning | awk '/Apple Distribution/{print $2; exit}')"
|
||||
[ -n "$IDENTITY" ] || { echo "::error::No Apple Distribution identity found"; exit 1; }
|
||||
echo "Signing identity: $IDENTITY"
|
||||
|
||||
TMP="$RUNNER_TEMP/sign"; mkdir -p "$TMP"
|
||||
|
||||
# Decode a profile's bundle id (application-identifier minus team prefix).
|
||||
profile_bundle_id() {
|
||||
security cms -D -i "$1" -o "$TMP/_p.plist" 2>/dev/null
|
||||
local appid; appid="$(plutil -extract Entitlements.application-identifier raw -o - "$TMP/_p.plist")"
|
||||
echo "${appid#*.}"
|
||||
}
|
||||
# Find the profile matching a bundle id; echoes its path.
|
||||
profile_for() {
|
||||
local want="$1" p
|
||||
for p in "$PROFILES"/*.mobileprovision; do
|
||||
[ -e "$p" ] || continue
|
||||
[ "$(profile_bundle_id "$p")" = "$want" ] && { echo "$p"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# Embed the matching profile and sign a bundle with the profile's entitlements.
|
||||
sign_bundle() {
|
||||
local bundle="$1" bid prof ent
|
||||
bid="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$bundle/Info.plist")"
|
||||
prof="$(profile_for "$bid")" || { echo "::error::No provisioning profile for $bid"; exit 1; }
|
||||
ent="$TMP/ent-$bid.plist"
|
||||
security cms -D -i "$prof" -o "$TMP/_p.plist"
|
||||
plutil -extract Entitlements xml1 -o "$ent" "$TMP/_p.plist"
|
||||
cp "$prof" "$bundle/embedded.mobileprovision"
|
||||
echo "Signing $(basename "$bundle") ($bid)"
|
||||
codesign --force --sign "$IDENTITY" --entitlements "$ent" "$bundle"
|
||||
}
|
||||
|
||||
# Sign inside-out: nested code first, then extensions, then the app.
|
||||
if [ -d "$APP/Frameworks" ]; then
|
||||
find "$APP/Frameworks" -maxdepth 1 \( -name '*.framework' -o -name '*.dylib' \) -print0 \
|
||||
| while IFS= read -r -d '' f; do codesign --force --sign "$IDENTITY" "$f"; done
|
||||
fi
|
||||
if [ -d "$APP/PlugIns" ]; then
|
||||
for appex in "$APP/PlugIns"/*.appex; do
|
||||
[ -e "$appex" ] || continue
|
||||
if [ -d "$appex/Frameworks" ]; then
|
||||
find "$appex/Frameworks" -maxdepth 1 \( -name '*.framework' -o -name '*.dylib' \) -print0 \
|
||||
| while IFS= read -r -d '' f; do codesign --force --sign "$IDENTITY" "$f"; done
|
||||
fi
|
||||
sign_bundle "$appex"
|
||||
done
|
||||
fi
|
||||
sign_bundle "$APP"
|
||||
|
||||
codesign --verify --deep --strict --verbose=2 "$APP"
|
||||
|
||||
# Package into an IPA.
|
||||
rm -rf "$TMP/Payload"; mkdir -p "$TMP/Payload"
|
||||
cp -R "$APP" "$TMP/Payload/"
|
||||
( cd "$TMP" && zip -qry "$RUNNER_TEMP/Notesnook.ipa" Payload )
|
||||
|
||||
# Fail fast if the App Group didn't make it into the signed app -- its
|
||||
# absence is the launch crash (MMKV nil group path). Never distribute
|
||||
# a build without it.
|
||||
echo "::group::Signed app entitlements"
|
||||
codesign -d --entitlements :- "$APP" 2>/dev/null || true
|
||||
echo "::endgroup::"
|
||||
codesign -d --entitlements :- "$APP" 2>/dev/null | grep -q "group.org.streetwriters.notesnook" \
|
||||
|| { echo "::error::Signed app is missing the App Group entitlement; it would crash on launch."; exit 1; }
|
||||
echo "App Group entitlement present."
|
||||
|
||||
- name: Upload signed IPA
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-preview-ipa
|
||||
path: ${{ runner.temp }}/Notesnook.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Must be Linux: wzieba/Firebase-Distribution-Github-Action is a Docker
|
||||
# container action, which GitHub only supports on Linux runners.
|
||||
distribute:
|
||||
needs: export
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Download signed IPA
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ios-preview-ipa
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Publish to Firebase
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_IOS_APP_ID }}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: ${{ runner.temp }}/Notesnook.ipa
|
||||
releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ needs.export.outputs.pr_number }}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
PR_NUMBER: ${{ needs.export.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ needs.export.outputs.head_sha }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- ios-preview-comment -->';
|
||||
const prNumber = Number(process.env.PR_NUMBER);
|
||||
if (!prNumber) return;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.HEAD_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
2
.github/workflows/ios.publish.yml
vendored
2
.github/workflows/ios.publish.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/monograph.publish.yml
vendored
2
.github/workflows/monograph.publish.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/theme-builder.publish.yml
vendored
2
.github/workflows/theme-builder.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/themes.publish.yml
vendored
2
.github/workflows/themes.publish.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Collect package metadata
|
||||
id: package_metadata
|
||||
|
||||
2
.github/workflows/vericrypt.publish.yml
vendored
2
.github/workflows/vericrypt.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
6
.github/workflows/web.preview.yml
vendored
6
.github/workflows/web.preview.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -27,9 +27,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
run: npm ci
|
||||
|
||||
- name: Build web
|
||||
run: npm run build:web
|
||||
|
||||
2
.github/workflows/web.publish.yml
vendored
2
.github/workflows/web.publish.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
4
.github/workflows/web.tests.yml
vendored
4
.github/workflows/web.tests.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Migrating & Importing your data from other apps — Importer](https://notesnook.com/help/importing-notes)
|
||||
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
|
||||
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
|
||||
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
|
||||
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)
|
||||
|
||||
51
agent.md
Normal file
51
agent.md
Normal file
@@ -0,0 +1,51 @@
|
||||
## Notesnook Design Agent
|
||||
|
||||
### Purpose
|
||||
|
||||
- Implement Figma designs from user-provided links into the Notesnook app with high visual fidelity.
|
||||
- Use related components and files provided by the user as the source of truth for local patterns.
|
||||
- Preserve behavior, navigation, and existing data flow while updating UI.
|
||||
|
||||
### When To Use This Agent
|
||||
|
||||
- Use this agent when the task is design-to-code for app UI.
|
||||
- Prefer this agent over a general coding agent when the user provides a Figma link and asks for redesign implementation.
|
||||
|
||||
### Scope
|
||||
|
||||
- Primary focus: apps/mobile.
|
||||
- Secondary scope: shared UI dependencies needed for the design update (tokens, icons, strings, and generated assets).
|
||||
- Do not perform unrelated refactors.
|
||||
|
||||
### Tool Preferences
|
||||
|
||||
- Prefer Figma design-context tools to extract layout, spacing rhythm, typography intent, and asset references from the exact node.
|
||||
- Prefer direct workspace edits and targeted validation for touched files.
|
||||
- Avoid introducing new UI frameworks or web-specific styling approaches.
|
||||
|
||||
### Repo Conventions
|
||||
|
||||
- Use theme and token systems first: useThemeColors, spacing/radius constants, and shared typography primitives.
|
||||
- Reuse existing UI primitives and patterns before creating new abstractions.
|
||||
- Keep dark and light mode compatibility by default.
|
||||
- Keep changes minimal, isolated, and review-friendly.
|
||||
|
||||
### Icon Conventions
|
||||
|
||||
- Add new icon SVGs to packages/icons/svgs with lowercase kebab-case names.
|
||||
- Regenerate icon assets from apps/mobile using npx react-native-nano-icons.
|
||||
- Use generated icon names through AppIcon with icon family notesnook where applicable.
|
||||
|
||||
### Figma To Code Workflow
|
||||
|
||||
1. Read the exact Figma node from the provided link.
|
||||
2. Compare against existing repo patterns and referenced components.
|
||||
3. Implement with React Native conventions already used in the app.
|
||||
4. Preserve behavior and interactions unless explicitly changed.
|
||||
5. Validate compile and lint state for edited files.
|
||||
|
||||
### Output Expectations
|
||||
|
||||
- Provide a concise summary of what changed.
|
||||
- List modified files and any generated artifacts.
|
||||
- Call out follow-up steps only when needed.
|
||||
@@ -17,9 +17,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test, expect } from "@nn/test";
|
||||
import { test } from "@nn/test";
|
||||
import { gt, lt } from "semver";
|
||||
import { AppModel } from "../../web/__e2e__/models/app.model.js";
|
||||
import { expect } from "playwright/test";
|
||||
|
||||
test.extend({ options: { version: "3.0.0" } })(
|
||||
"update starts downloading if version is outdated",
|
||||
|
||||
@@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { mergeTests, test } from "@playwright/test";
|
||||
import { mergeTests } from "@playwright/test";
|
||||
import { test } from "@playwright/test";
|
||||
import type { CommonFixtures, CommonWorkerFixtures } from "./common-fixtures";
|
||||
import { commonFixtures } from "./common-fixtures";
|
||||
import { platformTest } from "./platform-fixtures";
|
||||
|
||||
@@ -21,7 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { AppContext, buildAndLaunchApp, TestOptions } from "./utils";
|
||||
import path from "path";
|
||||
import { version } from "../../package.json";
|
||||
import type { ElectronApplication, Page } from "@playwright/test";
|
||||
import type { Browser, ElectronApplication, Page } from "@playwright/test";
|
||||
import type { TraceViewerFixtures } from "./trace-viewer-fixtures";
|
||||
import { traceViewerFixtures } from "./trace-viewer-fixtures";
|
||||
import { PageTestFixtures, PageWorkerFixtures } from "./page-test-api";
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
/* eslint-disable header/header */
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/// @ts-ignore
|
||||
import { oop, client } from "playwright-core/lib/coreBundle";
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
export type TestModeName = "default" | "driver";
|
||||
This program 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.
|
||||
|
||||
const { start } = oop;
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
// @ts-ignore
|
||||
import { start } from "playwright-core/lib/outofprocess";
|
||||
// @ts-ignore
|
||||
import type { Playwright } from "playwright-core/lib/client/playwright";
|
||||
|
||||
export type TestModeName =
|
||||
| "default"
|
||||
| "driver"
|
||||
| "service"
|
||||
| "service2"
|
||||
| "wsl";
|
||||
|
||||
interface TestMode {
|
||||
setup(): Promise<client.Playwright>;
|
||||
setup(): Promise<Playwright>;
|
||||
teardown(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DriverTestMode implements TestMode {
|
||||
private _impl: { playwright: client.Playwright; stop: () => Promise<void> };
|
||||
private _impl: { playwright: Playwright; stop: () => Promise<void> };
|
||||
|
||||
async setup() {
|
||||
this._impl = await start({
|
||||
|
||||
@@ -20,11 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { execSync } from "child_process";
|
||||
import { cp } from "fs/promises";
|
||||
import path, { join, resolve } from "path";
|
||||
import {
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page
|
||||
} from "@playwright/test";
|
||||
import { _electron as electron, ElectronApplication, Page } from "playwright";
|
||||
import { existsSync } from "fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
|
||||
@@ -34,7 +30,7 @@ const root = path.resolve(__dirname, "..", "..");
|
||||
const SOURCE_DIR = resolve(root, "output", productName);
|
||||
|
||||
export interface AppContext {
|
||||
app: ElectronApplication;
|
||||
app: import("playwright").ElectronApplication;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
@@ -42,7 +38,6 @@ export interface AppContext {
|
||||
|
||||
export interface TestOptions {
|
||||
version: string;
|
||||
args?: string[];
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -73,8 +68,7 @@ export async function buildAndLaunchApp(
|
||||
const { app } = await launchApp(
|
||||
executablePath,
|
||||
userDataDir,
|
||||
options?.version,
|
||||
options?.args
|
||||
options?.version
|
||||
);
|
||||
const ctx: AppContext = {
|
||||
app,
|
||||
@@ -84,8 +78,7 @@ export async function buildAndLaunchApp(
|
||||
const { app } = await launchApp(
|
||||
executablePath,
|
||||
userDataDir,
|
||||
options?.version,
|
||||
options?.args
|
||||
options?.version
|
||||
);
|
||||
ctx.app = app;
|
||||
ctx.userDataDir = userDataDir;
|
||||
@@ -97,12 +90,11 @@ export async function buildAndLaunchApp(
|
||||
async function launchApp(
|
||||
executablePath: string,
|
||||
userDataDir: string,
|
||||
version?: string,
|
||||
args: string[] = []
|
||||
version?: string
|
||||
) {
|
||||
const app = await electron.launch({
|
||||
executablePath,
|
||||
args: IS_DEBUG ? [...args] : ["--hidden", ...args],
|
||||
args: IS_DEBUG ? [] : ["--hidden"],
|
||||
baseURL: "https://app.notesnook.com",
|
||||
acceptDownloads: true,
|
||||
env: {
|
||||
@@ -158,7 +150,6 @@ export async function buildApp(version?: string) {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false",
|
||||
NOTESNOOK_STAGING: "true",
|
||||
NN_PRODUCT_NAME: productName,
|
||||
NN_APP_ID: `com.notesnook.test.${productName}`,
|
||||
|
||||
@@ -17,65 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test, expect } from "@nn/test";
|
||||
import type { ElectronApplication } from "@playwright/test";
|
||||
|
||||
async function getMainWindowState(app: ElectronApplication) {
|
||||
return await app.evaluate((window) => {
|
||||
const { BrowserWindow } = window;
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
if (!mainWindow) throw new Error("Main window not found");
|
||||
return {
|
||||
isMinimized: mainWindow.isMinimized(),
|
||||
isVisible: mainWindow.isVisible()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
import { test } from "@nn/test";
|
||||
test("make sure app loads", async ({ page }) => {
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
});
|
||||
|
||||
test("hidden launch minimizes when tray is disabled", async ({
|
||||
launchElectronApp,
|
||||
options
|
||||
}) => {
|
||||
const app = await launchElectronApp({
|
||||
version: options.version,
|
||||
args: ["--hidden"],
|
||||
config: {
|
||||
desktopSettings: {
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
const state = await getMainWindowState(app);
|
||||
expect(state.isMinimized).toBe(true);
|
||||
});
|
||||
|
||||
test("hidden launch does not minimize when close-to-tray is enabled", async ({
|
||||
launchElectronApp,
|
||||
options
|
||||
}) => {
|
||||
const app = await launchElectronApp({
|
||||
version: options.version,
|
||||
args: ["--hidden"],
|
||||
config: {
|
||||
desktopSettings: {
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
const state = await getMainWindowState(app);
|
||||
expect(state.isMinimized).toBe(false);
|
||||
});
|
||||
|
||||
@@ -203,12 +203,11 @@ module.exports = {
|
||||
toolsets: {
|
||||
appimage: "1.0.2"
|
||||
},
|
||||
snapcraft: {
|
||||
base: "core24",
|
||||
core24: {
|
||||
confinement: "strict",
|
||||
autoStart: false
|
||||
}
|
||||
snap: {
|
||||
autoStart: false,
|
||||
confinement: "strict",
|
||||
allowNativeWayland: true,
|
||||
base: "core22"
|
||||
},
|
||||
extraResources: ["app-update.yml", "./assets/**"],
|
||||
extraMetadata: {
|
||||
|
||||
10769
apps/desktop/package-lock.json
generated
10769
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.4.6",
|
||||
"version": "3.4.0-beta.1",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -54,18 +54,18 @@
|
||||
"zod": "3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.61.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.10.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "0.21.5",
|
||||
"fkill": "^10.0.3",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
"playwright-core": "1.61.1",
|
||||
"playwright": "1.58.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -86,7 +86,7 @@
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package",
|
||||
"test": "playwright test --tsconfig __tests__/tsconfig.json --project notesnook-desktop"
|
||||
"test": "playwright test --tsconfig tsconfig.json"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
index 7d8468f..ab252ef 100644
|
||||
--- a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
+++ b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
@@ -16010,6 +16010,7 @@ var init_validator = __esm({
|
||||
args: tOptional(tArray(tString)),
|
||||
chromiumSandbox: tOptional(tBoolean),
|
||||
cwd: tOptional(tString),
|
||||
+ baseURL: tOptional(tString),
|
||||
env: tOptional(tArray(tType("NameValue"))),
|
||||
timeout: tFloat,
|
||||
acceptDownloads: tOptional(tEnum(["accept", "deny", "internal-browser-default"])),
|
||||
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
index 0bff9d3..d195697 100644
|
||||
--- a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
+++ b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
@@ -21813,6 +21813,7 @@ export interface Electron {
|
||||
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
|
||||
*/
|
||||
args?: Array<string>;
|
||||
+ baseURL?: string;
|
||||
|
||||
/**
|
||||
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory
|
||||
24
apps/desktop/patches/playwright-core+1.58.2.patch
Normal file
24
apps/desktop/patches/playwright-core+1.58.2.patch
Normal file
@@ -0,0 +1,24 @@
|
||||
diff --git a/node_modules/playwright-core/lib/protocol/validator.js b/node_modules/playwright-core/lib/protocol/validator.js
|
||||
index b36c979..9e8ca15 100644
|
||||
--- a/node_modules/playwright-core/lib/protocol/validator.js
|
||||
+++ b/node_modules/playwright-core/lib/protocol/validator.js
|
||||
@@ -2536,6 +2536,7 @@ import_validatorPrimitives.scheme.ElectronLaunchParams = (0, import_validatorPri
|
||||
executablePath: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
args: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)(import_validatorPrimitives.tString)),
|
||||
cwd: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
+ baseURL: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
env: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)((0, import_validatorPrimitives.tType)("NameValue"))),
|
||||
timeout: import_validatorPrimitives.tFloat,
|
||||
acceptDownloads: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tEnum)(["accept", "deny", "internal-browser-default"])),
|
||||
diff --git a/node_modules/playwright-core/types/types.d.ts b/node_modules/playwright-core/types/types.d.ts
|
||||
index f6e6165..4efd4d4 100644
|
||||
--- a/node_modules/playwright-core/types/types.d.ts
|
||||
+++ b/node_modules/playwright-core/types/types.d.ts
|
||||
@@ -19233,6 +19233,7 @@ export interface Electron {
|
||||
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
|
||||
*/
|
||||
args?: Array<string>;
|
||||
+ baseURL?: string;
|
||||
|
||||
/**
|
||||
* Toggles bypassing page's Content-Security-Policy. Defaults to `false`.
|
||||
@@ -46,7 +46,7 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions> = {
|
||||
globalTimeout: 5400000,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
reporter: process.env.CI
|
||||
? [["dot"], ["json", { outputFile: path.join(outputDir, "report.json") }]]
|
||||
: "line",
|
||||
|
||||
@@ -33,38 +33,17 @@ function generateId() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
function getStreamOrThrow(id: string, operation: "write" | "close") {
|
||||
const stream = activeStreams.get(id);
|
||||
if (!stream) {
|
||||
throw new Error(
|
||||
`Backup stream not found during ${operation}. The stream may have already been closed or was never opened. Stream id: ${id}`
|
||||
);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
export const backupsRouter = t.router({
|
||||
open: t.procedure
|
||||
.input(z.object({ filename: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const { filename } = input;
|
||||
if (!filename.trim()) {
|
||||
throw new Error("Invalid backup filename: filename cannot be empty.");
|
||||
}
|
||||
if (filename.includes(path.sep) || filename.includes("\\"))
|
||||
throw new Error(
|
||||
`Invalid backup filename: expected a plain file name without path separators, received "${filename}".`
|
||||
);
|
||||
throw new Error("Invalid filename");
|
||||
const resolvedBackupDir = resolvePath(config.backupDirectory);
|
||||
const backupPath = path.resolve(resolvedBackupDir, filename);
|
||||
const relativeBackupPath = path.relative(resolvedBackupDir, backupPath);
|
||||
if (
|
||||
relativeBackupPath.startsWith("..") ||
|
||||
path.isAbsolute(relativeBackupPath)
|
||||
)
|
||||
throw new Error(
|
||||
`Invalid backup filename: resolved path "${backupPath}" is outside the configured backup directory "${resolvedBackupDir}". The configured backup directory may be invalid.`
|
||||
);
|
||||
if (!backupPath.startsWith(resolvedBackupDir))
|
||||
throw new Error("Invalid filename");
|
||||
|
||||
mkdirSync(resolvedBackupDir, { recursive: true });
|
||||
const stream = createWriteStream(backupPath, { encoding: "utf-8" });
|
||||
@@ -76,7 +55,8 @@ export const backupsRouter = t.router({
|
||||
write: t.procedure
|
||||
.input(z.object({ id: z.string(), chunk: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = getStreamOrThrow(input.id, "write");
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
stream.write(Buffer.from(input.chunk, "base64"), (err) =>
|
||||
err ? reject(err) : resolve()
|
||||
@@ -87,7 +67,8 @@ export const backupsRouter = t.router({
|
||||
close: t.procedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = getStreamOrThrow(input.id, "close");
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
return new Promise<void>((resolve) => {
|
||||
stream.end(() => {
|
||||
activeStreams.delete(input.id);
|
||||
|
||||
@@ -33,7 +33,7 @@ import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
import { getTheme, setTheme, Theme } from "../utils/theme";
|
||||
import { existsSync } from "fs";
|
||||
import { normalizePathString, resolvePath } from "../utils/resolve-path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak, isPortable, isSnap } from "../utils";
|
||||
@@ -60,13 +60,7 @@ export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
isSnap: t.procedure.query(() => isSnap()),
|
||||
isPortable: t.procedure.query(() => isPortable()),
|
||||
backupDirectory: t.procedure.query(() => {
|
||||
const backupDirectory = normalizePathString(config.backupDirectory);
|
||||
if (backupDirectory !== config.backupDirectory) {
|
||||
config.backupDirectory = backupDirectory;
|
||||
}
|
||||
return backupDirectory;
|
||||
}),
|
||||
backupDirectory: t.procedure.query(() => config.backupDirectory),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
|
||||
@@ -40,7 +40,6 @@ import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||
import { Messages, setI18nGlobal } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { PATHS } from "./constants";
|
||||
import { normalizePathString } from "./utils/resolve-path";
|
||||
|
||||
const locale =
|
||||
process.env.NODE_ENV === "development"
|
||||
@@ -92,17 +91,6 @@ async function createWindow() {
|
||||
const cliOptions = await parseArguments(process.argv);
|
||||
setTheme(getTheme());
|
||||
|
||||
// this workaround is necessary because macos doesn't support
|
||||
// the --hidden flag when launching the app on startup
|
||||
if (
|
||||
process.platform === "darwin" &&
|
||||
app.getLoginItemSettings().wasOpenedAtLogin &&
|
||||
config.desktopSettings.autoStart &&
|
||||
config.desktopSettings.startMinimized
|
||||
) {
|
||||
cliOptions.hidden = true;
|
||||
}
|
||||
|
||||
const mainWindowState = new WindowState({});
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: !cliOptions.hidden,
|
||||
@@ -152,13 +140,7 @@ async function createWindow() {
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
if (
|
||||
cliOptions.hidden &&
|
||||
!(
|
||||
config.desktopSettings.minimizeToSystemTray ||
|
||||
config.desktopSettings.closeToSystemTray
|
||||
)
|
||||
)
|
||||
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
|
||||
mainWindow.minimize();
|
||||
|
||||
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
|
||||
@@ -181,14 +163,6 @@ async function createWindow() {
|
||||
);
|
||||
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
|
||||
|
||||
mainWindow.on("show", () =>
|
||||
/**
|
||||
* We may set `skipTaskbar` to true at startup.
|
||||
* This also removes the window from the Alt-Tab switcher.
|
||||
* To fix that, whenever the app is shown, we set `skipTaskbar` to false.
|
||||
*/
|
||||
mainWindow.setSkipTaskbar(false)
|
||||
);
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
});
|
||||
@@ -320,7 +294,7 @@ async function migrateBackupDirectory() {
|
||||
`localStorage.getItem("backupStorageLocation")`
|
||||
);
|
||||
if (!oldPath || oldPath === PATHS.backupsDirectory) return;
|
||||
config.backupDirectory = normalizePathString(oldPath);
|
||||
config.backupDirectory = oldPath;
|
||||
} catch (e) {
|
||||
console.error("Failed to migrate backup directory", e);
|
||||
const pressedButton = dialog.showMessageBoxSync(globalThis.window, {
|
||||
|
||||
@@ -20,30 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { app } from "electron";
|
||||
import { isAbsolute, join } from "path";
|
||||
|
||||
export function normalizePathString(_path: string) {
|
||||
let normalizedPath = _path.trim();
|
||||
|
||||
try {
|
||||
const parsedPath = JSON.parse(normalizedPath);
|
||||
if (typeof parsedPath === "string") normalizedPath = parsedPath;
|
||||
} catch {
|
||||
// ignore invalid JSON and keep the original path string
|
||||
}
|
||||
|
||||
if (normalizedPath === "~") return app.getPath("home");
|
||||
if (/^~[\\/]/.test(normalizedPath)) {
|
||||
return join(app.getPath("home"), normalizedPath.slice(2));
|
||||
}
|
||||
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
export function resolvePath(_path: string) {
|
||||
const normalizedPath = normalizePathString(_path);
|
||||
if (isAbsolute(normalizedPath)) return normalizedPath;
|
||||
if (isAbsolute(_path)) return _path;
|
||||
|
||||
return join(
|
||||
...normalizedPath.split("/").map((segment) => {
|
||||
..._path.split("/").map((segment) => {
|
||||
let resolved = segment;
|
||||
try {
|
||||
resolved = app.getPath(resolved as any);
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
9
apps/mobile/.nanoicons.json
Normal file
9
apps/mobile/.nanoicons.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"iconSets": [
|
||||
{
|
||||
"inputDir": "../../packages/icons/svgs",
|
||||
"fontFamily": "notesnook-icons",
|
||||
"outputDir": "./fonts"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -140,7 +140,7 @@ android {
|
||||
if (project.hasProperty("prBuildNumber")) {
|
||||
versionCode Integer.parseInt(prBuildNumber())
|
||||
} else {
|
||||
versionCode 3116
|
||||
versionCode 3108
|
||||
}
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
@@ -233,9 +233,6 @@ dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
// Lets the widgets push their list data straight into the RemoteViews on every API level,
|
||||
// instead of the deprecated RemoteViewsService adapter (which needs API 31 to do natively).
|
||||
implementation("androidx.core:core-remoteviews:1.0.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
|
||||
@@ -86,9 +86,8 @@
|
||||
android:name=".NotePreviewWidget"
|
||||
android:exported="false"
|
||||
android:label="@string/note">
|
||||
<intent-filter>
|
||||
<intent-filter>"
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_RESTORED" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
@@ -96,15 +95,6 @@
|
||||
android:resource="@xml/note_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".WidgetTimeChangeReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.TIME_SET" />
|
||||
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".ReminderWidgetProvider"
|
||||
android:exported="false"
|
||||
@@ -240,6 +230,11 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".ReminderViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
||||
@@ -5,11 +5,15 @@ import android.appwidget.AppWidgetManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
import com.google.gson.Gson;
|
||||
import com.streetwriters.notesnook.datatypes.Note;
|
||||
|
||||
public class NotePreviewConfigureActivity extends ReactActivity {
|
||||
|
||||
@@ -36,32 +40,18 @@ public class NotePreviewConfigureActivity extends ReactActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
activity = this;
|
||||
readAppWidgetId(getIntent());
|
||||
}
|
||||
|
||||
/**
|
||||
* We launch as singleTask, so configuring a second widget while this screen is still alive
|
||||
* arrives here rather than in onCreate(). Without this the activity would keep writing to
|
||||
* whichever widget it happened to be opened for first.
|
||||
*/
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
activity = this;
|
||||
readAppWidgetId(intent);
|
||||
}
|
||||
|
||||
private void readAppWidgetId(Intent intent) {
|
||||
Bundle extras = intent != null ? intent.getExtras() : null;
|
||||
int appWidgetId = extras != null
|
||||
? extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
: AppWidgetManager.INVALID_APPWIDGET_ID;
|
||||
|
||||
NotePreviewConfigureActivity.appWidgetId = appWidgetId;
|
||||
Intent intent = getIntent();
|
||||
Bundle extras = intent.getExtras();
|
||||
int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
NotePreviewConfigureActivity.appWidgetId = appWidgetId;
|
||||
}
|
||||
Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
setResult(Activity.RESULT_CANCELED, resultValue);
|
||||
activity = this;
|
||||
}
|
||||
|
||||
public static void saveAndFinish(Context context) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
@@ -7,125 +8,51 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
import com.google.gson.Gson;
|
||||
import com.streetwriters.notesnook.datatypes.Note;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
public class NotePreviewWidget extends AppWidgetProvider {
|
||||
static String OpenNoteId = "com.streetwriters.notesnook.OpenNoteId";
|
||||
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
|
||||
int appWidgetId) {
|
||||
String data = context.getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), "");
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget);
|
||||
|
||||
Note note = WidgetUtils.parseNote(data);
|
||||
if (note == null) {
|
||||
// Either the widget was never configured, or we lost the note it pointed at (ids
|
||||
// reassigned, data cleared). Point it back at the picker rather than leaving the user
|
||||
// with an inert widget they can only fix by deleting and re-adding it.
|
||||
views.setTextViewText(R.id.widget_title, context.getString(R.string.widget_note_unconfigured_title));
|
||||
views.setTextViewText(R.id.widget_body, context.getString(R.string.widget_note_unconfigured_body));
|
||||
views.setOnClickPendingIntent(R.id.open_note, getConfigurePendingIntent(context, appWidgetId));
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), "");
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Gson gson = new Gson();
|
||||
Note note = gson.fromJson(data, Note.class);
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget);
|
||||
views.setTextViewText(R.id.widget_title, note.getTitle());
|
||||
views.setTextViewText(R.id.widget_body, note.getHeadline());
|
||||
// Once the user shrinks the widget down to a single row there is no room for the preview
|
||||
// text, and a clipped half-line of it looks like a rendering glitch.
|
||||
views.setViewVisibility(R.id.widget_body,
|
||||
hasRoomForBody(appWidgetManager, appWidgetId) ? View.VISIBLE : View.GONE);
|
||||
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
intent.putExtra(OpenNoteId, note.getId());
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
|
||||
intent.setData(Uri.parse("nn://note/" + note.getId()));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopens the configure screen for this widget. The launcher's own "reconfigure" gesture is
|
||||
* hard to discover and not offered by every launcher, so an unconfigured widget needs its own
|
||||
* way back in.
|
||||
*/
|
||||
private static PendingIntent getConfigurePendingIntent(Context context, int appWidgetId) {
|
||||
Intent intent = new Intent(context, NotePreviewConfigureActivity.class);
|
||||
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
// PendingIntent equality ignores extras, so the widget id has to be the request code for
|
||||
// each widget to get its own.
|
||||
return PendingIntent.getActivity(context, appWidgetId, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE,
|
||||
WidgetUtils.getActivityOptionsBundle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Height below which the note preview text is dropped, leaving just the title.
|
||||
*/
|
||||
private static final int MIN_HEIGHT_FOR_BODY_DP = 70;
|
||||
|
||||
private static boolean hasRoomForBody(AppWidgetManager appWidgetManager, int appWidgetId) {
|
||||
Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId);
|
||||
if (options == null) return true;
|
||||
|
||||
int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
|
||||
// Not reported yet (the widget has just been placed): assume there is room.
|
||||
return minHeight <= 0 || minHeight >= MIN_HEIGHT_FOR_BODY_DP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
|
||||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
||||
// This used to do nothing at all, so resizing the widget left it rendered for its old size.
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The note shown by each widget is stored in the "appPreview" preferences under its widget id.
|
||||
* When the system restores our widgets it hands out fresh ids, so unless we move the stored
|
||||
* notes over to the new ids the widgets are left permanently blank with no way to recover
|
||||
* other than removing and re-adding them.
|
||||
*
|
||||
* AppWidgetProvider calls onUpdate() with the new ids right after this, which re-renders them.
|
||||
*/
|
||||
@Override
|
||||
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
|
||||
super.onRestored(context, oldWidgetIds, newWidgetIds);
|
||||
if (oldWidgetIds == null || newWidgetIds == null) return;
|
||||
|
||||
int count = Math.min(oldWidgetIds.length, newWidgetIds.length);
|
||||
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
|
||||
// Read everything up front: an old id can collide with the new id of another widget.
|
||||
String[] notes = new String[count];
|
||||
Set<String> newKeys = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
notes[i] = preferences.getString(String.valueOf(oldWidgetIds[i]), "");
|
||||
newKeys.add(String.valueOf(newWidgetIds[i]));
|
||||
}
|
||||
|
||||
SharedPreferences.Editor edit = preferences.edit();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String oldKey = String.valueOf(oldWidgetIds[i]);
|
||||
if (!newKeys.contains(oldKey)) {
|
||||
edit.remove(oldKey);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (notes[i].isEmpty()) continue;
|
||||
edit.putString(String.valueOf(newWidgetIds[i]), notes[i]);
|
||||
}
|
||||
edit.apply();
|
||||
private static Bundle getActivityOptionsBundle() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ActivityOptions activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||||
return activityOptions.toBundle();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
@@ -22,10 +24,20 @@ public class NoteWidget extends AppWidgetProvider {
|
||||
|
||||
static void setClickIntent(Context context, RemoteViews views) {
|
||||
Intent intent = new Intent(context, ShareActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.new_note, pendingIntent);
|
||||
}
|
||||
|
||||
private static Bundle getActivityOptionsBundle() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ActivityOptions activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||||
return activityOptions.toBundle();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
|
||||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -28,6 +29,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.streetwriters.notesnook.datatypes.Note;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -136,7 +138,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
if (Objects.equals(extras.getString(IntentType), "NewReminder")) {
|
||||
map.putString(ReminderWidgetProvider.NewReminder, extras.getString(ReminderWidgetProvider.NewReminder));
|
||||
} else if (Objects.equals(extras.getString(IntentType), "OpenReminder")) {
|
||||
map.putString(ReminderWidgetProvider.OpenReminderId, extras.getString(ReminderWidgetProvider.OpenReminderId));
|
||||
map.putString(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId));
|
||||
} else if (Objects.equals(extras.getString(IntentType), "OpenNote")) {
|
||||
map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId));
|
||||
}
|
||||
@@ -154,8 +156,14 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@ReactMethod
|
||||
public void getWidgetNotes(Promise promise) {
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Map<String, ?> map = pref.getAll();
|
||||
WritableArray arr = Arguments.createArray();
|
||||
for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) {
|
||||
for(Map.Entry<String,?> entry : map.entrySet()){
|
||||
if (entry.getKey().equals("remindersList")) continue;
|
||||
String value = (String) entry.getValue();
|
||||
Gson gson = new Gson();
|
||||
Note note = gson.fromJson(value, Note.class);
|
||||
arr.pushString(note.getId());
|
||||
}
|
||||
promise.resolve(arr);
|
||||
@@ -163,46 +171,36 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@ReactMethod
|
||||
public void hasWidgetNote(final String noteId, Promise promise) {
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Map<String, ?> map = pref.getAll();
|
||||
boolean found = false;
|
||||
for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) {
|
||||
if (note.getId().equals(noteId)) {
|
||||
for(Map.Entry<String,?> entry : map.entrySet()){
|
||||
String value = (String) entry.getValue();
|
||||
if (value.contains(noteId)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
promise.resolve(found);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateWidgetNote(final String noteId, final String data) {
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE);
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Map<String, ?> map = pref.getAll();
|
||||
SharedPreferences.Editor edit = pref.edit();
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
|
||||
// Match on the note's id, not on the raw JSON containing it somewhere: a note whose body
|
||||
// happens to mention another note's id is not the same note.
|
||||
for (Map.Entry<Integer, Note> entry : WidgetUtils.getWidgetNotes(getReactApplicationContext()).entrySet()) {
|
||||
if (!noteId.equals(entry.getValue().getId())) continue;
|
||||
edit.putString(String.valueOf(entry.getKey()), data);
|
||||
ids.add(entry.getKey());
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
for(Map.Entry<String,?> entry : map.entrySet()) {
|
||||
String value = (String) entry.getValue();
|
||||
if (value.contains(noteId)) {
|
||||
edit.putString(entry.getKey(), data);
|
||||
ids.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
edit.apply();
|
||||
|
||||
for (int id : ids) {
|
||||
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), id);
|
||||
for (String id: ids) {
|
||||
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws every widget from scratch. Needed because the app can be stopped while its widgets
|
||||
* stay on the home screen: clearing app data empties the store without the widgets ever being
|
||||
* told, so they keep showing content that is gone until something forces a redraw.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void refreshWidgets() {
|
||||
WidgetUtils.refreshAll(mContext);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateReminderWidget() {
|
||||
AppWidgetManager wm = AppWidgetManager.getInstance(mContext);
|
||||
@@ -210,8 +208,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
for (int id: ids) {
|
||||
Log.d("Reminders", "Updating" + id);
|
||||
RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_reminders);
|
||||
// The rows are part of this update, so there is nothing left to invalidate afterwards.
|
||||
ReminderWidgetProvider.updateAppWidget(mContext, wm, id, views);
|
||||
wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViewsService;
|
||||
import android.content.Context;
|
||||
import android.widget.RemoteViews;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.streetwriters.notesnook.datatypes.Reminder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ReminderViewsService extends RemoteViewsService {
|
||||
static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId";
|
||||
@Override
|
||||
public RemoteViewsFactory onGetViewFactory(Intent intent) {
|
||||
return new ReminderRemoteViewsFactory(this.getApplicationContext(), intent);
|
||||
}
|
||||
}
|
||||
|
||||
class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
|
||||
private Context context;
|
||||
private List<Reminder> reminders;
|
||||
|
||||
public ReminderRemoteViewsFactory(Context context, Intent intent) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Initialize reminders list
|
||||
reminders = new ArrayList<Reminder>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataSetChanged() {
|
||||
reminders.clear();
|
||||
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Gson gson = new Gson();
|
||||
reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken<List<Reminder>>(){}.getType());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
reminders.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return reminders.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteViews getViewAt(int position) {
|
||||
Reminder reminder = reminders.get(position);
|
||||
|
||||
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
|
||||
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
|
||||
|
||||
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
|
||||
if (!useMiniLayout) {
|
||||
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
|
||||
}
|
||||
views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime());
|
||||
final Intent fillInIntent = new Intent();
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(ReminderViewsService.OpenReminderId, reminder.getId());
|
||||
fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId()));
|
||||
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
|
||||
fillInIntent.putExtras(extras);
|
||||
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
|
||||
return views;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RemoteViews getLoadingView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,18 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import androidx.core.widget.RemoteViewsCompat;
|
||||
|
||||
import com.streetwriters.notesnook.datatypes.Reminder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ReminderWidgetProvider extends AppWidgetProvider {
|
||||
static String NewReminder = "com.streetwriters.notesnook.NewReminder";
|
||||
static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId";
|
||||
|
||||
@Override
|
||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
||||
@@ -27,10 +23,20 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
|
||||
}
|
||||
|
||||
|
||||
private static Bundle getActivityOptionsBundle() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ActivityOptions activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||||
return activityOptions.toBundle();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) {
|
||||
Intent listview_intent_template = new Intent(context, MainActivity.class);
|
||||
listview_intent_template.setAction(Intent.ACTION_VIEW);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, WidgetUtils.getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle());
|
||||
views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent);
|
||||
|
||||
Intent new_reminder_intent = new Intent(context, MainActivity.class);
|
||||
@@ -38,31 +44,13 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
|
||||
new_reminder_intent.setAction(Intent.ACTION_VIEW);
|
||||
new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder");
|
||||
new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder"));
|
||||
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.add_button, pendingIntent2);
|
||||
|
||||
// The rows travel with the update itself, so there is no bound service to keep in sync and
|
||||
// nothing to invalidate separately: every update redraws from the current data.
|
||||
List<Reminder> reminders = WidgetUtils.getWidgetReminders(context);
|
||||
RemoteViewsCompat.RemoteCollectionItems.Builder items =
|
||||
new RemoteViewsCompat.RemoteCollectionItems.Builder();
|
||||
for (Reminder reminder : reminders) {
|
||||
items.addItem(getItemId(reminder), WidgetUtils.createReminderItem(context, reminder));
|
||||
}
|
||||
// Two, because a reminder without a description uses the compact row layout.
|
||||
items.setViewTypeCount(2);
|
||||
items.setHasStableIds(true);
|
||||
|
||||
RemoteViewsCompat.setRemoteAdapter(context, views, appWidgetId, R.id.widget_list_view, items.build());
|
||||
Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class);
|
||||
list_remote_adapter_intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
views.setRemoteAdapter(R.id.widget_list_view, list_remote_adapter_intent);
|
||||
views.setEmptyView(R.id.widget_list_view, R.id.empty_view);
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ties a row to its reminder rather than to its position, so rows keep their identity when the
|
||||
* list shifts around them.
|
||||
*/
|
||||
private static long getItemId(Reminder reminder) {
|
||||
return reminder.getId() == null ? 0 : reminder.getId().hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Redraws the widgets when the clock or timezone changes.
|
||||
*
|
||||
* Reminder rows are described relative to the current time ("Upcoming"/"Last", "Today"/"Tomorrow"),
|
||||
* and a reminder that has passed drops off the list entirely. All of that is decided when the
|
||||
* widget is drawn, so moving the clock leaves the previous drawing in place: a reminder can still
|
||||
* read "Upcoming" long after its time has gone by.
|
||||
*
|
||||
* The app normally redraws the list when a reminder notification is delivered, but that does not
|
||||
* happen if the reminder never fires, which is exactly the case when the clock jumps past it.
|
||||
*
|
||||
* Only TIME_SET and TIMEZONE_CHANGED are handled here: DATE_CHANGED is not exempt from the
|
||||
* Android 8 limits on manifest-registered implicit broadcasts, so a receiver for it would never
|
||||
* run. Crossing midnight is instead picked up by the widget's own periodic update.
|
||||
*/
|
||||
public class WidgetTimeChangeReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
WidgetUtils.refreshAll(context);
|
||||
}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.streetwriters.notesnook.datatypes.Note;
|
||||
import com.streetwriters.notesnook.datatypes.Reminder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Shared helpers for the home screen widgets.
|
||||
*/
|
||||
public class WidgetUtils {
|
||||
|
||||
static final String PREFERENCES = "appPreview";
|
||||
static final String REMINDERS_KEY = "remindersList";
|
||||
|
||||
/**
|
||||
* Every row is serialized into the widget update itself, which has to fit inside a binder
|
||||
* transaction, so the list cannot grow without bound. Far more than fits on screen anyway.
|
||||
*/
|
||||
private static final int MAX_REMINDERS = 50;
|
||||
|
||||
/**
|
||||
* Redraws every widget that currently exists, and drops stored notes for widgets that no
|
||||
* longer do.
|
||||
*
|
||||
* Everything else keys off what we have stored, which is fine while the app is running but
|
||||
* leaves widgets showing content that no longer exists once the store is emptied underneath
|
||||
* them (clearing app data) or a widget is removed while the app is stopped (onDeleted never
|
||||
* arrives). Starting from the widgets the system knows about, rather than from our own data,
|
||||
* is what makes this self-correcting.
|
||||
*
|
||||
* NoteWidget is left alone deliberately: it is a static button with no stored state, and its
|
||||
* layout depends on the size it was last given.
|
||||
*/
|
||||
static void refreshAll(Context context) {
|
||||
AppWidgetManager manager = AppWidgetManager.getInstance(context);
|
||||
|
||||
int[] noteWidgetIds = manager.getAppWidgetIds(
|
||||
new ComponentName(context, NotePreviewWidget.class));
|
||||
removeOrphanedNotes(context, noteWidgetIds);
|
||||
for (int appWidgetId : noteWidgetIds) {
|
||||
NotePreviewWidget.updateAppWidget(context, manager, appWidgetId);
|
||||
}
|
||||
|
||||
for (int appWidgetId : manager.getAppWidgetIds(
|
||||
new ComponentName(context, ReminderWidgetProvider.class))) {
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders);
|
||||
ReminderWidgetProvider.updateAppWidget(context, manager, appWidgetId, views);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops stored notes whose widget is gone, so the preferences file cannot grow forever.
|
||||
*/
|
||||
private static void removeOrphanedNotes(Context context, int[] liveWidgetIds) {
|
||||
Set<String> live = new HashSet<>();
|
||||
for (int appWidgetId : liveWidgetIds) live.add(String.valueOf(appWidgetId));
|
||||
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = preferences.edit();
|
||||
boolean changed = false;
|
||||
|
||||
for (String key : preferences.getAll().keySet()) {
|
||||
// Leave anything that is not a widget id alone, the reminders list included.
|
||||
if (parseWidgetId(key) == null || live.contains(key)) continue;
|
||||
edit.remove(key);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) edit.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* The note each note widget is showing, keyed by widget id.
|
||||
*
|
||||
* The preferences file mixes two things: one note per widget id, and the reminders list under
|
||||
* its own key. Only numeric keys are widget notes, so anything else is skipped rather than
|
||||
* being treated as a note.
|
||||
*/
|
||||
static Map<Integer, Note> getWidgetNotes(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
|
||||
Map<Integer, Note> notes = new LinkedHashMap<>();
|
||||
|
||||
for (Map.Entry<String, ?> entry : preferences.getAll().entrySet()) {
|
||||
Integer widgetId = parseWidgetId(entry.getKey());
|
||||
if (widgetId == null) continue;
|
||||
if (!(entry.getValue() instanceof String)) continue;
|
||||
|
||||
Note note = parseNote((String) entry.getValue());
|
||||
if (note == null || note.getId() == null) continue;
|
||||
notes.put(widgetId, note);
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The widget id a preferences key refers to, or null if the key is not a widget id at all.
|
||||
*/
|
||||
private static Integer parseWidgetId(String key) {
|
||||
try {
|
||||
return Integer.valueOf(key);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Note parseNote(String data) {
|
||||
if (data == null || data.isEmpty()) return null;
|
||||
try {
|
||||
return new Gson().fromJson(data, Note.class);
|
||||
} catch (Exception e) {
|
||||
Log.e("NotePreviewWidget", "Could not read a stored note", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The reminders the app last wrote out, minus any that have now dropped out of view. Reading
|
||||
* and filtering happens here so the provider can push the rows straight into the widget.
|
||||
*/
|
||||
static List<Reminder> getWidgetReminders(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
|
||||
List<Reminder> stored = null;
|
||||
try {
|
||||
stored = new Gson().fromJson(preferences.getString(REMINDERS_KEY, "[]"),
|
||||
new TypeToken<List<Reminder>>() {}.getType());
|
||||
} catch (Exception e) {
|
||||
Log.e("Reminders", "Could not read the stored reminders list", e);
|
||||
}
|
||||
|
||||
List<Reminder> active = new ArrayList<>();
|
||||
if (stored == null) return active;
|
||||
|
||||
for (Reminder reminder : stored) {
|
||||
if (!isVisibleInWidget(reminder)) continue;
|
||||
if (active.size() >= MAX_REMINDERS) {
|
||||
Log.w("Reminders", "Widget list truncated to " + MAX_REMINDERS + " reminders");
|
||||
break;
|
||||
}
|
||||
active.add(reminder);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a single row of the reminders list.
|
||||
*/
|
||||
static RemoteViews createReminderItem(Context context, Reminder reminder) {
|
||||
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
|
||||
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(),
|
||||
useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
|
||||
|
||||
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
|
||||
if (!useMiniLayout) {
|
||||
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
|
||||
}
|
||||
views.setTextViewText(R.id.reminder_time, formatReminderTime(context, reminder));
|
||||
|
||||
Intent fillInIntent = new Intent();
|
||||
fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId()));
|
||||
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
|
||||
fillInIntent.putExtra(ReminderWidgetProvider.OpenReminderId, reminder.getId());
|
||||
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
|
||||
return views;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options attached to the PendingIntents our widgets hand to the launcher, opting the creator
|
||||
* (us) in to background activity starts so a tap on the widget can bring up an activity.
|
||||
*
|
||||
* MODE_BACKGROUND_ACTIVITY_START_ALLOWED is deprecated since API 36 and Android 17 extends the
|
||||
* background activity launch restrictions to IntentSender, so on API 36+ we use the narrower
|
||||
* MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE instead. That is enough for widgets: the
|
||||
* sender is the launcher, which is visible whenever the user taps the widget.
|
||||
*/
|
||||
static Bundle getActivityOptionsBundle() {
|
||||
ActivityOptions activityOptions;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
||||
activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE);
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return activityOptions.toBundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* How long a reminder keeps its place in the list after going off, so the user can see that it
|
||||
* happened rather than watching it vanish. Must match RECENTLY_PASSED_WINDOW in
|
||||
* services/notifications.ts, which decides what gets written out in the first place.
|
||||
*/
|
||||
private static final long RECENTLY_PASSED_WINDOW_MS = TimeUnit.HOURS.toMillis(3);
|
||||
|
||||
/**
|
||||
* Whether a reminder should still be drawn.
|
||||
*
|
||||
* We re-check here rather than trusting the stored list because that list is only rewritten
|
||||
* while the app runs. This is what actually retires a reminder once its grace period is up:
|
||||
* every redraw re-evaluates it against the current time.
|
||||
*/
|
||||
static boolean isVisibleInWidget(Reminder reminder) {
|
||||
if (reminder == null) return false;
|
||||
if (reminder.isDisabled()) return false;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (reminder.getSnoozeUntil() > now) return true;
|
||||
if (!"once".equals(reminder.getMode())) return true;
|
||||
|
||||
long triggerDate = reminder.getTriggerDate() > 0 ? reminder.getTriggerDate() : reminder.getDate();
|
||||
return triggerDate > now - RECENTLY_PASSED_WINDOW_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the label shown under a reminder. The app sends us the absolute trigger time plus the
|
||||
* parts that never change ("5:00 PM", "12-05-2026, 5:00 PM"); everything that depends on the
|
||||
* current time is decided here so it stays right as the widget redraws.
|
||||
*
|
||||
* Falls back to the pre-formatted string for lists written by an older version of the app.
|
||||
*/
|
||||
static String formatReminderTime(Context context, Reminder reminder) {
|
||||
long triggerDate = reminder.getTriggerDate();
|
||||
String timeOfDay = reminder.getFormattedTimeOfDay();
|
||||
if (triggerDate <= 0 || timeOfDay == null || timeOfDay.isEmpty()) {
|
||||
return reminder.getFormattedTime();
|
||||
}
|
||||
|
||||
if ("permanent".equals(reminder.getMode())) {
|
||||
return context.getString(R.string.reminder_ongoing);
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (reminder.getSnoozeUntil() > now) {
|
||||
return context.getString(R.string.reminder_snoozed_until, timeOfDay);
|
||||
}
|
||||
|
||||
String text;
|
||||
long dayOffset = daysFromToday(triggerDate, now);
|
||||
if (dayOffset == 0) {
|
||||
text = context.getString(R.string.reminder_today, timeOfDay);
|
||||
} else if (dayOffset == 1) {
|
||||
text = context.getString(R.string.reminder_tomorrow, timeOfDay);
|
||||
} else if (dayOffset == -1) {
|
||||
text = context.getString(R.string.reminder_yesterday, timeOfDay);
|
||||
} else {
|
||||
text = reminder.getFormattedDateTime();
|
||||
if (text == null || text.isEmpty()) return reminder.getFormattedTime();
|
||||
}
|
||||
|
||||
return context.getString(
|
||||
triggerDate <= now ? R.string.reminder_last : R.string.reminder_upcoming, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar days between two instants. Compares midnights rather than subtracting the raw
|
||||
* difference so that "tomorrow" is still tomorrow across a DST change or just before midnight.
|
||||
*/
|
||||
private static long daysFromToday(long time, long now) {
|
||||
long target = startOfDay(time);
|
||||
long today = startOfDay(now);
|
||||
return Math.round((target - today) / (double) DateUtils.DAY_IN_MILLIS);
|
||||
}
|
||||
|
||||
private static long startOfDay(long time) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(time);
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||
calendar.set(Calendar.MINUTE, 0);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
return calendar.getTimeInMillis();
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@ package com.streetwriters.notesnook.datatypes;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Keep
|
||||
public class Reminder extends BaseItem {
|
||||
private String title;
|
||||
private String description;
|
||||
private String formattedTime;
|
||||
private String formattedTimeOfDay; // e.g. "5:00 PM"
|
||||
private String formattedDateTime; // e.g. "12-05-2026, 5:00 PM"
|
||||
private long triggerDate; // absolute time this reminder next fires
|
||||
private String priority; // "silent", "vibrate", "urgent"
|
||||
private long date;
|
||||
private String mode; // "repeat", "once", "permanent"
|
||||
@@ -108,27 +107,32 @@ public class Reminder extends BaseItem {
|
||||
this.formattedTime = formattedTime;
|
||||
}
|
||||
|
||||
public String getFormattedTimeOfDay() {
|
||||
return formattedTimeOfDay;
|
||||
}
|
||||
public String formatTime(long timeInMillis) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long diff = timeInMillis - currentTime;
|
||||
|
||||
public void setFormattedTimeOfDay(String formattedTimeOfDay) {
|
||||
this.formattedTimeOfDay = formattedTimeOfDay;
|
||||
}
|
||||
|
||||
public String getFormattedDateTime() {
|
||||
return formattedDateTime;
|
||||
}
|
||||
|
||||
public void setFormattedDateTime(String formattedDateTime) {
|
||||
this.formattedDateTime = formattedDateTime;
|
||||
}
|
||||
|
||||
public long getTriggerDate() {
|
||||
return triggerDate;
|
||||
}
|
||||
|
||||
public void setTriggerDate(long triggerDate) {
|
||||
this.triggerDate = triggerDate;
|
||||
if (diff < TimeUnit.MINUTES.toMillis(1)) {
|
||||
return "in " + (diff / 1000) + " seconds";
|
||||
} else if (diff < TimeUnit.HOURS.toMillis(1)) {
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(diff);
|
||||
return "in " + minutes + " minute" + (minutes > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(1)) {
|
||||
long hours = TimeUnit.MILLISECONDS.toHours(diff);
|
||||
return "in " + hours + " hour" + (hours > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(2)) {
|
||||
return "tomorrow";
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(7)) {
|
||||
long days = TimeUnit.MILLISECONDS.toDays(diff);
|
||||
return "in " + days + " day" + (days > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(30)) {
|
||||
long weeks = TimeUnit.MILLISECONDS.toDays(diff) / 7;
|
||||
return "in " + weeks + " week" + (weeks > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(365)) {
|
||||
long months = TimeUnit.MILLISECONDS.toDays(diff) / 30;
|
||||
return "in " + months + " month" + (months > 1 ? "s" : "");
|
||||
} else {
|
||||
long years = TimeUnit.MILLISECONDS.toDays(diff) / 365;
|
||||
return "in " + years + " year" + (years > 1 ? "s" : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/background"/>
|
||||
<stroke android:width="0dp" android:color="#B1BCBE" />
|
||||
<corners android:radius="@dimen/widget_background_radius"/>
|
||||
<corners android:radius="10dp"/>
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -2,7 +2,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent">
|
||||
android:background="@android:color/transparent"
|
||||
android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent">
|
||||
android:background="@android:color/transparent"
|
||||
android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -24,7 +25,7 @@
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/widget_note_unconfigured_title" />
|
||||
android:text="Widget unconfigured" />
|
||||
<TextView
|
||||
android:id="@+id/widget_body"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -33,7 +34,7 @@
|
||||
android:layout_marginLeft="8dp"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="14sp"
|
||||
android:text="@string/widget_note_unconfigured_body" />
|
||||
android:text="Configure this widget to show a note here." />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Shown in the widget picker only. Mirrors note_widget.xml, but with sample content, since the
|
||||
real layout has nothing to show until the widget has been configured.
|
||||
-->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/layout_bg"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="@string/widget_preview_note_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="@string/widget_preview_note_body"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="No upcoming reminders"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:gravity="center" />
|
||||
@@ -12,23 +12,22 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingTop="12dp"
|
||||
>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textSize="15sp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Reminders"/>
|
||||
android:text="Upcoming Reminders"/>
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="25dp"
|
||||
android:layout_width="35dp"
|
||||
android:id="@+id/add_button"
|
||||
android:layout_height="25dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:background="@drawable/ic_newnote" />
|
||||
</RelativeLayout>
|
||||
@@ -58,8 +57,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:textAlignment="center"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/widget_reminders_empty"/>
|
||||
android:text="Tap on + to add reminder"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Shown in the widget picker only. Mirrors widget_reminders.xml, but with the list replaced by
|
||||
sample rows, since an adapter-backed list renders empty in the picker.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/layout_bg"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="Reminders"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:background="@drawable/ic_newnote"
|
||||
android:contentDescription="@string/add_widget" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@color/border" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_preview_reminder_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:text="@string/widget_preview_reminder_time"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_preview_reminder_title_alt"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_preview_reminder_time_alt"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="light_blue_50">#FFE1F5FE</color>
|
||||
<color name="light_blue_200">#FF81D4FA</color>
|
||||
<color name="light_blue_600">#FF039BE5</color>
|
||||
<color name="light_blue_900">#FF01579B</color>
|
||||
<color name="bootsplash_background">#1f1f1f</color>
|
||||
<color name="background">#1D1D1D</color>
|
||||
<color name="border">#2E2E2E</color>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Android 12 onwards the launcher tells us what radius widgets should use, so ours line up
|
||||
with every other widget on the home screen instead of being a fixed 10dp. -->
|
||||
<dimen name="widget_background_radius">@android:dimen/system_app_widget_background_radius</dimen>
|
||||
|
||||
</resources>
|
||||
6
apps/mobile/android/app/src/main/res/values/attrs.xml
Normal file
6
apps/mobile/android/app/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<declare-styleable name="AppWidgetAttrs">
|
||||
<attr name="appWidgetBackgroundColor" format="color" />
|
||||
<attr name="appWidgetTextColor" format="color" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="light_blue_50">#FFE1F5FE</color>
|
||||
<color name="light_blue_200">#FF81D4FA</color>
|
||||
<color name="light_blue_600">#FF039BE5</color>
|
||||
<color name="light_blue_900">#FF01579B</color>
|
||||
<color name="bootsplash_background">#FFFFFF</color>
|
||||
<color name="background">#DCEDEDED</color>
|
||||
<color name="border">#BFBFBF</color>
|
||||
|
||||
@@ -7,8 +7,4 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
|
||||
-->
|
||||
<dimen name="widget_margin">0dp</dimen>
|
||||
|
||||
<!-- Overridden in values-v31 with the platform's own widget radius, so our widgets match the
|
||||
rest of the home screen. This is the fallback for older versions. -->
|
||||
<dimen name="widget_background_radius">10dp</dimen>
|
||||
|
||||
</resources>
|
||||
@@ -4,28 +4,9 @@
|
||||
<string name="appwidget_text">EXAMPLE</string>
|
||||
<string name="add_widget">Add widget</string>
|
||||
<string name="take_a_quick_note">Take a quick note.</string>
|
||||
<string name="reminders">Quick overview of reminders</string>
|
||||
<string name="reminders">Quick overview of upcoming reminders</string>
|
||||
<string name="reminders_title">Reminders</string>
|
||||
<string name="note">Note</string>
|
||||
<string name="note_description">Add a note to home screen</string>
|
||||
<string name="quick_note">Quick note</string>
|
||||
<string name="widget_reminders_empty">Tap + to add a reminder</string>
|
||||
|
||||
<!-- Sample content, only ever shown in the widget picker's preview. -->
|
||||
<string name="widget_preview_note_title">Meeting notes</string>
|
||||
<string name="widget_preview_note_body">Discuss the roadmap and agree on timelines.</string>
|
||||
<string name="widget_preview_reminder_title">Take a walk</string>
|
||||
<string name="widget_preview_reminder_time">Upcoming: Today, 5:00 PM</string>
|
||||
<string name="widget_preview_reminder_title_alt">Call the dentist</string>
|
||||
<string name="widget_preview_reminder_time_alt">Upcoming: Tomorrow, 9:00 AM</string>
|
||||
|
||||
<string name="widget_note_unconfigured_title">Tap to choose a note</string>
|
||||
<string name="widget_note_unconfigured_body">Pick the note you want shown here.</string>
|
||||
<string name="reminder_ongoing">Ongoing</string>
|
||||
<string name="reminder_snoozed_until">Snoozed until %1$s</string>
|
||||
<string name="reminder_today">Today, %1$s</string>
|
||||
<string name="reminder_tomorrow">Tomorrow, %1$s</string>
|
||||
<string name="reminder_yesterday">Yesterday, %1$s</string>
|
||||
<string name="reminder_upcoming">Upcoming: %1$s</string>
|
||||
<string name="reminder_last">Last: %1$s</string>
|
||||
</resources>
|
||||
|
||||
7
apps/mobile/android/app/src/main/res/values/themes.xml
Normal file
7
apps/mobile/android/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
|
||||
<style name="ThemeOverlay.Notesnook.AppWidgetContainer" parent="">
|
||||
<item name="appWidgetBackgroundColor">@color/light_blue_600</item>
|
||||
<item name="appWidgetTextColor">@color/light_blue_50</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Keep this in sync with res/xml/note_widget_info.xml. It exists only to add not_keyguard: from
|
||||
Android 16 QPR1 widgets are lock screen eligible by default, and this one renders the note's
|
||||
title and preview text, which should not be readable on a locked device.
|
||||
-->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialKeyguardLayout="@layout/note_widget"
|
||||
android:initialLayout="@layout/note_widget"
|
||||
android:configure="com.streetwriters.notesnook.NotePreviewConfigureActivity"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:minResizeWidth="100dp"
|
||||
android:minResizeHeight="50dp"
|
||||
android:minWidth="400dp"
|
||||
android:description="@string/note_description"
|
||||
android:minHeight="50dp"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="1"
|
||||
android:previewImage="@drawable/note_widget_preview"
|
||||
android:previewLayout="@layout/note_widget_preview"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:widgetCategory="home_screen|not_keyguard"/>
|
||||
@@ -1,15 +0,0 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_reminders"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="180dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:description="@string/reminders"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="2"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:previewImage="@drawable/reminder_preview"
|
||||
android:previewLayout="@layout/widget_reminders_preview"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen|not_keyguard"
|
||||
/>
|
||||
@@ -10,7 +10,6 @@
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="1"
|
||||
android:previewImage="@drawable/widget_preview"
|
||||
android:previewLayout="@layout/new_note_widget"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:widgetCategory="home_screen"/>
|
||||
@@ -12,7 +12,6 @@
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="1"
|
||||
android:previewImage="@drawable/note_widget_preview"
|
||||
android:previewLayout="@layout/note_widget_preview"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:widgetCategory="home_screen"/>
|
||||
@@ -1,15 +1,14 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_reminders"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="180dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:minWidth="400dp"
|
||||
android:minHeight="100dp"
|
||||
android:minResizeWidth="400dp"
|
||||
android:minResizeHeight="50dp"
|
||||
android:description="@string/reminders"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="2"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:previewImage="@drawable/reminder_preview"
|
||||
android:previewLayout="@layout/widget_reminders_preview"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:updatePeriodMillis="1024"
|
||||
android:widgetCategory="home_screen"
|
||||
/>
|
||||
@@ -13,7 +13,6 @@ buildscript {
|
||||
androidXCore = "1.7.0"
|
||||
androidXBrowser = "1.0.0"
|
||||
ndkVersion = "27.1.12297006"
|
||||
playBillingSdkVersion = "8.0.0"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -42,12 +41,3 @@ allprojects {
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
// Pin the ndkVersion for all subprojects
|
||||
subprojects { subproject ->
|
||||
["com.android.application", "com.android.library"].each { pluginId ->
|
||||
subproject.plugins.withId(pluginId) {
|
||||
subproject.android.ndkVersion = rootProject.ext.ndkVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
- Minor bug fixes and improvements
|
||||
- Inbox API is now in beta. Learn more about it at https://help.notesnook.com/inbox-api/getting-started
|
||||
- Added close all tabs button in editor
|
||||
- Deeplinking support for nn:// urls
|
||||
- Set default image compression mode in Settings -> Behavior
|
||||
- Minor bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { PropsWithChildren, useEffect, useState } from "react";
|
||||
import React, { PropsWithChildren, useEffect } from "react";
|
||||
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
@@ -35,7 +35,7 @@ import { Toast } from "./components/toast";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { NotePreviewConfigure } from "./screens/note-preview-configure";
|
||||
import { RootNavigation } from "./navigation/navigation-stack";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import { themeTrpcClient } from "./screens/settings/components/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
@@ -44,37 +44,27 @@ import { useUserStore } from "./stores/use-user-store";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import AppLocked from "./components/app-lock";
|
||||
import { useSettingStore } from "./stores/use-setting-store";
|
||||
import {
|
||||
initShortcutListener,
|
||||
launchNewNoteTab,
|
||||
registerAppShortcuts
|
||||
} from "./hooks/use-shortcut-manager";
|
||||
import Shortcuts from "react-native-actions-shortcuts";
|
||||
import { THEME_DARK, THEME_LIGHT } from "./common/design/theme";
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
RNBootSplash.hide({
|
||||
fade: true
|
||||
});
|
||||
Linking.getInitialURL().then((url) => {
|
||||
useSettingStore.setState({
|
||||
initialUrl: url
|
||||
});
|
||||
});
|
||||
const App = (props: { configureMode: "note-preview" }) => {
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (introCompleted) {
|
||||
registerAppShortcuts();
|
||||
}
|
||||
}, [introCompleted]);
|
||||
|
||||
useEffect(() => {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
SettingsService.onFirstLaunch();
|
||||
changeSystemBarColors();
|
||||
SettingsService.setPrivacyScreen(
|
||||
@@ -131,30 +121,30 @@ export const withTheme = (
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (!currentTheme) return;
|
||||
themeTrpcClient.updateTheme
|
||||
.query({
|
||||
version: currentTheme.version,
|
||||
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
id: currentTheme.id
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.setState({
|
||||
darkTheme: theme
|
||||
})
|
||||
: useThemeStore.setState({
|
||||
lightTheme: theme
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}, 1000);
|
||||
// setTimeout(() => {
|
||||
// const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
// if (!currentTheme) return;
|
||||
// themeTrpcClient.updateTheme
|
||||
// .query({
|
||||
// version: currentTheme.version,
|
||||
// compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
// id: currentTheme.id
|
||||
// })
|
||||
// .then((theme) => {
|
||||
// if (theme) {
|
||||
// theme.colorScheme === "dark"
|
||||
// ? useThemeStore.setState({
|
||||
// darkTheme: theme
|
||||
// })
|
||||
// : useThemeStore.setState({
|
||||
// lightTheme: theme
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .catch(() => {
|
||||
// /* empty */
|
||||
// });
|
||||
// }, 1000);
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
@@ -170,11 +160,11 @@ export const withTheme = (
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
const nextTheme = colorScheme === "dark" ? THEME_DARK : THEME_LIGHT;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
.setTheme(colorScheme === "dark" ? THEME_DARK : THEME_LIGHT);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
@@ -187,41 +177,4 @@ export const withTheme = (
|
||||
};
|
||||
};
|
||||
|
||||
export const withStartupBoundry = (
|
||||
Element: (props: PropsWithChildren) => JSX.Element
|
||||
) => {
|
||||
return function AppWithStartupBoundary(props: PropsWithChildren) {
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
try {
|
||||
const [url, shortcut] = await Promise.all([
|
||||
Linking.getInitialURL(),
|
||||
Shortcuts.getInitialShortcut()
|
||||
]);
|
||||
console.log(url, shortcut);
|
||||
if (shortcut?.type === "notesnook.action.newnote") {
|
||||
launchNewNoteTab();
|
||||
}
|
||||
useSettingStore.setState({
|
||||
initialUrl: url,
|
||||
pendingShortcut: shortcut ?? null
|
||||
});
|
||||
|
||||
initShortcutListener();
|
||||
} finally {
|
||||
setReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
}, []);
|
||||
|
||||
if (!ready) return null;
|
||||
|
||||
return <Element {...props} />;
|
||||
};
|
||||
};
|
||||
|
||||
export default withStartupBoundry(withTheme(withErrorBoundry(App, "App")));
|
||||
export default withTheme(withErrorBoundry(App, "App"));
|
||||
|
||||
File diff suppressed because one or more lines are too long
56
apps/mobile/app/common/design/font.ts
Normal file
56
apps/mobile/app/common/design/font.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program 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.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const FontSizes = {
|
||||
XXS: 10,
|
||||
XS: 12,
|
||||
SM: 14,
|
||||
MD: 16,
|
||||
LG: 18,
|
||||
XL: 20,
|
||||
XXL: 32
|
||||
};
|
||||
|
||||
export const FontFamily = {
|
||||
REGULAR: "Inter-Regular", // 400
|
||||
MEDIUM: "Inter-Medium", // 500
|
||||
SEMI_BOLD: "Inter-SemiBold", // 600
|
||||
BOLD: "Inter-Bold" // 700
|
||||
};
|
||||
|
||||
const LineHeightMultipliers = {
|
||||
"100%": 1,
|
||||
"110%": 1.1,
|
||||
"120%": 1.2,
|
||||
"130%": 1.3,
|
||||
"140%": 1.4,
|
||||
"150%": 1.5
|
||||
};
|
||||
export type LineHeightVariants =
|
||||
| "100%"
|
||||
| "110%"
|
||||
| "120%"
|
||||
| "130%"
|
||||
| "140%"
|
||||
| "150%";
|
||||
export const getLineHeight = (
|
||||
fontSize: keyof typeof FontSizes,
|
||||
type: LineHeightVariants
|
||||
) => {
|
||||
return FontSizes[fontSize] * LineHeightMultipliers[type];
|
||||
};
|
||||
39
apps/mobile/app/common/design/spacing.ts
Normal file
39
apps/mobile/app/common/design/spacing.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program 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.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const Spacing = {
|
||||
LEVEL_0: 4,
|
||||
LEVEL_1: 8,
|
||||
LEVEL_2: 12,
|
||||
LEVEL_3: 16,
|
||||
LEVEL_4: 20,
|
||||
LEVEL_5: 24,
|
||||
LEVEL_6: 28,
|
||||
LEVEL_7: 32,
|
||||
LEVEL_8: 40
|
||||
};
|
||||
|
||||
export const Radius = {
|
||||
XXS: 4,
|
||||
XS: 8,
|
||||
S: 12,
|
||||
MD: 16,
|
||||
LG: 20,
|
||||
LG_2: 32,
|
||||
XXL: 40
|
||||
};
|
||||
283
apps/mobile/app/common/design/theme.ts
Normal file
283
apps/mobile/app/common/design/theme.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program 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.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ThemeDefinition } from "@notesnook/theme";
|
||||
|
||||
export const THEME_LIGHT: any = {
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json",
|
||||
name: "Notesnook Light",
|
||||
id: "default-light",
|
||||
version: 2.1,
|
||||
license: "GPL-3.0-or-later",
|
||||
homepage: "https://notesnook.com",
|
||||
description: "The default light theme for Notesnook app.",
|
||||
colorScheme: "light",
|
||||
codeBlockCSS: ``,
|
||||
compatibilityVersion: 1,
|
||||
authors: [
|
||||
{
|
||||
name: "Streetwriters",
|
||||
email: "support@streetwriters.co",
|
||||
url: "https://streetwriters.co"
|
||||
}
|
||||
],
|
||||
scopes: {
|
||||
base: {
|
||||
primary: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#666666",
|
||||
background: "#ffffff",
|
||||
border: "#E2E2E2",
|
||||
heading: "#181818",
|
||||
icon: "#181818",
|
||||
separator: "#F0F0F0",
|
||||
placeholder: "#858585",
|
||||
hover: "#eee",
|
||||
backdrop: "#0000001a",
|
||||
buttonForeground: "#181818"
|
||||
},
|
||||
secondary: {
|
||||
accent: "#0D9E46",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#858585",
|
||||
background: "#F7F7F7",
|
||||
border: "#EFEFEF",
|
||||
heading: "#4B4B4B",
|
||||
icon: "#666666",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#999999",
|
||||
hover: "#dbdbdb",
|
||||
backdrop: "#0000001a",
|
||||
buttonForeground: "#181818"
|
||||
},
|
||||
tertiary: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#777",
|
||||
background: "#ECECEC",
|
||||
border: "#E8E8E8",
|
||||
heading: "#666",
|
||||
icon: "#777",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#dbdbdb",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
disabled: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#A6A6A6",
|
||||
background: "#ffffff",
|
||||
border: "#E8E8E8",
|
||||
heading: "#212121",
|
||||
icon: "#DADADA",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#00000010",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
selected: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#181818",
|
||||
background: "#00883610",
|
||||
border: "#008836",
|
||||
heading: "#212121",
|
||||
icon: "#181818",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#eee",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
error: {
|
||||
accent: "#f54b42",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#FF242E",
|
||||
background: "#FFEDEE",
|
||||
border: "#F7999D",
|
||||
heading: "#f54b42",
|
||||
icon: "#FB2C36",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#00000010",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
success: {
|
||||
accent: "#4F8A10",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#4F8A10",
|
||||
background: "#85ce68",
|
||||
border: "#E8E8E8",
|
||||
heading: "#4F8A10",
|
||||
icon: "#4F8A10",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#00000010",
|
||||
backdrop: "#0000001a"
|
||||
}
|
||||
},
|
||||
statusBar: {
|
||||
primary: { paragraph: "#5E5E5E", background: "#f7f7f7" }
|
||||
},
|
||||
navigationMenu: {
|
||||
primary: {
|
||||
background: "#f7f7f7",
|
||||
paragraph: "#666",
|
||||
icon: "#666",
|
||||
hover: "#eee"
|
||||
},
|
||||
secondary: { background: "#EEEEEE" },
|
||||
selected: {
|
||||
background: "#00883610",
|
||||
hover: "#eee"
|
||||
}
|
||||
},
|
||||
contextMenu: { primary: { background: "#f7f7f7" } },
|
||||
editor: { selected: { background: "#e1e1e1" } },
|
||||
sheet: { selected: { paragraph: "#008836" } },
|
||||
list: {
|
||||
secondary: {
|
||||
heading: "#A6A6A6"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const THEME_DARK: any = {
|
||||
name: "Notesnook Dark",
|
||||
id: "default-dark",
|
||||
version: 2.1,
|
||||
license: "GPL-3.0-or-later",
|
||||
homepage: "https://notesnook.com",
|
||||
description: "The default dark theme for the Notesnook app",
|
||||
colorScheme: "dark",
|
||||
codeBlockCSS: ``,
|
||||
compatibilityVersion: 1,
|
||||
authors: [
|
||||
{
|
||||
name: "Streetwriters",
|
||||
email: "support@streetwriters.co",
|
||||
url: "https://streetwriters.co"
|
||||
}
|
||||
],
|
||||
scopes: {
|
||||
base: {
|
||||
primary: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#F2F2F2",
|
||||
background: "#181818",
|
||||
border: "#2A2A2A",
|
||||
heading: "#FFFFFF",
|
||||
icon: "#FFFFFF",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#2b2b2b",
|
||||
backdrop: "#35353580",
|
||||
buttonForeground: "#FFFFFF"
|
||||
},
|
||||
secondary: {
|
||||
accent: "#0D9E46",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#A1A1A1",
|
||||
background: "#252525",
|
||||
border: "#0A5729",
|
||||
heading: "#C0BFBF",
|
||||
icon: "#808080",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580",
|
||||
buttonForeground: "#BFBFBF"
|
||||
},
|
||||
tertiary: {
|
||||
accent: "#0D9E46",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#818589",
|
||||
background: "#2C2C2C",
|
||||
border: "#2b2b2b",
|
||||
heading: "#808080",
|
||||
icon: "#808080",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
disabled: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#505050",
|
||||
background: "#ffffff",
|
||||
border: "#383838",
|
||||
heading: "#212121",
|
||||
icon: "#FFFFFF",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
selected: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#FBFBFB",
|
||||
background: "#494949",
|
||||
border: "#008836",
|
||||
heading: "#e3e3e3",
|
||||
icon: "#008836",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
error: {
|
||||
accent: "#f54b42",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#f54b42",
|
||||
background: "#250b0a",
|
||||
border: "#F7999D",
|
||||
heading: "#f54b42",
|
||||
icon: "#f54b42",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
success: {
|
||||
accent: "#4F8A10",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#4F8A10",
|
||||
background: "#132204",
|
||||
border: "#383838",
|
||||
heading: "#4F8A10",
|
||||
icon: "#4F8A10",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
}
|
||||
},
|
||||
list: { selected: { background: "#202020" } },
|
||||
contextMenu: { primary: { background: "#202020" } },
|
||||
sheet: { selected: { paragraph: "#008836" } }
|
||||
},
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json"
|
||||
};
|
||||
@@ -71,7 +71,7 @@ export const Announcement = () => {
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
borderBottomColor: colors.primary.separator,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -55,6 +55,7 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -96,9 +97,6 @@ const AppLocked = () => {
|
||||
const lastAppState = useRef<AppStateStatus>(appState);
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
const { height } = useWindowDimensions();
|
||||
const keyboardType = useSettingStore(
|
||||
(state) => state.settings.applockKeyboardType
|
||||
);
|
||||
const appLockHasPasswordSecurity = useSettingStore(
|
||||
(state) => state.settings.appLockHasPasswordSecurity
|
||||
);
|
||||
@@ -157,8 +155,7 @@ const AppLocked = () => {
|
||||
if (!appLockHasPasswordSecurity) {
|
||||
await setAppLockVerificationCipher(password.current);
|
||||
SettingsService.set({
|
||||
appLockHasPasswordSecurity: true,
|
||||
applockKeyboardType: "default"
|
||||
appLockHasPasswordSecurity: true
|
||||
});
|
||||
DatabaseLogger.info("App lock migrated to password security");
|
||||
}
|
||||
@@ -167,7 +164,7 @@ const AppLocked = () => {
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.invalid(keyboardType),
|
||||
heading: strings.invalid("password"),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -290,9 +287,7 @@ const AppLocked = () => {
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
secureTextEntry
|
||||
keyboardType={
|
||||
appLockHasPasswordSecurity ? keyboardType : "default"
|
||||
}
|
||||
keyboardType={"default"}
|
||||
onLayout={async () => {
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
@@ -303,13 +298,7 @@ const AppLocked = () => {
|
||||
}, 32);
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
appLockHasPasswordSecurity
|
||||
? keyboardType === "numeric"
|
||||
? strings.enterApplockPassword()
|
||||
: strings.enterApplockPin()
|
||||
: strings.enterAccountPassword()
|
||||
}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
@@ -26,6 +27,7 @@ import { TextInput, View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
@@ -37,6 +39,7 @@ import {
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import {
|
||||
@@ -46,20 +49,19 @@ import {
|
||||
eOnLoadNote
|
||||
} from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { openNote } from "../list-items/note/wrapper";
|
||||
import { DateMeta } from "../properties/date-meta";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { createFormRef, validators } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import LineSeparator from "../ui/seperator/line-separator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { createFormRef, validators } from "../ui/input/form-input";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -94,7 +96,7 @@ const Actions = ({
|
||||
downloadAttachment(attachment.hash, context === "global");
|
||||
fwdRef.current?.hide();
|
||||
},
|
||||
icon: "download"
|
||||
icon: "download-simple"
|
||||
},
|
||||
{
|
||||
name: strings.network.reupload(),
|
||||
@@ -114,7 +116,7 @@ const Actions = ({
|
||||
type: attachment.mimeType.startsWith("image") ? "image" : "file"
|
||||
});
|
||||
},
|
||||
icon: "upload"
|
||||
icon: "upload-simple"
|
||||
},
|
||||
{
|
||||
name: strings.fileCheck(),
|
||||
@@ -149,7 +151,7 @@ const Actions = ({
|
||||
name: undefined
|
||||
});
|
||||
},
|
||||
icon: "file-check"
|
||||
icon: "file"
|
||||
},
|
||||
{
|
||||
name: strings.rename(),
|
||||
@@ -197,7 +199,7 @@ const Actions = ({
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "form-textbox"
|
||||
icon: "pencil-simple"
|
||||
},
|
||||
{
|
||||
name: strings.delete(),
|
||||
@@ -249,7 +251,7 @@ const Actions = ({
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "delete-outline"
|
||||
icon: "trash"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -267,6 +269,7 @@ const Actions = ({
|
||||
onMomentumScrollEnd={() => {
|
||||
fwdRef?.current?.handleChildScrollEnd();
|
||||
}}
|
||||
bounces={false}
|
||||
nestedScrollEnabled={true}
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
@@ -277,42 +280,34 @@ const Actions = ({
|
||||
<SheetProvider context={contextId} />
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom:
|
||||
notes && notes.length > 0 ? 0 : DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_4,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
>
|
||||
{filename}
|
||||
</Heading>
|
||||
<Heading fontSize="LG">{filename}</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
gap: 10
|
||||
gap: Spacing.LEVEL_1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{attachment.mimeType}
|
||||
</Paragraph>
|
||||
<Paragraph fontSize="XS">•</Paragraph>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph fontSize="XS">•</Paragraph>
|
||||
{notes.length ? (
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{strings.notes(notes.length)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<Paragraph fontSize="XS">•</Paragraph>
|
||||
<Paragraph
|
||||
onPress={() => {
|
||||
Clipboard.setString(attachment.hash);
|
||||
@@ -322,82 +317,123 @@ const Actions = ({
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
size={AppFontSize.xs}
|
||||
fontSize="XS"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
<AppIcon
|
||||
name="copy"
|
||||
size={10}
|
||||
color={colors.secondary.paragraph}
|
||||
iconFamily="notesnook"
|
||||
/>{" "}
|
||||
{attachment.hash}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<LineSeparator paddingVertical={Spacing.LEVEL_2} />
|
||||
|
||||
<DateMeta item={attachment} />
|
||||
</View>
|
||||
|
||||
{notes && notes.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
{strings.listOf()} {strings.dataTypesPlural.note()}:
|
||||
</Heading>
|
||||
<>
|
||||
<LineSeparator
|
||||
paddingHorizontal={Spacing.LEVEL_3}
|
||||
paddingVertical={Spacing.LEVEL_3}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Heading size={AppFontSize.sm}>
|
||||
{strings.notes(notes.length)}
|
||||
</Heading>
|
||||
|
||||
{notes.map((item) => (
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
close?.();
|
||||
eSendEvent(eCloseAttachmentDialog);
|
||||
Navigation.navigate("FluidPanelsView");
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
alignItems: "flex-start",
|
||||
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs}>{item.title}</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
</View>
|
||||
{notes.map((item) => (
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
close?.();
|
||||
eSendEvent(eCloseAttachmentDialog);
|
||||
Navigation.navigate("FluidPanelsView");
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
style={{
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph fontSize="XS">{item.title}</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{actions.map((item) => (
|
||||
<Button
|
||||
key={item.name}
|
||||
buttonType={{
|
||||
text:
|
||||
item.name === "Delete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
title={item.name}
|
||||
icon={item.icon}
|
||||
loading={loading?.name === item.name}
|
||||
type="plain"
|
||||
fontSize={AppFontSize.sm}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
justifyContent: "flex-start",
|
||||
alignSelf: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<LineSeparator
|
||||
paddingHorizontal={Spacing.LEVEL_3}
|
||||
paddingVertical={Spacing.LEVEL_3}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{actions.map((item) => (
|
||||
<Pressable
|
||||
key={item.name}
|
||||
onPress={item.onPress}
|
||||
type="plain"
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_1,
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.XS,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={
|
||||
item.icon === "trash"
|
||||
? colors.error.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
name={item.icon}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
color={
|
||||
item.icon === "trash"
|
||||
? colors.error.accent
|
||||
: colors.primary.heading
|
||||
}
|
||||
fontSize="SM"
|
||||
fontFamily="MEDIUM"
|
||||
>
|
||||
{item.name}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -17,26 +17,29 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { formatBytes, getFormattedDate } from "@notesnook/common";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { View } from "react-native";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Actions from "./actions";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
function getFileExtension(filename: string) {
|
||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
return ext == null ? "" : ext[1];
|
||||
function getAttachmentIcon(attachment: Attachment) {
|
||||
const mimeType = attachment.mimeType || "";
|
||||
if (mimeType.startsWith("image/")) return "image";
|
||||
if (mimeType.startsWith("video/")) return "video-camera";
|
||||
if (mimeType.startsWith("audio/")) return "music-notes";
|
||||
return "file-text";
|
||||
}
|
||||
|
||||
export const AttachmentItem = ({
|
||||
@@ -75,11 +78,15 @@ export const AttachmentItem = ({
|
||||
onPress={onPress}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
minHeight: 45
|
||||
gap: Spacing.LEVEL_2,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: Radius.S,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_3,
|
||||
marginBottom: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{!attachment ? null : (
|
||||
@@ -89,107 +96,112 @@ export const AttachmentItem = ({
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: Radius.XS,
|
||||
backgroundColor: colors.secondary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: -5,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
paddingHorizontal: 2,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: defaultBorderRadius
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
adjustsFontSizeToFit
|
||||
size={8}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
maxWidth: 30
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{getFileExtension(attachment.filename).toUpperCase() ||
|
||||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
|
||||
</Paragraph>
|
||||
<AppIcon
|
||||
name={getAttachmentIcon(attachment)}
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
flexShrink: 1,
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
numberOfLines={1}
|
||||
<Heading
|
||||
fontSize="MD"
|
||||
lineBreakMode="middle"
|
||||
color={colors.primary.paragraph}
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{attachment.filename}
|
||||
</Paragraph>
|
||||
book.pdf
|
||||
</Heading>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xxs}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{attachment.failed ? (
|
||||
<AppIcon
|
||||
size={13}
|
||||
name="alert-circle-outline"
|
||||
color={colors.error.paragraph}
|
||||
/>
|
||||
) : null}
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph
|
||||
fontSize="XS"
|
||||
numberOfLines={1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{getFormattedDate(attachment.dateModified)} |{" "}
|
||||
{formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{currentProgress ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_0,
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
{strings.fileSize()}: {formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
<ProgressBarComponent
|
||||
height={6}
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={currentProgress.value}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
<Paragraph color={colors.secondary.paragraph} fontSize="XS">
|
||||
{currentProgress.percent} |{" "}
|
||||
{currentProgress.type === "upload"
|
||||
? strings.uploading()
|
||||
: strings.downloading()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentProgress ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs().cancel(attachment.hash);
|
||||
setCurrentProgress(undefined);
|
||||
}}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
marginLeft: 5,
|
||||
marginTop: 5,
|
||||
marginRight: -5
|
||||
}}
|
||||
>
|
||||
<ProgressCircleComponent
|
||||
size={AppFontSize.xxl}
|
||||
progress={
|
||||
currentProgress?.value ? currentProgress?.value / 100 : 0
|
||||
}
|
||||
showsText
|
||||
textStyle={{
|
||||
fontSize: 10
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
formatText={(progress) => (progress * 100).toFixed(0)}
|
||||
borderWidth={0}
|
||||
thickness={2}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<>
|
||||
{attachment.failed ? (
|
||||
<IconButton
|
||||
onPress={onPress}
|
||||
name="alert-circle-outline"
|
||||
color={colors.error.paragraph}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
color={colors.secondary.icon}
|
||||
name="dots-three"
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
onPress={onPress}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
height: undefined,
|
||||
width: undefined,
|
||||
borderRadius: 100,
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,10 +23,9 @@ import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import BackupService from "../../services/backup";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -36,6 +35,8 @@ import FormInput, { createFormRef, validators } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { TextInput } from "react-native-gesture-handler";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -92,7 +93,7 @@ export const ChangePassword = () => {
|
||||
});
|
||||
setLoading(false);
|
||||
Navigation.goBack();
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
RecoveryKeySheet.present();
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
setLoading(false);
|
||||
@@ -109,12 +110,15 @@ export const ChangePassword = () => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
paddingTop: Spacing.LEVEL_4,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<FormInput
|
||||
name="oldPassword"
|
||||
label={strings.oldPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={oldPasswordInputRef}
|
||||
loading={loading}
|
||||
@@ -125,7 +129,7 @@ export const ChangePassword = () => {
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.currentPassword()}
|
||||
placeholder={"•••••••••"}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
@@ -133,6 +137,7 @@ export const ChangePassword = () => {
|
||||
|
||||
<FormInput
|
||||
name="password"
|
||||
label={strings.newPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
loading={loading}
|
||||
@@ -143,7 +148,28 @@ export const ChangePassword = () => {
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.newPassword()}
|
||||
placeholder={"•••••••••"}
|
||||
onSubmitEditing={() => {
|
||||
changePassword();
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
name="confirmPassword"
|
||||
label={strings.confirmPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
loading={loading}
|
||||
validators={[
|
||||
validators.matchField("password", strings.confirmPasswordRequired())
|
||||
]}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={"•••••••••"}
|
||||
onSubmitEditing={() => {
|
||||
changePassword();
|
||||
}}
|
||||
@@ -168,22 +194,39 @@ export const ChangePassword = () => {
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Notice text={strings.changePasswordNotice()} type="alert" />
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Notice text={strings.changePasswordNotice2()} type="alert" />
|
||||
<Notice
|
||||
text={strings.changePasswordNotice()}
|
||||
type="information"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
marginTop: Spacing.LEVEL_1
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={changePassword}
|
||||
type="accent"
|
||||
title={loading ? null : strings.changePasswordConfirm()}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1,
|
||||
alignSelf: "center",
|
||||
marginTop: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
size={15}
|
||||
name="shield-check"
|
||||
iconFamily="notesnook"
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph>{strings.yourSecurityIsPriority()}</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,8 +24,9 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
import { AuthParams } from "../../stores/use-navigation-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { FontSizes } from "../../common/design/font";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const route = useRoute();
|
||||
@@ -39,38 +40,42 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
{props.welcome ? (
|
||||
<ProgressPills activePillIndex={1} />
|
||||
) : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
name="arrow-back"
|
||||
iconFamily="notesnook"
|
||||
onPress={() => {
|
||||
hideAuth((route.params as AuthParams)?.context);
|
||||
}}
|
||||
size={24}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.welcome ? null : (
|
||||
<Button
|
||||
title={strings.skipAndGoToApp()}
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
title: strings.offlineMode(),
|
||||
paragraph: strings.offlineModeDesc(),
|
||||
positiveText: strings.understand(),
|
||||
positivePress: hideAuth as any
|
||||
});
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={24}
|
||||
fontSize={FontSizes.SM}
|
||||
type="plain"
|
||||
height={25}
|
||||
fontFamily="REGULAR"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
paddingRight: 0,
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,15 +17,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { NavigationProps } from "../../services/navigation";
|
||||
import { Toast } from "../toast";
|
||||
import { AuthMode, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { NavigationProps } from "../../services/navigation";
|
||||
|
||||
const Auth = ({ navigation, route }: NavigationProps<"Auth">) => {
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(
|
||||
|
||||
@@ -21,8 +21,10 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
@@ -33,19 +35,20 @@ import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { AuthHeader } from "./header";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { useLogin } from "./use-login";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -68,8 +71,9 @@ export const Login = ({
|
||||
passwordInputRef,
|
||||
loading,
|
||||
setLoading,
|
||||
login,
|
||||
error,
|
||||
login,
|
||||
mfaData,
|
||||
formRef
|
||||
} = useLogin(async () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
@@ -91,8 +95,6 @@ export const Login = ({
|
||||
Progress.present();
|
||||
}
|
||||
});
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
const onContinue = () => {
|
||||
login();
|
||||
@@ -114,249 +116,257 @@ export const Login = ({
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<Dialog context="two_factor_verify" />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginTop: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<ProgressPills
|
||||
count={2}
|
||||
activePillIndex={step === LoginSteps.emailAuth ? 0 : 1}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
minHeight: "99%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
{step === LoginSteps.emailAuth || step === LoginSteps.passwordAuth ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : undefined,
|
||||
borderColor: isTablet ? colors.primary.border : undefined,
|
||||
borderRadius: isTablet ? 20 : undefined,
|
||||
marginTop: isTablet ? 50 : undefined,
|
||||
width: !isTablet ? undefined : "50%",
|
||||
minHeight: height * 0.4
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
fontSize="XL"
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
testID="input.email"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
onContinue();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
presentSheet({
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
onContinue();
|
||||
>
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
testID="input.email"
|
||||
label={strings.email()}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
label={strings.password()}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading || !formRef.current.getValue("email")) return;
|
||||
presentSheet({
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
fontFamily="REGULAR"
|
||||
fontSize={AppFontSize.sm}
|
||||
type="plain"
|
||||
textStyle={{
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_1,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={strings.cancelLogin()}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
type="secondaryAccented"
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingVertical: 0
|
||||
}}
|
||||
>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.signUp()}
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
style={{ color: colors.primary.accent }}
|
||||
fontFamily="SEMI_BOLD"
|
||||
>
|
||||
{strings.signUp()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<TwoFactorVerification
|
||||
onMfaLogin={async (
|
||||
mfa: any,
|
||||
callback: (success: boolean) => void,
|
||||
onerror: (error: Error) => void
|
||||
) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
await login();
|
||||
|
||||
if (success) {
|
||||
setLoading(false);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
} else {
|
||||
onerror(e as Error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
mfaInfo={
|
||||
mfaData.current || {
|
||||
primaryMethod: "email",
|
||||
secondaryMethod: "sms",
|
||||
token: ""
|
||||
}
|
||||
}
|
||||
onCancel={() => {
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -45,11 +45,12 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import { useLogin } from "./use-login";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getObfuscatedEmail } from "../../utils/functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
export const SessionExpired = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -195,24 +196,22 @@ export const SessionExpired = () => {
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{step === LoginSteps.passwordAuth ? (
|
||||
<FormInput
|
||||
fwdRef={passwordInputRef}
|
||||
formRef={formRef}
|
||||
name="password"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmitEditing={() => {
|
||||
login();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<FormInput
|
||||
fwdRef={passwordInputRef}
|
||||
formRef={formRef}
|
||||
name="password"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
onSubmitEditing={() => {
|
||||
login();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
|
||||
@@ -21,12 +21,7 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||
import React, { useRef, useState } from "react";
|
||||
import {
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import { TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -46,6 +41,29 @@ import { SignupContext } from "./signup-context";
|
||||
import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { SETTING_ACCOUNT_SVG } from "../../assets/images/assets";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
const getSettingAccountSvg = (dark: boolean) => {
|
||||
if (dark) return SETTING_ACCOUNT_SVG;
|
||||
|
||||
return SETTING_ACCOUNT_SVG.replace('stroke="#1F2722"', 'stroke="#E8F4ED"')
|
||||
.replace(
|
||||
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
|
||||
'fill="#008836" fill-opacity="0.04" stroke="#E3F1E8"'
|
||||
)
|
||||
.replace(
|
||||
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
|
||||
'fill="#008836" fill-opacity="0.05" stroke="#E8F0EC"'
|
||||
)
|
||||
.replace(
|
||||
'fill="#008836" fill-opacity="0.08" stroke="#233C2D"',
|
||||
'fill="#008836" fill-opacity="0.06" stroke="#D3E8DB"'
|
||||
);
|
||||
};
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
@@ -61,7 +79,7 @@ export const Signup = ({
|
||||
welcome: boolean;
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
@@ -76,8 +94,6 @@ export const Signup = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
const route = useRoute<RouteProp<RouteParams, "Auth">>();
|
||||
|
||||
const signup = async () => {
|
||||
@@ -89,8 +105,9 @@ export const Signup = ({
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(values.email.toLowerCase(), values.password);
|
||||
await sleep(1000);
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
const user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
@@ -133,8 +150,9 @@ export const Signup = ({
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
flex: 1
|
||||
}}
|
||||
bounces={false}
|
||||
nestedScrollEnabled
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
@@ -145,68 +163,24 @@ export const Signup = ({
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<Heading
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : undefined,
|
||||
borderColor: isTablet ? colors.primary.border : undefined,
|
||||
borderRadius: isTablet ? 20 : undefined,
|
||||
marginTop: isTablet ? 50 : undefined,
|
||||
width: !isTablet ? undefined : "50%",
|
||||
minHeight: height * 0.25
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
fontSize="XL"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DDS.isTab ? 0 : 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
@@ -214,6 +188,7 @@ export const Signup = ({
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
loading={loading}
|
||||
label={strings.email()}
|
||||
testID="input.email"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
@@ -221,7 +196,7 @@ export const Signup = ({
|
||||
keyboardType="email-address"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
placeholder="you@example.com"
|
||||
blurOnSubmit={false}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
@@ -245,9 +220,11 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
label={strings.password()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
if (formRef.current.validateField("password")) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
@@ -265,8 +242,8 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
label={strings.confirmPassword()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField(
|
||||
@@ -278,7 +255,17 @@ export const Signup = ({
|
||||
signup();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
@@ -296,18 +283,21 @@ export const Signup = ({
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
textAlign: "center"
|
||||
}}
|
||||
fontSize="SM"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
fontSize="SM"
|
||||
fontFamily="SEMI_BOLD"
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
@@ -347,18 +337,16 @@ export const Signup = ({
|
||||
marginBottom: 25,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontFamily="SEMI_BOLD"
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
@@ -366,31 +354,38 @@ export const Signup = ({
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontFamily="SEMI_BOLD"
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{welcome ? <ProgressPills activePillIndex={2} /> : null}
|
||||
<Loading
|
||||
title={"Setting up your account..."}
|
||||
description="Your account is almost ready, please wait..."
|
||||
title={strings.settingUpYourAccount()}
|
||||
svgSrc={getSettingAccountSvg(isDark)}
|
||||
description={strings.accountAlmostReady()}
|
||||
style={{
|
||||
height: undefined,
|
||||
marginTop: Spacing.LEVEL_6
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
</SignupContext.Provider>
|
||||
);
|
||||
|
||||
@@ -20,19 +20,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { db } from "../../common/database/index";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import PinInput from "../ui/pin-input/index";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -73,11 +72,19 @@ const TwoFactorVerification = ({
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [codeInput, setCodeInput] = useState("");
|
||||
|
||||
const isRecoveryCode = currentMethod.method === "recoveryCode";
|
||||
const codeLength = isRecoveryCode ? 10 : 6;
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6) {
|
||||
setError(
|
||||
new Error("Please provide a valid multi-factor authentication code.")
|
||||
new Error(
|
||||
currentMethod.method === "recoveryCode"
|
||||
? strings.provideValidMultiFactorAuthRecoveryCode()
|
||||
: strings.provideValidMultiFactorAuthCode()
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -94,10 +101,7 @@ const TwoFactorVerification = ({
|
||||
method: currentMethod.method,
|
||||
code: code.current
|
||||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
}
|
||||
() => {
|
||||
setLoading(false);
|
||||
},
|
||||
(e) => {
|
||||
@@ -118,12 +122,14 @@ const TwoFactorVerification = ({
|
||||
{
|
||||
id: "sms",
|
||||
title: strings.sendCodeSms(),
|
||||
icon: "message-plus-outline"
|
||||
icon: "chat",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
title: strings.sendCodeEmail(),
|
||||
icon: "email-outline"
|
||||
icon: "envelope-simple",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "app",
|
||||
@@ -133,7 +139,8 @@ const TwoFactorVerification = ({
|
||||
{
|
||||
id: "recoveryCode",
|
||||
title: strings.recoveryCode(),
|
||||
icon: "key"
|
||||
icon: "lock-simple",
|
||||
iconFamily: "notesnook"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -155,6 +162,7 @@ const TwoFactorVerification = ({
|
||||
setSending(false);
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
console.log("error e");
|
||||
setError(
|
||||
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
|
||||
);
|
||||
@@ -168,48 +176,56 @@ const TwoFactorVerification = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentMethod.method]);
|
||||
|
||||
useEffect(() => {
|
||||
setCodeInput("");
|
||||
code.current = "";
|
||||
}, [currentMethod.method]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 60
|
||||
}}
|
||||
<View
|
||||
onLayout={() => {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 500);
|
||||
}}
|
||||
style={{
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
paddingHorizontal: currentMethod.method ? 12 : 0,
|
||||
gap: 12
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
width: 70,
|
||||
height: 70
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: colors.primary.shade,
|
||||
borderRadius: Radius.XS,
|
||||
marginBottom: Spacing.LEVEL_7
|
||||
}}
|
||||
size={50}
|
||||
name="key"
|
||||
size={25}
|
||||
iconFamily="notesnook"
|
||||
name="shield-check"
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
|
||||
{currentMethod.method === "recoveryCode"
|
||||
? strings.recoveryCodeTitle()
|
||||
: currentMethod.method
|
||||
? strings["2fa"]()
|
||||
: strings.select2faMethod()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
width: "80%",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
@@ -219,61 +235,35 @@ const TwoFactorVerification = ({
|
||||
]?.() || strings.select2faCodeHelpText()
|
||||
: strings.select2faCodeHelpText()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={seconds ? "plain" : "transparent"}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
: `${
|
||||
seconds
|
||||
? strings.resend2faCode(`${seconds}`)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
}
|
||||
loading={sending}
|
||||
height={30}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{currentMethod.method ? (
|
||||
<>
|
||||
<Input
|
||||
placeholder={
|
||||
currentMethod.method === "recoveryCode"
|
||||
? "xxxxx-xxxxx"
|
||||
: "xxxxxx"
|
||||
}
|
||||
{currentMethod.method ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: Radius.S,
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<PinInput
|
||||
testID={"input.totp"}
|
||||
maxLength={
|
||||
currentMethod.method === "recoveryCode" ? undefined : 6
|
||||
}
|
||||
fwdRef={inputRef}
|
||||
textAlign="center"
|
||||
onChangeText={(value) => {
|
||||
setError(undefined);
|
||||
inputRef={inputRef}
|
||||
length={codeLength}
|
||||
value={codeInput}
|
||||
onChangeText={(value: string) => {
|
||||
setCodeInput(value);
|
||||
code.current = value;
|
||||
}}
|
||||
cursorColor={colors.selected.accent}
|
||||
selectionHandleColor={colors.selected.accent}
|
||||
selectionColor={colors.selected.accent}
|
||||
onSubmitEditing={onNext}
|
||||
height={60}
|
||||
marginBottom={0}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
textAlign: "center",
|
||||
letterSpacing: 7,
|
||||
width: 250
|
||||
}}
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
minWidth: "50%"
|
||||
keyboardType={isRecoveryCode ? "web-search" : "number-pad"}
|
||||
showDashAfterDigit={isRecoveryCode ? 5 : undefined}
|
||||
sanitize={(value: string) => {
|
||||
if (isRecoveryCode) {
|
||||
return value.replace(/[^0-9a-zA-Z]/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
return value.replace(/[^0-9]/g, "");
|
||||
}}
|
||||
/>
|
||||
{error ? (
|
||||
@@ -284,7 +274,9 @@ const TwoFactorVerification = ({
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
maxWidth: 250
|
||||
maxWidth: 250,
|
||||
alignSelf: "center",
|
||||
marginTop: -10
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
@@ -296,76 +288,144 @@ const TwoFactorVerification = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
title={loading ? null : strings.next()}
|
||||
type="accent"
|
||||
width={250}
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
/>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
reset();
|
||||
onCancel();
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
width={250}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings["2faCodeSecondaryMethodText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
|
||||
]()}
|
||||
type="plain"
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
method: item.id,
|
||||
isPrimary: false
|
||||
});
|
||||
reset();
|
||||
onCancel();
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 0,
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start"
|
||||
width: "48%"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={loading ? null : strings.continue()}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
style={{
|
||||
width: "48%"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_1,
|
||||
marginTop: -Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
{currentMethod.method === "sms" ||
|
||||
currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={"plain"}
|
||||
disabled={parseInt(`${seconds}`) !== 0}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
: `${
|
||||
seconds
|
||||
? strings.resend2faCode(`${seconds}`)
|
||||
: strings.resendCode()
|
||||
}`
|
||||
}
|
||||
loading={sending}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontFamily="REGULAR"
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
onPress={onRequestSecondaryMethod}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={15}
|
||||
color={colors.primary.accent}
|
||||
name={item.icon}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
<Paragraph
|
||||
fontFamily={isRecoveryCode ? "REGULAR" : "SEMI_BOLD"}
|
||||
fontSize="SM"
|
||||
color={
|
||||
isRecoveryCode
|
||||
? colors.primary.paragraph
|
||||
: colors.primary.accent
|
||||
}
|
||||
>
|
||||
<Paragraph size={AppFontSize.md}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
{strings["2faCodeSecondaryMethodText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
|
||||
]()}
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
method: item.id,
|
||||
isPrimary: false
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
padding: Spacing.LEVEL_2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
flexDirection: "row",
|
||||
borderRadius: Radius.S,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
borderRadius: Radius.XS,
|
||||
padding: Spacing.LEVEL_1,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
backgroundColor: RGB_Linear_Shade(
|
||||
0.04,
|
||||
hexToRGBA(colors.secondary.background)
|
||||
)
|
||||
}}
|
||||
size={17}
|
||||
color={colors.primary.icon}
|
||||
name={item.icon}
|
||||
iconFamily={item.iconFamily as "notesnook"}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,13 +21,11 @@ import { strings } from "@notesnook/intl";
|
||||
import { useRef, useState } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { createFormRef } from "../ui/input/form-input";
|
||||
|
||||
export const LoginSteps = {
|
||||
@@ -46,6 +44,7 @@ export const useLogin = (
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const mfaData = useRef<any>(undefined);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
@@ -60,69 +59,43 @@ export const useLogin = (
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
if (formRef.current.validateField("email")) {
|
||||
if (!formRef.current.validate()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const mfaInfo = await db.user.authenticateEmail(
|
||||
formRef.current.getValue("email")
|
||||
);
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 500);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
ToastManager.error(new Error("Token expired, try logging in again"));
|
||||
} else {
|
||||
onerror(e as Error);
|
||||
}
|
||||
}
|
||||
},
|
||||
mfaInfo,
|
||||
() => {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
);
|
||||
mfaData.current = mfaInfo;
|
||||
setStep(LoginSteps.mfaAuth);
|
||||
setLoading(false);
|
||||
} else {
|
||||
finishWithError(new Error(strings.unableToSend2faCode()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
case LoginSteps.passwordAuth:
|
||||
case LoginSteps.mfaAuth: {
|
||||
if (!formRef.current.validate()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const values = formRef.current.getValues();
|
||||
await db.user.authenticatePassword(
|
||||
try {
|
||||
await db.user.authenticatePassword(
|
||||
values.email,
|
||||
values.password,
|
||||
undefined,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
finishLogin();
|
||||
} catch(e) {
|
||||
formRef.current.setError("password", (e as Error).message);
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -172,6 +145,7 @@ export const useLogin = (
|
||||
setLoading,
|
||||
error,
|
||||
setError,
|
||||
formRef
|
||||
formRef,
|
||||
mfaData
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ import { getElevationStyle } from "../../utils/elevation";
|
||||
import { AppFontSize, normalize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
|
||||
interface FloatingButtonProps {
|
||||
onPress: () => void;
|
||||
@@ -48,7 +48,6 @@ interface FloatingButtonProps {
|
||||
position?: "left" | "right";
|
||||
size?: "small" | "large";
|
||||
style?: ViewStyle;
|
||||
hideOnKeyboard?: boolean;
|
||||
}
|
||||
|
||||
const FloatingButton = ({
|
||||
@@ -59,28 +58,26 @@ const FloatingButton = ({
|
||||
testID,
|
||||
position = "right",
|
||||
size = "large",
|
||||
style,
|
||||
hideOnKeyboard = true
|
||||
style
|
||||
}: FloatingButtonProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const translate = useSharedValue(0);
|
||||
const keyboardHeight = useSharedValue(0);
|
||||
const route = useRoute();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateX: hideOnKeyboard ? translate.value : 0
|
||||
},
|
||||
{
|
||||
translateY: hideOnKeyboard ? translate.value : 0
|
||||
}
|
||||
],
|
||||
bottom: hideOnKeyboard ? 15 : keyboardHeight.value + 15
|
||||
}));
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateX: translate.value
|
||||
},
|
||||
{
|
||||
translateY: translate.value
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const animate = useCallback(
|
||||
(toValue: number) => {
|
||||
@@ -101,22 +98,12 @@ const FloatingButton = ({
|
||||
editorState().keyboardState = false;
|
||||
if (deviceMode !== "mobile") return;
|
||||
animate(0);
|
||||
keyboardHeight.value = withTiming(0);
|
||||
};
|
||||
|
||||
const onKeyboardShow = (e: any) => {
|
||||
const onKeyboardShow = () => {
|
||||
editorState().keyboardState = true;
|
||||
if (deviceMode !== "mobile") return;
|
||||
if (hideOnKeyboard) {
|
||||
animate(150);
|
||||
} else {
|
||||
keyboardHeight.value = withTiming(
|
||||
e.endCoordinates.height - insets.bottom,
|
||||
{
|
||||
duration: 250
|
||||
}
|
||||
);
|
||||
}
|
||||
animate(150);
|
||||
};
|
||||
|
||||
const sub = [
|
||||
@@ -124,9 +111,9 @@ const FloatingButton = ({
|
||||
Keyboard.addListener("keyboardDidHide", onKeyboardHide)
|
||||
];
|
||||
return () => {
|
||||
sub.forEach((sub) => sub.remove());
|
||||
sub.forEach((sub) => sub?.remove?.());
|
||||
};
|
||||
}, [deviceMode, animate, hideOnKeyboard, keyboardHeight, insets.bottom]);
|
||||
}, [deviceMode, animate]);
|
||||
|
||||
return deviceMode !== "mobile" && !alwaysVisible ? null : (
|
||||
<Animated.View
|
||||
@@ -154,7 +141,7 @@ const FloatingButton = ({
|
||||
borderBottomWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderRightWidth: 0,
|
||||
backgroundColor: colors.primary.background
|
||||
backgroundColor: colors.primary.accent
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -162,26 +149,19 @@ const FloatingButton = ({
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: normalize(size === "small" ? 40 : 60),
|
||||
width: normalize(size === "small" ? 40 : 60),
|
||||
height: 55,
|
||||
width: 55,
|
||||
backgroundColor: color
|
||||
? RGB_Linear_Shade(0.87, hexToRGBA(color))
|
||||
: colors.primary.shade,
|
||||
: colors.primary.accent,
|
||||
borderRadius: size === "small" ? 15 : 20
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
icon
|
||||
? icon
|
||||
: route.name === "Notebooks"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
}
|
||||
color={color || colors.primary.accent}
|
||||
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}
|
||||
<AppIcon
|
||||
name={icon ? icon : route.name === "Trash" ? "trash-alt" : "plus"}
|
||||
iconFamily="notesnook"
|
||||
color={color || colors.primary.accentForeground}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program 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.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from "react";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRef } from "react";
|
||||
import { useWindowDimensions, View } from "react-native";
|
||||
import { defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import DatePicker from "react-native-date-picker";
|
||||
import dayjs from "dayjs";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export default function DatePickerComponent(props: {
|
||||
onConfirm: (date: Date) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const dateRef = useRef<Date>(dayjs().add(1, "week").toDate());
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.primary.border,
|
||||
width: "80%",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<DatePicker
|
||||
style={{
|
||||
width: width * 0.8 - DefaultAppStyles.GAP * 2
|
||||
}}
|
||||
theme={isDark ? "dark" : "light"}
|
||||
mode="date"
|
||||
minimumDate={dayjs().add(1, "day").toDate()}
|
||||
onCancel={() => {
|
||||
close?.();
|
||||
}}
|
||||
date={dateRef.current}
|
||||
onDateChange={(date) => {
|
||||
dateRef.current = date;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.setExpiry()}
|
||||
type="accent"
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!dateRef.current) return;
|
||||
props.onConfirm(dateRef.current);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondary"
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={async () => {
|
||||
props.onCancel();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
200
apps/mobile/app/components/date-time-picker/date-time-picker.tsx
Normal file
200
apps/mobile/app/components/date-time-picker/date-time-picker.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program 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.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import dayjs, { locale } from "dayjs";
|
||||
import React, { useMemo } from "react";
|
||||
import { I18nManager, ViewStyle } from "react-native";
|
||||
import RNDateTimePicker, {
|
||||
DateType,
|
||||
useDefaultStyles
|
||||
} from "react-native-ui-datepicker";
|
||||
import { FontFamily } from "../../common/design/font";
|
||||
import { Radius } from "../../common/design/spacing";
|
||||
import { db } from "../../common/database";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
|
||||
export type DateTimePickerMode = "date" | "time" | "datetime";
|
||||
|
||||
export type DateTimePickerProps = {
|
||||
/**
|
||||
* Which parts of the value can be picked. `date` shows only the calendar,
|
||||
* `time` shows only the time wheel and `datetime` shows both.
|
||||
*/
|
||||
mode?: DateTimePickerMode;
|
||||
date: Date;
|
||||
onChange: (date: Date) => void;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
/**
|
||||
* Force 24-hour time. Defaults to the user's time format setting.
|
||||
*/
|
||||
is24Hour?: boolean;
|
||||
/**
|
||||
* First day of the week (0 = Sunday, 1 = Monday). Defaults to the user's
|
||||
* week format setting.
|
||||
*/
|
||||
firstDayOfWeek?: number;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
const toDate = (value: DateType) =>
|
||||
value ? dayjs(value).toDate() : new Date();
|
||||
|
||||
/**
|
||||
* Themed date/time picker built on top of `react-native-ui-datepicker`.
|
||||
* Renders inline; use `DateTimePicker.present()` (see index) for the sheet.
|
||||
*/
|
||||
export default function DateTimePicker({
|
||||
mode = "date",
|
||||
date,
|
||||
onChange,
|
||||
minDate,
|
||||
maxDate,
|
||||
is24Hour,
|
||||
firstDayOfWeek,
|
||||
style
|
||||
}: DateTimePickerProps) {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const defaultStyles = useDefaultStyles(isDark ? "dark" : "light");
|
||||
const weekFormat = useSettingStore((state) => state.weekFormat);
|
||||
|
||||
const use12Hours =
|
||||
(is24Hour ?? db.settings.getTimeFormat() === "24-hour") === false;
|
||||
const firstDay = firstDayOfWeek ?? (weekFormat === "Mon" ? 1 : 0);
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
({
|
||||
...defaultStyles,
|
||||
today: {
|
||||
borderColor: colors.primary.accent,
|
||||
borderWidth: 1,
|
||||
borderRadius: Radius.S
|
||||
},
|
||||
today_label: {
|
||||
color: colors.primary.accent
|
||||
},
|
||||
selected: {
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: Radius.S
|
||||
},
|
||||
selected_label: {
|
||||
color: colors.static.white
|
||||
},
|
||||
day_label: {
|
||||
color: colors.primary.paragraph,
|
||||
fontFamily: FontFamily.REGULAR
|
||||
},
|
||||
disabled_label: {
|
||||
color: colors.disabled.paragraph
|
||||
},
|
||||
outside_label: {
|
||||
color: colors.secondary.paragraph
|
||||
},
|
||||
weekday_label: {
|
||||
color: colors.secondary.paragraph,
|
||||
fontFamily: FontFamily.MEDIUM
|
||||
},
|
||||
month_selector_label: {
|
||||
color: colors.primary.heading,
|
||||
fontFamily: FontFamily.SEMI_BOLD
|
||||
},
|
||||
year_selector_label: {
|
||||
color: colors.primary.heading,
|
||||
fontFamily: FontFamily.SEMI_BOLD
|
||||
},
|
||||
time_selector_label: {
|
||||
color: colors.primary.heading,
|
||||
fontFamily: FontFamily.SEMI_BOLD
|
||||
},
|
||||
month_label: {
|
||||
color: colors.primary.paragraph
|
||||
},
|
||||
year_label: {
|
||||
color: colors.primary.paragraph
|
||||
},
|
||||
selected_month: {
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: Radius.S
|
||||
},
|
||||
selected_month_label: {
|
||||
color: colors.static.white
|
||||
},
|
||||
selected_year: {
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: Radius.S
|
||||
},
|
||||
selected_year_label: {
|
||||
color: colors.static.white
|
||||
},
|
||||
active_year: {
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.S
|
||||
},
|
||||
time_label: {
|
||||
color: colors.primary.heading,
|
||||
fontFamily: FontFamily.SEMI_BOLD
|
||||
},
|
||||
time_selected_indicator: {
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.S
|
||||
}
|
||||
}) as ReturnType<typeof useDefaultStyles>,
|
||||
[colors, defaultStyles]
|
||||
);
|
||||
|
||||
return (
|
||||
<RNDateTimePicker
|
||||
mode="single"
|
||||
date={date}
|
||||
timePicker={mode !== "date"}
|
||||
initialView={mode === "time" ? "time" : "day"}
|
||||
hideHeader={mode === "time"}
|
||||
use12Hours={use12Hours}
|
||||
firstDayOfWeek={firstDay}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
styles={styles}
|
||||
components={{
|
||||
IconPrev: (
|
||||
<AppIcon
|
||||
name="chevron-left"
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
),
|
||||
IconNext: (
|
||||
<AppIcon
|
||||
name="chevron-right"
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
onChange={(params) => {
|
||||
const changed = (params as { date: DateType }).date;
|
||||
onChange(toDate(changed));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
224
apps/mobile/app/components/date-time-picker/index.tsx
Normal file
224
apps/mobile/app/components/date-time-picker/index.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program 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.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { View, ViewStyle } from "react-native";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import DateTimePicker, {
|
||||
DateTimePickerMode,
|
||||
DateTimePickerProps
|
||||
} from "./date-time-picker";
|
||||
|
||||
export { default } from "./date-time-picker";
|
||||
export type {
|
||||
DateTimePickerMode,
|
||||
DateTimePickerProps
|
||||
} from "./date-time-picker";
|
||||
|
||||
const DEFAULT_ICONS: Record<DateTimePickerMode, string> = {
|
||||
date: "calendar-dots",
|
||||
time: "clock",
|
||||
datetime: "calendar-dots"
|
||||
};
|
||||
|
||||
export type PresentDateTimePickerOptions = Pick<
|
||||
DateTimePickerProps,
|
||||
"minDate" | "maxDate" | "is24Hour" | "firstDayOfWeek"
|
||||
> & {
|
||||
mode?: DateTimePickerMode;
|
||||
/** Initial value the picker opens on. Defaults to now. */
|
||||
date?: Date;
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
/** Label for the confirm button. Defaults to `strings.done()`. */
|
||||
confirmText?: string;
|
||||
/** Dialog context to render into (dialog presentation only). */
|
||||
context?: string;
|
||||
onConfirm: (date: Date) => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
type Presentation = "dialog" | "sheet";
|
||||
|
||||
type DateTimePickerContentProps = PresentDateTimePickerOptions & {
|
||||
presentation: Presentation;
|
||||
close?: (ctx?: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The shared picker body used by both the dialog and the sheet: a header,
|
||||
* the themed picker, and Cancel/Confirm actions. Presentation-specific chrome
|
||||
* (the dialog card vs. the sheet surface) is applied by the outer container.
|
||||
*/
|
||||
function DateTimePickerContent({
|
||||
presentation,
|
||||
mode = "date",
|
||||
date,
|
||||
minDate,
|
||||
maxDate,
|
||||
is24Hour,
|
||||
firstDayOfWeek,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
confirmText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
close
|
||||
}: DateTimePickerContentProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const [selected, setSelected] = useState<Date>(date || new Date());
|
||||
|
||||
const containerStyle: ViewStyle =
|
||||
presentation === "dialog"
|
||||
? {
|
||||
width: "90%",
|
||||
maxWidth: 400,
|
||||
alignSelf: "center",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderRadius: Radius.S,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.primary.border,
|
||||
overflow: "hidden"
|
||||
}
|
||||
: {
|
||||
width: "100%"
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_4,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={icon || DEFAULT_ICONS[mode]}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flexShrink: 1, gap: Spacing.LEVEL_1 }}>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{title ||
|
||||
(mode === "time"
|
||||
? strings.selectTimeHeading()
|
||||
: strings.selectDate())}
|
||||
</Heading>
|
||||
{description ? (
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* <View style={{ height: 1, backgroundColor: colors.primary.border }} /> */}
|
||||
|
||||
<DateTimePicker
|
||||
mode={mode}
|
||||
date={selected}
|
||||
onChange={setSelected}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
is24Hour={is24Hour}
|
||||
firstDayOfWeek={firstDayOfWeek}
|
||||
/>
|
||||
|
||||
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="plain-outline"
|
||||
style={{ flex: 1, width: "auto" }}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
onCancel?.();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={confirmText || strings.done()}
|
||||
type="accent"
|
||||
style={{ flex: 1, width: "auto" }}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
onConfirm(selected);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the themed date/time picker in a dialog. This is the default surface
|
||||
* used across the app. Resolves the picked value through `onConfirm`;
|
||||
* dismissing without confirming calls `onCancel`.
|
||||
*/
|
||||
export function presentDateTimePicker(options: PresentDateTimePickerOptions) {
|
||||
presentDialog({
|
||||
context: options.context || "global",
|
||||
component: (close) => (
|
||||
<DateTimePickerContent {...options} presentation="dialog" close={close} />
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the same date/time picker in a bottom sheet. Preferred on the
|
||||
* add-reminder screen; elsewhere use {@link presentDateTimePicker}.
|
||||
*/
|
||||
export function presentDateTimePickerSheet(
|
||||
options: PresentDateTimePickerOptions
|
||||
) {
|
||||
presentSheet({
|
||||
context: options.context || "global",
|
||||
component: (_ref, close) => (
|
||||
<DateTimePickerContent {...options} presentation="sheet" close={close} />
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -31,8 +31,7 @@ import ImagePreview from "../image-preview";
|
||||
import MergeConflicts from "../merge-conflicts";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import Progress from "../dialogs/progress";
|
||||
import Progress from "../dialogs/progress/progress";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const DialogProvider = () => {
|
||||
@@ -51,7 +50,6 @@ const DialogProvider = () => {
|
||||
{isAppLoading ? null : (
|
||||
<>
|
||||
<MergeConflicts />
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
|
||||
@@ -20,14 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Button, ButtonProps } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
const DialogButtons = ({
|
||||
onPressPositive,
|
||||
@@ -51,82 +48,41 @@ const DialogButtons = ({
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 60,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderTopWidth: 0.7,
|
||||
borderTopColor: getColorLinearShade(
|
||||
colors.secondary.background,
|
||||
0.05,
|
||||
isDark
|
||||
)
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}
|
||||
]}
|
||||
>
|
||||
{doneText ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
name="check-circle-outline"
|
||||
size={AppFontSize.md}
|
||||
/>
|
||||
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<ActivityIndicator
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
|
||||
<View
|
||||
<Button
|
||||
onPress={onPressNegative}
|
||||
fontSize={AppFontSize.md}
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="plain-outline"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
title={negativeTitle}
|
||||
/>
|
||||
{onPressPositive ? (
|
||||
<Button
|
||||
onPress={onPressNegative}
|
||||
fontSize={AppFontSize.sm}
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="plain"
|
||||
onPress={onPressPositive}
|
||||
fontSize={AppFontSize.md}
|
||||
testID={notesnook.ids.default.dialog.yes}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
loading={loading}
|
||||
bold
|
||||
title={negativeTitle}
|
||||
type={positiveType || "accent"}
|
||||
title={positiveTitle}
|
||||
/>
|
||||
{onPressPositive ? (
|
||||
<Button
|
||||
onPress={onPressPositive}
|
||||
fontSize={AppFontSize.sm}
|
||||
testID={notesnook.ids.default.dialog.yes}
|
||||
style={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
bold
|
||||
type={positiveType || "transparent"}
|
||||
title={positiveTitle}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogButtons;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user