mirror of
https://github.com/standardnotes/app
synced 2026-09-16 00:46:00 -04:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eadc16a6c | ||
|
|
7c3c59dd92 | ||
|
|
297b3bb4f8 | ||
|
|
c4ac9fb8d6 | ||
|
|
44d6bdf147 | ||
|
|
df87a38b56 | ||
|
|
87213e9f5d | ||
|
|
53ff07244a | ||
|
|
70c1dd7827 | ||
|
|
47a0551967 | ||
|
|
6960bec339 | ||
|
|
7c2b8fe545 | ||
|
|
eac62fef1a | ||
|
|
8b845ea424 | ||
|
|
604520f1ae | ||
|
|
98ed6472c0 | ||
|
|
fad80af0ff | ||
|
|
0644490e43 | ||
|
|
2da360234d | ||
|
|
e86ea8837a | ||
|
|
7851e12e9e | ||
|
|
82d13b41ed | ||
|
|
4f5d092f89 | ||
|
|
cd0859e908 |
+11
@@ -0,0 +1,11 @@
|
||||
PORT=3001
|
||||
|
||||
DEFAULT_SYNC_SERVER=https://api.standardnotes.com
|
||||
|
||||
# Subscription related endpoints
|
||||
DASHBOARD_URL=http://standardnotes.com/dashboard
|
||||
PLANS_URL=https://standardnotes.com/plans
|
||||
PURCHASE_URL=https://standardnotes.com/purchase
|
||||
|
||||
# Used by Rails internals and not Standard Notes related
|
||||
SECRET_KEY_BASE=<enter a secret string here>
|
||||
@@ -159,6 +159,8 @@ jobs:
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: assets
|
||||
|
||||
@@ -14,6 +14,10 @@ jobs:
|
||||
destination_branch: "refs/heads/*"
|
||||
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Sleep to allow git-sync to fully complete
|
||||
run: sleep 10
|
||||
shell: bash
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: Mobile TestFlight
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/mobile*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -51,8 +54,8 @@ jobs:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
tag_name: "@standardnotes/mobile@${{ steps.package-version.outputs.current-version}}"
|
||||
prerelease: true
|
||||
draft: false
|
||||
name: "Mobile Beta ${{ steps.package-version.outputs.current-version }}"
|
||||
draft: true
|
||||
name: "Mobile Dev ${{ steps.package-version.outputs.current-version }}"
|
||||
files: |
|
||||
packages/mobile/android/app/build/outputs/apk/dev/release/app-dev-release.apk
|
||||
ios:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Standard Notes
|
||||
### A safe and private place for your life's work.
|
||||
|
||||
Standard Notes is a simple and private notes app available on most platforms, including Web, Mac, Windows, Linux, iOS, and Android. It focuses on simplicity, and encrypts data locally before it ever touches a cloud. This means no one can read your notes but you (not even us).
|
||||
Standard Notes is an end-to-end encrypted note-taking app for digitalists and professionals. Capture your notes, files, and life’s work all in one secure place.
|
||||
|
||||

|
||||
|
||||
@@ -16,43 +16,37 @@ Standard Notes is a simple and private notes app available on most platforms, in
|
||||
|
||||
### Why Standard Notes?
|
||||
|
||||
- Simple and easy to use
|
||||
- Fast and encrypted cross-platform sync
|
||||
- Free sync on unlimited devices
|
||||
- Extensible with editors (such as Markdown and Code), themes, and components. [Learn more](https://standardnotes.com/features).
|
||||
- Open-source and the option to self-host your notes server. You can [host your own Standard Server](https://docs.standardnotes.com/self-hosting/getting-started) in a few easy steps.
|
||||
- End-to-end encrypted sync. Only you can read your notes.
|
||||
- Fast, free, and encrypted cross-platform sync on unlimited devices.
|
||||
- Open-source with ability to self-host your own server in a [few easy steps](https://docs.standardnotes.com/self-hosting/getting-started).
|
||||
- A strong focus on longevity and sustainability. [Learn more](https://standardnotes.com/longevity).
|
||||
|
||||
### Creating your private notes account
|
||||
|
||||
1. Launch the web app at [app.standardnotes.com](https://app.standardnotes.com).
|
||||
2. Click Register to create your private notes account.
|
||||
3. Download Standard Notes on all your devices.
|
||||
- [Mac](https://standardnotes.com/download/mac)
|
||||
- [Windows](https://standardnotes.com/download/windows)
|
||||
- [Linux](https://standardnotes.com/download/linux)
|
||||
3. Download Standard Notes on your devices.
|
||||
- [Mac](https://standardnotes.com/download)
|
||||
- [Windows](https://standardnotes.com/download)
|
||||
- [Linux](https://standardnotes.com/download)
|
||||
- [iOS](https://standardnotes.com/download/https://itunes.apple.com/us/app/standard-notes/id1285392450?mt=8)
|
||||
- [Android](https://play.google.com/store/apps/details?id=com.standardnotes)
|
||||
4. You're all set to begin enjoying a new, more freeing notes life. Standard Notes comes out of the box with end-to-end encrypted sync on all your devices.
|
||||
|
||||
### Do More
|
||||
|
||||
If you're looking to power up your experience with extensions, and help support future development, [learn more about our paid plans](https://standardnotes.com/plans). Our paid plans offer:
|
||||
|
||||
- Powerful editors, including the Plus Editor, Simple Markdown, Advanced Markdown, Code Editor, Vim Editor, and the popular Simple Task Editor.
|
||||
- Beautiful themes to help you find inspiration in any mood, like Midnight, Focused, Futura, Titanium, and Solarized Dark.
|
||||
- Powerful data care options, including daily encrypted backups delivered directly to your email inbox, as well as automated Dropbox, Google Drive, and OneDrive backups. You'll also be able to configure two-factor authentication to add an additional layer of security to your account.
|
||||
- Productivity-enhancing components like Folders, Autocomplete Tags, the ever-handy Action Bar, and GitHub Push.
|
||||
4. You're all set. Standard Notes comes out of the box with end-to-end encrypted sync on all your devices.
|
||||
|
||||
### Publish a Blog
|
||||
|
||||
It's no secret we love to write. Standard Notes has become a dependable environment to do your most important work, and this includes publishing your ideas to the world. That's why we created Listed. Listed allows you to create an online publication with automatic email newsletters delivered to your readers, directly from Standard Notes.
|
||||
Standard Notes is a dependable environment to do your most important work, including publishing your ideas to the world. Listed allows you to create an online publication with automatic email newsletters delivered to your readers, directly from Standard Notes.
|
||||
|
||||
[Learn more about Listed.](https://listed.to/)
|
||||
|
||||
### Plug In
|
||||
### Community
|
||||
|
||||
Plug in to the community of note-lovers and privacy-enthusiasts. Join us on [Slack](https://standardnotes.com/slack), on our [GitHub forum](https://forum.standardnotes.org), and follow new updates on [Twitter](https://twitter.com/StandardNotes).
|
||||
Join us on
|
||||
|
||||
- [Discord](https://standardnotes.com/discord)
|
||||
- [Twitter](https://twitter.com/StandardNotes).
|
||||
- [Forum](https://forum.standardnotes.org)
|
||||
- [Slack](https://standardnotes.com/slack)
|
||||
|
||||
Developers can create and publish their own extensions. Visit the [documentation hub](https://docs.standardnotes.com/) to learn more.
|
||||
|
||||
@@ -62,53 +56,26 @@ Questions? Find answers on our [Help page](https://standardnotes.com/help).
|
||||
|
||||
### Docker setup
|
||||
|
||||
Docker is the quick and easy way to try out Standard Notes. We highly recommend using our official [Docker hub image](https://hub.docker.com/repository/docker/standardnotes/web).
|
||||
Docker is the quickest way to try out Standard Notes. We recommend using our official [Docker hub image](https://hub.docker.com/repository/docker/standardnotes/web).
|
||||
|
||||
### Standalone instance
|
||||
|
||||
Before you start make sure you have a `.env` file copied from the sample `.env.sample` and configured with your parameters.
|
||||
|
||||
If your intention is not contributing but just running the app we recommend using our official image from Docker hub like this:
|
||||
```
|
||||
docker run -d -p 3001:3001 --env-file=your-env-file standardnotes/web:stable
|
||||
docker run -d -p 3001:3001 --env-file=.env.sample standardnotes/web:stable
|
||||
```
|
||||
|
||||
Or if you want to use the `develop` branch that is in a work-in-progress state please use:
|
||||
```
|
||||
docker run -d -p 3001:3001 --env-file=your-env-file standardnotes/web:latest
|
||||
```
|
||||
|
||||
You can then access the app at `http://localhost:3001` (please check Docker container logs if the server has started already and is listening on connections).
|
||||
You can then access the app at `http://localhost:3001`. It may take a minute for the server to start up.
|
||||
|
||||
### Running Locally
|
||||
|
||||
**Instructions:**
|
||||
|
||||
1. Ensure you have [Yarn](https://classic.yarnpkg.com) installed
|
||||
2. Clone the repo
|
||||
3. `yarn install`
|
||||
4. `yarn start`
|
||||
|
||||
Then open your browser to `http://localhost:3001`.
|
||||
4. `yarn build:web-server`
|
||||
5. `yarn start:server:web`
|
||||
6. Open your browser to `http://localhost:3001`.
|
||||
|
||||
---
|
||||
|
||||
You can also set the `DEFAULT_SYNC_SERVER` environment variable to set the default server for login and registration.
|
||||
You can configure the `DEFAULT_SYNC_SERVER` environment variable to set the default server for login and registration.
|
||||
|
||||
```
|
||||
DEFAULT_SYNC_SERVER=https://sync.myserver
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
For contributing we highly recommend you use our docker-compose setup that is provided in this repository.
|
||||
|
||||
### Docker compose setup
|
||||
|
||||
Use the included [docker-compose.yml](docker-compose.yml) file to build Standard Notes with `docker-compose`. Once your `.env` file has been copied and configured, simply run:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This should load the app container and run the necessary scripts. You should then be able to reach the app at `http://localhost:3001`
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.5.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* debounce saving task description/draft ([#1187](https://github.com/standardnotes/app/issues/1187)) ([47a0551](https://github.com/standardnotes/app/commit/47a0551967ca420a957e2123d56bd7f0c8a95c53))
|
||||
|
||||
## [2.5.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/components-meta
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
@@ -90,9 +90,9 @@
|
||||
"binary": "5c8ec3876eef0ff3058fd1b2804fa01b53a869a281a0ff3430f4ddd08abc824d"
|
||||
},
|
||||
"org.standardnotes.advanced-checklist": {
|
||||
"version": "0.1.2",
|
||||
"base64": "224996cc2218868d7cc24026f15cadd866e1fc9fddec120a026f4767b20c6310",
|
||||
"binary": "aafe7e8c1753fb3ae65a38d703ec3491096f24106826da51911333f859266fb6"
|
||||
"version": "0.1.3",
|
||||
"base64": "70d3b2ab4d1a59aadea48df653d2fc6677ad725e8b734e11b477460d4ac994b3",
|
||||
"binary": "7b8ffd212b105e06e458568a188539845f05d1e5da252a9689e946006896cb55"
|
||||
},
|
||||
"org.standardnotes.file-safe": {
|
||||
"version": "2.0.16",
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/components-meta",
|
||||
"version": "2.5.7",
|
||||
"version": "2.5.8",
|
||||
"private": true,
|
||||
"author": "Standard Notes.",
|
||||
"main": "dist",
|
||||
|
||||
+6
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.1.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* debounce saving task description/draft ([#1187](https://github.com/standardnotes/app/issues/1187)) ([47a0551](https://github.com/standardnotes/app/commit/47a0551967ca420a957e2123d56bd7f0c8a95c53))
|
||||
|
||||
## [0.1.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/advanced-checklist",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "A task editor with grouping functionality.",
|
||||
"author": "Standard Notes.",
|
||||
"keywords": [
|
||||
|
||||
+11
-1
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import useResizeObserver from '@react-hook/resize-observer'
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||
import type { AppDispatch, RootState } from './store'
|
||||
@@ -32,3 +32,13 @@ export const useResize = (ref: React.RefObject<HTMLElement>, effect: (target: HT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useDebouncedCallback = (callback: () => void, waitMs: number = 500) => {
|
||||
const timeout = useRef<any>()
|
||||
|
||||
clearTimeout(timeout.current)
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
callback()
|
||||
}, waitMs)
|
||||
}
|
||||
|
||||
+8
-4
@@ -2,7 +2,7 @@ import { ChangeEvent, createRef, KeyboardEvent, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks'
|
||||
import { useAppDispatch, useAppSelector, useDebouncedCallback } from '../../app/hooks'
|
||||
import { GroupModel, taskAdded, tasksGroupDraft } from './tasks-slice'
|
||||
|
||||
import { TextInput } from '../../common/components'
|
||||
@@ -37,9 +37,7 @@ const CreateTask: React.FC<CreateTaskProps> = ({ group }) => {
|
||||
const [taskDraft, setTaskDraft] = useState<string>(group.draft ?? '')
|
||||
|
||||
function onTextChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const draft = event.target.value
|
||||
dispatch(tasksGroupDraft({ groupName, draft }))
|
||||
setTaskDraft(draft)
|
||||
setTaskDraft(event.target.value)
|
||||
}
|
||||
|
||||
function handleKeyPress(event: KeyboardEvent<HTMLInputElement>) {
|
||||
@@ -54,6 +52,12 @@ const CreateTask: React.FC<CreateTaskProps> = ({ group }) => {
|
||||
}
|
||||
}
|
||||
|
||||
useDebouncedCallback(() => {
|
||||
if (group.draft !== undefined && taskDraft !== group.draft) {
|
||||
dispatch(tasksGroupDraft({ groupName, draft: taskDraft }))
|
||||
}
|
||||
})
|
||||
|
||||
if (!canEdit) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
+6
-13
@@ -3,7 +3,7 @@ import './TaskItem.scss'
|
||||
import { ChangeEvent, createRef, KeyboardEvent, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useAppDispatch, useAppSelector, useDidMount, useResize } from '../../app/hooks'
|
||||
import { useAppDispatch, useAppSelector, useDebouncedCallback, useResize } from '../../app/hooks'
|
||||
import { taskDeleted, TaskModel, taskModified, taskToggled } from './tasks-slice'
|
||||
|
||||
import { CheckBoxInput, TextAreaInput } from '../../common/components'
|
||||
@@ -116,18 +116,11 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, groupName, innerRef, ...props
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the task after the user has stopped typing.
|
||||
*/
|
||||
useDidMount(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (description !== task.description) {
|
||||
dispatch(taskModified({ task: { id: task.id, description }, groupName }))
|
||||
}
|
||||
}, 500)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [description, groupName])
|
||||
useDebouncedCallback(() => {
|
||||
if (description !== task.description) {
|
||||
dispatch(taskModified({ task: { id: task.id, description }, groupName }))
|
||||
}
|
||||
})
|
||||
|
||||
useResize(textAreaRef, resizeTextArea)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"extends": ["../../.eslintrc.json"],
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.23.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **desktop:** release notes ([44d6bdf](https://github.com/standardnotes/app/commit/44d6bdf147d26701683cc9f0819a1f68faed7a17))
|
||||
|
||||
## [3.23.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clear desktop web cache on first version launch ([#1184](https://github.com/standardnotes/app/issues/1184)) ([7851e12](https://github.com/standardnotes/app/commit/7851e12e9e9c52dba36ad2c2f3c362a07b95288b))
|
||||
|
||||
## [3.23.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { MessageType } from '../test/TestIpcMessage'
|
||||
import { Store } from './javascripts/Main/Store/Store'
|
||||
import { StoreKeys } from './javascripts/Main/Store/StoreKeys'
|
||||
import { Paths, Urls } from './javascripts/Main/Types/Paths'
|
||||
import { UpdateState } from './javascripts/Main/UpdateManager'
|
||||
import { handleTestMessage } from './javascripts/Main/Utils/Testing'
|
||||
import { isTesting } from './javascripts/Main/Utils/Utils'
|
||||
import { WindowState } from './javascripts/Main/Window'
|
||||
|
||||
export class AppState {
|
||||
readonly version: string
|
||||
readonly store: Store
|
||||
readonly startUrl = Urls.indexHtml
|
||||
readonly isPrimaryInstance: boolean
|
||||
public willQuitApp = false
|
||||
public lastBackupDate: number | null = null
|
||||
public windowState?: WindowState
|
||||
public deepLinkUrl?: string
|
||||
public readonly updates: UpdateState
|
||||
public lastRunVersion: string
|
||||
|
||||
constructor(app: Electron.App) {
|
||||
this.version = app.getVersion()
|
||||
this.store = new Store(Paths.userDataDir)
|
||||
this.isPrimaryInstance = app.requestSingleInstanceLock()
|
||||
|
||||
this.lastRunVersion = this.store.get(StoreKeys.LastRunVersion) || 'unknown'
|
||||
this.store.set(StoreKeys.LastRunVersion, this.version)
|
||||
|
||||
makeObservable(this, {
|
||||
lastBackupDate: observable,
|
||||
setBackupCreationDate: action,
|
||||
})
|
||||
|
||||
this.updates = new UpdateState(this)
|
||||
|
||||
if (isTesting()) {
|
||||
handleTestMessage(MessageType.AppStateCall, (method, ...args) => {
|
||||
;(this as any)[method](...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public isRunningVersionForFirstTime(): boolean {
|
||||
return this.lastRunVersion !== this.version
|
||||
}
|
||||
|
||||
setBackupCreationDate(date: number | null): void {
|
||||
this.lastBackupDate = date
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,15 @@
|
||||
import { App, Shell } from 'electron'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { MessageType } from '../test/TestIpcMessage'
|
||||
import { AppState } from './AppState'
|
||||
import { createExtensionsServer } from './javascripts/Main/ExtensionsServer'
|
||||
import { Keychain } from './javascripts/Main/Keychain/Keychain'
|
||||
import { Store, StoreKeys } from './javascripts/Main/Store'
|
||||
import { StoreKeys } from './javascripts/Main/Store/StoreKeys'
|
||||
import { AppName, initializeStrings } from './javascripts/Main/Strings'
|
||||
import { Paths, Urls } from './javascripts/Main/Types/Paths'
|
||||
import { isLinux, isMac, isWindows } from './javascripts/Main/Types/Platforms'
|
||||
import { UpdateState } from './javascripts/Main/UpdateManager'
|
||||
import { handleTestMessage } from './javascripts/Main/Utils/Testing'
|
||||
import { isDev, isTesting } from './javascripts/Main/Utils/Utils'
|
||||
import { createWindowState, WindowState } from './javascripts/Main/Window'
|
||||
import { isDev } from './javascripts/Main/Utils/Utils'
|
||||
import { createWindowState } from './javascripts/Main/Window'
|
||||
|
||||
const deepLinkScheme = 'standardnotes'
|
||||
|
||||
export class AppState {
|
||||
readonly version: string
|
||||
readonly store: Store
|
||||
readonly startUrl = Urls.indexHtml
|
||||
readonly isPrimaryInstance: boolean
|
||||
public willQuitApp = false
|
||||
public lastBackupDate: number | null = null
|
||||
public windowState?: WindowState
|
||||
public deepLinkUrl?: string
|
||||
public readonly updates: UpdateState
|
||||
|
||||
constructor(app: Electron.App) {
|
||||
this.version = app.getVersion()
|
||||
this.store = new Store(Paths.userDataDir)
|
||||
this.isPrimaryInstance = app.requestSingleInstanceLock()
|
||||
makeObservable(this, {
|
||||
lastBackupDate: observable,
|
||||
setBackupCreationDate: action,
|
||||
})
|
||||
this.updates = new UpdateState(this)
|
||||
|
||||
if (isTesting()) {
|
||||
handleTestMessage(MessageType.AppStateCall, (method, ...args) => {
|
||||
;(this as any)[method](...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setBackupCreationDate(date: number | null): void {
|
||||
this.lastBackupDate = date
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeApplication(args: { app: Electron.App; ipcMain: Electron.IpcMain; shell: Shell }): void {
|
||||
const { app } = args
|
||||
|
||||
@@ -146,7 +109,6 @@ async function finishApplicationInitialization({ app, shell, state }: { app: App
|
||||
const keychainWindow = await Keychain.ensureKeychainAccess(state.store)
|
||||
|
||||
initializeStrings(app.getLocale())
|
||||
initializeExtensionsServer(state.store)
|
||||
|
||||
const windowState = await createWindowState({
|
||||
shell,
|
||||
@@ -157,6 +119,14 @@ async function finishApplicationInitialization({ app, shell, state }: { app: App
|
||||
},
|
||||
})
|
||||
|
||||
if (state.isRunningVersionForFirstTime()) {
|
||||
console.log('Clearing window cache')
|
||||
await windowState.window.webContents.session.clearCache()
|
||||
}
|
||||
|
||||
const host = createExtensionsServer()
|
||||
state.store.set(StoreKeys.ExtServerHost, host)
|
||||
|
||||
/**
|
||||
* Close the keychain window after the main window is created, otherwise the
|
||||
* app will quit automatically
|
||||
@@ -171,9 +141,3 @@ async function finishApplicationInitialization({ app, shell, state }: { app: App
|
||||
|
||||
void windowState.window.loadURL(state.startUrl)
|
||||
}
|
||||
|
||||
function initializeExtensionsServer(store: Store) {
|
||||
const host = createExtensionsServer()
|
||||
|
||||
store.set(StoreKeys.ExtServerHost, host)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import path from 'path'
|
||||
import './@types/modules'
|
||||
import { initializeApplication } from './application'
|
||||
import { enableExperimentalFeaturesForFileAccessFix } from './enableExperimentalWebFeatures'
|
||||
import { Store, StoreKeys } from './javascripts/Main/Store'
|
||||
import { Store } from './javascripts/Main/Store/Store'
|
||||
import { StoreKeys } from './javascripts/Main/Store/StoreKeys'
|
||||
import { isSnap } from './javascripts/Main/Types/Constants'
|
||||
import { Paths } from './javascripts/Main/Types/Paths'
|
||||
import { setupTesting } from './javascripts/Main/Utils/Testing'
|
||||
|
||||
@@ -2,9 +2,9 @@ import { dialog, shell, WebContents } from 'electron'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { AppMessageType, MessageType } from '../../../../test/TestIpcMessage'
|
||||
import { AppState } from '../../../application'
|
||||
import { AppState } from '../../../AppState'
|
||||
import { MessageToWebApp } from '../../Shared/IpcMessages'
|
||||
import { StoreKeys } from '../Store'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
import { backups as str } from '../Strings'
|
||||
import { Paths } from '../Types/Paths'
|
||||
import {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { URL } from 'url'
|
||||
import { extensions as str } from './Strings'
|
||||
import { Paths } from './Types/Paths'
|
||||
import { FileDoesNotExist } from './Utils/FileUtils'
|
||||
import { app } from 'electron'
|
||||
|
||||
const Protocol = 'http'
|
||||
|
||||
@@ -61,7 +62,8 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
||||
const mimeType = mime.lookup(path.parse(filePath).ext)
|
||||
|
||||
response.setHeader('Access-Control-Allow-Origin', '*')
|
||||
response.setHeader('Cache-Control', 'max-age=604800')
|
||||
response.setHeader('Cache-Control', 'no-cache')
|
||||
response.setHeader('ETag', app.getVersion())
|
||||
response.setHeader('Content-Type', `${mimeType}; charset=utf-8`)
|
||||
|
||||
const data = fs.readFileSync(filePath)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { AppState } from 'app/application'
|
||||
import { AppState } from 'app/AppState'
|
||||
import { shell } from 'electron'
|
||||
import { StoreKeys } from '../Store'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
import path from 'path'
|
||||
import {
|
||||
deleteFile,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import keytar from 'keytar'
|
||||
import { MessageToMainProcess } from '../../Shared/IpcMessages'
|
||||
import { Store, StoreKeys } from '../Store'
|
||||
import { Store } from '../Store/Store'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
import { AppName } from '../Strings'
|
||||
import { keychainAccessIsUserConfigurable } from '../Types/Constants'
|
||||
import { Paths, Urls } from '../Types/Paths'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { Store } from '../Store'
|
||||
import { Store } from '../Store/Store'
|
||||
|
||||
export interface KeychainInterface {
|
||||
ensureKeychainAccess(store: Store): Promise<BrowserWindow | undefined>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppState } from 'app/application'
|
||||
import { AppState } from 'app/AppState'
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
WebContents,
|
||||
} from 'electron'
|
||||
import { autorun } from 'mobx'
|
||||
import { Store, StoreKeys } from '../Store'
|
||||
import { Store } from '../Store/Store'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
import { appMenu as str, contextMenu } from '../Strings'
|
||||
import { TrayManager } from '../TrayManager'
|
||||
import { autoUpdatingAvailable } from '../Types/Constants'
|
||||
|
||||
@@ -19,11 +19,11 @@ import { downloadFile, getJSON } from './Networking'
|
||||
import { Component, MappingFile, PackageInfo, PackageManagerInterface, SyncTask } from './PackageManagerInterface'
|
||||
|
||||
function logMessage(...message: any) {
|
||||
log.info('PackageManager:', ...message)
|
||||
log.info('PackageManager[Info]:', ...message)
|
||||
}
|
||||
|
||||
function logError(...message: any) {
|
||||
console.error('PackageManager:', ...message)
|
||||
console.error('PackageManager[Error]:', ...message)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,10 +357,13 @@ async function installComponent(
|
||||
|
||||
function pathsForComponent(component: Pick<Component, 'content'>) {
|
||||
const relativePath = path.join(Paths.extensionsDirRelative, component.content!.package_info.identifier)
|
||||
const absolutePath = path.join(Paths.userDataDir, relativePath)
|
||||
const downloadPath = path.join(Paths.tempDir, AppName, 'downloads', component.content!.name + '.zip')
|
||||
|
||||
return {
|
||||
relativePath,
|
||||
absolutePath: path.join(Paths.userDataDir, relativePath),
|
||||
downloadPath: path.join(Paths.tempDir, AppName, 'downloads', component.content!.name + '.zip'),
|
||||
absolutePath,
|
||||
downloadPath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CrossProcessBridge } from '../../Renderer/CrossProcessBridge'
|
||||
import { Store, StoreKeys } from '../Store'
|
||||
import { Store } from '../Store/Store'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
|
||||
const path = require('path')
|
||||
const rendererPath = path.join('file://', __dirname, '/renderer.js')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-inline-comments */
|
||||
import { Store, StoreKeys } from './Store'
|
||||
import { Store } from './Store/Store'
|
||||
import { StoreKeys } from './Store/StoreKeys'
|
||||
import { isMac } from './Types/Platforms'
|
||||
import { isDev } from './Utils/Utils'
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { MessageType } from '../../../test/TestIpcMessage'
|
||||
import { BackupsDirectoryName } from './Backups/BackupsManager'
|
||||
import { Language } from './SpellcheckerManager'
|
||||
import { FileDoesNotExist } from './Utils/FileUtils'
|
||||
import { handleTestMessage } from './Utils/Testing'
|
||||
import { ensureIsBoolean, isBoolean, isDev, isTesting } from './Utils/Utils'
|
||||
|
||||
const app = process.type === 'browser' ? require('electron').app : require('@electron/remote').app
|
||||
|
||||
function logError(...message: any) {
|
||||
console.error('store:', ...message)
|
||||
}
|
||||
|
||||
export enum StoreKeys {
|
||||
ExtServerHost = 'extServerHost',
|
||||
UseSystemMenuBar = 'useSystemMenuBar',
|
||||
MenuBarVisible = 'isMenuBarVisible',
|
||||
BackupsLocation = 'backupsLocation',
|
||||
BackupsDisabled = 'backupsDisabled',
|
||||
MinimizeToTray = 'minimizeToTray',
|
||||
EnableAutoUpdate = 'enableAutoUpdates',
|
||||
ZoomFactor = 'zoomFactor',
|
||||
SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes',
|
||||
UseNativeKeychain = 'useNativeKeychain',
|
||||
|
||||
FileBackupsEnabled = 'fileBackupsEnabled',
|
||||
FileBackupsLocation = 'fileBackupsLocation',
|
||||
}
|
||||
|
||||
interface StoreData {
|
||||
[StoreKeys.ExtServerHost]: string
|
||||
[StoreKeys.UseSystemMenuBar]: boolean
|
||||
[StoreKeys.MenuBarVisible]: boolean
|
||||
[StoreKeys.BackupsLocation]: string
|
||||
[StoreKeys.BackupsDisabled]: boolean
|
||||
[StoreKeys.MinimizeToTray]: boolean
|
||||
[StoreKeys.EnableAutoUpdate]: boolean
|
||||
[StoreKeys.UseNativeKeychain]: boolean | null
|
||||
[StoreKeys.ZoomFactor]: number
|
||||
[StoreKeys.SelectedSpellCheckerLanguageCodes]: Set<Language> | null
|
||||
[StoreKeys.FileBackupsEnabled]: boolean
|
||||
[StoreKeys.FileBackupsLocation]: string
|
||||
}
|
||||
|
||||
function createSanitizedStoreData(data: any = {}): StoreData {
|
||||
return {
|
||||
[StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true),
|
||||
[StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false),
|
||||
[StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false),
|
||||
[StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false),
|
||||
[StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true),
|
||||
[StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain])
|
||||
? data[StoreKeys.UseNativeKeychain]
|
||||
: null,
|
||||
[StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost],
|
||||
[StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]),
|
||||
[StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]),
|
||||
[StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes(
|
||||
data[StoreKeys.SelectedSpellCheckerLanguageCodes],
|
||||
),
|
||||
[StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false),
|
||||
[StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation],
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeZoomFactor(factor?: any): number {
|
||||
if (typeof factor === 'number' && factor > 0) {
|
||||
return factor
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeBackupsLocation(location?: unknown): string {
|
||||
const defaultPath = path.join(
|
||||
isTesting() ? app.getPath('userData') : isDev() ? app.getPath('documents') : app.getPath('home'),
|
||||
BackupsDirectoryName,
|
||||
)
|
||||
|
||||
if (typeof location !== 'string') {
|
||||
return defaultPath
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = fs.lstatSync(location)
|
||||
if (stat.isDirectory()) {
|
||||
return location
|
||||
}
|
||||
/** Path points to something other than a directory */
|
||||
return defaultPath
|
||||
} catch (e) {
|
||||
/** Path does not point to a valid directory */
|
||||
logError(e)
|
||||
return defaultPath
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set<Language> | null {
|
||||
if (!languages) {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(languages)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const set = new Set<Language>()
|
||||
const validLanguages = Object.values(Language)
|
||||
for (const language of languages) {
|
||||
if (validLanguages.includes(language)) {
|
||||
set.add(language)
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
export function serializeStoreData(data: StoreData): string {
|
||||
return JSON.stringify(data, (_key, value) => {
|
||||
if (value instanceof Set) {
|
||||
return Array.from(value)
|
||||
}
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
function parseDataFile(filePath: string) {
|
||||
try {
|
||||
const fileData = fs.readFileSync(filePath)
|
||||
const userData = JSON.parse(fileData.toString())
|
||||
return createSanitizedStoreData(userData)
|
||||
} catch (error: any) {
|
||||
console.log('Error reading store file', error)
|
||||
if (error.code !== FileDoesNotExist) {
|
||||
logError(error)
|
||||
}
|
||||
|
||||
return createSanitizedStoreData({})
|
||||
}
|
||||
}
|
||||
|
||||
export class Store {
|
||||
static instance: Store
|
||||
readonly path: string
|
||||
readonly data: StoreData
|
||||
|
||||
static getInstance(): Store {
|
||||
if (!this.instance) {
|
||||
/**
|
||||
* Renderer process has to get `app` module via `remote`, whereas the main process
|
||||
* can get it directly app.getPath('userData') will return a string of the user's
|
||||
* app data directory path.
|
||||
* TODO(baptiste): stop using Store in the renderer process.
|
||||
*/
|
||||
const userDataPath = app.getPath('userData')
|
||||
this.instance = new Store(userDataPath)
|
||||
}
|
||||
return this.instance
|
||||
}
|
||||
|
||||
static get<T extends keyof StoreData>(key: T): StoreData[T] {
|
||||
return this.getInstance().get(key)
|
||||
}
|
||||
|
||||
constructor(userDataPath: string) {
|
||||
this.path = path.join(userDataPath, 'user-preferences.json')
|
||||
this.data = parseDataFile(this.path)
|
||||
|
||||
if (isTesting()) {
|
||||
handleTestMessage(MessageType.StoreSettingsLocation, () => this.path)
|
||||
handleTestMessage(MessageType.StoreSet, (key, value) => {
|
||||
this.set(key, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get<T extends keyof StoreData>(key: T): StoreData[T] {
|
||||
return this.data[key]
|
||||
}
|
||||
|
||||
set<T extends keyof StoreData>(key: T, val: StoreData[T]): void {
|
||||
this.data[key] = val
|
||||
fs.writeFileSync(this.path, serializeStoreData(this.data))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { MessageType } from '../../../../test/TestIpcMessage'
|
||||
import { handleTestMessage } from '../Utils/Testing'
|
||||
import { isTesting } from '../Utils/Utils'
|
||||
import { parseDataFile, serializeStoreData } from './createSanitizedStoreData'
|
||||
import { StoreData } from './StoreKeys'
|
||||
|
||||
export const app = process.type === 'browser' ? require('electron').app : require('@electron/remote').app
|
||||
|
||||
export function logError(...message: any) {
|
||||
console.error('store:', ...message)
|
||||
}
|
||||
|
||||
export class Store {
|
||||
static instance: Store
|
||||
readonly path: string
|
||||
readonly data: StoreData
|
||||
|
||||
static getInstance(): Store {
|
||||
if (!this.instance) {
|
||||
const userDataPath = app.getPath('userData')
|
||||
this.instance = new Store(userDataPath)
|
||||
}
|
||||
return this.instance
|
||||
}
|
||||
|
||||
static get<T extends keyof StoreData>(key: T): StoreData[T] {
|
||||
return this.getInstance().get(key)
|
||||
}
|
||||
|
||||
constructor(userDataPath: string) {
|
||||
this.path = path.join(userDataPath, 'user-preferences.json')
|
||||
this.data = parseDataFile(this.path)
|
||||
|
||||
if (isTesting()) {
|
||||
handleTestMessage(MessageType.StoreSettingsLocation, () => this.path)
|
||||
handleTestMessage(MessageType.StoreSet, (key, value) => {
|
||||
this.set(key, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get<T extends keyof StoreData>(key: T): StoreData[T] {
|
||||
return this.data[key]
|
||||
}
|
||||
|
||||
set<T extends keyof StoreData>(key: T, val: StoreData[T]): void {
|
||||
this.data[key] = val
|
||||
fs.writeFileSync(this.path, serializeStoreData(this.data))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Language } from '../SpellcheckerManager'
|
||||
|
||||
export enum StoreKeys {
|
||||
ExtServerHost = 'extServerHost',
|
||||
UseSystemMenuBar = 'useSystemMenuBar',
|
||||
MenuBarVisible = 'isMenuBarVisible',
|
||||
BackupsLocation = 'backupsLocation',
|
||||
BackupsDisabled = 'backupsDisabled',
|
||||
MinimizeToTray = 'minimizeToTray',
|
||||
EnableAutoUpdate = 'enableAutoUpdates',
|
||||
ZoomFactor = 'zoomFactor',
|
||||
SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes',
|
||||
UseNativeKeychain = 'useNativeKeychain',
|
||||
FileBackupsEnabled = 'fileBackupsEnabled',
|
||||
FileBackupsLocation = 'fileBackupsLocation',
|
||||
LastRunVersion = 'LastRunVersion',
|
||||
}
|
||||
|
||||
export interface StoreData {
|
||||
[StoreKeys.ExtServerHost]: string
|
||||
[StoreKeys.UseSystemMenuBar]: boolean
|
||||
[StoreKeys.MenuBarVisible]: boolean
|
||||
[StoreKeys.BackupsLocation]: string
|
||||
[StoreKeys.BackupsDisabled]: boolean
|
||||
[StoreKeys.MinimizeToTray]: boolean
|
||||
[StoreKeys.EnableAutoUpdate]: boolean
|
||||
[StoreKeys.UseNativeKeychain]: boolean | null
|
||||
[StoreKeys.ZoomFactor]: number
|
||||
[StoreKeys.SelectedSpellCheckerLanguageCodes]: Set<Language> | null
|
||||
[StoreKeys.FileBackupsEnabled]: boolean
|
||||
[StoreKeys.FileBackupsLocation]: string
|
||||
[StoreKeys.LastRunVersion]: string
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { BackupsDirectoryName } from '../Backups/BackupsManager'
|
||||
import { Language } from '../SpellcheckerManager'
|
||||
import { FileDoesNotExist } from '../Utils/FileUtils'
|
||||
import { ensureIsBoolean, isBoolean, isDev, isTesting } from '../Utils/Utils'
|
||||
import { StoreData, StoreKeys } from './StoreKeys'
|
||||
import { app, logError } from './Store'
|
||||
|
||||
export function createSanitizedStoreData(data: any = {}): StoreData {
|
||||
return {
|
||||
[StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true),
|
||||
[StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false),
|
||||
[StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false),
|
||||
[StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false),
|
||||
[StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true),
|
||||
[StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain])
|
||||
? data[StoreKeys.UseNativeKeychain]
|
||||
: null,
|
||||
[StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost],
|
||||
[StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]),
|
||||
[StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]),
|
||||
[StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes(
|
||||
data[StoreKeys.SelectedSpellCheckerLanguageCodes],
|
||||
),
|
||||
[StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false),
|
||||
[StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation],
|
||||
[StoreKeys.LastRunVersion]: data[StoreKeys.LastRunVersion],
|
||||
}
|
||||
}
|
||||
function sanitizeZoomFactor(factor?: any): number {
|
||||
if (typeof factor === 'number' && factor > 0) {
|
||||
return factor
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
function sanitizeBackupsLocation(location?: unknown): string {
|
||||
const defaultPath = path.join(
|
||||
isTesting() ? app.getPath('userData') : isDev() ? app.getPath('documents') : app.getPath('home'),
|
||||
BackupsDirectoryName,
|
||||
)
|
||||
|
||||
if (typeof location !== 'string') {
|
||||
return defaultPath
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = fs.lstatSync(location)
|
||||
if (stat.isDirectory()) {
|
||||
return location
|
||||
}
|
||||
/** Path points to something other than a directory */
|
||||
return defaultPath
|
||||
} catch (e) {
|
||||
/** Path does not point to a valid directory */
|
||||
logError(e)
|
||||
return defaultPath
|
||||
}
|
||||
}
|
||||
function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set<Language> | null {
|
||||
if (!languages) {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(languages)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const set = new Set<Language>()
|
||||
const validLanguages = Object.values(Language)
|
||||
for (const language of languages) {
|
||||
if (validLanguages.includes(language)) {
|
||||
set.add(language)
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
export function serializeStoreData(data: StoreData): string {
|
||||
return JSON.stringify(data, (_key, value) => {
|
||||
if (value instanceof Set) {
|
||||
return Array.from(value)
|
||||
}
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
export function parseDataFile(filePath: string) {
|
||||
try {
|
||||
const fileData = fs.readFileSync(filePath)
|
||||
const userData = JSON.parse(fileData.toString())
|
||||
return createSanitizedStoreData(userData)
|
||||
} catch (error: any) {
|
||||
console.log('Error reading store file', error)
|
||||
if (error.code !== FileDoesNotExist) {
|
||||
logError(error)
|
||||
}
|
||||
|
||||
return createSanitizedStoreData({})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Menu, Tray } from 'electron'
|
||||
import path from 'path'
|
||||
import { Store, StoreKeys } from './Store'
|
||||
import { Store } from './Store/Store'
|
||||
import { StoreKeys } from './Store/StoreKeys'
|
||||
import { AppName, tray as str } from './Strings'
|
||||
import { isLinux, isWindows } from './Types/Platforms'
|
||||
import { isDev } from './Utils/Utils'
|
||||
|
||||
@@ -4,9 +4,9 @@ import electronLog from 'electron-log'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { action, autorun, computed, makeObservable, observable } from 'mobx'
|
||||
import { MessageType } from '../../../test/TestIpcMessage'
|
||||
import { AppState } from '../../application'
|
||||
import { AppState } from '../../AppState'
|
||||
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
|
||||
import { StoreKeys } from './Store'
|
||||
import { StoreKeys } from './Store/StoreKeys'
|
||||
import { updates as str } from './Strings'
|
||||
import { autoUpdatingAvailable } from './Types/Constants'
|
||||
import { handleTestMessage } from './Utils/Testing'
|
||||
@@ -145,8 +145,10 @@ export function setupUpdates(window: BrowserWindow, appState: AppState, backupsM
|
||||
|
||||
export function openChangelog(state: UpdateState): void {
|
||||
const url = 'https://github.com/standardnotes/app/releases'
|
||||
if (state.latestVersion) {
|
||||
void shell.openExternal(`${url}/tag/v${state.latestVersion}`)
|
||||
const latestVersion = state.latestVersion
|
||||
if (latestVersion) {
|
||||
const tagPath = `tag/%40standardnotes%2Fdesktop%40${latestVersion}`
|
||||
void shell.openExternal(`${url}/${tagPath}`)
|
||||
} else {
|
||||
void shell.openExternal(url)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import fs from 'fs'
|
||||
import { debounce } from 'lodash'
|
||||
import path from 'path'
|
||||
import { AppMessageType, MessageType } from '../../../test/TestIpcMessage'
|
||||
import { AppState } from '../../application'
|
||||
import { AppState } from '../../AppState'
|
||||
import { MessageToWebApp } from '../Shared/IpcMessages'
|
||||
import { createBackupsManager } from './Backups/BackupsManager'
|
||||
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
|
||||
@@ -16,7 +16,8 @@ import { initializePackageManager } from './Packages/PackageManager'
|
||||
import { RemoteBridge } from './Remote/RemoteBridge'
|
||||
import { initializeSearchManager } from './Search/SearchManager'
|
||||
import { createSpellcheckerManager } from './SpellcheckerManager'
|
||||
import { Store, StoreKeys } from './Store'
|
||||
import { Store } from './Store/Store'
|
||||
import { StoreKeys } from './Store/StoreKeys'
|
||||
import { createTrayManager, TrayManager } from './TrayManager'
|
||||
import { Paths } from './Types/Paths'
|
||||
import { isMac, isWindows } from './Types/Platforms'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { Store, StoreKeys } from './Store'
|
||||
import { Store } from './Store/Store'
|
||||
import { StoreKeys } from './Store/StoreKeys'
|
||||
|
||||
export function initializeZoomManager(window: BrowserWindow, store: Store): void {
|
||||
window.webContents.on('dom-ready', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.23.19",
|
||||
"version": "3.23.26",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -21,6 +21,8 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
html/Web.bundle/src/web-src
|
||||
|
||||
.env
|
||||
|
||||
# Android/IntelliJ
|
||||
@@ -75,4 +77,4 @@ ios-release.bundle.map
|
||||
|
||||
codeqldb
|
||||
|
||||
vendor/bundle
|
||||
vendor/bundle
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.26.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.26.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.26.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.26.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.26.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.26.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mobile:** android icons loading issue ([98ed647](https://github.com/standardnotes/app/commit/98ed6472c079b5f32ee3d82a2c68a6eaca2a58ec))
|
||||
|
||||
## [3.26.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
# [3.26.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* **dev:** option to render web app inside the mobile app ([#1164](https://github.com/standardnotes/app/issues/1164)) ([4f5d092](https://github.com/standardnotes/app/commit/4f5d092f89a5488dc4c9f74730a110845230f2b3))
|
||||
|
||||
## [3.25.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { WebView } from 'react-native-webview'
|
||||
|
||||
export const MobileWebAppContainer = () => {
|
||||
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/loader.html'
|
||||
const params = 'platform=' + Platform.OS
|
||||
const injectedJS = `
|
||||
if (!window.location.search) {
|
||||
var link = document.getElementById('web-bundle-progress-bar');
|
||||
link.href = './src/index.html?${params}';
|
||||
link.click();
|
||||
}`
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
return (
|
||||
<WebView
|
||||
source={{ uri: sourceUri }}
|
||||
originWhitelist={['*']}
|
||||
onLoad={() => {}}
|
||||
onError={(err) => console.error('An error has occurred', err)}
|
||||
onHttpError={() => console.error('An HTTP error occurred')}
|
||||
onMessage={() => {}}
|
||||
allowFileAccess={true}
|
||||
injectedJavaScript={injectedJS}
|
||||
/>
|
||||
)
|
||||
/* eslint-enable @typescript-eslint/no-empty-function */
|
||||
}
|
||||
@@ -219,6 +219,7 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceSets.main.assets.srcDirs += ['src/main/assets', '../../html']
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -121,7 +121,7 @@ platform :android do
|
||||
|
||||
desc 'Deploy dev app'
|
||||
lane :dev do
|
||||
version = 300_023_000 + ENV['BUILD_NUMBER'].to_i
|
||||
version = 300_023_100 + ENV['BUILD_NUMBER'].to_i
|
||||
deploy_android 'dev', version
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<a id="web-bundle-progress-bar" href="#"/></a>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link rel="stylesheet" href="web-src/app.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
window.defaultSyncServer = "https://api.standardnotes.com";
|
||||
window.defaultFilesHost = "https://files.standardnotes.com";
|
||||
window.enabledUnfinishedFeatures = "" === 'true';
|
||||
window.websocketUrl = "";
|
||||
window.purchaseUrl = "https://standardnotes.com/purchase";
|
||||
window.plansUrl = "https://standardnotes.com/plans";
|
||||
window.dashboardUrl = "https://standardnotes.com/dashboard";
|
||||
</script>
|
||||
|
||||
<script src="web-src/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -434,7 +434,7 @@ PODS:
|
||||
- RNZipArchive/Core (6.0.8):
|
||||
- React-Core
|
||||
- SSZipArchive (= 2.2.3)
|
||||
- sn-textview (1.0.2):
|
||||
- sn-textview (1.1.0):
|
||||
- React-Core
|
||||
- SNReactNative (1.0.1):
|
||||
- React-Core
|
||||
@@ -749,7 +749,7 @@ SPEC CHECKSUMS:
|
||||
RNSVG: 302bfc9905bd8122f08966dc2ce2d07b7b52b9f8
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
RNZipArchive: 3f89b114cfeb89dac027fc5c7afd7e58d3dc4ebf
|
||||
sn-textview: aaeeb41791e7d1784072adfb6b610db9b764acda
|
||||
sn-textview: 494186ad03660daa52b1d3fd7b50b116b550f925
|
||||
SNReactNative: b5e9e529c175c13f3a618e27c76cf3071213d5e1
|
||||
SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9
|
||||
TrustKit: 073855e3adecd317417bda4ac9e9ac54a2e3b9f2
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
CD7D8400277FF8CB00915D89 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = CD7D83FD277FF8CB00915D89 /* [email protected] */; };
|
||||
CD7D8401277FF8CB00915D89 /* Red.png in Resources */ = {isa = PBXBuildFile; fileRef = CD7D83FE277FF8CB00915D89 /* Red.png */; };
|
||||
CD7D8402277FF8CB00915D89 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = CD7D83FF277FF8CB00915D89 /* [email protected] */; };
|
||||
D261494528699DCE00B17102 /* Web.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D261494428699DCE00B17102 /* Web.bundle */; };
|
||||
D261494628699DCE00B17102 /* Web.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D261494428699DCE00B17102 /* Web.bundle */; };
|
||||
DD3D1CE428EC1C8BA0C49211 /* libPods-StandardNotes.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 41388EB4F886116157DA092C /* libPods-StandardNotes.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -67,6 +69,7 @@
|
||||
CD7D83FD277FF8CB00915D89 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
|
||||
CD7D83FE277FF8CB00915D89 /* Red.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Red.png; sourceTree = "<group>"; };
|
||||
CD7D83FF277FF8CB00915D89 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
|
||||
D261494428699DCE00B17102 /* Web.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Web.bundle; path = ../html/Web.bundle; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -132,6 +135,7 @@
|
||||
13B07FAE1A68108700A75B9A /* StandardNotes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D261494428699DCE00B17102 /* Web.bundle */,
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */,
|
||||
CD7D5EE527801F10005FE1BF /* StandardNotes.entitlements */,
|
||||
@@ -308,6 +312,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D261494528699DCE00B17102 /* Web.bundle in Resources */,
|
||||
CD7D8401277FF8CB00915D89 /* Red.png in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
CD7D8402277FF8CB00915D89 /* [email protected] in Resources */,
|
||||
@@ -321,6 +326,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CD7D5ED4278015D2005FE1BF /* Red.png in Resources */,
|
||||
D261494628699DCE00B17102 /* Web.bundle in Resources */,
|
||||
CD7D5EE3278016E3005FE1BF /* Dev.xcassets in Resources */,
|
||||
CD7D5ED5278015D2005FE1BF /* Images.xcassets in Resources */,
|
||||
CD7D5ED6278015D2005FE1BF /* [email protected] in Resources */,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.25.15",
|
||||
"version": "3.26.7",
|
||||
"author": "Standard Notes",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -15,7 +15,8 @@
|
||||
"lint:fix": "yarn lint --fix",
|
||||
"format": "prettier ./src",
|
||||
"format:fix": "yarn format --write",
|
||||
"build": "yarn android:bundle && yarn install:pods",
|
||||
"build": "yarn web:bundle && yarn install:pods && yarn android:bundle",
|
||||
"web:bundle": "cp -r ../web/dist/. html/Web.bundle/src/web-src/",
|
||||
"pods": "yarn install:pods",
|
||||
"tsc": "tsc --noEmit",
|
||||
"start": "react-native start",
|
||||
@@ -43,6 +44,7 @@
|
||||
"@standardnotes/sncrypto-common": "1.9.0",
|
||||
"@standardnotes/snjs": "^2.118.3",
|
||||
"@standardnotes/stylekit": "5.29.3",
|
||||
"@standardnotes/web": "workspace:^",
|
||||
"@types/styled-components-react-native": "5.1.3",
|
||||
"js-base64": "^3.7.2",
|
||||
"moment": "^2.29.2",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { IsDev } from '@Lib/Utils'
|
||||
|
||||
export enum ErrorMessage {
|
||||
GeneralText = 'An error occurred. Please try again later.',
|
||||
}
|
||||
|
||||
export const WebAppOptionEnabled = IsDev
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SCREEN_MANAGE_SESSIONS,
|
||||
SCREEN_SETTINGS,
|
||||
SCREEN_UPLOADED_FILES_LIST,
|
||||
SCREEN_WEB_APP,
|
||||
} from '@Root/Screens/screens'
|
||||
import { Settings } from '@Root/Screens/Settings/Settings'
|
||||
import { UploadedFilesList } from '@Root/Screens/UploadedFilesList/UploadedFilesList'
|
||||
@@ -29,6 +30,7 @@ import React, { memo, useContext } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { HeaderButtons, Item } from 'react-navigation-header-buttons'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { MobileWebAppContainer } from '../MobileWebAppContainer'
|
||||
import { HeaderTitleParams, TEnvironment } from './App'
|
||||
import { ApplicationContext } from './ApplicationContext'
|
||||
import { AppStackComponent } from './AppStack'
|
||||
@@ -63,6 +65,7 @@ export type ModalStackNavigatorParamList = {
|
||||
title?: string
|
||||
text: string
|
||||
}
|
||||
[SCREEN_WEB_APP]: undefined
|
||||
}
|
||||
|
||||
export type ModalStackNavigationProp<T extends keyof ModalStackNavigatorParamList> = {
|
||||
@@ -303,6 +306,7 @@ export const MainStackComponent = ({ env }: { env: TEnvironment }) => {
|
||||
})}
|
||||
component={WorkspaceInputModal}
|
||||
/>
|
||||
<MainStack.Screen name={SCREEN_WEB_APP} options={() => ({ title: 'App' })} component={MobileWebAppContainer} />
|
||||
</MainStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WebAppOptionEnabled } from '@Lib/constants'
|
||||
import { useSignedIn } from '@Lib/SnjsHelperHooks'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
@@ -7,7 +8,7 @@ import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS } from '@Root/Screens/screens'
|
||||
import { SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS, SCREEN_WEB_APP } from '@Root/Screens/screens'
|
||||
import { ButtonType, PrefKey } from '@standardnotes/snjs'
|
||||
import moment from 'moment'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
@@ -182,6 +183,10 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
)
|
||||
}, [application.alertService])
|
||||
|
||||
const openWebApp = useCallback(() => {
|
||||
navigation.push(SCREEN_WEB_APP)
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={title} />
|
||||
@@ -221,6 +226,10 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
onPress={onExportPress}
|
||||
/>
|
||||
|
||||
{WebAppOptionEnabled && (
|
||||
<ButtonCell testID="openWebApp" leftAligned title={'Open Web App'} onPress={() => openWebApp()} />
|
||||
)}
|
||||
|
||||
{!signedIn && (
|
||||
<SectionedAccessoryTableCell
|
||||
testID="lastExportDate"
|
||||
|
||||
@@ -16,3 +16,5 @@ export const SCREEN_MANAGE_SESSIONS = 'ManageSessions' as const
|
||||
export const MODAL_BLOCKING_ALERT = 'ModalBlockingAlert' as const
|
||||
|
||||
export const SCREEN_VIEW_PROTECTED_NOTE = 'ViewProtectedNote' as const
|
||||
|
||||
export const SCREEN_WEB_APP = 'WebApp' as const
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.30](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.2.29",
|
||||
"version": "1.2.39",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.45](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.44](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.43](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web-server",
|
||||
"version": "1.2.43",
|
||||
"version": "1.2.48",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"author": "Standard Notes.",
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.29.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** file protected overlay not toggling off correctly ([#1190](https://github.com/standardnotes/app/issues/1190)) ([7c3c59d](https://github.com/standardnotes/app/commit/7c3c59dd926ac5561b147dd025f0c5479286a1bb))
|
||||
|
||||
# [3.29.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-01)
|
||||
|
||||
### Features
|
||||
|
||||
* allow previewing CSV files as plaintext ([#1188](https://github.com/standardnotes/app/issues/1188)) ([87213e9](https://github.com/standardnotes/app/commit/87213e9f5d3b4ecc9864b9b7b501262379644d47))
|
||||
|
||||
## [3.28.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.28.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* add tag id to tag context menu ([#1185](https://github.com/standardnotes/app/issues/1185)) ([8b845ea](https://github.com/standardnotes/app/commit/8b845ea424e0731d9c850c3e6604534bf5aeda86))
|
||||
|
||||
## [3.27.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.27.7",
|
||||
"version": "3.29.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes",
|
||||
@@ -15,6 +15,7 @@
|
||||
"prebuild": "yarn clean",
|
||||
"build": "webpack --config web.webpack.prod.js && yarn tsc",
|
||||
"lint": "eslint src/javascripts",
|
||||
"format": "prettier --write src/javascripts",
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
"test": "jest --config jest.config.js --coverage",
|
||||
"upgrade:snjs": "ncu -u '@standardnotes/*'"
|
||||
|
||||
@@ -12,7 +12,7 @@ import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import WorkspaceSwitcherOption from './WorkspaceSwitcher/WorkspaceSwitcherOption'
|
||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { formatLastSyncDate } from '@/Utils/FormatLastSyncDate'
|
||||
import { formatLastSyncDate } from '@/Utils/DateUtils'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -17,13 +17,13 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're
|
||||
import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal'
|
||||
import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
||||
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||
import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu'
|
||||
import { ToastContainer } from '@standardnotes/toast'
|
||||
import FilePreviewModalWrapper from '@/Components/FilePreview/FilePreviewModal'
|
||||
import ContentListView from '@/Components/ContentListView/ContentListView'
|
||||
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
|
||||
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -214,7 +214,10 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagsContextMenuWrapper viewControllerManager={viewControllerManager} />
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
|
||||
@@ -86,7 +86,7 @@ const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, s
|
||||
return (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="max-h-120 fixed flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||
className="max-h-120 fixed z-dropdown-menu flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||
style={{
|
||||
...contextMenuStyle,
|
||||
maxHeight: contextMenuMaxHeight,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const PreviewableTextFileTypes = ['text/plain', 'application/json']
|
||||
export const PreviewableTextFileTypes = ['text/plain', 'text/csv', 'application/json']
|
||||
|
||||
export const isFileTypePreviewable = (fileType: string) => {
|
||||
const isImage = fileType.startsWith('image/')
|
||||
|
||||
@@ -5,17 +5,27 @@ import FileViewWithoutProtection from './FileViewWithoutProtection'
|
||||
import { FileViewProps } from './FileViewProps'
|
||||
|
||||
const FileView = ({ application, viewControllerManager, file }: FileViewProps) => {
|
||||
const [shouldShowProtectedOverlay, setShouldShowProtectedOverlay] = useState(
|
||||
file.protected && !application.hasProtectionSources(),
|
||||
)
|
||||
const [shouldShowProtectedOverlay, setShouldShowProtectedOverlay] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
viewControllerManager.filesController.setShowProtectedOverlay(file.protected && !application.hasProtectionSources())
|
||||
}, [application, file.protected, viewControllerManager.filesController])
|
||||
|
||||
useEffect(() => {
|
||||
setShouldShowProtectedOverlay(viewControllerManager.filesController.showProtectedOverlay)
|
||||
}, [viewControllerManager.filesController.showProtectedOverlay])
|
||||
|
||||
const dismissProtectedWarning = useCallback(() => {
|
||||
void viewControllerManager.filesController.toggleFileProtection(file)
|
||||
}, [file, viewControllerManager.filesController])
|
||||
const dismissProtectedOverlay = useCallback(async () => {
|
||||
let showFileContents = true
|
||||
|
||||
if (application.hasProtectionSources()) {
|
||||
showFileContents = await application.protections.authorizeItemAccess(file)
|
||||
}
|
||||
|
||||
if (showFileContents) {
|
||||
setShouldShowProtectedOverlay(false)
|
||||
}
|
||||
}, [application, file])
|
||||
|
||||
return shouldShowProtectedOverlay ? (
|
||||
<div aria-label="Note" className="section editor sn-component">
|
||||
@@ -23,7 +33,7 @@ const FileView = ({ application, viewControllerManager, file }: FileViewProps) =
|
||||
<ProtectedItemOverlay
|
||||
viewControllerManager={viewControllerManager}
|
||||
hasProtectionSources={application.hasProtectionSources()}
|
||||
onViewItem={dismissProtectedWarning}
|
||||
onViewItem={dismissProtectedOverlay}
|
||||
itemType={'note'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||
import { NotesOptionsProps } from './NotesOptionsProps'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
closeOnBlur: NotesOptionsProps['closeOnBlur']
|
||||
@@ -78,13 +79,6 @@ const calculateReadTime = (words: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (date: Date | undefined) => {
|
||||
if (!date) {
|
||||
return
|
||||
}
|
||||
return `${date.toDateString()} ${date.toLocaleTimeString()}`
|
||||
}
|
||||
|
||||
const NoteAttributes: FunctionComponent<{
|
||||
application: SNApplication
|
||||
note: SNNote
|
||||
@@ -93,9 +87,9 @@ const NoteAttributes: FunctionComponent<{
|
||||
|
||||
const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words])
|
||||
|
||||
const dateLastModified = useMemo(() => formatDate(note.userModifiedDate), [note.userModifiedDate])
|
||||
const dateLastModified = useMemo(() => formatDateForContextMenu(note.userModifiedDate), [note.userModifiedDate])
|
||||
|
||||
const dateCreated = useMemo(() => formatDate(note.created_at), [note.created_at])
|
||||
const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at])
|
||||
|
||||
const editor = application.componentManager.editorForNote(note)
|
||||
const format = editor?.package_info?.file_type || 'txt'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { STRING_GENERIC_SYNC_ERROR } from '@/Constants/Strings'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { formatLastSyncDate } from '@/Utils/FormatLastSyncDate'
|
||||
import { formatLastSyncDate } from '@/Utils/DateUtils'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { AnyExtension } from './AnyExtension'
|
||||
|
||||
export interface ExtensionItemProps {
|
||||
application: WebApplication
|
||||
extension: AnyExtension
|
||||
first: boolean
|
||||
latestVersion: string | undefined
|
||||
uninstall: (extension: AnyExtension) => void
|
||||
toggleActivate?: (extension: AnyExtension) => void
|
||||
}
|
||||
+8
-8
@@ -1,29 +1,29 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import OfflineSubscription from '@/Components/Preferences/Panes/Account/OfflineSubscription'
|
||||
import OfflineSubscription from '@/Components/Preferences/Panes/General/Advanced/OfflineSubscription'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import Extensions from '@/Components/Preferences/Panes/Extensions/Extensions'
|
||||
import { ExtensionsLatestVersions } from '@/Components/Preferences/Panes/Extensions/ExtensionsLatestVersions'
|
||||
import PackagesPreferencesSection from '@/Components/Preferences/Panes/General/Advanced/Packages/Section'
|
||||
import { PackageProvider } from '@/Components/Preferences/Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||
import AccordionItem from '@/Components/Shared/AccordionItem'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
extensionsLatestVersions: ExtensionsLatestVersions
|
||||
extensionsLatestVersions: PackageProvider
|
||||
}
|
||||
|
||||
const Advanced: FunctionComponent<Props> = ({ application, viewControllerManager, extensionsLatestVersions }) => {
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<AccordionItem title={'Advanced Settings'}>
|
||||
<AccordionItem title={'Advanced Options'}>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<OfflineSubscription application={application} viewControllerManager={viewControllerManager} />
|
||||
<Extensions
|
||||
<PackagesPreferencesSection
|
||||
className={'mt-3'}
|
||||
application={application}
|
||||
extensionsLatestVersions={extensionsLatestVersions}
|
||||
+5
-5
@@ -2,11 +2,11 @@ import { DisplayStringForContentType } from '@standardnotes/snjs'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { Fragment, FunctionComponent } from 'react'
|
||||
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { AnyExtension } from './AnyExtension'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import { AnyPackageType } from './Types/AnyPackageType'
|
||||
import PreferencesSegment from '../../../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
const ConfirmCustomExtension: FunctionComponent<{
|
||||
component: AnyExtension
|
||||
const ConfirmCustomPackage: FunctionComponent<{
|
||||
component: AnyPackageType
|
||||
callback: (confirmed: boolean) => void
|
||||
}> = ({ component, callback }) => {
|
||||
const fields = [
|
||||
@@ -66,4 +66,4 @@ const ConfirmCustomExtension: FunctionComponent<{
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmCustomExtension
|
||||
export default ConfirmCustomPackage
|
||||
+16
-6
@@ -3,9 +3,10 @@ import { ComponentMutator, SNComponent } from '@standardnotes/snjs'
|
||||
import { SubtitleLight } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ExtensionInfoCell from './ExtensionInfoCell'
|
||||
import { ExtensionItemProps } from './ExtensionItemProps'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import PackageEntrySubInfo from './PackageEntrySubInfo'
|
||||
import PreferencesSegment from '../../../../PreferencesComponents/PreferencesSegment'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { AnyPackageType } from './Types/AnyPackageType'
|
||||
|
||||
const UseHosted: FunctionComponent<{
|
||||
offlineOnly: boolean
|
||||
@@ -17,7 +18,16 @@ const UseHosted: FunctionComponent<{
|
||||
</div>
|
||||
)
|
||||
|
||||
const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({ application, extension, uninstall }) => {
|
||||
interface PackageEntryProps {
|
||||
application: WebApplication
|
||||
extension: AnyPackageType
|
||||
first: boolean
|
||||
latestVersion: string | undefined
|
||||
uninstall: (extension: AnyPackageType) => void
|
||||
toggleActivate?: (extension: AnyPackageType) => void
|
||||
}
|
||||
|
||||
const PackageEntry: FunctionComponent<PackageEntryProps> = ({ application, extension, uninstall }) => {
|
||||
const [offlineOnly, setOfflineOnly] = useState(extension instanceof SNComponent ? extension.offlineOnly : false)
|
||||
const [extensionName, setExtensionName] = useState(extension.displayName)
|
||||
|
||||
@@ -56,7 +66,7 @@ const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({ application, ext
|
||||
|
||||
return (
|
||||
<PreferencesSegment classes={'mb-5'}>
|
||||
<ExtensionInfoCell isThirdParty={isThirdParty} extensionName={extensionName} changeName={changeExtensionName} />
|
||||
<PackageEntrySubInfo isThirdParty={isThirdParty} extensionName={extensionName} changeName={changeExtensionName} />
|
||||
|
||||
<div className="my-1" />
|
||||
|
||||
@@ -71,4 +81,4 @@ const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({ application, ext
|
||||
)
|
||||
}
|
||||
|
||||
export default ExtensionItem
|
||||
export default PackageEntry
|
||||
+2
-2
@@ -6,7 +6,7 @@ type Props = {
|
||||
isThirdParty: boolean
|
||||
}
|
||||
|
||||
const ExtensionInfoCell: FunctionComponent<Props> = ({ extensionName, changeName, isThirdParty }) => {
|
||||
const PackageEntrySubInfo: FunctionComponent<Props> = ({ extensionName, changeName, isThirdParty }) => {
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const [newExtensionName, setNewExtensionName] = useState<string>(extensionName)
|
||||
|
||||
@@ -73,4 +73,4 @@ const ExtensionInfoCell: FunctionComponent<Props> = ({ extensionName, changeName
|
||||
)
|
||||
}
|
||||
|
||||
export default ExtensionInfoCell
|
||||
export default PackageEntrySubInfo
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ClientDisplayableError, FeatureDescription } from '@standardnotes/snjs'
|
||||
import { makeAutoObservable, observable } from 'mobx'
|
||||
import { AnyExtension } from './AnyExtension'
|
||||
import { AnyPackageType } from '../Types/AnyPackageType'
|
||||
import { ComponentChecksumsType } from '@standardnotes/components-meta'
|
||||
import RawComponentChecksumsFile from '@standardnotes/components-meta/dist/zips/checksums.json'
|
||||
const ComponentChecksums = RawComponentChecksumsFile as ComponentChecksumsType
|
||||
|
||||
export class ExtensionsLatestVersions {
|
||||
static async load(application: WebApplication): Promise<ExtensionsLatestVersions | undefined> {
|
||||
export class PackageProvider {
|
||||
static async load(application: WebApplication): Promise<PackageProvider | undefined> {
|
||||
const response = await application.getAvailableSubscriptions()
|
||||
|
||||
if (response instanceof ClientDisplayableError) {
|
||||
@@ -18,16 +18,16 @@ export class ExtensionsLatestVersions {
|
||||
collectFeatures(response.PLUS_PLAN?.features as FeatureDescription[], versionMap)
|
||||
collectFeatures(response.PRO_PLAN?.features as FeatureDescription[], versionMap)
|
||||
|
||||
return new ExtensionsLatestVersions(versionMap)
|
||||
return new PackageProvider(versionMap)
|
||||
}
|
||||
|
||||
constructor(private readonly latestVersionsMap: Map<string, string>) {
|
||||
makeAutoObservable<ExtensionsLatestVersions, 'latestVersionsMap'>(this, {
|
||||
makeAutoObservable<PackageProvider, 'latestVersionsMap'>(this, {
|
||||
latestVersionsMap: observable.ref,
|
||||
})
|
||||
}
|
||||
|
||||
getVersion(extension: AnyExtension): string | undefined {
|
||||
getVersion(extension: AnyPackageType): string | undefined {
|
||||
return this.latestVersionsMap.get(extension.package_info.identifier)
|
||||
}
|
||||
}
|
||||
+39
-23
@@ -3,26 +3,34 @@ import Button from '@/Components/Button/Button'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FunctionComponent, useEffect, useRef, useState } from 'react'
|
||||
import { Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ExtensionsLatestVersions } from './ExtensionsLatestVersions'
|
||||
import ExtensionItem from './ExtensionItem'
|
||||
import ConfirmCustomExtension from './ConfirmCustomExtension'
|
||||
import { AnyExtension } from './AnyExtension'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import { PackageProvider } from './Provider/PackageProvider'
|
||||
import PackageEntry from './PackageEntry'
|
||||
import ConfirmCustomPackage from './ConfirmCustomPackage'
|
||||
import { AnyPackageType } from './Types/AnyPackageType'
|
||||
import PreferencesSegment from '../../../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
const loadExtensions = (application: WebApplication) =>
|
||||
application.items.getItems([ContentType.ActionsExtension, ContentType.Component, ContentType.Theme]) as AnyExtension[]
|
||||
application.items.getItems([
|
||||
ContentType.ActionsExtension,
|
||||
ContentType.Component,
|
||||
ContentType.Theme,
|
||||
]) as AnyPackageType[]
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
extensionsLatestVersions: ExtensionsLatestVersions
|
||||
extensionsLatestVersions: PackageProvider
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Extensions: FunctionComponent<Props> = ({ application, extensionsLatestVersions, className = '' }) => {
|
||||
const PackagesPreferencesSection: FunctionComponent<Props> = ({
|
||||
application,
|
||||
extensionsLatestVersions,
|
||||
className = '',
|
||||
}) => {
|
||||
const [customUrl, setCustomUrl] = useState('')
|
||||
const [confirmableExtension, setConfirmableExtension] = useState<AnyExtension | undefined>(undefined)
|
||||
const [confirmableExtension, setConfirmableExtension] = useState<AnyPackageType | undefined>(undefined)
|
||||
const [extensions, setExtensions] = useState(loadExtensions(application))
|
||||
|
||||
const confirmableEnd = useRef<HTMLDivElement>(null)
|
||||
@@ -33,7 +41,7 @@ const Extensions: FunctionComponent<Props> = ({ application, extensionsLatestVer
|
||||
}
|
||||
}, [confirmableExtension, confirmableEnd])
|
||||
|
||||
const uninstallExtension = async (extension: AnyExtension) => {
|
||||
const uninstallExtension = async (extension: AnyPackageType) => {
|
||||
application.alertService
|
||||
.confirm(
|
||||
'Are you sure you want to uninstall this extension? Note that extensions managed by your subscription will automatically be re-installed on application restart.',
|
||||
@@ -69,7 +77,7 @@ const Extensions: FunctionComponent<Props> = ({ application, extensionsLatestVer
|
||||
}
|
||||
|
||||
const confirmExtension = async () => {
|
||||
await application.mutator.insertItem(confirmableExtension as AnyExtension)
|
||||
await application.mutator.insertItem(confirmableExtension as AnyPackageType)
|
||||
application.sync.sync().catch(console.error)
|
||||
setExtensions(loadExtensions(application))
|
||||
}
|
||||
@@ -95,7 +103,7 @@ const Extensions: FunctionComponent<Props> = ({ application, extensionsLatestVer
|
||||
{visibleExtensions
|
||||
.sort((e1, e2) => e1.displayName?.toLowerCase().localeCompare(e2.displayName?.toLowerCase()))
|
||||
.map((extension, i) => (
|
||||
<ExtensionItem
|
||||
<PackageEntry
|
||||
key={extension.uuid}
|
||||
application={application}
|
||||
extension={extension}
|
||||
@@ -110,20 +118,28 @@ const Extensions: FunctionComponent<Props> = ({ application, extensionsLatestVer
|
||||
<div>
|
||||
{!confirmableExtension && (
|
||||
<PreferencesSegment>
|
||||
<Title>Install Custom Extension</Title>
|
||||
<DecoratedInput
|
||||
placeholder={'Enter Extension URL'}
|
||||
value={customUrl}
|
||||
onChange={(value) => {
|
||||
setCustomUrl(value)
|
||||
}}
|
||||
<Subtitle>Install External Package</Subtitle>
|
||||
<div className={'mt-2'}>
|
||||
<DecoratedInput
|
||||
placeholder={'Enter Package URL'}
|
||||
value={customUrl}
|
||||
onChange={(value) => {
|
||||
setCustomUrl(value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={customUrl.length === 0}
|
||||
className="mt-3 min-w-20"
|
||||
primary
|
||||
label="Install"
|
||||
onClick={() => submitExtensionUrl(customUrl)}
|
||||
/>
|
||||
<Button className="mt-3 min-w-20" label="Install" onClick={() => submitExtensionUrl(customUrl)} />
|
||||
</PreferencesSegment>
|
||||
)}
|
||||
{confirmableExtension && (
|
||||
<PreferencesSegment>
|
||||
<ConfirmCustomExtension component={confirmableExtension} callback={handleConfirmExtensionSubmit} />
|
||||
<ConfirmCustomPackage component={confirmableExtension} callback={handleConfirmExtensionSubmit} />
|
||||
<div ref={confirmableEnd} />
|
||||
</PreferencesSegment>
|
||||
)}
|
||||
@@ -132,4 +148,4 @@ const Extensions: FunctionComponent<Props> = ({ application, extensionsLatestVer
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Extensions)
|
||||
export default observer(PackagesPreferencesSection)
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import { SNActionsExtension, SNComponent, SNTheme } from '@standardnotes/snjs'
|
||||
|
||||
export type AnyExtension = SNComponent | SNTheme | SNActionsExtension
|
||||
export type AnyPackageType = SNComponent | SNTheme | SNActionsExtension
|
||||
@@ -1,18 +1,18 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { ExtensionsLatestVersions } from '@/Components/Preferences/Panes/Extensions/ExtensionsLatestVersions'
|
||||
import { PackageProvider } from '@/Components/Preferences/Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Tools from './Tools'
|
||||
import Defaults from './Defaults'
|
||||
import LabsPane from './Labs/Labs'
|
||||
import Advanced from '@/Components/Preferences/Panes/Account/Advanced'
|
||||
import Advanced from '@/Components/Preferences/Panes/General/Advanced/AdvancedSection'
|
||||
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
application: WebApplication
|
||||
extensionsLatestVersions: ExtensionsLatestVersions
|
||||
extensionsLatestVersions: PackageProvider
|
||||
}
|
||||
|
||||
const General: FunctionComponent<Props> = ({ viewControllerManager, application, extensionsLatestVersions }) => (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { FunctionComponent } from 'react'
|
||||
import TwoFactorAuthWrapper from '../TwoFactorAuth/TwoFactorAuthWrapper'
|
||||
import { MfaProps } from '../TwoFactorAuth/MfaProps'
|
||||
import TwoFactorAuthWrapper from './TwoFactorAuth/TwoFactorAuthWrapper'
|
||||
import { MfaProps } from './TwoFactorAuth/MfaProps'
|
||||
import Encryption from './Encryption'
|
||||
import PasscodeLock from './PasscodeLock'
|
||||
import Privacy from './Privacy'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { action, makeAutoObservable, observable } from 'mobx'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ExtensionsLatestVersions } from './Panes/Extensions/ExtensionsLatestVersions'
|
||||
import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||
import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble'
|
||||
|
||||
const PREFERENCE_IDS = [
|
||||
@@ -59,7 +59,7 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
export class PreferencesMenu {
|
||||
private _selectedPane: PreferenceId = 'account'
|
||||
private _menu: PreferencesMenuItem[]
|
||||
private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions(new Map())
|
||||
private _extensionLatestVersions: PackageProvider = new PackageProvider(new Map())
|
||||
|
||||
constructor(private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean) {
|
||||
this._menu = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS
|
||||
@@ -79,7 +79,7 @@ export class PreferencesMenu {
|
||||
}
|
||||
|
||||
private loadLatestVersions(): void {
|
||||
ExtensionsLatestVersions.load(this.application)
|
||||
PackageProvider.load(this.application)
|
||||
.then((versions) => {
|
||||
if (versions) {
|
||||
this._extensionLatestVersions = versions
|
||||
@@ -88,7 +88,7 @@ export class PreferencesMenu {
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
get extensionsLatestVersions(): ExtensionsLatestVersions {
|
||||
get extensionsLatestVersions(): PackageProvider {
|
||||
return this._extensionLatestVersions
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { MfaProps } from './Panes/TwoFactorAuth/MfaProps'
|
||||
import { MfaProps } from './Panes/Security/TwoFactorAuth/MfaProps'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
|
||||
export interface PreferencesProps extends MfaProps {
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef, useMemo } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { isControllerDealloced } from '@/Controllers/Abstract/IsControllerDealloced'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
|
||||
type WrapperProps = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
|
||||
type ContextMenuProps = WrapperProps & {
|
||||
type ContextMenuProps = {
|
||||
navigationController: NavigationController
|
||||
isEntitledToFolders: boolean
|
||||
selectedTag: SNTag
|
||||
}
|
||||
|
||||
const TagsContextMenu = observer(({ viewControllerManager, selectedTag }: ContextMenuProps) => {
|
||||
const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => {
|
||||
const premiumModal = usePremiumModal()
|
||||
|
||||
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = viewControllerManager.navigationController
|
||||
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = navigationController
|
||||
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) =>
|
||||
viewControllerManager.navigationController.setContextMenuOpen(open),
|
||||
)
|
||||
useCloseOnClickOutside(contextMenuRef, () => navigationController.setContextMenuOpen(false))
|
||||
|
||||
const reloadContextMenuLayout = useCallback(() => {
|
||||
viewControllerManager.navigationController.reloadContextMenuLayout()
|
||||
}, [viewControllerManager])
|
||||
navigationController.reloadContextMenuLayout()
|
||||
}, [navigationController])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', reloadContextMenuLayout)
|
||||
@@ -40,28 +37,35 @@ const TagsContextMenu = observer(({ viewControllerManager, selectedTag }: Contex
|
||||
}, [reloadContextMenuLayout])
|
||||
|
||||
const onClickAddSubtag = useCallback(() => {
|
||||
if (!viewControllerManager.featuresController.hasFolders) {
|
||||
if (!isEntitledToFolders) {
|
||||
premiumModal.activate('Folders')
|
||||
return
|
||||
}
|
||||
|
||||
viewControllerManager.navigationController.setContextMenuOpen(false)
|
||||
viewControllerManager.navigationController.setAddingSubtagTo(selectedTag)
|
||||
}, [viewControllerManager, selectedTag, premiumModal])
|
||||
navigationController.setContextMenuOpen(false)
|
||||
navigationController.setAddingSubtagTo(selectedTag)
|
||||
}, [isEntitledToFolders, navigationController, selectedTag, premiumModal])
|
||||
|
||||
const onClickRename = useCallback(() => {
|
||||
viewControllerManager.navigationController.setContextMenuOpen(false)
|
||||
viewControllerManager.navigationController.editingTag = selectedTag
|
||||
}, [viewControllerManager, selectedTag])
|
||||
navigationController.setContextMenuOpen(false)
|
||||
navigationController.editingTag = selectedTag
|
||||
}, [navigationController, selectedTag])
|
||||
|
||||
const onClickDelete = useCallback(() => {
|
||||
viewControllerManager.navigationController.remove(selectedTag, true).catch(console.error)
|
||||
}, [viewControllerManager, selectedTag])
|
||||
navigationController.remove(selectedTag, true).catch(console.error)
|
||||
}, [navigationController, selectedTag])
|
||||
|
||||
const tagLastModified = useMemo(
|
||||
() => formatDateForContextMenu(selectedTag.userModifiedDate),
|
||||
[selectedTag.userModifiedDate],
|
||||
)
|
||||
|
||||
const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at])
|
||||
|
||||
return contextMenuOpen ? (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="max-h-120 fixed flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||
className="max-h-120 fixed z-dropdown-menu flex min-w-60 flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||
style={{
|
||||
...contextMenuPosition,
|
||||
maxHeight: contextMenuMaxHeight,
|
||||
@@ -71,48 +75,39 @@ const TagsContextMenu = observer(({ viewControllerManager, selectedTag }: Contex
|
||||
a11yLabel="Tag context menu"
|
||||
isOpen={contextMenuOpen}
|
||||
closeMenu={() => {
|
||||
viewControllerManager.navigationController.setContextMenuOpen(false)
|
||||
navigationController.setContextMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onBlur={closeOnBlur}
|
||||
className={'justify-between py-1.5'}
|
||||
onClick={onClickAddSubtag}
|
||||
>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
|
||||
<div className="flex items-center">
|
||||
<Icon type="add" className="mr-2 text-neutral" />
|
||||
Add subtag
|
||||
</div>
|
||||
{!viewControllerManager.featuresController.hasFolders && <Icon type="premium-feature" />}
|
||||
{!isEntitledToFolders && <Icon type="premium-feature" />}
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} onBlur={closeOnBlur} className={'py-1.5'} onClick={onClickRename}>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickRename}>
|
||||
<Icon type="pencil-filled" className="mr-2 text-neutral" />
|
||||
Rename
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} onBlur={closeOnBlur} className={'py-1.5'} onClick={onClickDelete}>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickDelete}>
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="px-3 pt-1 pb-1.5 text-xs font-medium text-neutral">
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Last modified:</span> {tagLastModified}
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Created:</span> {tagCreatedAt}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Tag ID:</span> {selectedTag.uuid}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
})
|
||||
|
||||
TagsContextMenu.displayName = 'TagsContextMenu'
|
||||
|
||||
const TagsContextMenuWrapper = ({ viewControllerManager }: WrapperProps) => {
|
||||
if (isControllerDealloced(viewControllerManager)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const selectedTag = viewControllerManager.navigationController.selected
|
||||
|
||||
if (!selectedTag || !(selectedTag instanceof SNTag)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <TagsContextMenu viewControllerManager={viewControllerManager} selectedTag={selectedTag} />
|
||||
}
|
||||
|
||||
export default observer(TagsContextMenuWrapper)
|
||||
export default observer(TagContextMenu)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import TagContextMenu from './TagContextMenu'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
|
||||
type Props = {
|
||||
navigationController: NavigationController
|
||||
featuresController: FeaturesController
|
||||
}
|
||||
|
||||
const TagContextMenuWrapper = ({ navigationController, featuresController }: Props) => {
|
||||
const selectedTag = navigationController.selected
|
||||
|
||||
if (!selectedTag || !(selectedTag instanceof SNTag)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<TagContextMenu
|
||||
navigationController={navigationController}
|
||||
isEntitledToFolders={featuresController.hasFolders}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(TagContextMenuWrapper)
|
||||
@@ -423,6 +423,7 @@ export class FilesController extends AbstractViewController {
|
||||
this.setShowProtectedOverlay(false)
|
||||
}
|
||||
}
|
||||
void this.application.sync.sync()
|
||||
}
|
||||
|
||||
downloadFiles = async (files: FileItem[]) => {
|
||||
|
||||
+8
@@ -3,3 +3,11 @@ import { dateToLocalizedString } from '@standardnotes/snjs/'
|
||||
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate)
|
||||
}
|
||||
|
||||
export const formatDateForContextMenu = (date: Date | undefined) => {
|
||||
if (!date) {
|
||||
return
|
||||
}
|
||||
|
||||
return `${date.toDateString()} ${date.toLocaleTimeString()}`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user