Compare commits

...
Author SHA1 Message Date
StandardNotes CI 7eadc16a6c chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-07-01 13:52:02 +00:00
Aman Harwara 7c3c59dd92 fix(web): file protected overlay not toggling off correctly (#1190) 2022-07-01 19:20:15 +05:30
Mo 297b3bb4f8 chore(workflow): git sync sleep 2022-07-01 08:29:23 -05:00
StandardNotes CI c4ac9fb8d6 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-07-01 13:22:01 +00:00
Mo 44d6bdf147 fix(desktop): release notes 2022-07-01 08:19:30 -05:00
StandardNotes CI df87a38b56 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-07-01 13:06:11 +00:00
Aman Harwara 87213e9f5d feat: allow previewing CSV files as plaintext (#1188) 2022-07-01 18:34:24 +05:30
StandardNotes CI 53ff07244a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-06-30 21:28:22 +00:00
StandardNotes CI 70c1dd7827 chore(release): components 2022-06-30 21:28:05 +00:00
Johnny AandJohnny Almonte 47a0551967 fix: debounce saving task description/draft (#1187)
Co-authored-by: Johnny Almonte <[email protected]>
2022-06-30 17:21:48 -04:00
StandardNotes CI 6960bec339 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-06-30 18:51:35 +00:00
Mo 7c2b8fe545 chore: improve preferences folder hierarchies (#1186) 2022-06-30 13:49:03 -05:00
StandardNotes CI eac62fef1a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-06-30 16:29:23 +00:00
Aman Harwara 8b845ea424 feat: add tag id to tag context menu (#1185) 2022-06-30 21:57:33 +05:30
StandardNotes CI 604520f1ae chore(release): publish
- @standardnotes/[email protected].2
 - @standardnotes/[email protected]
2022-06-30 15:42:27 +00:00
Mo 98ed6472c0 fix(mobile): android icons loading issue 2022-06-30 10:40:01 -05:00
Mo fad80af0ff chore: mobile dev workflow 2022-06-30 10:24:34 -05:00
StandardNotes CI 0644490e43 chore(release): publish
- @standardnotes/[email protected].1
 - @standardnotes/[email protected]
2022-06-30 14:28:18 +00:00
Mo 2da360234d chore(mobile): bump default build number after rename of testflight workflow 2022-06-30 09:26:38 -05:00
StandardNotes CI e86ea8837a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-06-30 14:21:51 +00:00
Mo 7851e12e9e fix: clear desktop web cache on first version launch (#1184) 2022-06-30 09:20:00 -05:00
StandardNotes CI 82d13b41ed chore(release): publish
- @standardnotes/[email protected].0
 - @standardnotes/[email protected]
2022-06-30 09:55:17 +00:00
Vardan HakobyanandMo 4f5d092f89 feat(dev): option to render web app inside the mobile app (#1164)
* feat (WIP): render web app inside the mobile app

* fix: web app loading

* chore: build scripts related to mobile web bundle

* feat: show WebView header, which lets to close the WebView

* refactor: remove extra component

* chore: correct type

* chore: remove TODO

Co-authored-by: Mo <[email protected]>
2022-06-30 13:53:29 +04:00
Mo cd0859e908 chore: update readme 2022-06-29 22:33:38 -05:00
102 changed files with 832 additions and 502 deletions
+11
View File
@@ -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
+4
View File
@@ -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 }}
+5 -2
View File
@@ -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:
+24 -57
View File
@@ -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 lifes work all in one secure place.
![](https://standardnotes.com/assets/homepage-hero.png)
@@ -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`
+6
View File
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/components-meta",
"version": "2.5.7",
"version": "2.5.8",
"private": true,
"author": "Standard Notes.",
"main": "dist",
@@ -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,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": [
@@ -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)
}
@@ -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 <></>
}
@@ -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)
+1 -1
View File
@@ -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"
},
+32
View File
@@ -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
+52
View File
@@ -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
}
}
+12 -48
View File
@@ -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)
}
+2 -1
View File
@@ -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 -1
View File
@@ -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,
+3 -1
View File
@@ -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
+36
View File
@@ -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
+29
View File
@@ -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 */
}
+1
View File
@@ -219,6 +219,7 @@ android {
}
}
}
sourceSets.main.assets.srcDirs += ['src/main/assets', '../../html']
}
dependencies {
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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 */,
+4 -2
View File
@@ -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",
+4
View File
@@ -1,3 +1,7 @@
import { IsDev } from '@Lib/Utils'
export enum ErrorMessage {
GeneralText = 'An error occurred. Please try again later.',
}
export const WebAppOptionEnabled = IsDev
+4
View File
@@ -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"
+2
View File
@@ -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
+40
View File
@@ -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 -1
View File
@@ -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",
+20
View File
@@ -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 -1
View File
@@ -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.",
+22
View File
@@ -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
+2 -1
View File
@@ -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'
@@ -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
}
@@ -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}
@@ -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
@@ -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
@@ -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
@@ -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)
}
}
@@ -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,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[]) => {
@@ -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