Merge branch 'develop' into ai-llm-integration

This commit is contained in:
perf3ct 2025-04-16 00:18:10 +00:00
commit 2acc30f94a
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
20 changed files with 374 additions and 192 deletions

View File

@ -16,7 +16,7 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
### Migrating from Trilium?
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Just upgrade your Trilium instance to the latest version and [install TriliumNext/Notes as usual](#-installation)
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database.
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
@ -66,17 +66,7 @@ To use TriliumNext on your desktop machine (Linux, MacOS, and Windows) you have
* Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the ```trilium``` executable.
* Access TriliumNext via the web interface of a server installation (see below)
* Currently only the latest versions of Chrome & Firefox are supported (and tested).
* (Coming Soon) TriliumNext will also be provided as a Flatpak
#### MacOS
Currently when running TriliumNext/Notes on MacOS, you may get the following error:
> Apple could not verify "TriliumNext Notes" is free of malware and may harm your Mac or compromise your privacy.
You will need to run the command on your shell to resolve the error (documented [here](https://github.com/TriliumNext/Notes/issues/329#issuecomment-2287164137)):
```bash
xattr -c "/path/to/Trilium Next.app"
```
* TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
### Mobile

View File

@ -61,7 +61,7 @@
"hD3V4hiu2VW4",
"VN3xnce1vLkX"
],
"title": "v0.92.8-beta",
"title": "v0.93.0",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
@ -69,7 +69,7 @@
"mime": "text/html",
"attributes": [],
"format": "markdown",
"dataFileName": "v0.92.8-beta.md",
"dataFileName": "v0.93.0.md",
"attachments": []
},
{

View File

@ -1,4 +1,4 @@
# v0.92.8-beta
# v0.93.0
## 💡 Key highlights
* …
@ -11,6 +11,10 @@
* Note tree not closing when selecting some of the menu actions.
* [Most tree context menu on mobile are broken](https://github.com/TriliumNext/Notes/issues/671)
* [Quick search launch bar item does nothing in vertical layout](https://github.com/TriliumNext/Notes/issues/1680)
* [Note background is gray in 0.92.7 (light theme)](https://github.com/TriliumNext/Notes/issues/1689)
* [config.Session.cookieMaxAge is ignored](https://github.com/TriliumNext/Notes/issues/1709) by @pano9000
* [Return correct HTTP status code on failed login attempts instead of 200](https://github.com/TriliumNext/Notes/issues/1707) by @pano9000
* [Calendar stops displaying notes after adding a Day Note](https://github.com/TriliumNext/Notes/issues/1705)
## ✨ Improvements
@ -29,6 +33,8 @@
* [Center Search results under quick search bar](https://github.com/TriliumNext/Notes/issues/1679)
* Native ARM builds for Windows are now back.
* Basic Touch Bar support for macOS.
* [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701)
* The tab bar is now scrollable when there are many tabs by @SiriusXT
## 🌍 Internationalization

View File

@ -9636,6 +9636,13 @@
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
@ -9649,13 +9656,6 @@
"value": "default-note-title",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 20
}
],
"format": "markdown",
@ -10014,6 +10014,13 @@
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
@ -10027,13 +10034,6 @@
"value": "bx bx-list-plus",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
@ -11066,32 +11066,32 @@
"mime": "text/markdown",
"attributes": [
{
"type": "label",
"name": "shareAlias",
"value": "script-api",
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"value": "Q2z6av6JZVWm",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "Q2z6av6JZVWm",
"value": "MEtfsqa5VwNi",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "MEtfsqa5VwNi",
"type": "label",
"name": "shareAlias",
"value": "script-api",
"isInheritable": false,
"position": 40
"position": 10
}
],
"format": "markdown",

View File

@ -9,16 +9,26 @@ As an alternative to calling the API directly, there are client libraries to sim
* [trilium-py](https://github.com/Nriver/trilium-py), you can use Python to communicate with Trilium.
## Obtaining a token
All operations with the REST API have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)).
## Authentication
All operations have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)):
### Via the `Authorization` header
```
GET https://myserver.com/etapi/app-info
Authorization: ETAPITOKEN
```
Alternatively, since 0.56 you can also use basic auth format:
where `ETAPITOKEN` is the token obtained in the previous step.
For compatibility with various tools, it's also possible to specify the value of the `Authorization` header in the format `Bearer ETAPITOKEN` (since 0.93.0).
### Basic authentication
Since v0.56 you can also use basic auth format:
```
GET https://myserver.com/etapi/app-info

98
package-lock.json generated
View File

@ -197,7 +197,7 @@
"typedoc": "0.28.2",
"typedoc-plugin-missing-exports": "4.0.0",
"typescript": "5.8.3",
"typescript-eslint": "8.29.1",
"typescript-eslint": "8.30.1",
"vanilla-js-wheel-zoom": "9.0.4",
"vitest": "3.1.1",
"webpack": "5.99.5",
@ -5571,17 +5571,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz",
"integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz",
"integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.29.1",
"@typescript-eslint/type-utils": "8.29.1",
"@typescript-eslint/utils": "8.29.1",
"@typescript-eslint/visitor-keys": "8.29.1",
"@typescript-eslint/scope-manager": "8.30.1",
"@typescript-eslint/type-utils": "8.30.1",
"@typescript-eslint/utils": "8.30.1",
"@typescript-eslint/visitor-keys": "8.30.1",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -5601,16 +5601,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz",
"integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz",
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.29.1",
"@typescript-eslint/types": "8.29.1",
"@typescript-eslint/typescript-estree": "8.29.1",
"@typescript-eslint/visitor-keys": "8.29.1",
"@typescript-eslint/scope-manager": "8.30.1",
"@typescript-eslint/types": "8.30.1",
"@typescript-eslint/typescript-estree": "8.30.1",
"@typescript-eslint/visitor-keys": "8.30.1",
"debug": "^4.3.4"
},
"engines": {
@ -5626,14 +5626,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz",
"integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz",
"integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.29.1",
"@typescript-eslint/visitor-keys": "8.29.1"
"@typescript-eslint/types": "8.30.1",
"@typescript-eslint/visitor-keys": "8.30.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5644,14 +5644,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz",
"integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz",
"integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.29.1",
"@typescript-eslint/utils": "8.29.1",
"@typescript-eslint/typescript-estree": "8.30.1",
"@typescript-eslint/utils": "8.30.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.0.1"
},
@ -5668,9 +5668,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz",
"integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz",
"integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==",
"dev": true,
"license": "MIT",
"engines": {
@ -5682,14 +5682,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz",
"integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz",
"integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.29.1",
"@typescript-eslint/visitor-keys": "8.29.1",
"@typescript-eslint/types": "8.30.1",
"@typescript-eslint/visitor-keys": "8.30.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -5735,16 +5735,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz",
"integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz",
"integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.29.1",
"@typescript-eslint/types": "8.29.1",
"@typescript-eslint/typescript-estree": "8.29.1"
"@typescript-eslint/scope-manager": "8.30.1",
"@typescript-eslint/types": "8.30.1",
"@typescript-eslint/typescript-estree": "8.30.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5759,13 +5759,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz",
"integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz",
"integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.29.1",
"@typescript-eslint/types": "8.30.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -20458,15 +20458,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.29.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.1.tgz",
"integrity": "sha512-f8cDkvndhbQMPcysk6CUSGBWV+g1utqdn71P5YKwMumVMOG/5k7cHq0KyG4O52nB0oKS4aN2Tp5+wB4APJGC+w==",
"version": "8.30.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.30.1.tgz",
"integrity": "sha512-D7lC0kcehVH7Mb26MRQi64LMyRJsj3dToJxM1+JVTl53DQSV5/7oUGWQLcKl1C1KnoVHxMMU2FNQMffr7F3Row==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.29.1",
"@typescript-eslint/parser": "8.29.1",
"@typescript-eslint/utils": "8.29.1"
"@typescript-eslint/eslint-plugin": "8.30.1",
"@typescript-eslint/parser": "8.30.1",
"@typescript-eslint/utils": "8.30.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@ -254,7 +254,7 @@
"typedoc": "0.28.2",
"typedoc-plugin-missing-exports": "4.0.0",
"typescript": "5.8.3",
"typescript-eslint": "8.29.1",
"typescript-eslint": "8.30.1",
"vanilla-js-wheel-zoom": "9.0.4",
"vitest": "3.1.1",
"webpack": "5.99.5",

View File

@ -8,12 +8,19 @@
<li><a href="https://github.com/Nriver/trilium-py">trilium-py</a>, you can
use Python to communicate with Trilium.</li>
</ul>
<h2>Obtaining a token</h2>
<p>All operations with the REST API have to be authenticated using a token.
You can get this token either from Options -&gt; ETAPI or programmatically
using the <code>/auth/login</code> REST call (see the <a href="https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml">spec</a>).</p>
<h2>Authentication</h2>
<p>All operations have to be authenticated using a token. You can get this
token either from Options -&gt; ETAPI or programmatically using the <code>/auth/login</code> REST
call (see the <a href="https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml">spec</a>):</p><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
<h3>Via the <code>Authorization</code> header</h3><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
Authorization: ETAPITOKEN</code></pre>
<p>Alternatively, since 0.56 you can also use basic auth format:</p><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
<p>where <code>ETAPITOKEN</code> is the token obtained in the previous step.</p>
<p>For compatibility with various tools, it's also possible to specify the
value of the <code>Authorization</code> header in the format <code>Bearer ETAPITOKEN</code> (since
0.93.0).</p>
<h3>Basic authentication</h3>
<p>Since v0.56 you can also use basic auth format:</p><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
Authorization: Basic BATOKEN</code></pre>
<ul>
<li>Where <code>BATOKEN = BASE64(username + ':' + password)</code> - this is

View File

@ -36,8 +36,8 @@
<h3>Running the Docker Container</h3>
<h4>Local Access Only</h4>
<p>Run the container to make it accessible only from the localhost. This
setup is suitable for testing or when using a prox ay server like Nginx
or Apache.</p><pre><code class="language-text-x-trilium-auto">sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/notes:[VERSION]</code></pre>
setup is suitable for testing or when using a proxy server like Nginx or
Apache.</p><pre><code class="language-text-x-trilium-auto">sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/notes:[VERSION]</code></pre>
<ol>
<li>Verify the container is running using <code>docker ps</code>.</li>
<li>Access Trilium via a web browser at <code>127.0.0.1:8080</code>.</li>

View File

@ -1,11 +1,10 @@
<p>For <a href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">script code notes</a>,
Trilium offers an API that gives them access to various features of the
application.</p>
<p>For <a href="#root/_help_CdNpE2pqjmI6">script code notes</a>, Trilium offers
an API that gives them access to various features of the application.</p>
<p>There are two APIs:</p>
<ul>
<li>One for the front-end scripts:&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/GLks18SNjxmC/_help_Q2z6av6JZVWm">Frontend API</a>
<li>One for the front-end scripts:&nbsp;<a class="reference-link" href="#root/_help_Q2z6av6JZVWm">Frontend API</a>
</li>
<li>One for the back-end scripts:&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/GLks18SNjxmC/_help_MEtfsqa5VwNi">Backend API</a>
<li>One for the back-end scripts:&nbsp;<a class="reference-link" href="#root/_help_MEtfsqa5VwNi">Backend API</a>
</li>
</ul>
<p>In both cases, the API resides in a global variable, <code>api</code>,

View File

@ -122,6 +122,7 @@ export default class DesktopLayout {
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
.optChild(
fullWidthTabBar,
new FlexContainer("row")

View File

@ -7,8 +7,6 @@ import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
import ConfirmDialog from "../widgets/dialogs/confirm.js";
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import EditButton from "../widgets/floating_buttons/edit_button.js";
@ -118,6 +116,7 @@ export default class MobileLayout {
getRootWidget(appContext: typeof AppContext) {
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class("horizontal-layout")
.cssBlock(MOBILE_CSS)
.child(new FlexContainer("column").id("mobile-sidebar-container"))
.child(

View File

@ -13,7 +13,7 @@ declare module "draggabilly" {
containment: HTMLElement
});
element: HTMLElement;
on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback);
on(event: "staticClick" | "dragStart" | "dragEnd" | "dragMove", callback: Callback);
dragEnd();
isDragging: boolean;
positionDrag: () => void;

View File

@ -19,7 +19,6 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.id("root-widget");
this.css("height", "100dvh");
this.class((isHorizontalLayout ? "horizontal" : "vertical") + "-layout");
this.originalViewportHeight = getViewportHeight();
}

View File

@ -11,10 +11,11 @@ import type NoteContext from "../components/note_context.js";
const isDesktop = utils.isDesktop();
const TAB_CONTAINER_MIN_WIDTH = 24;
const TAB_CONTAINER_MIN_WIDTH = 100;
const TAB_CONTAINER_MAX_WIDTH = 240;
const TAB_CONTAINER_LEFT_PADDING = 5;
const NEW_TAB_WIDTH = 32;
const SCROLL_BUTTON_WIDTH = 36;
const NEW_TAB_WIDTH = 36;
const MIN_FILLER_WIDTH = isDesktop ? 50 : 15;
const MARGIN_WIDTH = 5;
@ -32,6 +33,8 @@ const TAB_TPL = `
</div>
</div>`;
const CONTAINER_ANCHOR_TPL = `<div class="tab-row-container-anchor"></div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="${t("tab_row.add_new_tab")}">+</div>`;
const FILLER_TPL = `<div class="tab-row-filler"></div>`;
@ -39,11 +42,11 @@ const TAB_ROW_TPL = `
<div class="tab-row-widget">
<style>
.tab-row-widget {
display:flex;
box-sizing: border-box;
position: relative;
width: 100%;
background: var(--main-background-color);
overflow: hidden;
user-select: none;
}
@ -59,7 +62,6 @@ const TAB_ROW_TPL = `
.tab-row-widget .tab-row-widget-container {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
}
@ -74,15 +76,12 @@ const TAB_ROW_TPL = `
}
.note-new-tab {
position: absolute;
left: 0;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 ${NEW_TAB_WIDTH}px;
height: ${NEW_TAB_WIDTH}px;
padding: 1px;
border: 0;
margin: 0;
z-index: 1;
text-align: center;
font-size: 24px;
cursor: pointer;
box-sizing: border-box;
@ -96,11 +95,22 @@ const TAB_ROW_TPL = `
.tab-row-filler {
box-sizing: border-box;
-webkit-app-region: drag;
position: absolute;
left: 0;
height: 100%;
min-width: ${MIN_FILLER_WIDTH}px;
flex-grow: 1;
}
.tab-row-container-anchor{
position: absolute;
left: 0;
width: 0px;
height: 36px;
border: 0;
margin: 0;
z-index: 1;
cursor: pointer;
box-sizing: border-box;
}
body.mobile .tab-row-filler {
display: none;
}
@ -184,6 +194,38 @@ const TAB_ROW_TPL = `
text-align: center;
}
.tab-scroll-button-left, .tab-scroll-button-right {
display: none;
flex: 0 0 ${SCROLL_BUTTON_WIDTH}px;
height: ${SCROLL_BUTTON_WIDTH}px;
padding: 1px 1px 1px 1px;
align-items: center;
justify-content: center;
cursor: pointer;
}
.tab-scroll-button-left {
color: var(--active-tab-text-color);
box-shadow: inset -1px 0 0 0 var(--main-border-color);
}
.tab-scroll-button-right {
color: var(--active-tab-text-color);
box-shadow: inset 1px 0 0 0 var(--main-border-color);
}
.tab-scroll-button-left.disabled,
.tab-scroll-button-right.disabled {
color: var(--inactive-tab-text-color);
box-shadow: none;
pointer-events: none;
}
.tab-scroll-button-left:hover,
.tab-scroll-button-right:hover {
background-color: var(--tab-background-color, var(--inactive-tab-hover-background-color));
}
.tab-row-widget .note-tab:hover .note-tab-wrapper {
background-color: var(--tab-background-color, var(--inactive-tab-hover-background-color));
}
@ -231,9 +273,29 @@ const TAB_ROW_TPL = `
.tab-row-widget:not(.tab-row-widget-is-sorting) .note-tab.note-tab-was-just-dragged {
transition: transform 120ms ease-in-out;
}
.tab-row-widget-wrapper {
display: flex;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.tab-row-widget-scrolling-container {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none; /* Firefox */
}
/* Chrome/Safari */
.tab-row-widget-scrolling-container::-webkit-scrollbar {
display: none;
}
</style>
<div class="tab-row-widget-container"></div>
<div class="tab-scroll-button-left bx bx-chevron-left"></div>
<div class="tab-row-widget-scrolling-container">
<div class="tab-row-widget-container"></div>
</div>
<div class="tab-scroll-button-right bx bx-chevron-right"></div>
</div>`;
export default class TabRowWidget extends BasicWidget {
@ -244,11 +306,24 @@ export default class TabRowWidget extends BasicWidget {
private draggabillyDragging?: Draggabilly | null;
private $style!: JQuery<HTMLElement>;
private $tabScrollingContainer!: JQuery<HTMLElement>;
private $tabContainer!: JQuery<HTMLElement>;
private $scrollButtonLeft!: JQuery<HTMLElement>;
private $scrollButtonRight!: JQuery<HTMLElement>;
private $containerAnchor!: JQuery<HTMLElement>;
private $filler!: JQuery<HTMLElement>;
private $newTab!: JQuery<HTMLElement>;
private updateScrollTimeout: ReturnType<typeof setTimeout> | undefined;
private newTabOuterWidth: number = 0;
private scrollButtonsOuterWidth: number = 0;
doRender() {
this.$widget = $(TAB_ROW_TPL);
this.$tabScrollingContainer = this.$widget.children(".tab-row-widget-scrolling-container");
this.$tabContainer = this.$widget.find(".tab-row-widget-container");
this.$scrollButtonLeft = this.$widget.children(".tab-scroll-button-left");
this.$scrollButtonRight = this.$widget.children(".tab-scroll-button-right");
const documentStyle = window.getComputedStyle(document.documentElement);
this.showNoteIcons = documentStyle.getPropertyValue("--tab-note-icons") === "true";
@ -257,11 +332,13 @@ export default class TabRowWidget extends BasicWidget {
this.setupStyle();
this.setupEvents();
this.setupContainerAnchor();
this.setupDraggabilly();
this.setupNewButton();
this.setupFiller();
this.layoutTabs();
this.setVisibility();
this.setupScrollEvents();
this.$widget.on("contextmenu", ".note-tab", (e) => {
e.preventDefault();
@ -300,6 +377,60 @@ export default class TabRowWidget extends BasicWidget {
this.$widget.append(this.$style);
}
scrollTabContainer(direction: number, behavior: ScrollBehavior = "smooth") {
const currentScrollLeft = this.$tabScrollingContainer[0]?.scrollLeft;
this.$tabScrollingContainer[0].scrollTo({
left: currentScrollLeft + direction,
behavior
});
};
setupScrollEvents() {
let isScrolling = false;
this.$tabScrollingContainer[0].addEventListener('wheel', (event) => {
if (!isScrolling) {
isScrolling = true;
requestAnimationFrame(() => {
this.scrollTabContainer(event.deltaY * 1.5, 'instant');
isScrolling = false;
});
}
});
this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-200));
this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(200));
this.$tabScrollingContainer[0].addEventListener('scroll', () => {
clearTimeout(this.updateScrollTimeout);
this.updateScrollTimeout = setTimeout(() => {
this.updateScrollButtonState();
}, 100);
});
}
updateScrollButtonState() {
const scrollLeft = this.$tabScrollingContainer[0].scrollLeft;
const scrollWidth = this.$tabScrollingContainer[0].scrollWidth;
const clientWidth = this.$tabScrollingContainer[0].clientWidth;
// Detect whether the scrollbar is at the far left or far right.
this.$scrollButtonLeft.toggleClass("disabled", Math.abs(scrollLeft) <= 1);
this.$scrollButtonRight.toggleClass("disabled", Math.abs(scrollLeft + clientWidth - scrollWidth) <= 1);
}
setScrollButtonVisibility(show: boolean = true) {
if (show) {
this.$scrollButtonLeft.css("display", "flex");
this.$scrollButtonRight.css("display", "flex");
clearTimeout(this.updateScrollTimeout);
this.updateScrollTimeout = setTimeout(() => {
this.updateScrollButtonState();
}, 200);
} else {
this.$scrollButtonLeft.css("display", "none");
this.$scrollButtonRight.css("display", "none");
}
}
setupEvents() {
new ResizeObserver((_) => {
this.cleanUpPreviouslyDraggedTabs();
@ -317,14 +448,32 @@ export default class TabRowWidget extends BasicWidget {
return Array.prototype.slice.call(this.$widget.find(".note-tab"));
}
get $tabContainer() {
return this.$widget.find(".tab-row-widget-container");
updateOuterWidth() {
if (this.newTabOuterWidth == 0) {
this.newTabOuterWidth = this.$newTab?.outerWidth(true) ?? 0;
}
if (this.scrollButtonsOuterWidth == 0) {
this.scrollButtonsOuterWidth = (this.$scrollButtonLeft?.outerWidth(true) ?? 0) + (this.$scrollButtonRight?.outerWidth(true) ?? 0);
}
}
get tabWidths() {
const numberOfTabs = this.tabEls.length;
const tabsContainerWidth = this.$tabContainer[0].clientWidth - NEW_TAB_WIDTH - MIN_FILLER_WIDTH;
const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH;
// this.$newTab may include margin, and using NEW_TAB_WIDTH could cause tabsContainerWidth to be slightly larger,
// resulting in misaligned scrollbars/buttons. Therefore, use outerwidth.
this.updateOuterWidth();
let tabsContainerWidth = Math.floor(this.$widget.width() ?? 0);
tabsContainerWidth -= this.newTabOuterWidth + MIN_FILLER_WIDTH;
// Check whether the scroll buttons need to be displayed.
if ((TAB_CONTAINER_MIN_WIDTH + MARGIN_WIDTH) * numberOfTabs > tabsContainerWidth) {
tabsContainerWidth -= this.scrollButtonsOuterWidth;
this.setScrollButtonVisibility(true);
} else {
this.setScrollButtonVisibility(false);
}
const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH + TAB_CONTAINER_LEFT_PADDING;
const targetWidth = (tabsContainerWidth - marginWidth) / numberOfTabs;
const clampedTargetWidth = Math.max(TAB_CONTAINER_MIN_WIDTH, Math.min(TAB_CONTAINER_MAX_WIDTH, targetWidth));
const flooredClampedTargetWidth = Math.floor(clampedTargetWidth);
@ -344,10 +493,6 @@ export default class TabRowWidget extends BasicWidget {
}
}
if (this.$filler) {
this.$filler.css("width", `${extraWidthRemaining + MIN_FILLER_WIDTH}px`);
}
return widths;
}
@ -362,10 +507,9 @@ export default class TabRowWidget extends BasicWidget {
position -= MARGIN_WIDTH; // the last margin should not be applied
const newTabPosition = position;
const fillerPosition = position + 32;
const anchorPosition = position;
return { tabPositions, newTabPosition, fillerPosition };
return { tabPositions, anchorPosition };
}
layoutTabs() {
@ -386,15 +530,14 @@ export default class TabRowWidget extends BasicWidget {
let styleHTML = "";
const { tabPositions, newTabPosition, fillerPosition } = this.getTabPositions();
const { tabPositions, anchorPosition } = this.getTabPositions();
tabPositions.forEach((position, i) => {
styleHTML += `.note-tab:nth-child(${i + 1}) { transform: translate3d(${position}px, 0, 0)} `;
});
styleHTML += `.note-new-tab { transform: translate3d(${newTabPosition}px, 0, 0) } `;
styleHTML += `.tab-row-filler { transform: translate3d(${fillerPosition}px, 0, 0) } `;
styleHTML += `.tab-row-container-anchor { transform: translate3d(${anchorPosition}px, 0, 0) } `;
styleHTML += `.tab-row-widget-container {width: ${anchorPosition}px}`;
this.$style.html(styleHTML);
}
@ -406,8 +549,7 @@ export default class TabRowWidget extends BasicWidget {
$tab.addClass("note-tab-was-just-added");
setTimeout(() => $tab.removeClass("note-tab-was-just-added"), 500);
this.$newTab.before($tab);
this.$containerAnchor.before($tab);
this.setVisibility();
this.setTabCloseEvent($tab);
this.updateTitle($tab, t("tab_row.new_tab"));
@ -507,6 +649,7 @@ export default class TabRowWidget extends BasicWidget {
setupDraggabilly() {
const tabEls = this.tabEls;
const { tabPositions } = this.getTabPositions();
let initialScrollLeft = 0;
if (this.isDragging && this.draggabillyDragging) {
this.isDragging = false;
@ -533,7 +676,7 @@ export default class TabRowWidget extends BasicWidget {
this.draggabillies.push(draggabilly);
draggabilly.on("pointerDown", () => {
draggabilly.on("staticClick", () => {
appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id"));
});
@ -542,11 +685,20 @@ export default class TabRowWidget extends BasicWidget {
this.draggabillyDragging = draggabilly;
tabEl.classList.add("note-tab-is-dragging");
this.$widget.addClass("tab-row-widget-is-sorting");
initialScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0;
draggabilly.positionDrag = () => { };
});
draggabilly.on("dragEnd", () => {
this.isDragging = false;
const finalTranslateX = parseFloat(tabEl.style.left);
const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0;
const scrollDelta = currentScrollLeft - initialScrollLeft;
const translateX = parseFloat(tabEl.style.left) + scrollDelta;
const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth;
const minTranslateX = 0;
const finalTranslateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX));
tabEl.style.transform = `translate3d(0, 0, 0)`;
// Animate dragged tab back into its place
@ -570,12 +722,31 @@ export default class TabRowWidget extends BasicWidget {
});
});
draggabilly.on("dragMove", (event: unknown, pointer: unknown, moveVector: MoveVector) => {
draggabilly.on("dragMove", (event: unknown, pointer: PointerEvent, moveVector: MoveVector) => {
// The current index be computed within the event since it can change during the dragMove
const tabEls = this.tabEls;
const currentIndex = tabEls.indexOf(tabEl);
const currentTabPositionX = originalTabPositionX + moveVector.x;
const scorllContainerBounds = this.$tabScrollingContainer[0]?.getBoundingClientRect();
const pointerX = pointer.pageX;
const scrollSpeed = 100; // The increment of each scroll.
// Check if the mouse is near the edge of the container and trigger scrolling.
if (pointerX < scorllContainerBounds.left) {
this.scrollTabContainer(- scrollSpeed);
} else if (pointerX > scorllContainerBounds.right) {
this.scrollTabContainer(scrollSpeed);
}
const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0;
const scrollDelta = currentScrollLeft - initialScrollLeft;
let translateX = moveVector.x + scrollDelta;
// Limit the `translateX` so that `tabEl` cannot exceed the left and right boundaries of the container.
const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth - originalTabPositionX;
const minTranslateX = - originalTabPositionX;
translateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX));
tabEl.style.transform = `translate3d(${translateX}px, 0, 0)`;
const currentTabPositionX = originalTabPositionX + translateX;
const destinationIndexTarget = this.closest(currentTabPositionX, tabPositions);
const destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget));
@ -594,8 +765,7 @@ export default class TabRowWidget extends BasicWidget {
if (destinationIndex < originIndex) {
tabEl.parentNode?.insertBefore(tabEl, this.tabEls[destinationIndex]);
} else {
const beforeEl = this.tabEls[destinationIndex + 1] || this.$newTab[0];
const beforeEl = this.tabEls[destinationIndex + 1] || this.$containerAnchor[0];
tabEl.parentNode?.insertBefore(tabEl, beforeEl);
}
this.triggerEvent("tabReorder", { ntxIdsInOrder: this.getNtxIdsInOrder() });
@ -604,14 +774,19 @@ export default class TabRowWidget extends BasicWidget {
setupNewButton() {
this.$newTab = $(NEW_TAB_BUTTON_TPL);
this.$tabContainer.append(this.$newTab);
this.$widget.append(this.$newTab);
}
setupFiller() {
this.$filler = $(FILLER_TPL);
this.$tabContainer.append(this.$filler);
this.$widget.append(this.$filler);
}
setupContainerAnchor() {
this.$containerAnchor = $(CONTAINER_ANCHOR_TPL);
this.$tabContainer.append(this.$containerAnchor);
}
closest(value: number, array: number[]) {
@ -660,7 +835,9 @@ export default class TabRowWidget extends BasicWidget {
updateTabById(ntxId: string | null) {
const $tab = this.getTabById(ntxId);
$tab[0].scrollIntoView({
behavior: 'smooth'
});
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
this.updateTab($tab, noteContext);

View File

@ -74,7 +74,7 @@ export function buildConfig() {
heading: {
options: [
{ model: "paragraph" as const, title: "Paragraph", class: "ck-heading_paragraph" },
// // heading1 is not used since that should be a note's title
// heading1 is not used since that should be a note's title
{ model: "heading2" as const, view: "h2", title: "Heading 2", class: "ck-heading_heading2" },
{ model: "heading3" as const, view: "h3", title: "Heading 3", class: "ck-heading_heading3" },
{ model: "heading4" as const, view: "h4", title: "Heading 4", class: "ck-heading_heading4" },

View File

@ -869,46 +869,23 @@ body.mobile .fancytree-node > span {
position: relative;
}
/* #region Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */
body.layout-horizontal .tab-row-widget,
body.layout-horizontal .tab-row-widget-container,
body.layout-horizontal .tab-row-container .note-tab[active] {
overflow: visible !important;
}
body.layout-horizontal .tab-row-container .note-tab[active]:before {
content: "";
position: absolute;
bottom: 0;
left: -100vw;
top: var(--tab-height);
right: calc(100% - 1px);
height: 1px;
/* Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */
body.layout-horizontal .tab-row-container {
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
}
body.layout-horizontal .tab-row-container .note-tab[active]:after {
content: "";
position: absolute;
bottom: 0;
left: 100%;
top: var(--tab-height);
right: 0;
width: 100vw;
height: 1px;
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
}
/* #endregion */
body.layout-vertical.electron.platform-darwin .tab-row-container {
border-bottom: 1px solid var(--subtle-border-color);
}
.tab-row-widget-container {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2);
height: var(--tab-height) !important;
}
.tab-row-widget > * {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2);
}
body.layout-horizontal .tab-row-container {
padding-top: calc((var(--tab-bar-height) - var(--tab-height)));
}
@ -923,11 +900,12 @@ body.layout-vertical #left-pane .quick-search {
/* Limit the drag area for the previous elements to include just to the element itself
and not its descendants also */
body.layout-horizontal .tab-row-container > *,
body.layout-vertical .tab-row-widget > *,
body.layout-vertical .tab-row-widget > *:not(.tab-row-filler),
body.layout-vertical #left-pane .quick-search > * {
-webkit-app-region: no-drag;
}
body.layout-horizontal .tab-row-widget,
body.layout-horizontal .tab-row-widget-container {
margin-top: 0;
position: relative;
@ -991,7 +969,7 @@ body.layout-horizontal .tab-row-widget .note-tab .note-tab-wrapper {
text-overflow: ellipsis;
}
body.layout-vertical .tab-row-widget-is-sorting .note-tab[active] .note-tab-wrapper {
body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .note-tab-wrapper {
transform: scale(0.85);
box-shadow: var(--active-tab-dragging-shadow) !important;
}

View File

@ -92,11 +92,10 @@ function login(req: Request, res: Response) {
const rememberMe = req.body.rememberMe;
req.session.regenerate(() => {
if (rememberMe) {
req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks
} else {
if (!rememberMe) {
// unset default maxAge set by sessionParser
// Cookie becomes non-persistent and expires after current browser session (e.g. when browser is closed)
// Cookie becomes non-persistent and expires
// after current browser session (e.g. when browser is closed)
req.session.cookie.maxAge = undefined;
}
@ -134,7 +133,7 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
}
res.render('login', {
res.status(401).render('login', {
wrongPassword: errorType === 'password',
wrongTotp: errorType === 'totp',
totpEnabled: totp.isTotpEnabled(),

View File

@ -48,6 +48,11 @@ function parseAuthToken(auth: string | undefined) {
auth = basicAuthChunks[1];
}
if (auth.startsWith("Bearer ")) {
// allow also bearer auth format
auth = auth.substring(7);
}
const chunks = auth.split("_");
if (chunks.length === 1) {

View File

@ -112,12 +112,21 @@ function upsert<T extends {}>(tableName: string, primaryKey: string, rec: T) {
execute(query, rec);
}
function stmt(sql: string) {
if (!(sql in statementCache)) {
statementCache[sql] = dbConnection.prepare(sql);
/**
* For the given SQL query, returns a prepared statement. For the same query (string comparison), the same statement is returned.
*
* @param sql the SQL query for which to return a prepared statement.
* @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query.
* @returns the corresponding {@link Statement}.
*/
function stmt(sql: string, isRaw?: boolean) {
const key = (isRaw ? "raw/" + sql : sql);
if (!(key in statementCache)) {
statementCache[key] = dbConnection.prepare(sql);
}
return statementCache[sql];
return statementCache[key];
}
function getRow<T>(query: string, params: Params = []): T {
@ -172,7 +181,7 @@ function getRows<T>(query: string, params: Params = []): T[] {
}
function getRawRows<T extends {} | unknown[]>(query: string, params: Params = []): T[] {
return (wrap(query, (s) => s.raw().all(params)) as T[]) || [];
return (wrap(query, (s) => s.raw().all(params), true) as T[]) || [];
}
function iterateRows<T>(query: string, params: Params = []): IterableIterator<T> {
@ -234,7 +243,10 @@ function executeScript(query: string): DatabaseType {
return dbConnection.exec(query);
}
function wrap(query: string, func: (statement: Statement) => unknown): unknown {
/**
* @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query.
*/
function wrap(query: string, func: (statement: Statement) => unknown, isRaw?: boolean): unknown {
const startTimestamp = Date.now();
let result;
@ -243,7 +255,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown {
}
try {
result = func(stmt(query));
result = func(stmt(query, isRaw));
} catch (e: any) {
if (e.message.includes("The database connection is not open")) {
// this often happens on killing the app which puts these alerts in front of user