mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
140 Commits
web/auth-r
...
fix/androi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eef56916de | ||
|
|
7f5215403b | ||
|
|
8e1d59d016 | ||
|
|
9833c7636b | ||
|
|
71cae07dc0 | ||
|
|
77e56f9da1 | ||
|
|
b066fac692 | ||
|
|
8d2e1c65bc | ||
|
|
93337b6560 | ||
|
|
70fd3096d2 | ||
|
|
1889b13459 | ||
|
|
314c484ea0 | ||
|
|
6b5e6f62c5 | ||
|
|
55b51b81e3 | ||
|
|
b2c7f732a0 | ||
|
|
88eb3ab714 | ||
|
|
942c9287fe | ||
|
|
343d2b0131 | ||
|
|
9b29503b17 | ||
|
|
0939f0f805 | ||
|
|
771a8bb4e3 | ||
|
|
4f816b47ba | ||
|
|
1071249baa | ||
|
|
d4775ad340 | ||
|
|
0d8c855cd0 | ||
|
|
d63ce5a213 | ||
|
|
a1d6bb91e5 | ||
|
|
a20f6960d7 | ||
|
|
6b2b224f41 | ||
|
|
054a023142 | ||
|
|
441a6396be | ||
|
|
987671e59c | ||
|
|
0fc705ce1e | ||
|
|
b3805534d6 | ||
|
|
77c009e83d | ||
|
|
c9f1cba702 | ||
|
|
637cf914ee | ||
|
|
64877c519b | ||
|
|
229b620473 | ||
|
|
658e915589 | ||
|
|
d4658aa032 | ||
|
|
489c793806 | ||
|
|
eeae993736 | ||
|
|
c8cf4ce52a | ||
|
|
f6c31d19d8 | ||
|
|
a94e8d6852 | ||
|
|
2edbc3bf2e | ||
|
|
e4a7d63e07 | ||
|
|
76fd61af4f | ||
|
|
98386a6d71 | ||
|
|
539a630f77 | ||
|
|
6b65f8031d | ||
|
|
3c85cddfe7 | ||
|
|
e426426468 | ||
|
|
109e46bd06 | ||
|
|
281a973c37 | ||
|
|
2738207c20 | ||
|
|
1aa73c1f23 | ||
|
|
9ec22724e9 | ||
|
|
4a7aa27bd9 | ||
|
|
6051614d96 | ||
|
|
295a461dda | ||
|
|
a4cbe83211 | ||
|
|
afd63db043 | ||
|
|
aadb6e3a01 | ||
|
|
7c3fdab6ee | ||
|
|
14f727d6e6 | ||
|
|
688a646181 | ||
|
|
7a7f0db941 | ||
|
|
96be35c105 | ||
|
|
36bacce3ac | ||
|
|
c89e333c11 | ||
|
|
d521eb3bd8 | ||
|
|
20bf090c7e | ||
|
|
6cc9b8d1e1 | ||
|
|
e931423eba | ||
|
|
743d47e9e9 | ||
|
|
37c96177a7 | ||
|
|
834479d095 | ||
|
|
643515c6c4 | ||
|
|
0b0a4f1b6f | ||
|
|
48d88069c0 | ||
|
|
9d7754f1e1 | ||
|
|
b2ed115011 | ||
|
|
3447e8133c | ||
|
|
4473d8ec67 | ||
|
|
08cf21b05a | ||
|
|
10dccba5b6 | ||
|
|
23c9a98013 | ||
|
|
473fae4a30 | ||
|
|
e9808c08f0 | ||
|
|
4bb46ea149 | ||
|
|
01811372fc | ||
|
|
6d2097259b | ||
|
|
4c3e065928 | ||
|
|
e45b72a8c4 | ||
|
|
aceb6db8b8 | ||
|
|
5a0ca0bed9 | ||
|
|
8dd5779308 | ||
|
|
dc958098ea | ||
|
|
bf536eb183 | ||
|
|
eb3eea031e | ||
|
|
d376e00a1e | ||
|
|
aca36b60b7 | ||
|
|
033666795c | ||
|
|
c0872c98a8 | ||
|
|
f2173a42d9 | ||
|
|
cbddea4518 | ||
|
|
c69304781a | ||
|
|
412631bf67 | ||
|
|
8c2be00920 | ||
|
|
553e2060be | ||
|
|
7603de25c6 | ||
|
|
146216949d | ||
|
|
c4d8abef16 | ||
|
|
baf823264b | ||
|
|
7f0a517ee1 | ||
|
|
da48cf0bd1 | ||
|
|
4c41e13c5d | ||
|
|
82e2953bc8 | ||
|
|
fc244b1196 | ||
|
|
98ecad3450 | ||
|
|
54dcbd9be6 | ||
|
|
d26c99c487 | ||
|
|
0922d9d833 | ||
|
|
0cdeee80fa | ||
|
|
3aa23d3215 | ||
|
|
936b6cb4aa | ||
|
|
e0b9932aba | ||
|
|
f9f974c9f0 | ||
|
|
e3816234ff | ||
|
|
8b36ae6dee | ||
|
|
0bd030dc78 | ||
|
|
63eb42eb9e | ||
|
|
eb75df360f | ||
|
|
89f3a7945b | ||
|
|
7af1387691 | ||
|
|
1d292d740c | ||
|
|
4f884a8411 | ||
|
|
3122e8a379 |
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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
|
||||
104
.github/workflows/android.preview.build.yml
vendored
Normal file
104
.github/workflows/android.preview.build.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
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
135
.github/workflows/android.preview.firebase.yml
vendored
@@ -1,135 +0,0 @@
|
||||
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
Normal file
72
.github/workflows/android.preview.publish.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
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@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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-latest
|
||||
runs-on: windows-2022
|
||||
outputs:
|
||||
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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.publish-github && inputs.build-linux
|
||||
if: inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
|
||||
@@ -127,6 +127,13 @@ 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
|
||||
@@ -135,7 +142,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -182,7 +189,9 @@ 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
|
||||
@@ -237,7 +246,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -264,6 +273,7 @@ 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
|
||||
@@ -304,7 +314,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -331,6 +341,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
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
@@ -363,11 +374,11 @@ jobs:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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,12 +18,13 @@ on:
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
runs-on: macos-15-intel
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -33,44 +34,10 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- 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
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
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
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
@@ -87,33 +54,24 @@ jobs:
|
||||
|
||||
test-macos:
|
||||
name: Test macOS
|
||||
needs: build
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
@@ -130,39 +88,24 @@ jobs:
|
||||
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
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
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
@@ -179,44 +122,26 @@ jobs:
|
||||
|
||||
test-windows:
|
||||
name: Test for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/help.publish.yml
vendored
2
.github/workflows/help.publish.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
152
.github/workflows/ios.preview.build.yml
vendored
Normal file
152
.github/workflows/ios.preview.build.yml
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
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
173
.github/workflows/ios.preview.firebase.yml
vendored
@@ -1,173 +0,0 @@
|
||||
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
Normal file
222
.github/workflows/ios.preview.publish.yml
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
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@v3
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/web.preview.yml
vendored
2
.github/workflows/web.preview.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/checkout@v5
|
||||
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@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
|
||||
@@ -17,10 +17,9 @@ 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 } from "@nn/test";
|
||||
import { test, expect } 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,8 +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 { mergeTests } from "@playwright/test";
|
||||
import { test } from "@playwright/test";
|
||||
import { mergeTests, 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 { Browser, ElectronApplication, Page } from "@playwright/test";
|
||||
import type { 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,42 +1,35 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/// @ts-ignore
|
||||
import { oop, client } from "playwright-core/lib/coreBundle";
|
||||
|
||||
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.
|
||||
export type TestModeName = "default" | "driver";
|
||||
|
||||
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";
|
||||
const { start } = oop;
|
||||
|
||||
interface TestMode {
|
||||
setup(): Promise<Playwright>;
|
||||
setup(): Promise<client.Playwright>;
|
||||
teardown(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DriverTestMode implements TestMode {
|
||||
private _impl: { playwright: Playwright; stop: () => Promise<void> };
|
||||
private _impl: { playwright: client.Playwright; stop: () => Promise<void> };
|
||||
|
||||
async setup() {
|
||||
this._impl = await start({
|
||||
|
||||
@@ -20,7 +20,11 @@ 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";
|
||||
import {
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page
|
||||
} from "@playwright/test";
|
||||
import { existsSync } from "fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
|
||||
@@ -30,7 +34,7 @@ const root = path.resolve(__dirname, "..", "..");
|
||||
const SOURCE_DIR = resolve(root, "output", productName);
|
||||
|
||||
export interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
app: ElectronApplication;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
|
||||
10297
apps/desktop/package-lock.json
generated
10297
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.0-beta.1",
|
||||
"version": "3.4.4",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -54,18 +54,18 @@
|
||||
"zod": "3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron": "^37.10.3",
|
||||
"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": "1.58.2",
|
||||
"playwright-core": "1.61.1",
|
||||
"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 tsconfig.json"
|
||||
"test": "playwright test --tsconfig __tests__/tsconfig.json --project notesnook-desktop"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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
|
||||
@@ -1,24 +0,0 @@
|
||||
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 ? 3 : 0,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: process.env.CI
|
||||
? [["dot"], ["json", { outputFile: path.join(outputDir, "report.json") }]]
|
||||
: "line",
|
||||
|
||||
@@ -33,17 +33,38 @@ 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 filename");
|
||||
throw new Error(
|
||||
`Invalid backup filename: expected a plain file name without path separators, received "${filename}".`
|
||||
);
|
||||
const resolvedBackupDir = resolvePath(config.backupDirectory);
|
||||
const backupPath = path.resolve(resolvedBackupDir, filename);
|
||||
if (!backupPath.startsWith(resolvedBackupDir))
|
||||
throw new Error("Invalid 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.`
|
||||
);
|
||||
|
||||
mkdirSync(resolvedBackupDir, { recursive: true });
|
||||
const stream = createWriteStream(backupPath, { encoding: "utf-8" });
|
||||
@@ -55,8 +76,7 @@ export const backupsRouter = t.router({
|
||||
write: t.procedure
|
||||
.input(z.object({ id: z.string(), chunk: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
const stream = getStreamOrThrow(input.id, "write");
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
stream.write(Buffer.from(input.chunk, "base64"), (err) =>
|
||||
err ? reject(err) : resolve()
|
||||
@@ -67,8 +87,7 @@ export const backupsRouter = t.router({
|
||||
close: t.procedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
const stream = getStreamOrThrow(input.id, "close");
|
||||
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 { resolvePath } from "../utils/resolve-path";
|
||||
import { normalizePathString, resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak, isPortable, isSnap } from "../utils";
|
||||
@@ -60,7 +60,13 @@ export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
isSnap: t.procedure.query(() => isSnap()),
|
||||
isPortable: t.procedure.query(() => isPortable()),
|
||||
backupDirectory: t.procedure.query(() => config.backupDirectory),
|
||||
backupDirectory: t.procedure.query(() => {
|
||||
const backupDirectory = normalizePathString(config.backupDirectory);
|
||||
if (backupDirectory !== config.backupDirectory) {
|
||||
config.backupDirectory = backupDirectory;
|
||||
}
|
||||
return backupDirectory;
|
||||
}),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ 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"
|
||||
@@ -140,7 +141,8 @@ async function createWindow() {
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
|
||||
if (cliOptions.hidden && !(config.desktopSettings.minimizeToSystemTray
|
||||
|| config.desktopSettings.closeToSystemTray))
|
||||
mainWindow.minimize();
|
||||
|
||||
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
|
||||
@@ -163,6 +165,14 @@ 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;
|
||||
});
|
||||
@@ -294,7 +304,7 @@ async function migrateBackupDirectory() {
|
||||
`localStorage.getItem("backupStorageLocation")`
|
||||
);
|
||||
if (!oldPath || oldPath === PATHS.backupsDirectory) return;
|
||||
config.backupDirectory = oldPath;
|
||||
config.backupDirectory = normalizePathString(oldPath);
|
||||
} catch (e) {
|
||||
console.error("Failed to migrate backup directory", e);
|
||||
const pressedButton = dialog.showMessageBoxSync(globalThis.window, {
|
||||
|
||||
@@ -20,11 +20,30 @@ 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) {
|
||||
if (isAbsolute(_path)) return _path;
|
||||
const normalizedPath = normalizePathString(_path);
|
||||
if (isAbsolute(normalizedPath)) return normalizedPath;
|
||||
|
||||
return join(
|
||||
..._path.split("/").map((segment) => {
|
||||
...normalizedPath.split("/").map((segment) => {
|
||||
let resolved = segment;
|
||||
try {
|
||||
resolved = app.getPath(resolved as any);
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ android {
|
||||
if (project.hasProperty("prBuildNumber")) {
|
||||
versionCode Integer.parseInt(prBuildNumber())
|
||||
} else {
|
||||
versionCode 3108
|
||||
versionCode 3113
|
||||
}
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
@@ -233,6 +233,9 @@ 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,8 +86,9 @@
|
||||
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
|
||||
@@ -95,6 +96,15 @@
|
||||
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"
|
||||
@@ -230,11 +240,6 @@
|
||||
</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,15 +5,11 @@ 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 {
|
||||
|
||||
@@ -40,18 +36,32 @@ public class NotePreviewConfigureActivity extends ReactActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
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;
|
||||
}
|
||||
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 resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
setResult(Activity.RESULT_CANCELED, resultValue);
|
||||
activity = this;
|
||||
}
|
||||
|
||||
public static void saveAndFinish(Context context) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
@@ -8,51 +7,125 @@ 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("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), "");
|
||||
if (data.isEmpty()) {
|
||||
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);
|
||||
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, getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.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);
|
||||
}
|
||||
|
||||
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;
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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;
|
||||
|
||||
@@ -24,20 +22,10 @@ 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, getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.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,7 +12,6 @@ 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;
|
||||
@@ -29,7 +28,6 @@ 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;
|
||||
@@ -138,7 +136,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(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId));
|
||||
map.putString(ReminderWidgetProvider.OpenReminderId, extras.getString(ReminderWidgetProvider.OpenReminderId));
|
||||
} else if (Objects.equals(extras.getString(IntentType), "OpenNote")) {
|
||||
map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId));
|
||||
}
|
||||
@@ -156,14 +154,8 @@ 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(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);
|
||||
for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) {
|
||||
arr.pushString(note.getId());
|
||||
}
|
||||
promise.resolve(arr);
|
||||
@@ -171,36 +163,46 @@ 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(Map.Entry<String,?> entry : map.entrySet()){
|
||||
String value = (String) entry.getValue();
|
||||
if (value.contains(noteId)) {
|
||||
for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) {
|
||||
if (note.getId().equals(noteId)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
promise.resolve(found);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateWidgetNote(final String noteId, final String data) {
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Map<String, ?> map = pref.getAll();
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = pref.edit();
|
||||
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());
|
||||
}
|
||||
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());
|
||||
}
|
||||
edit.apply();
|
||||
for (String id: ids) {
|
||||
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id));
|
||||
|
||||
for (int id : ids) {
|
||||
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), 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);
|
||||
@@ -208,8 +210,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
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,18 +1,22 @@
|
||||
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) {
|
||||
@@ -23,20 +27,10 @@ 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, getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, WidgetUtils.getActivityOptionsBundle());
|
||||
views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent);
|
||||
|
||||
Intent new_reminder_intent = new Intent(context, MainActivity.class);
|
||||
@@ -44,13 +38,31 @@ 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, getActivityOptionsBundle());
|
||||
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.add_button, pendingIntent2);
|
||||
|
||||
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);
|
||||
// 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());
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
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,13 +2,14 @@ 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"
|
||||
@@ -107,32 +108,27 @@ public class Reminder extends BaseItem {
|
||||
this.formattedTime = formattedTime;
|
||||
}
|
||||
|
||||
public String formatTime(long timeInMillis) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long diff = timeInMillis - currentTime;
|
||||
public String getFormattedTimeOfDay() {
|
||||
return formattedTimeOfDay;
|
||||
}
|
||||
|
||||
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" : "");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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="10dp"/>
|
||||
<corners android:radius="@dimen/widget_background_radius"/>
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -2,8 +2,7 @@
|
||||
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:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
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:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -25,7 +24,7 @@
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:text="Widget unconfigured" />
|
||||
android:text="@string/widget_note_unconfigured_title" />
|
||||
<TextView
|
||||
android:id="@+id/widget_body"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -34,7 +33,7 @@
|
||||
android:layout_marginLeft="8dp"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="14sp"
|
||||
android:text="Configure this widget to show a note here." />
|
||||
android:text="@string/widget_note_unconfigured_body" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<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,22 +12,23 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textSize="16sp"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Upcoming Reminders"/>
|
||||
android:text="Reminders"/>
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="35dp"
|
||||
android:layout_width="25dp"
|
||||
android:id="@+id/add_button"
|
||||
android:layout_height="35dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:background="@drawable/ic_newnote" />
|
||||
</RelativeLayout>
|
||||
@@ -57,7 +58,8 @@
|
||||
android:layout_height="match_parent"
|
||||
android:textAlignment="center"
|
||||
android:gravity="center"
|
||||
android:text="Tap on + to add reminder"/>
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/widget_reminders_empty"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?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,9 +1,5 @@
|
||||
<?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>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
@@ -1,6 +0,0 @@
|
||||
<resources>
|
||||
<declare-styleable name="AppWidgetAttrs">
|
||||
<attr name="appWidgetBackgroundColor" format="color" />
|
||||
<attr name="appWidgetTextColor" format="color" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -1,9 +1,5 @@
|
||||
<?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,4 +7,8 @@ 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,9 +4,28 @@
|
||||
<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 upcoming reminders</string>
|
||||
<string name="reminders">Quick overview of 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>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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"/>
|
||||
@@ -0,0 +1,15 @@
|
||||
<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,6 +10,7 @@
|
||||
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,6 +12,7 @@
|
||||
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,14 +1,15 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_reminders"
|
||||
android:minWidth="400dp"
|
||||
android:minHeight="100dp"
|
||||
android:minResizeWidth="400dp"
|
||||
android:minResizeHeight="50dp"
|
||||
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:updatePeriodMillis="1024"
|
||||
android:previewLayout="@layout/widget_reminders_preview"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen"
|
||||
/>
|
||||
@@ -1,7 +1,4 @@
|
||||
- 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
|
||||
- Add option to clear note version history
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
@@ -66,12 +66,17 @@ import {
|
||||
VAULT_ERRORS
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
|
||||
export const VaultDialog: React.FC = () => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
// UI State
|
||||
const [visible, setVisible] = useState(false);
|
||||
const isUserLoggedIn = useUserStore((state) => !!state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
const [biometricUnlock, setBiometricUnlock] = useState(false);
|
||||
@@ -196,9 +201,12 @@ export const VaultDialog: React.FC = () => {
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
close();
|
||||
}, 100);
|
||||
close();
|
||||
ToastManager.show({
|
||||
message: strings.vaultDeleted(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
formRef.current.setError("password", strings.passwordIncorrect());
|
||||
@@ -489,6 +497,13 @@ export const VaultDialog: React.FC = () => {
|
||||
|
||||
const { password, newPassword } = formRef.current.getValues();
|
||||
|
||||
if (requestType === VaultRequestType.DeleteVault && !isUserLoggedIn) {
|
||||
setLoading(true);
|
||||
await deleteVault();
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestType === VaultRequestType.CreateVault) {
|
||||
createVault();
|
||||
} else if (requestType === VaultRequestType.ChangePassword) {
|
||||
@@ -629,16 +644,22 @@ export const VaultDialog: React.FC = () => {
|
||||
// Auto-unlock with fingerprint if applicable
|
||||
const canAutoUnlock =
|
||||
fingerprint &&
|
||||
available &&
|
||||
data.requestType !== VaultRequestType.EnableFingerprint &&
|
||||
data.requestType !== VaultRequestType.RevokeFingerprint &&
|
||||
data.requestType !== VaultRequestType.ChangePassword &&
|
||||
data.requestType !== VaultRequestType.ClearVault &&
|
||||
data.requestType !== VaultRequestType.DeleteVault &&
|
||||
data.requestType !== VaultRequestType.CustomAction &&
|
||||
data.requestType !== VaultRequestType.PermanentUnlock;
|
||||
data.requestType !== VaultRequestType.PermanentUnlock &&
|
||||
data.requestType !== VaultRequestType.CreateVault;
|
||||
|
||||
if (canAutoUnlock) {
|
||||
await onPressFingerprintAuth(data.title, data.description);
|
||||
if (canAutoUnlock && available) {
|
||||
try {
|
||||
await onPressFingerprintAuth(data.title, data.description);
|
||||
} catch (e) {
|
||||
setVisible(true);
|
||||
}
|
||||
} else {
|
||||
setVisible(true);
|
||||
}
|
||||
@@ -700,7 +721,6 @@ export const VaultDialog: React.FC = () => {
|
||||
icon="shield"
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
<View
|
||||
style={{
|
||||
@@ -709,8 +729,14 @@ export const VaultDialog: React.FC = () => {
|
||||
>
|
||||
{(isChangePassword ||
|
||||
isClearVault ||
|
||||
!isCreateVault ||
|
||||
isDeleteVault ||
|
||||
requestType === VaultRequestType.LockNote ||
|
||||
requestType === VaultRequestType.UnlockNote ||
|
||||
requestType === VaultRequestType.PermanentUnlock ||
|
||||
isGoToEditor ||
|
||||
isShareNote ||
|
||||
isDeleteNote ||
|
||||
isEnableFingerprint ||
|
||||
(isDeleteVault && isUserLoggedIn) ||
|
||||
isCustomAction) &&
|
||||
!isRevokeFingerprint ? (
|
||||
<>
|
||||
@@ -772,20 +798,32 @@ export const VaultDialog: React.FC = () => {
|
||||
) : null}
|
||||
|
||||
{isDeleteVault && (
|
||||
<Button
|
||||
<Pressable
|
||||
onPress={() => setDeleteAll(!deleteAll)}
|
||||
icon={
|
||||
deleteAll
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingVertical: 0,
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
marginTop: isUserLoggedIn
|
||||
? DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
: 0,
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
width="100%"
|
||||
title={strings.deleteAllNotes()}
|
||||
type="errorShade"
|
||||
/>
|
||||
>
|
||||
<AppIcon
|
||||
name={
|
||||
deleteAll
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
color={colors.error.accent}
|
||||
size={AppFontSize.md}
|
||||
/>
|
||||
|
||||
<Paragraph color={colors.error.accent} size={AppFontSize.sm}>
|
||||
{strings.deleteAllNotes()}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{isChangePassword ? (
|
||||
|
||||
@@ -39,6 +39,7 @@ import Share from "react-native-share";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
|
||||
const ImagePreview = () => {
|
||||
const { colors } = useThemeColors("dialog");
|
||||
@@ -87,9 +88,8 @@ const ImagePreview = () => {
|
||||
return;
|
||||
}
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
const path = `${cacheDir}/${
|
||||
"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
|
||||
}`;
|
||||
const path = `${cacheDir}/${"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
|
||||
}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
@@ -136,17 +136,23 @@ const ImagePreview = () => {
|
||||
borderWidth: 0
|
||||
}}
|
||||
onPress={async () => {
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
await Share.open({
|
||||
url: image
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(false);
|
||||
if (!image) return;
|
||||
|
||||
try {
|
||||
if (Platform.OS === "android") {
|
||||
await FileViewer.open(image.replace("file://", ""), {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any);
|
||||
} else {
|
||||
await Share.open({
|
||||
url: image
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
|
||||
@@ -72,6 +72,7 @@ const MergeConflicts = () => {
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const content = useRef<UnencryptedContentItem>(null);
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const keepScreenOn = useSettingStore((state) => state.settings.keepScreenOn);
|
||||
|
||||
const applyChanges = async () => {
|
||||
const contentToSave = selectedContent;
|
||||
@@ -155,9 +156,9 @@ const MergeConflicts = () => {
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
@@ -182,9 +183,9 @@ const MergeConflicts = () => {
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
@@ -385,7 +386,7 @@ const MergeConflicts = () => {
|
||||
paddingTop: insets.top
|
||||
}}
|
||||
>
|
||||
<KeepAwake />
|
||||
{keepScreenOn && <KeepAwake />}
|
||||
{dialogVisible && (
|
||||
<BaseDialog visible={true}>
|
||||
<DialogContainer>
|
||||
|
||||
@@ -29,7 +29,7 @@ import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
@@ -39,15 +39,19 @@ import { Pressable } from "../ui/pressable";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import NotePreview from "./preview";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Dialog } from "../dialog";
|
||||
|
||||
const HistoryItem = ({
|
||||
index,
|
||||
items,
|
||||
note
|
||||
note,
|
||||
refresh
|
||||
}: {
|
||||
index: number;
|
||||
items?: VirtualizedGrouping<HistorySession>;
|
||||
note: Note;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const [item] = useDBItem(index, "noteHistory", items);
|
||||
const { colors } = useThemeColors();
|
||||
@@ -75,12 +79,13 @@ const HistoryItem = ({
|
||||
}}
|
||||
content={content}
|
||||
note={note}
|
||||
update={refresh}
|
||||
/>
|
||||
),
|
||||
context: "note_history"
|
||||
});
|
||||
},
|
||||
[note]
|
||||
[note, refresh]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -112,7 +117,8 @@ const HistoryItem = ({
|
||||
};
|
||||
|
||||
export default function NoteHistory({
|
||||
note
|
||||
note,
|
||||
fwdRef
|
||||
}: {
|
||||
note: Note;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
@@ -121,7 +127,7 @@ export default function NoteHistory({
|
||||
const [_loading, setLoading] = useState(true);
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = useCallback(() => {
|
||||
db.noteHistory
|
||||
.get(note.id)
|
||||
.sorted({
|
||||
@@ -137,17 +143,56 @@ export default function NoteHistory({
|
||||
});
|
||||
}, [note.id]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ index }: { index: number }) => (
|
||||
<HistoryItem index={index} items={history} note={note} />
|
||||
<HistoryItem
|
||||
index={index}
|
||||
items={history}
|
||||
note={note}
|
||||
refresh={refresh}
|
||||
/>
|
||||
),
|
||||
[history, note]
|
||||
[history, note, refresh]
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<SheetProvider context="note_history" />
|
||||
<DialogHeader title={strings.noteHistory()} padding={12} />
|
||||
<Dialog context="local" />
|
||||
<DialogHeader
|
||||
title={strings.noteHistory()}
|
||||
padding={12}
|
||||
button={
|
||||
history?.placeholders.length
|
||||
? {
|
||||
title: strings.clearHistory(),
|
||||
type: "secondary",
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
title: strings.clearHistory(),
|
||||
paragraph: strings.clearHistoryConfirmation(),
|
||||
positiveText: strings.clear(),
|
||||
negativeText: strings.cancel(),
|
||||
positiveType: "errorShade",
|
||||
context: "local",
|
||||
positivePress: async () => {
|
||||
await db.noteHistory.clearSessions(note.id);
|
||||
fwdRef.current?.hide();
|
||||
ToastManager.show({
|
||||
heading: strings.historyCleared(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<Seperator />
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ import {
|
||||
export default function NotePreview({
|
||||
session,
|
||||
content,
|
||||
note
|
||||
note,
|
||||
update
|
||||
}: {
|
||||
session?: HistorySession & { session: string };
|
||||
content:
|
||||
@@ -64,6 +65,7 @@ export default function NotePreview({
|
||||
>
|
||||
| undefined;
|
||||
note: TrashOrItem<Note>;
|
||||
update?: () => void;
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const [locked, setLocked] = useState(false);
|
||||
@@ -107,24 +109,35 @@ export default function NotePreview({
|
||||
}, [note]);
|
||||
|
||||
const deleteNote = async () => {
|
||||
const isSession = !!session;
|
||||
|
||||
presentDialog({
|
||||
title: strings.deleteNote(),
|
||||
paragraph: strings.deleteNoteConfirmation(),
|
||||
title: isSession ? strings.deleteVersion() : strings.deleteNote(),
|
||||
paragraph: isSession
|
||||
? strings.deleteVersionConfirmation()
|
||||
: strings.deleteNoteConfirmation(),
|
||||
positiveText: strings.delete(),
|
||||
negativeText: strings.cancel(),
|
||||
context: "local",
|
||||
context: "preview",
|
||||
positivePress: async () => {
|
||||
if (note) {
|
||||
if (isSession) {
|
||||
await db.noteHistory.remove(session.id);
|
||||
update?.();
|
||||
eSendEvent(eCloseSheet, "note_history");
|
||||
Navigation.queueRoutesForUpdate();
|
||||
} else if (note.type === "trash") {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().refresh();
|
||||
useSelectionStore.getState().setSelectionMode();
|
||||
ToastManager.show({
|
||||
heading: strings.noteDeleted(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
eSendEvent(eCloseSheet);
|
||||
}
|
||||
ToastManager.show({
|
||||
heading: isSession ? strings.versionDeleted() : strings.noteDeleted(),
|
||||
type: "success",
|
||||
context: isSession ? "local" : "global"
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
positiveType: "error"
|
||||
});
|
||||
@@ -137,7 +150,7 @@ export default function NotePreview({
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<Dialog context="preview" />
|
||||
<DialogHeader
|
||||
padding={12}
|
||||
title={content?.title || note.title || session?.session}
|
||||
|
||||
@@ -112,7 +112,8 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
}
|
||||
setStep(Steps.buy);
|
||||
}
|
||||
}, [pricingPlans, routeParams.state]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [routeParams.state]);
|
||||
|
||||
useEffect(() => {
|
||||
let listener: NativeEventSubscription;
|
||||
|
||||
@@ -170,147 +170,147 @@ export const SelectionHeader = React.memo(
|
||||
{!selectedItemsList.length
|
||||
? null
|
||||
: (focusedRouteId === "Monographs"
|
||||
? [
|
||||
{
|
||||
title: strings.unpublish(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
title: strings.doActions.unpublish.note(
|
||||
selectedItemsList.length
|
||||
),
|
||||
positiveText: strings.unpublish(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
for (const id of selectedItemsList) {
|
||||
await db.monographs.unpublish(id);
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
},
|
||||
visible: true,
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: strings.manageTags(),
|
||||
onPress: async () => {
|
||||
ManageTags.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "pound"
|
||||
},
|
||||
{
|
||||
title: strings.export(),
|
||||
onPress: async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "export"
|
||||
},
|
||||
{
|
||||
title: strings.addToNotebook(),
|
||||
onPress: async () => {
|
||||
Navigation.navigate("LinkNotebooks", {
|
||||
noteIds: selectedItemsList
|
||||
});
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "plus"
|
||||
},
|
||||
{
|
||||
title: strings.unlinkNotebook(),
|
||||
onPress: async () => {
|
||||
if (!id) return;
|
||||
await db.notes.removeFromNotebook(
|
||||
id,
|
||||
...selectedItemsList
|
||||
);
|
||||
updateNotebook(id);
|
||||
? [
|
||||
{
|
||||
title: strings.unpublish(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
title: strings.doActions.unpublish.note(
|
||||
selectedItemsList.length
|
||||
),
|
||||
positiveText: strings.unpublish(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
for (const id of selectedItemsList) {
|
||||
await db.monographs.unpublish(id);
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
visible: renderedInRoute === "Notebook",
|
||||
icon: "minus"
|
||||
},
|
||||
{
|
||||
title: strings.unfavorite(),
|
||||
onPress: addToFavorite,
|
||||
visible: focusedRouteId === "Favorites",
|
||||
icon: "star-off"
|
||||
},
|
||||
{
|
||||
title: strings.moveToTrash(),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible: type === "note" || type === "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.doActions.delete.unknown(
|
||||
type!,
|
||||
selectedItemsList.length
|
||||
),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible:
|
||||
type !== "trash" &&
|
||||
type !== "note" &&
|
||||
type !== "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.restore(),
|
||||
onPress: restoreItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete-restore"
|
||||
},
|
||||
{
|
||||
title: strings.delete(),
|
||||
onPress: deleteItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
).map((item) =>
|
||||
!item.visible ? null : (
|
||||
<IconButton
|
||||
size={AppFontSize.lg}
|
||||
type="plain"
|
||||
tooltipText={item.title}
|
||||
tooltipPosition={NativeTooltip.POSITIONS.TOP}
|
||||
testID={`select-${item.icon}`}
|
||||
name={item.icon}
|
||||
key={item.title}
|
||||
color={colors.primary.icon}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
if (Platform.OS === "ios") await sleep(300);
|
||||
item.onPress();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
},
|
||||
visible: true,
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: strings.manageTags(),
|
||||
onPress: async () => {
|
||||
ManageTags.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "pound"
|
||||
},
|
||||
{
|
||||
title: strings.export(),
|
||||
onPress: async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "export"
|
||||
},
|
||||
{
|
||||
title: strings.addToNotebook(),
|
||||
onPress: async () => {
|
||||
Navigation.navigate("LinkNotebooks", {
|
||||
noteIds: selectedItemsList
|
||||
});
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "plus"
|
||||
},
|
||||
{
|
||||
title: strings.unlinkNotebook(),
|
||||
onPress: async () => {
|
||||
if (!id) return;
|
||||
await db.notes.removeFromNotebook(
|
||||
id,
|
||||
...selectedItemsList
|
||||
);
|
||||
updateNotebook(id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
visible: renderedInRoute === "Notebook",
|
||||
icon: "minus"
|
||||
},
|
||||
{
|
||||
title: strings.unfavorite(),
|
||||
onPress: addToFavorite,
|
||||
visible: focusedRouteId === "Favorites",
|
||||
icon: "star-off"
|
||||
},
|
||||
{
|
||||
title: strings.moveToTrash(),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible: type === "note" || type === "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.doActions.delete.unknown(
|
||||
type!,
|
||||
selectedItemsList.length
|
||||
),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible:
|
||||
type !== "trash" &&
|
||||
type !== "note" &&
|
||||
type !== "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.restore(),
|
||||
onPress: restoreItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete-restore"
|
||||
},
|
||||
{
|
||||
title: strings.delete(),
|
||||
onPress: deleteItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
).map((item) =>
|
||||
!item.visible ? null : (
|
||||
<IconButton
|
||||
size={AppFontSize.lg}
|
||||
type="plain"
|
||||
tooltipText={item.title}
|
||||
tooltipPosition={NativeTooltip.POSITIONS.TOP}
|
||||
testID={`select-${item.icon}`}
|
||||
name={item.icon}
|
||||
key={item.title}
|
||||
color={colors.primary.icon}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
if (Platform.OS === "ios") await sleep(300);
|
||||
item.onPress();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
|
||||
@@ -28,7 +28,7 @@ import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
@@ -40,6 +40,8 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
@@ -118,8 +120,8 @@ const ListBlockItem = ({
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: !item.content || item.content.trim() === ""
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
@@ -158,6 +160,7 @@ export default function LinkNote(props: {
|
||||
|
||||
const [selectedNote, setSelectedNote] = useState<Note>();
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string>();
|
||||
const [blocksLoading, setBlocksLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
|
||||
@@ -206,10 +209,12 @@ export default function LinkNote(props: {
|
||||
|
||||
const onSelectNote = async (note: Note) => {
|
||||
setSelectedNote(note);
|
||||
setBlocksLoading(true);
|
||||
inputRef.current?.clear();
|
||||
setTimeout(async () => {
|
||||
nodesRef.current = await db.notes.contentBlocks(note.id);
|
||||
setNodes(nodesRef.current);
|
||||
setBlocksLoading(false);
|
||||
});
|
||||
// Fetch and set note's nodes.
|
||||
};
|
||||
@@ -224,6 +229,7 @@ export default function LinkNote(props: {
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingBottom: DefaultAppStyles.GAP,
|
||||
minHeight: "100%",
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
@@ -330,28 +336,42 @@ export default function LinkNote(props: {
|
||||
windowSize={3}
|
||||
keyExtractor={(item) => item.id}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{blockLinking?.error}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
!blockLinking || blocksLoading ? (
|
||||
<ActivityIndicator size={25} color={colors.primary.accent} />
|
||||
) : !blockLinking.isAllowed ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
alignItems: "center"
|
||||
}}
|
||||
type="accent"
|
||||
/>
|
||||
</View>
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{blockLinking?.error}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
onPress={() => {
|
||||
Navigation.navigate("PayWall", {
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out",
|
||||
canGoBack: true
|
||||
});
|
||||
|
||||
props.close?.();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
data={blockLinking?.isAllowed ? nodes : []}
|
||||
/>
|
||||
|
||||
@@ -198,7 +198,7 @@ const PublishNoteSheet = ({
|
||||
};
|
||||
|
||||
const monographMetadata =
|
||||
monographData.result ?? lastMonographDataResult.current;
|
||||
monographData.result?.metadata ?? lastMonographDataResult.current;
|
||||
|
||||
useEffect(() => {
|
||||
const prevState = previousAppState.current;
|
||||
@@ -432,7 +432,7 @@ const PublishNoteSheet = ({
|
||||
{isFeatureAvailable?.isAllowed &&
|
||||
!selfDestruct &&
|
||||
monographMetadata &&
|
||||
monographMetadata?.totalViews > 0 ? (
|
||||
monographMetadata?.analytics?.totalViews > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -456,7 +456,9 @@ const PublishNoteSheet = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
|
||||
<Paragraph>{monographMetadata?.totalViews || 0}</Paragraph>
|
||||
<Paragraph>
|
||||
{monographMetadata?.analytics?.totalViews || 0}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -156,7 +156,8 @@ export function MenuItem({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{renderIcon ? (
|
||||
@@ -185,6 +186,7 @@ export function MenuItem({
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{ flexShrink: 1 }}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
@@ -195,6 +197,9 @@ export function MenuItem({
|
||||
color={
|
||||
isFocused ? colors.primary.paragraph : colors.secondary.paragraph
|
||||
}
|
||||
style={{
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{menuItemCount}
|
||||
</Paragraph>
|
||||
|
||||
@@ -72,7 +72,7 @@ export const NotebookItem = ({
|
||||
const notebook = item.notebook;
|
||||
const isFocused = focused;
|
||||
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
const getTotalNotesRef = React.useRef(getTotalNotes);
|
||||
getTotalNotesRef.current = getTotalNotes;
|
||||
const { colors } = useThemeColors();
|
||||
@@ -101,8 +101,8 @@ export const NotebookItem = ({
|
||||
item.depth === 0
|
||||
? undefined
|
||||
: item.depth < 6
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
width: "100%",
|
||||
marginTop: 2,
|
||||
opacity: item.disabled ? 0.5 : 1
|
||||
@@ -162,7 +162,8 @@ export const NotebookItem = ({
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@@ -191,8 +192,8 @@ export const NotebookItem = ({
|
||||
!item.hasChildren || disableExpand
|
||||
? "book-outline"
|
||||
: expanded
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -201,6 +202,7 @@ export const NotebookItem = ({
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{ flexShrink: 1 }}
|
||||
>
|
||||
{notebook?.title}
|
||||
</Paragraph>
|
||||
@@ -211,7 +213,8 @@ export const NotebookItem = ({
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{selectionEnabled ? (
|
||||
|
||||
@@ -56,7 +56,7 @@ const TagItem = (props: {
|
||||
const totalNotes = useTotalNotes("tag");
|
||||
const totalNotesRef = React.useRef(totalNotes);
|
||||
totalNotesRef.current = totalNotes;
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.id) {
|
||||
@@ -67,6 +67,8 @@ const TagItem = (props: {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 35,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop:
|
||||
(props.id as number) === 0 ? DefaultAppStyles.GAP_VERTICAL : 2
|
||||
@@ -123,7 +125,8 @@ const TagItem = (props: {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -146,6 +149,9 @@ const TagItem = (props: {
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item?.title}
|
||||
</Paragraph>
|
||||
@@ -173,6 +179,9 @@ const TagItem = (props: {
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{totalNotes.totalNotes(item?.id)}
|
||||
</Paragraph>
|
||||
|
||||
@@ -1333,7 +1333,7 @@ export const useActions = ({
|
||||
icon: "link",
|
||||
onPress: () => {
|
||||
const link = createInternalLink(item.type, item.id);
|
||||
Clipboard.setString(link);
|
||||
Clipboard.setHTML(`<a href="${link}" >${item.title}</a>`, link);
|
||||
ToastManager.show({
|
||||
heading: strings.linkCopied(),
|
||||
message: link,
|
||||
|
||||
@@ -81,6 +81,7 @@ import {
|
||||
setUpdateAvailableMessage
|
||||
} from "../services/message";
|
||||
import Navigation from "../services/navigation";
|
||||
import { NotePreviewWidget } from "../services/note-preview-widget";
|
||||
import Notifications from "../services/notifications";
|
||||
import PremiumService from "../services/premium";
|
||||
import SettingsService from "../services/settings";
|
||||
@@ -574,6 +575,11 @@ export const useAppEvents = () => {
|
||||
useEffect(() => {
|
||||
if (isAppLoading) return;
|
||||
|
||||
// Widgets outlive the app process, so they can be left showing content the app no longer has
|
||||
// (most obviously after the user clears app data). Nothing can run at that moment, so the
|
||||
// first launch afterwards is the earliest chance to put them right.
|
||||
NotePreviewWidget.updateNotes();
|
||||
|
||||
let subscriptions: EventManagerSubscription[] = [];
|
||||
const eventManager = db.eventManager;
|
||||
subscriptions = [
|
||||
@@ -766,6 +772,26 @@ export const useAppEvents = () => {
|
||||
db.eventManager.subscribe(EVENTS.uploadCanceled, (data) => {
|
||||
useAttachmentStore.getState().setUploading(data);
|
||||
}),
|
||||
db.eventManager.subscribeMulti(
|
||||
[EVENTS.vaultAutoLocked, EVENTS.vaultLocked],
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
message: strings.vaultLocked(),
|
||||
type: "info"
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
undefined
|
||||
),
|
||||
db.eventManager.subscribe(EVENTS.vaultUnlocked, () => {
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
message: strings.vaultUnlocked(),
|
||||
type: "info"
|
||||
});
|
||||
}, 300);
|
||||
}),
|
||||
EV.subscribe(EVENTS.migrationStarted, (name) => {
|
||||
if (
|
||||
name !== "notesnook" ||
|
||||
@@ -820,7 +846,13 @@ export const useAppEvents = () => {
|
||||
|
||||
return () => {
|
||||
emitterSubscriptions.forEach((sub) => sub?.remove?.());
|
||||
subscriptions.forEach((sub) => sub?.unsubscribe?.());
|
||||
subscriptions.forEach((sub) => {
|
||||
if (Array.isArray(sub)) {
|
||||
sub.forEach((s) => s.unsubscribe());
|
||||
} else {
|
||||
sub?.unsubscribe?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [onSyncComplete, onUserUpdated]);
|
||||
|
||||
|
||||
@@ -447,7 +447,9 @@ const onChangeTab = async (event: { i: number; from: number }) => {
|
||||
if (event.i === 2) {
|
||||
editorState().movedAway = false;
|
||||
editorState().isFocused = true;
|
||||
activateKeepAwake();
|
||||
if (useSettingStore.getState().settings.keepScreenOn) {
|
||||
activateKeepAwake();
|
||||
}
|
||||
eSendEvent(eOnEnterEditor);
|
||||
|
||||
if (
|
||||
|
||||
@@ -949,6 +949,7 @@ export const useEditor = (
|
||||
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
|
||||
);
|
||||
if (
|
||||
ignoreEdit ||
|
||||
lock.current ||
|
||||
(currentLoadingNoteId.current &&
|
||||
currentLoadingNoteId.current === noteId)
|
||||
@@ -957,6 +958,7 @@ export const useEditor = (
|
||||
|
||||
lock.current: ${lock.current}
|
||||
currentLoadingNoteId.current: ${currentLoadingNoteId.current}
|
||||
ignoreEdit: ${ignoreEdit}
|
||||
`);
|
||||
if (lock.current) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -163,7 +163,20 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
|
||||
}, [noteIds]);
|
||||
|
||||
const onSave = async () => {
|
||||
const changedNotebooks = useNotebookSelectionStore.getState().selection;
|
||||
const selectionState = useNotebookSelectionStore.getState();
|
||||
const changedNotebooks = { ...selectionState.selection };
|
||||
|
||||
if (!selectionState.multiSelect) {
|
||||
for (const id in selectionState.initialState) {
|
||||
if (
|
||||
selectionState.initialState[id] === "selected" &&
|
||||
changedNotebooks[id] !== "selected"
|
||||
) {
|
||||
changedNotebooks[id] = "deselected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in changedNotebooks) {
|
||||
const item = await db.notebooks.notebook(id);
|
||||
if (!item) continue;
|
||||
@@ -207,11 +220,11 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
|
||||
rightButton={
|
||||
hasSelection
|
||||
? {
|
||||
name: "restore",
|
||||
onPress: () => {
|
||||
updateInitialSelectionState(noteIds);
|
||||
}
|
||||
name: "restore",
|
||||
onPress: () => {
|
||||
updateInitialSelectionState(noteIds);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -398,7 +398,8 @@ const TagItem = ({
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "flex-start",
|
||||
height: 40
|
||||
minHeight: 40,
|
||||
alignItems: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!tag) return;
|
||||
@@ -438,7 +439,9 @@ const TagItem = ({
|
||||
/>
|
||||
)}
|
||||
{tag ? (
|
||||
<Paragraph size={AppFontSize.sm}>{"#" + tag?.title}</Paragraph>
|
||||
<Paragraph style={{ flexShrink: 1 }} size={AppFontSize.sm}>
|
||||
{"#" + tag?.title}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -168,7 +168,7 @@ export const FailedInboxItems = () => {
|
||||
const result = usePromise(() => db.inboxItemsHistory.failed.items());
|
||||
|
||||
async function deleteItem(id: string) {
|
||||
await db.inboxItemsHistory.delete(id);
|
||||
await db.inboxItemsHistory.delete([id]);
|
||||
ToastManager.show({
|
||||
message: strings.itemDeleted(),
|
||||
type: "success"
|
||||
|
||||
@@ -210,7 +210,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
: status === SubscriptionStatus.ACTIVE
|
||||
? strings.subRenewOn(expiryDate)
|
||||
: status === SubscriptionStatus.CANCELED ||
|
||||
status === SubscriptionStatus.PAUSED
|
||||
status === SubscriptionStatus.PAUSED
|
||||
? strings.subEndsOn(expiryDate)
|
||||
: status === SubscriptionStatus.EXPIRED
|
||||
? subscriptionDaysLeft.time < -3
|
||||
@@ -965,6 +965,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
property: "checkForUpdates",
|
||||
icon: "update"
|
||||
},
|
||||
{
|
||||
id: "keep-screen-on",
|
||||
type: "switch",
|
||||
name: strings.keepScreenOn(),
|
||||
description: strings.keepScreenOnDesc(),
|
||||
property: "keepScreenOn",
|
||||
icon: "cellphone-screenshot",
|
||||
},
|
||||
{
|
||||
id: "image-compression",
|
||||
type: "component",
|
||||
@@ -1839,7 +1847,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: Platform.OS === "ios" ? "3.4.0-beta.1" : getVersion()
|
||||
description: getVersion()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ export const NotePreviewWidget = {
|
||||
|
||||
NotesnookModule.updateWidgetNote(id, JSON.stringify(newNote));
|
||||
}
|
||||
// Redraw from the widgets that actually exist rather than only the ones we
|
||||
// have notes for. After app data is cleared there are none, and the widgets
|
||||
// would otherwise keep showing content that no longer exists.
|
||||
NotesnookModule.refreshWidgets();
|
||||
}, 500);
|
||||
},
|
||||
updateNote: async (id: string, note: Note) => {
|
||||
|
||||
@@ -17,8 +17,12 @@ 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 { getFormattedReminderTime } from "@notesnook/common";
|
||||
import { isReminderActive, Reminder } from "@notesnook/core";
|
||||
import { getFormattedDate, getFormattedReminderTime } from "@notesnook/common";
|
||||
import {
|
||||
getUpcomingReminderTime,
|
||||
isReminderActive,
|
||||
Reminder
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import notifee, {
|
||||
AndroidStyle,
|
||||
@@ -35,7 +39,7 @@ import notifee, {
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { encodeNonAsciiHTML } from "entities";
|
||||
import { Platform } from "react-native";
|
||||
import { AppState, Platform } from "react-native";
|
||||
import { db, setupDatabase } from "../common/database";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
@@ -45,7 +49,7 @@ import { useRelationStore } from "../stores/use-relation-store";
|
||||
import { useReminderStore } from "../stores/use-reminder-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import { eOnLoadNote } from "../utils/events";
|
||||
import { eCloseSimpleDialog, eOnLoadNote } from "../utils/events";
|
||||
import { fluidTabsRef } from "../utils/global-refs";
|
||||
import { convertNoteToText } from "../utils/note-to-text";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
@@ -262,8 +266,13 @@ const onEvent = async ({ type, detail }: Event) => {
|
||||
|
||||
type ReminderWithFormattedTime = Reminder & {
|
||||
formattedTime?: string;
|
||||
triggerDate?: number;
|
||||
formattedTimeOfDay?: string;
|
||||
formattedDateTime?: string;
|
||||
};
|
||||
|
||||
const RECENTLY_PASSED_WINDOW = 3 * 60 * 60 * 1000;
|
||||
|
||||
async function updateRemindersForWidget() {
|
||||
if (Platform.OS === "ios") return;
|
||||
const reminders: ReminderWithFormattedTime[] = await db.reminders?.all.items(
|
||||
@@ -273,18 +282,33 @@ async function updateRemindersForWidget() {
|
||||
sortDirection: "asc"
|
||||
}
|
||||
);
|
||||
const activeReminders = [];
|
||||
const widgetReminders = [];
|
||||
if (!reminders) return;
|
||||
for (const reminder of reminders) {
|
||||
if (isReminderActive(reminder)) {
|
||||
reminder.formattedTime = getFormattedReminderTime(reminder);
|
||||
activeReminders.push(reminder);
|
||||
}
|
||||
const triggerDate =
|
||||
reminder.snoozeUntil && reminder.snoozeUntil > Date.now()
|
||||
? reminder.snoozeUntil
|
||||
: reminder.mode === "repeat"
|
||||
? getUpcomingReminderTime(reminder)
|
||||
: reminder.date;
|
||||
|
||||
const recentlyPassed =
|
||||
reminder.mode === "once" &&
|
||||
!reminder.disabled &&
|
||||
triggerDate > Date.now() - RECENTLY_PASSED_WINDOW;
|
||||
|
||||
if (!isReminderActive(reminder) && !recentlyPassed) continue;
|
||||
|
||||
reminder.triggerDate = triggerDate;
|
||||
reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time");
|
||||
reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time");
|
||||
reminder.formattedTime = getFormattedReminderTime(reminder);
|
||||
widgetReminders.push(reminder);
|
||||
}
|
||||
NotesnookModule.setString(
|
||||
"appPreview",
|
||||
"remindersList",
|
||||
JSON.stringify(activeReminders)
|
||||
JSON.stringify(widgetReminders)
|
||||
);
|
||||
NotesnookModule.updateReminderWidget();
|
||||
}
|
||||
@@ -577,7 +601,7 @@ async function displayNotification({
|
||||
}
|
||||
|
||||
function openSettingsDialog(context: string) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
presentDialog({
|
||||
title: strings.notificationsDisabled(),
|
||||
paragraph: strings.notificationsDisabledDesc(),
|
||||
@@ -597,49 +621,94 @@ function openSettingsDialog(context: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function waitForAppFocusAfterSettings(timeoutMs = 120000): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let didLeaveApp = AppState.currentState !== "active";
|
||||
const timeout = setTimeout(() => {
|
||||
subscription.remove();
|
||||
resolve();
|
||||
}, timeoutMs);
|
||||
|
||||
const subscription = AppState.addEventListener("change", (state) => {
|
||||
if (state !== "active") {
|
||||
didLeaveApp = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (didLeaveApp && state === "active") {
|
||||
clearTimeout(timeout);
|
||||
subscription.remove();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function checkAndRequestPermissions(
|
||||
promptUser?: boolean
|
||||
): Promise<boolean> {
|
||||
let permissionStatus = await notifee.getNotificationSettings();
|
||||
if (Platform.OS === "android") {
|
||||
if (
|
||||
permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED &&
|
||||
permissionStatus.android.alarm === 1
|
||||
)
|
||||
const hasNotificationPermission =
|
||||
permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED;
|
||||
const hasAlarmPermission = permissionStatus.android.alarm === 1;
|
||||
|
||||
if (hasNotificationPermission && hasAlarmPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (permissionStatus.authorizationStatus === AuthorizationStatus.DENIED) {
|
||||
permissionStatus = await notifee.requestPermission();
|
||||
}
|
||||
|
||||
// Wait for user to return from exact alarm settings
|
||||
if (permissionStatus.android.alarm !== 1) {
|
||||
const waitForAppFocus = waitForAppFocusAfterSettings();
|
||||
|
||||
await notifee.openAlarmPermissionSettings();
|
||||
await waitForAppFocus;
|
||||
}
|
||||
permissionStatus = await notifee.getNotificationSettings();
|
||||
|
||||
if (
|
||||
const authorized =
|
||||
permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED &&
|
||||
permissionStatus.android.alarm === 1
|
||||
)
|
||||
return true;
|
||||
permissionStatus.android.alarm === 1;
|
||||
|
||||
if (promptUser) {
|
||||
if (await openSettingsDialog("local")) {
|
||||
await notifee.openNotificationSettings();
|
||||
return false;
|
||||
}
|
||||
if (authorized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
permissionStatus = await notifee.requestPermission();
|
||||
if (permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED)
|
||||
return true;
|
||||
if (promptUser) {
|
||||
await openSettingsDialog("local");
|
||||
const openSettings = await openSettingsDialog("local");
|
||||
if (openSettings) {
|
||||
const waitForAppFocus = waitForAppFocusAfterSettings();
|
||||
await notifee.openNotificationSettings();
|
||||
await waitForAppFocus;
|
||||
permissionStatus = await notifee.getNotificationSettings();
|
||||
const finalAuthorized =
|
||||
permissionStatus.authorizationStatus ===
|
||||
AuthorizationStatus.AUTHORIZED &&
|
||||
permissionStatus.android.alarm === 1;
|
||||
if (finalAuthorized) {
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// iOS
|
||||
permissionStatus = await notifee.requestPermission();
|
||||
if (permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED) {
|
||||
return true;
|
||||
}
|
||||
if (promptUser) {
|
||||
await openSettingsDialog("local");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async function getTriggers(
|
||||
reminder: Reminder
|
||||
): Promise<(Trigger & { id: string })[] | undefined> {
|
||||
|
||||
@@ -28,7 +28,17 @@ import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { scale, updateSize } from "../utils/size";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
ScreenGuardModule.initSettings();
|
||||
|
||||
let isScreenGuardModuleReady = false;
|
||||
async function callScreenGuard(callback: () => void) {
|
||||
try {
|
||||
if (!isScreenGuardModuleReady) {
|
||||
await ScreenGuardModule.initSettings();
|
||||
isScreenGuardModuleReady = true;
|
||||
}
|
||||
callback();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
const settings = get();
|
||||
@@ -129,15 +139,21 @@ function setPrivacyScreen(enabled?: boolean) {
|
||||
if (enabled) {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
ScreenGuardModule.registerWithoutEffect();
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.registerWithoutEffect();
|
||||
});
|
||||
} else {
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(false);
|
||||
}
|
||||
ScreenGuardModule.unregister();
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +272,8 @@ export const SettingsService = {
|
||||
canLockAppInBackground,
|
||||
appEnteredBackground,
|
||||
setPrivacyScreen,
|
||||
getBackgroundEnterTime
|
||||
getBackgroundEnterTime,
|
||||
callScreenGuard
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
@@ -104,6 +104,7 @@ export type Settings = {
|
||||
checkForUpdates?: boolean;
|
||||
defaultLineHeight: number;
|
||||
imageCompression: "ask-every-time" | "enabled" | "disabled";
|
||||
keepScreenOn?: boolean;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -213,7 +214,8 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
lastFullBackupDate: 0,
|
||||
checkForUpdates: true,
|
||||
defaultLineHeight: EDITOR_LINE_HEIGHT.DEFAULT,
|
||||
imageCompression: "ask-every-time"
|
||||
imageCompression: "ask-every-time",
|
||||
keepScreenOn: true
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
|
||||
@@ -45,6 +45,7 @@ interface NotesnookModuleInterface {
|
||||
hasWidgetNote: (noteId: string) => Promise<boolean>;
|
||||
updateWidgetNote: (noteId: string, data: string) => void;
|
||||
updateReminderWidget: () => void;
|
||||
refreshWidgets: () => void;
|
||||
isGestureNavigationEnabled: () => boolean;
|
||||
addShortcut: (
|
||||
id: string,
|
||||
@@ -81,6 +82,7 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
hasWidgetNote: () => {},
|
||||
updateWidgetNote: () => {},
|
||||
updateReminderWidget: () => {},
|
||||
refreshWidgets: () => {},
|
||||
isGestureNavigationEnabled: () => true,
|
||||
addShortcut: () => Promise.resolve(false),
|
||||
removeShortcut: () => Promise.resolve(false),
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
// Production iOS build identifiers
|
||||
IOS_CURRENT_PROJECT_VERSION = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
IOS_CURRENT_PROJECT_VERSION = 2191
|
||||
IOS_MARKETING_VERSION = 3.4.7
|
||||
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
|
||||
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
|
||||
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Production iOS build identifiers
|
||||
IOS_CURRENT_PROJECT_VERSION = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
IOS_CURRENT_PROJECT_VERSION = 2191
|
||||
IOS_MARKETING_VERSION = 3.4.7
|
||||
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
|
||||
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
|
||||
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Staging iOS build identifiers
|
||||
IOS_CURRENT_PROJECT_VERSION = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
IOS_CURRENT_PROJECT_VERSION = 2191
|
||||
IOS_MARKETING_VERSION = 3.4.7
|
||||
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
|
||||
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
|
||||
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
|
||||
|
||||
6
apps/mobile/package-lock.json
generated
6
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -290,7 +290,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook-importer/core": "^2.4.5",
|
||||
"@notesnook/common": "file:../common",
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user