Compare commits

...
Author SHA1 Message Date
StandardNotes CI ac28b29d65 chore(release): publish
- @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-21 16:52:42 +00:00
Mo 18235751d4 ci: ios fix 2022-10-21 11:22:04 -05:00
Mo cbd0063926 feat: ability to favorite tags + customize icon (#1858) 2022-10-21 11:11:31 -05:00
StandardNotes CI 3b048a31aa chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-21 14:06:04 +00:00
Aman Harwara 4a2dc1ef7a chore: lint 2022-10-21 18:59:07 +05:30
Aman Harwara 21f725bb2a feat: "create new note" floating button on mobile (#1857) 2022-10-21 18:36:55 +05:30
StandardNotes CI 173bacdfa3 chore(release): publish
- @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
2022-10-21 13:01:13 +00:00
Karol Sójko c503d781ae fix(web): event observer interface import 2022-10-21 14:31:07 +02:00
Karol Sójko 9fb85cd77a fix: refactor application event observing on web 2022-10-21 14:28:11 +02:00
StandardNotes CI bd40a49484 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2022-10-21 12:07:49 +00:00
Aman Harwara 4a1612c7dc fix: item selection/unselection on deleting 2022-10-21 17:09:24 +05:30
Aman Harwara d3ed4391b7 fix: template note getting unselected after typing 2022-10-21 16:58:28 +05:30
StandardNotes CI 856e499d89 chore(release): publish
- @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2022-10-21 08:55:09 +00:00
Karol Sójko 4f0805d5b0 feat(api): add accepting invites by the client 2022-10-21 10:24:49 +02:00
Karol Sójko 401a35f4db fix(ui-services): add route type getter 2022-10-21 10:15:13 +02:00
Karol Sójko 83b484002c feat: add accepting subscription invite route type 2022-10-21 10:05:52 +02:00
StandardNotes CI 30310dbb45 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2022-10-21 07:23:34 +00:00
Aman Harwara c6e3580678 fix: tag collapse/expand not persisting 2022-10-21 12:24:46 +05:30
Aman Harwara ea285a36d8 Merge branch 'main' of github.com:standardnotes/app 2022-10-21 12:18:22 +05:30
Aman Harwara 1addaf4c39 fix: default auto-dark theme 2022-10-21 12:17:58 +05:30
StandardNotes CI 92bba942f1 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-10-21 06:45:01 +00:00
Aman Harwara f71e6fe703 fix: improve plaintext editor performance on mobile (#1855) 2022-10-21 11:49:23 +05:30
StandardNotes CI 143b528b73 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-10-20 18:35:14 +00:00
Mo 5d202a823e Revert "feat: ability to favorite tags (#1852)"
This reverts commit 4f3b258363.
2022-10-20 13:00:22 -05:00
StandardNotes CI 8d7d577b1c chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-20 15:23:16 +00:00
Aman Harwara 76f23e13ec fix: mobx warnings and react render phase set state error 2022-10-20 20:24:56 +05:30
Mo 4f3b258363 feat: ability to favorite tags (#1852) 2022-10-20 09:28:35 -05:00
StandardNotes CI 9cc3304741 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-20 13:54:34 +00:00
Aman Harwara 4432f1cb4c feat: persist selected tag & note locally (#1851) 2022-10-20 18:56:59 +05:30
StandardNotes CI 6c47f95748 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-10-20 11:34:22 +00:00
Aman Harwara 57fab4f670 fix: "Forgot passcode" and "Switch workspace" lock screen options not working on mobile (#1850) 2022-10-20 16:26:21 +05:30
StandardNotes CI fb7b9350b2 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-20 09:09:38 +00:00
Aman Harwara fb72c2fdae fix: help & feedback links on mobile (#1849) 2022-10-20 14:10:23 +05:30
Aman Harwara 050fa9cabc feat: make sans-serif font default 2022-10-20 13:36:00 +05:30
Aman Harwara a8535cfa23 fix: make tags non-tabbable 2022-10-20 13:34:28 +05:30
Aman Harwara 27bb4ebdae fix: remove tinted border from change note type button 2022-10-20 13:29:59 +05:30
Aman Harwara 60a8e8b92b fix: links for template notes 2022-10-20 13:06:38 +05:30
Aman Harwara 1f07d9fa17 Merge branch 'main' of github.com:standardnotes/app 2022-10-20 12:45:03 +05:30
Aman Harwara d333d4f188 fix: file not linking to note when uploaded by dragging on note 2022-10-20 12:44:50 +05:30
StandardNotes CI 7e3d7a8825 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-19 21:18:20 +00:00
Aman Harwara e99c7b7c51 feat: improved file drag-n-drop experience (#1848) 2022-10-20 02:18:52 +05:30
144 changed files with 2558 additions and 1835 deletions
+3
View File
@@ -1,4 +1,7 @@
{
"extends": ["./node_modules/@standardnotes/config/src/.eslintrc"],
"rules": {
"max-classes-per-file": ["error", 1]
},
"ignorePatterns": [".eslintrc.js", "*.webpack.*.js", "webpack-defaults.js", "jest.config.js", "__mocks__", "**/**/coverage"]
}
@@ -70,6 +70,8 @@ jobs:
packages/mobile/android/app/build/outputs/apk/prod/release/app-prod-release.apk
ios:
env:
ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD: true
defaults:
run:
working-directory: packages/mobile
@@ -49,6 +49,8 @@ jobs:
with:
path: packages/mobile
ios:
env:
ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD: true
defaults:
run:
working-directory: packages/mobile
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.17.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/api
## [1.17.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
# [1.17.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Features
* **api:** add accepting invites by the client ([4f0805d](https://github.com/standardnotes/app/commit/4f0805d5b078e5a3524f5d6699b45dfd7e3456f3))
## [1.16.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.16.5",
"version": "1.17.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -2,4 +2,5 @@ export enum SubscriptionApiOperations {
Inviting,
CancelingInvite,
ListingInvites,
AcceptingInvite,
}
@@ -1,5 +1,6 @@
import { Invitation } from '@standardnotes/models'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
@@ -24,6 +25,9 @@ describe('SubscriptionApiService', () => {
subscriptionServer.listInvites = jest.fn().mockReturnValue({
data: { invitations: [{} as jest.Mocked<Invitation>] },
} as jest.Mocked<SubscriptionInviteListResponse>)
subscriptionServer.acceptInvite = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
})
it('should invite a user', async () => {
@@ -160,4 +164,48 @@ describe('SubscriptionApiService', () => {
expect(error).not.toBeNull()
})
it('should accept an invite', async () => {
const response = await createService().acceptInvite('1-2-3')
expect(response).toEqual({
data: {
success: true,
},
})
expect(subscriptionServer.acceptInvite).toHaveBeenCalledWith({
inviteUuid: '1-2-3',
})
})
it('should not accept an invite if it is already accepting', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[SubscriptionApiOperations.AcceptingInvite, true]]),
})
let error = null
try {
await service.acceptInvite('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not accept an invite if the server fails', async () => {
subscriptionServer.acceptInvite = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().acceptInvite('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})
@@ -6,9 +6,11 @@ import { SubscriptionServerInterface } from '../../Server/Subscription/Subscript
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionApiServiceInterface } from './SubscriptionApiServiceInterface'
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
import { Uuid } from '@standardnotes/common'
export class SubscriptionApiService implements SubscriptionApiServiceInterface {
private operationsInProgress: Map<SubscriptionApiOperations, boolean>
@@ -78,4 +80,24 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface {
throw new ApiCallError(ErrorMessage.GenericFail)
}
}
async acceptInvite(inviteUuid: Uuid): Promise<SubscriptionInviteAcceptResponse> {
if (this.operationsInProgress.get(SubscriptionApiOperations.AcceptingInvite)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(SubscriptionApiOperations.AcceptingInvite, true)
try {
const response = await this.subscriptionServer.acceptInvite({
inviteUuid,
})
this.operationsInProgress.set(SubscriptionApiOperations.AcceptingInvite, false)
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
}
}
}
@@ -1,5 +1,6 @@
import { Uuid } from '@standardnotes/common'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
@@ -8,4 +9,5 @@ export interface SubscriptionApiServiceInterface {
invite(inviteeEmail: string): Promise<SubscriptionInviteResponse>
listInvites(): Promise<SubscriptionInviteListResponse>
cancelInvite(inviteUuid: Uuid): Promise<SubscriptionInviteCancelResponse>
acceptInvite(inviteUuid: Uuid): Promise<SubscriptionInviteAcceptResponse>
}
@@ -1,10 +1,6 @@
import { Uuid } from '@standardnotes/common'
import { ApiEndpointParam } from '../ApiEndpointParam'
import { ApiVersion } from '../../Api/ApiVersion'
export type SubscriptionInviteAcceptRequestParams = {
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
inviteUuid: Uuid
[additionalParam: string]: unknown
}
@@ -1,3 +1 @@
export type SubscriptionInviteAcceptResponseBody = {
success: boolean
}
export type SubscriptionInviteAcceptResponseBody = { success: true } | { success: false; message: string }
+52
View File
@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.23.236](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.235](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.234](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.233](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.232](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.231](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.230](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.229](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.228](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.227](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.226](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.225](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.224](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.223](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.23.223",
"version": "3.23.236",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.18.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/encryption
## [1.18.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.18.4",
"version": "1.18.5",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.24.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.24.6",
"version": "1.24.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/files
## [1.11.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.11.6",
"version": "1.11.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+54
View File
@@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.43.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
### Bug Fixes
* help & feedback links on mobile ([#1849](https://github.com/standardnotes/app/issues/1849)) ([fb72c2f](https://github.com/standardnotes/app/commit/fb72c2fdae277cb4e348ce5ee550f0545f281dcd))
## [3.43.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/mobile
## [3.43.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/mobile
@@ -49,6 +49,10 @@
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="mailto"/>
</intent>
</queries>
</manifest>
@@ -74,6 +74,7 @@
<array>
<string>http</string>
<string>https</string>
<string>mailto</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
@@ -52,6 +52,7 @@
<array>
<string>http</string>
<string>https</string>
<string>mailto</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.43.10",
"version": "3.43.23",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+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.
# [1.30.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Features
* ability to favorite tags + customize icon ([#1858](https://github.com/standardnotes/app/issues/1858)) ([cbd0063](https://github.com/standardnotes/app/commit/cbd0063926a416c23225242a4a1796712b6b224e))
## [1.29.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
### Bug Fixes
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.29.1",
"version": "1.30.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -25,6 +25,7 @@
"@types/jest": "^28.1.5",
"@types/lodash": "^4.14.182",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"eslint": "*",
"eslint-plugin-prettier": "*",
"jest": "^28.1.2",
"ts-jest": "^28.0.5",
@@ -1,41 +1,33 @@
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { PredicateInterface, PredicateJsonForm } from '../../Runtime/Predicate/Interface'
import { PredicateInterface } from '../../Runtime/Predicate/Interface'
import { predicateFromJson } from '../../Runtime/Predicate/Generators'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { SystemViewId } from './SystemViewId'
import { EmojiString, IconType } from '../../Utilities/Icon/IconType'
import { SmartViewDefaultIconName, systemViewIcon } from './SmartViewIcons'
import { SmartViewContent } from './SmartViewContent'
export const SMART_TAG_DSL_PREFIX = '!['
export enum SystemViewId {
AllNotes = 'all-notes',
Files = 'files',
ArchivedNotes = 'archived-notes',
TrashedNotes = 'trashed-notes',
UntaggedNotes = 'untagged-notes',
StarredNotes = 'starred-notes',
}
export interface SmartViewContent extends ItemContent {
title: string
predicate: PredicateJsonForm
}
export function isSystemView(view: SmartView): boolean {
return Object.values(SystemViewId).includes(view.uuid as SystemViewId)
}
/**
* A tag that defines a predicate that consumers can use
* to retrieve a dynamic list of items.
*/
export class SmartView extends DecryptedItem<SmartViewContent> {
public readonly predicate!: PredicateInterface<DecryptedItem>
public readonly title: string
public readonly iconString: IconType | EmojiString
constructor(payload: DecryptedPayloadInterface<SmartViewContent>) {
super(payload)
this.title = String(this.content.title || '')
if (isSystemView(this)) {
this.iconString = systemViewIcon(this.uuid as SystemViewId)
} else {
this.iconString = this.payload.content.iconString || SmartViewDefaultIconName
}
try {
this.predicate = this.content.predicate && predicateFromJson(this.content.predicate)
} catch (error) {
@@ -1,6 +1,8 @@
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
import { SNNote } from '../Note/Note'
import { SmartViewContent, SmartView, SystemViewId } from './SmartView'
import { SmartView } from './SmartView'
import { SmartViewContent } from './SmartViewContent'
import { SystemViewId } from './SystemViewId'
import { ItemWithTags } from '../../Runtime/Display/Search/ItemWithTags'
import { ContentType } from '@standardnotes/common'
import { FillItemContent } from '../../Abstract/Content/ItemContent'
@@ -0,0 +1,6 @@
import { TagContent } from './../Tag/TagContent'
import { PredicateJsonForm } from '../../Runtime/Predicate/Interface'
export interface SmartViewContent extends TagContent {
predicate: PredicateJsonForm
}
@@ -0,0 +1,17 @@
import { SystemViewId } from './SystemViewId'
import { IconType } from '../../Utilities/Icon/IconType'
export const SmartViewIcons: Record<SystemViewId, IconType> = {
[SystemViewId.AllNotes]: 'notes',
[SystemViewId.Files]: 'folder',
[SystemViewId.ArchivedNotes]: 'archive',
[SystemViewId.TrashedNotes]: 'trash',
[SystemViewId.UntaggedNotes]: 'hashtag-off',
[SystemViewId.StarredNotes]: 'star-filled',
}
export function systemViewIcon(id: SystemViewId): IconType {
return SmartViewIcons[id]
}
export const SmartViewDefaultIconName: IconType = 'restore'
@@ -0,0 +1,8 @@
export enum SystemViewId {
AllNotes = 'all-notes',
Files = 'files',
ArchivedNotes = 'archived-notes',
TrashedNotes = 'trashed-notes',
UntaggedNotes = 'untagged-notes',
StarredNotes = 'starred-notes',
}
@@ -1,2 +1,4 @@
export * from './SmartView'
export * from './SmartViewBuilder'
export * from './SystemViewId'
export * from './SmartViewContent'
@@ -1,10 +1,11 @@
import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource'
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
import { SNTag, TagContent } from './Tag'
import { SNTag } from './Tag'
import { ContentType } from '@standardnotes/common'
import { FillItemContent } from '../../Abstract/Content/ItemContent'
import { ContentReference } from '../../Abstract/Reference/ContentReference'
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
import { TagContent } from './TagContent'
const randUuid = () => String(Math.random())
+6 -10
View File
@@ -1,32 +1,28 @@
import { VectorIconNameOrEmoji, IconType } from './../../Utilities/Icon/IconType'
import { ContentType, Uuid } from '@standardnotes/common'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ContentReference } from '../../Abstract/Reference/ContentReference'
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { TagContent, TagContentSpecialized } from './TagContent'
export const TagFolderDelimitter = '.'
interface TagInterface {
title: string
expanded: boolean
}
export type TagContent = TagInterface & ItemContent
export const DefaultTagIconName: IconType = 'hashtag'
export const isTag = (x: ItemInterface): x is SNTag => x.content_type === ContentType.Tag
export class SNTag extends DecryptedItem<TagContent> implements TagInterface {
export class SNTag extends DecryptedItem<TagContent> implements TagContentSpecialized {
public readonly title: string
/** Whether to render child tags in view hierarchy. Opposite of collapsed. */
public readonly iconString: VectorIconNameOrEmoji
public readonly expanded: boolean
constructor(payload: DecryptedPayloadInterface<TagContent>) {
super(payload)
this.title = this.payload.content.title || ''
this.expanded = this.payload.content.expanded != undefined ? this.payload.content.expanded : true
this.iconString = this.payload.content.iconString || DefaultTagIconName
}
get noteReferences(): ContentReference[] {
@@ -0,0 +1,11 @@
import { IconType } from './../../Utilities/Icon/IconType'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { EmojiString } from '../../Utilities/Icon/IconType'
export interface TagContentSpecialized {
title: string
expanded: boolean
iconString: IconType | EmojiString
}
export type TagContent = TagContentSpecialized & ItemContent
@@ -1,5 +1,6 @@
import { ContentType } from '@standardnotes/common'
import { TagContent, SNTag } from './Tag'
import { SNTag } from './Tag'
import { TagContent } from './TagContent'
import { FileItem } from '../File'
import { SNNote } from '../Note'
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
@@ -17,6 +18,10 @@ export class TagMutator extends DecryptedItemMutator<TagContent> {
this.mutableContent.expanded = expanded
}
set iconString(iconString: string) {
this.mutableContent.iconString = iconString
}
public makeChildOf(tag: SNTag): void {
const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref))
@@ -1,2 +1,3 @@
export * from './Tag'
export * from './TagMutator'
export * from './TagContent'
@@ -1,3 +1,7 @@
export type VectorIconNameOrEmoji = EmojiString | IconType
export type EmojiString = string
export type IconType =
| 'accessibility'
| 'account-card-details-outline'
@@ -1,4 +1,4 @@
import { TagContent } from './../../Syncable/Tag/Tag'
import { TagContent } from './../../Syncable/Tag/TagContent'
import { ContentType } from '@standardnotes/common'
import { FillItemContent, ItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedPayload, PayloadSource, PayloadTimestampDefaults } from '../../Abstract/Payload'
+4 -3
View File
@@ -1,7 +1,7 @@
export * from './Abstract/Component/ActionObserver'
export * from './Abstract/Component/ComponentViewerEvent'
export * from './Abstract/Component/ComponentMessage'
export * from './Abstract/Component/ComponentEventObserver'
export * from './Abstract/Component/ComponentMessage'
export * from './Abstract/Component/ComponentViewerEvent'
export * from './Abstract/Component/IncomingComponentItemPayload'
export * from './Abstract/Component/KeyboardModifier'
export * from './Abstract/Component/MessageData'
@@ -43,9 +43,9 @@ export * from './Runtime/Collection/Payload/ImmutablePayloadCollection'
export * from './Runtime/Collection/Payload/PayloadCollection'
export * from './Runtime/Deltas'
export * from './Runtime/DirtyCounter/DirtyCounter'
export * from './Runtime/Display'
export * from './Runtime/Display/ItemDisplayController'
export * from './Runtime/Display/Types'
export * from './Runtime/Display'
export * from './Runtime/History'
export * from './Runtime/Index/ItemDelta'
export * from './Runtime/Index/SNIndex'
@@ -70,6 +70,7 @@ export * from './Syncable/SmartView'
export * from './Syncable/Tag'
export * from './Syncable/Theme'
export * from './Syncable/UserPrefs'
export * from './Utilities/Icon/IconType'
export * from './Utilities/Item/FindItem'
export * from './Utilities/Item/ItemContentsDiffer'
export * from './Utilities/Item/ItemContentsEqual'
+52
View File
@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.165](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.164](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.163](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.162](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.161](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.160](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.159](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.158](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.157](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.156](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.155](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.154](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.153](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.152](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.3.152",
"version": "1.3.165",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.34.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/services
## [1.34.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/services
## [1.34.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/services
## [1.34.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.34.1",
"version": "1.34.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.143.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Features
* ability to favorite tags + customize icon ([#1858](https://github.com/standardnotes/app/issues/1858)) ([cbd0063](https://github.com/standardnotes/app/commit/cbd0063926a416c23225242a4a1796712b6b224e))
## [2.142.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
## [2.142.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/snjs
## [2.142.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
### Bug Fixes
+1 -1
View File
@@ -1,5 +1,5 @@
import { NoteType } from '@standardnotes/features'
import { IconType } from '@Lib/Types/IconType'
import { IconType } from '@standardnotes/models'
export class IconsController {
getIconForFileType(type: string): IconType {
@@ -139,7 +139,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
return isNullOrUndefined(this.apiService.getSession())
}
public getUser() {
public getUser(): Responses.User | undefined {
return this.user
}
@@ -1,8 +1,8 @@
import { ClientDisplayableError } from '@standardnotes/responses'
import { ClientDisplayableError, User } from '@standardnotes/responses'
import { Base64String } from '@standardnotes/sncrypto-common'
export interface SessionsClientInterface {
createDemoShareToken(): Promise<Base64String | ClientDisplayableError>
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
getUser(): User | undefined
}
-1
View File
@@ -1,3 +1,2 @@
export * from './ApplicationEventPayload'
export * from './IconType'
export * from './UuidString'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.142.1",
"version": "2.143.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+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.9.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.9.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
# [1.9.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* **ui-services:** add route type getter ([401a35f](https://github.com/standardnotes/app/commit/401a35f4db9e7f090ad84c2d9e0ce5b465c7e790))
### Features
* add accepting subscription invite route type ([83b4840](https://github.com/standardnotes/app/commit/83b484002c066675e58625d7c0a1f7a7566df787))
## [1.8.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.8.5",
"version": "1.9.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -0,0 +1,3 @@
export type DemoParams = {
token: string
}
@@ -0,0 +1,3 @@
export type OnboardingParams = {
fromHomepage: boolean
}
@@ -0,0 +1,4 @@
export type PurchaseParams = {
plan: string
period: string
}
@@ -0,0 +1,5 @@
import { PreferenceId } from '../../Preferences/PreferenceId'
export type SettingsParams = {
panel: PreferenceId
}
@@ -0,0 +1,5 @@
import { Uuid } from '@standardnotes/common'
export type SubscriptionInviteParams = {
inviteUuid: Uuid
}
@@ -0,0 +1,6 @@
export enum RootQueryParam {
Purchase = 'purchase',
Settings = 'settings',
DemoToken = 'demo-token',
AcceptSubscriptionInvite = 'accept-subscription-invite',
}
@@ -0,0 +1,4 @@
export enum RootRoutes {
Onboarding = '/onboard',
None = '/',
}
@@ -1,18 +0,0 @@
import { PreferenceId } from '../Preferences/PreferenceId'
export type OnboardingParams = {
fromHomepage: boolean
}
export type SettingsParams = {
panel: PreferenceId
}
export type DemoParams = {
token: string
}
export type PurchaseParams = {
plan: string
period: string
}
@@ -48,4 +48,12 @@ describe('route parser', () => {
expect(() => parser.onboardingParams).toThrowError('Accessing invalid params')
})
it('routes to subscription sharing', () => {
const url = 'https://app.standardnotes.com/?accept-subscription-invite=1-2-3'
const parser = new RouteParser(url)
expect(parser.type).toEqual(RouteType.AcceptSubscriptionInvite)
expect(parser.subscriptionInviteParams.inviteUuid).toEqual('1-2-3')
})
})
+57 -43
View File
@@ -1,54 +1,43 @@
import { Uuid } from '@standardnotes/common'
import { PreferenceId } from './../Preferences/PreferenceId'
import { DemoParams, OnboardingParams, PurchaseParams, SettingsParams } from './RouteParams'
import { DemoParams } from './Params/DemoParams'
import { OnboardingParams } from './Params/OnboardingParams'
import { PurchaseParams } from './Params/PurchaseParams'
import { SettingsParams } from './Params/SettingsParams'
import { SubscriptionInviteParams } from './Params/SubscriptionInviteParams'
import { RootQueryParam } from './RootQueryParam'
import { RootRoutes } from './RootRoutes'
import { RouteParserInterface } from './RouteParserInterface'
import { RouteType } from './RouteType'
enum RootRoutes {
Onboarding = '/onboard',
None = '/',
}
enum RootQueryParam {
Purchase = 'purchase',
Settings = 'settings',
DemoToken = 'demo-token',
}
export class RouteParser {
export class RouteParser implements RouteParserInterface {
private url: URL
private readonly path: string
public readonly type: RouteType
private readonly parsedType: RouteType
private readonly searchParams: URLSearchParams
constructor(url: string) {
this.url = new URL(url)
this.path = this.url.pathname
this.searchParams = this.url.searchParams
this.parsedType = this.parseTypeFromQueryParameters()
}
const pathUsesRootQueryParams = this.path === RootRoutes.None
get type(): RouteType {
return this.parsedType
}
if (pathUsesRootQueryParams) {
if (this.searchParams.has(RootQueryParam.Purchase)) {
this.type = RouteType.Purchase
} else if (this.searchParams.has(RootQueryParam.Settings)) {
this.type = RouteType.Settings
} else if (this.searchParams.has(RootQueryParam.DemoToken)) {
this.type = RouteType.Demo
} else {
this.type = RouteType.None
}
} else {
if (this.path === RootRoutes.Onboarding) {
this.type = RouteType.Onboarding
} else {
this.type = RouteType.None
}
get subscriptionInviteParams(): SubscriptionInviteParams {
this.checkForProperRouteType(RouteType.AcceptSubscriptionInvite)
return {
inviteUuid: this.searchParams.get(RootQueryParam.AcceptSubscriptionInvite) as Uuid,
}
}
get demoParams(): DemoParams {
if (this.type !== RouteType.Demo) {
throw new Error('Accessing invalid params')
}
this.checkForProperRouteType(RouteType.Demo)
return {
token: this.searchParams.get(RootQueryParam.DemoToken) as string,
@@ -56,9 +45,7 @@ export class RouteParser {
}
get settingsParams(): SettingsParams {
if (this.type !== RouteType.Settings) {
throw new Error('Accessing invalid params')
}
this.checkForProperRouteType(RouteType.Settings)
return {
panel: this.searchParams.get(RootQueryParam.Settings) as PreferenceId,
@@ -66,9 +53,7 @@ export class RouteParser {
}
get purchaseParams(): PurchaseParams {
if (this.type !== RouteType.Purchase) {
throw new Error('Accessing invalid params')
}
this.checkForProperRouteType(RouteType.Purchase)
return {
plan: this.searchParams.get('plan') as string,
@@ -77,12 +62,41 @@ export class RouteParser {
}
get onboardingParams(): OnboardingParams {
if (this.type !== RouteType.Onboarding) {
throw new Error('Accessing invalid params')
}
this.checkForProperRouteType(RouteType.Onboarding)
return {
fromHomepage: !!this.searchParams.get('from_homepage'),
}
}
private checkForProperRouteType(type: RouteType): void {
if (this.parsedType !== type) {
throw new Error('Accessing invalid params')
}
}
private parseTypeFromQueryParameters(): RouteType {
if (this.path === RootRoutes.Onboarding) {
return RouteType.Onboarding
}
if (this.path !== RootRoutes.None) {
return RouteType.None
}
const rootQueryParametersMap: Map<RootQueryParam, RouteType> = new Map([
[RootQueryParam.Purchase, RouteType.Purchase],
[RootQueryParam.Settings, RouteType.Settings],
[RootQueryParam.DemoToken, RouteType.Demo],
[RootQueryParam.AcceptSubscriptionInvite, RouteType.AcceptSubscriptionInvite],
])
for (const rootQueryParam of rootQueryParametersMap.keys()) {
if (this.searchParams.has(rootQueryParam)) {
return rootQueryParametersMap.get(rootQueryParam) as RouteType
}
}
return RouteType.None
}
}
@@ -0,0 +1,15 @@
import { DemoParams } from './Params/DemoParams'
import { OnboardingParams } from './Params/OnboardingParams'
import { PurchaseParams } from './Params/PurchaseParams'
import { SettingsParams } from './Params/SettingsParams'
import { SubscriptionInviteParams } from './Params/SubscriptionInviteParams'
import { RouteType } from './RouteType'
export interface RouteParserInterface {
get demoParams(): DemoParams
get settingsParams(): SettingsParams
get purchaseParams(): PurchaseParams
get onboardingParams(): OnboardingParams
get subscriptionInviteParams(): SubscriptionInviteParams
get type(): RouteType
}
+10 -4
View File
@@ -4,10 +4,16 @@ import {
ApplicationInterface,
InternalEventBusInterface,
} from '@standardnotes/services'
import { RouteParser } from './RouteParser'
import { RouteServiceEvent } from './RouteServiceEvent'
export class RouteService extends AbstractService<RouteServiceEvent, RouteParser> {
import { RouteParser } from './RouteParser'
import { RouteParserInterface } from './RouteParserInterface'
import { RouteServiceEvent } from './RouteServiceEvent'
import { RouteServiceInterface } from './RouteServiceInterface'
export class RouteService
extends AbstractService<RouteServiceEvent, RouteParserInterface>
implements RouteServiceInterface
{
private unsubApp!: () => void
constructor(
@@ -24,7 +30,7 @@ export class RouteService extends AbstractService<RouteServiceEvent, RouteParser
this.unsubApp()
}
public getRoute(): RouteParser {
public getRoute(): RouteParserInterface {
return new RouteParser(window.location.href)
}
@@ -0,0 +1,7 @@
import { RouteParserInterface } from './RouteParserInterface'
export interface RouteServiceInterface {
deinit(): void
getRoute(): RouteParserInterface
removeSettingsFromURLQueryParameters(): void
}
@@ -2,6 +2,7 @@ export enum RouteType {
Onboarding = 'onboarding',
Settings = 'settings',
Purchase = 'purchase',
AcceptSubscriptionInvite = 'accept-subscription-invite',
Demo = 'demo',
None = 'none',
}
+6 -1
View File
@@ -3,10 +3,15 @@ export * from './Alert/WebAlertService'
export * from './Archive/ArchiveManager'
export * from './IO/IOService'
export * from './Preferences/PreferenceId'
export * from './Route/RouteParams'
export * from './Route/Params/DemoParams'
export * from './Route/Params/OnboardingParams'
export * from './Route/Params/PurchaseParams'
export * from './Route/Params/SettingsParams'
export * from './Route/Params/SubscriptionInviteParams'
export * from './Route/RouteParser'
export * from './Route/RouteType'
export * from './Route/RouteService'
export * from './Route/RouteServiceInterface'
export * from './Route/RouteServiceEvent'
export * from './Security/AutolockService'
export * from './Storage/LocalStorage'
+91
View File
@@ -3,6 +3,97 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.84.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Features
* ability to favorite tags + customize icon ([#1858](https://github.com/standardnotes/app/issues/1858)) ([cbd0063](https://github.com/standardnotes/app/commit/cbd0063926a416c23225242a4a1796712b6b224e))
# [3.83.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Features
* "create new note" floating button on mobile ([#1857](https://github.com/standardnotes/app/issues/1857)) ([21f725b](https://github.com/standardnotes/app/commit/21f725bb2a8d7d9be3f879cc078dd0f8134c2365))
## [3.82.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
* **web:** event observer interface import ([c503d78](https://github.com/standardnotes/app/commit/c503d781aefcc1a15d3a8da1ee09294d176e4b89))
## [3.82.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* item selection/unselection on deleting ([4a1612c](https://github.com/standardnotes/app/commit/4a1612c7dc8ba2af0d28ac71d5dfcdf059cd5ea0))
* template note getting unselected after typing ([d3ed439](https://github.com/standardnotes/app/commit/d3ed4391b7f6b77ced485884a97190449a1b5507))
## [3.82.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
**Note:** Version bump only for package @standardnotes/web
## [3.82.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* default auto-dark theme ([1addaf4](https://github.com/standardnotes/app/commit/1addaf4c3941364f695545669158f09f4c012dea))
* tag collapse/expand not persisting ([c6e3580](https://github.com/standardnotes/app/commit/c6e3580678dbed6562041ce5afed47563276843a))
## [3.82.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
### Bug Fixes
* improve plaintext editor performance on mobile ([#1855](https://github.com/standardnotes/app/issues/1855)) ([f71e6fe](https://github.com/standardnotes/app/commit/f71e6fe70397cf9ab2c109cad52780088337fc47))
## [3.82.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
### Reverts
* Revert "feat: ability to favorite tags (#1852)" ([5d202a8](https://github.com/standardnotes/app/commit/5d202a823e2a89c5e6fe7e07306957f2b2ea68b1)), closes [#1852](https://github.com/standardnotes/app/issues/1852)
# [3.82.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
### Bug Fixes
* mobx warnings and react render phase set state error ([76f23e1](https://github.com/standardnotes/app/commit/76f23e13ecc8795b4e6cbbb37c32db262c0084c2))
### Features
* ability to favorite tags ([#1852](https://github.com/standardnotes/app/issues/1852)) ([4f3b258](https://github.com/standardnotes/app/commit/4f3b258363ded1a340bd1a7702da7cfd6415b954))
# [3.81.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
### Features
* persist selected tag & note locally ([#1851](https://github.com/standardnotes/app/issues/1851)) ([4432f1c](https://github.com/standardnotes/app/commit/4432f1cb4cfc2042ddfb7bd574d53270626ebe0d))
## [3.80.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
### Bug Fixes
* "Forgot passcode" and "Switch workspace" lock screen options not working on mobile ([#1850](https://github.com/standardnotes/app/issues/1850)) ([57fab4f](https://github.com/standardnotes/app/commit/57fab4f670faeaa0018d9d1a678e50d1e90052aa))
# [3.80.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
### Bug Fixes
* file not linking to note when uploaded by dragging on note ([d333d4f](https://github.com/standardnotes/app/commit/d333d4f1887672b526a5d8fc659e6dd2b1b12e21))
* help & feedback links on mobile ([#1849](https://github.com/standardnotes/app/issues/1849)) ([fb72c2f](https://github.com/standardnotes/app/commit/fb72c2fdae277cb4e348ce5ee550f0545f281dcd))
* links for template notes ([60a8e8b](https://github.com/standardnotes/app/commit/60a8e8b92b1e5c0f4c4c007591aa115fce51930b))
* make tags non-tabbable ([a8535cf](https://github.com/standardnotes/app/commit/a8535cfa2380a4ddd648d5d099701c0b5e8f3a22))
* remove tinted border from change note type button ([27bb4eb](https://github.com/standardnotes/app/commit/27bb4ebdae259718c0e13134c2ffdce7c681f0da))
### Features
* make sans-serif font default ([050fa9c](https://github.com/standardnotes/app/commit/050fa9cabca65bba4845fc9b315c81ce524e8b0d))
# [3.79.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
### Features
* improved file drag-n-drop experience ([#1848](https://github.com/standardnotes/app/issues/1848)) ([e99c7b7](https://github.com/standardnotes/app/commit/e99c7b7c516b0c822f0e663843146c7f5cf1fb86))
## [3.78.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.78.1",
"version": "3.84.0",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -31,6 +31,7 @@ import {
AutolockService,
IOService,
RouteService,
RouteServiceInterface,
ThemeManager,
WebAlertService,
} from '@standardnotes/ui-services'
@@ -50,7 +51,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
private onVisibilityChange: () => void
private mobileWebReceiver?: MobileWebReceiver
private androidBackHandler?: AndroidBackHandler
public readonly routeService: RouteService
public readonly routeService: RouteServiceInterface
constructor(
deviceInterface: WebOrDesktopDevice,
@@ -221,6 +221,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
/>
<NoteGroupView application={application} />
</FileDragNDropProvider>
@@ -1,171 +0,0 @@
import { WebApplication } from '@/Application/Application'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import AttachedFilesPopover from './AttachedFilesPopover'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { PopoverTabs } from './PopoverTabs'
import { NotesController } from '@/Controllers/NotesController'
import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { useFileDragNDrop } from '@/Components/FileDragNDropProvider/FileDragNDropProvider'
import { FileItem, SNNote } from '@standardnotes/snjs'
import { addToast, ToastType } from '@standardnotes/toast'
import { classNames } from '@/Utils/ConcatenateClassNames'
import Popover from '../Popover/Popover'
type Props = {
application: WebApplication
featuresController: FeaturesController
filePreviewModalController: FilePreviewModalController
filesController: FilesController
navigationController: NavigationController
notesController: NotesController
selectionController: SelectedItemsController
onClickPreprocessing?: () => Promise<void>
}
const AttachedFilesButton: FunctionComponent<Props> = ({
application,
featuresController,
filesController,
navigationController,
notesController,
selectionController,
onClickPreprocessing,
}: Props) => {
const { allFiles, attachedFiles } = filesController
const attachedFilesCount = attachedFiles.length
const premiumModal = usePremiumModal()
const note: SNNote | undefined = notesController.firstSelectedNote
const [isOpen, setIsOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [currentTab, setCurrentTab] = useState(
navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles,
)
const isAttachedTabDisabled = navigationController.isInFilesView || selectionController.selectedItemsCount > 1
useEffect(() => {
if (isAttachedTabDisabled && currentTab === PopoverTabs.AttachedFiles) {
setCurrentTab(PopoverTabs.AllFiles)
}
}, [currentTab, isAttachedTabDisabled])
const toggleAttachedFilesMenu = useCallback(async () => {
const newOpenState = !isOpen
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setIsOpen(newOpenState)
}, [onClickPreprocessing, isOpen])
const prospectivelyShowFilesPremiumModal = useCallback(() => {
if (!featuresController.hasFiles) {
premiumModal.activate('Files')
}
}, [featuresController.hasFiles, premiumModal])
const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => {
prospectivelyShowFilesPremiumModal()
await toggleAttachedFilesMenu()
}, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal])
const attachFileToNote = useCallback(
async (file: FileItem) => {
if (!note) {
addToast({
type: ToastType.Error,
message: 'Could not attach file because selected note was unselected or deleted',
})
return
}
await application.items.associateFileWithNote(file, note)
},
[application.items, note],
)
const { isDraggingFiles, addFilesDragInCallback, addFilesDropCallback } = useFileDragNDrop()
useEffect(() => {
if (isDraggingFiles && !isOpen) {
void toggleAttachedFilesMenu()
}
}, [isDraggingFiles, isOpen, toggleAttachedFilesMenu])
const filesDragInCallback = useCallback((tab: PopoverTabs) => {
setCurrentTab(tab)
}, [])
useEffect(() => {
addFilesDragInCallback(filesDragInCallback)
}, [addFilesDragInCallback, filesDragInCallback])
const filesDropCallback = useCallback(
(uploadedFiles: FileItem[]) => {
if (currentTab === PopoverTabs.AttachedFiles) {
uploadedFiles.forEach((file) => {
attachFileToNote(file).catch(console.error)
})
}
},
[attachFileToNote, currentTab],
)
useEffect(() => {
addFilesDropCallback(filesDropCallback)
}, [addFilesDropCallback, filesDropCallback])
return (
<div ref={containerRef}>
<button
className={classNames(
'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast',
attachedFilesCount > 0 ? 'py-1 px-3' : '',
)}
title="Attached files"
aria-label="Attached files"
onClick={toggleAttachedFilesMenuWithEntitlementCheck}
ref={buttonRef}
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsOpen(false)
}
}}
>
<Icon type="folder" />
{attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>}
</button>
<Popover
togglePopover={toggleAttachedFilesMenuWithEntitlementCheck}
anchorElement={buttonRef.current}
open={isOpen}
className="pt-2 md:pt-0"
>
<AttachedFilesPopover
application={application}
filesController={filesController}
attachedFiles={attachedFiles}
allFiles={allFiles}
currentTab={currentTab}
isDraggingFiles={isDraggingFiles}
setCurrentTab={setCurrentTab}
attachedTabDisabled={isAttachedTabDisabled}
/>
</Popover>
</div>
)
}
export default observer(AttachedFilesButton)
@@ -1,206 +0,0 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { WebApplication } from '@/Application/Application'
import { FileItem } from '@standardnotes/snjs'
import { FilesIllustration } from '@standardnotes/icons'
import { observer } from 'mobx-react-lite'
import { Dispatch, FunctionComponent, SetStateAction, useRef, useState } from 'react'
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import PopoverFileItem from './PopoverFileItem'
import { PopoverFileItemActionType } from './PopoverFileItemAction'
import { PopoverTabs } from './PopoverTabs'
import { FilesController } from '@/Controllers/FilesController'
import { StreamingFileReader } from '@standardnotes/filepicker'
import ClearInputButton from '../ClearInputButton/ClearInputButton'
import DecoratedInput from '../Input/DecoratedInput'
type Props = {
application: WebApplication
filesController: FilesController
allFiles: FileItem[]
attachedFiles: FileItem[]
currentTab: PopoverTabs
isDraggingFiles: boolean
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
attachedTabDisabled: boolean
}
const AttachedFilesPopover: FunctionComponent<Props> = ({
application,
filesController,
allFiles,
attachedFiles,
currentTab,
isDraggingFiles,
setCurrentTab,
attachedTabDisabled,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null)
const [searchQuery, setSearchQuery] = useState('')
const searchInputRef = useRef<HTMLInputElement>(null)
const filesList = currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles
const filteredList =
searchQuery.length > 0
? filesList.filter((file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1)
: filesList
const attachFilesIfRequired = (files: FileItem[]) => {
if (currentTab === PopoverTabs.AttachedFiles) {
files.forEach((file) => {
filesController
.handleFileAction({
type: PopoverFileItemActionType.AttachFileToNote,
payload: { file },
})
.catch(console.error)
})
}
}
const handleAttachFilesClick = async () => {
if (!StreamingFileReader.available()) {
fileInputRef.current?.click()
return
}
const uploadedFiles = await filesController.uploadNewFile()
if (!uploadedFiles) {
return
}
attachFilesIfRequired(uploadedFiles)
}
const previewHandler = (file: FileItem) => {
filesController
.handleFileAction({
type: PopoverFileItemActionType.PreviewFile,
payload: { file, otherFiles: currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles },
})
.catch(console.error)
}
return (
<div
className="flex flex-col"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
style={{
border: isDraggingFiles ? '2px dashed var(--sn-stylekit-info-color)' : '',
}}
>
<div className="flex border-b border-solid border-border">
<button
id={PopoverTabs.AttachedFiles}
className={`relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:bg-info-backdrop focus:shadow-bottom ${
currentTab === PopoverTabs.AttachedFiles ? 'font-medium text-info shadow-bottom' : 'text-text'
} ${attachedTabDisabled ? 'cursor-not-allowed text-neutral' : ''}`}
onClick={() => {
setCurrentTab(PopoverTabs.AttachedFiles)
}}
disabled={attachedTabDisabled}
>
Attached
</button>
<button
id={PopoverTabs.AllFiles}
className={`relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:bg-info-backdrop focus:shadow-bottom ${
currentTab === PopoverTabs.AllFiles ? 'font-medium text-info shadow-bottom' : 'text-text'
}`}
onClick={() => {
setCurrentTab(PopoverTabs.AllFiles)
}}
>
All files
</button>
</div>
<div className="max-h-110 min-h-0 overflow-y-auto">
{filteredList.length > 0 || searchQuery.length > 0 ? (
<div className="sticky top-0 left-0 border-b border-solid border-border bg-default p-3">
<DecoratedInput
type="text"
className={{ container: searchQuery.length < 1 ? 'py-1.5 px-0.5' : 'py-0' }}
placeholder="Search items..."
value={searchQuery}
onChange={setSearchQuery}
ref={searchInputRef}
right={[
searchQuery.length > 0 && (
<ClearInputButton
onClick={() => {
setSearchQuery('')
searchInputRef.current?.focus()
}}
/>
),
]}
/>
</div>
) : null}
{filteredList.length > 0 ? (
filteredList.map((file: FileItem) => {
return (
<PopoverFileItem
key={file.uuid}
file={file}
isAttachedToNote={attachedFiles.includes(file)}
handleFileAction={filesController.handleFileAction}
getIconType={application.iconsController.getIconForFileType}
previewHandler={previewHandler}
/>
)
})
) : (
<div className="flex w-full flex-col items-center justify-center py-8">
<div className="mb-2 h-18 w-18">
<FilesIllustration />
</div>
<div className="mb-3 text-sm font-medium">
{searchQuery.length > 0
? 'No result found'
: currentTab === PopoverTabs.AttachedFiles
? 'No files attached to this note'
: 'No files found in this account'}
</div>
<Button onClick={handleAttachFilesClick}>
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
</Button>
<div className="mt-3 text-xs text-passive-0">Or drop your files here</div>
</div>
)}
</div>
<input
type="file"
className="absolute top-0 left-0 -z-50 h-px w-px opacity-0"
multiple
ref={fileInputRef}
onChange={async (event) => {
const files = event.currentTarget.files
if (!files) {
return
}
for (const file of files) {
const uploadedFiles = await filesController.uploadNewFile(file)
if (uploadedFiles) {
attachFilesIfRequired(uploadedFiles)
}
}
}}
/>
{filteredList.length > 0 && (
<button
className="flex w-full cursor-pointer items-center border-0 border-t border-solid border-border bg-transparent px-3 py-3 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={handleAttachFilesClick}
>
<Icon type="add" className="mr-2 text-neutral" />
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
</button>
)}
</div>
)
}
export default observer(AttachedFilesPopover)
@@ -1,124 +0,0 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { KeyboardKey } from '@standardnotes/ui-services'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import {
FormEventHandler,
FunctionComponent,
KeyboardEventHandler,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import Icon from '@/Components/Icon/Icon'
import { PopoverFileItemActionType } from './PopoverFileItemAction'
import PopoverFileSubmenu from './PopoverFileSubmenu'
import { getFileIconComponent } from './getFileIconComponent'
import { PopoverFileItemProps } from './PopoverFileItemProps'
const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
file,
isAttachedToNote,
handleFileAction,
getIconType,
previewHandler,
}) => {
const [fileName, setFileName] = useState(file.name)
const [isRenamingFile, setIsRenamingFile] = useState(false)
const itemRef = useRef<HTMLDivElement>(null)
const fileNameInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (isRenamingFile) {
fileNameInputRef.current?.focus()
}
}, [isRenamingFile])
const renameFile = useCallback(
async (file: FileItem, name: string) => {
if (name.length < 1) {
return
}
await handleFileAction({
type: PopoverFileItemActionType.RenameFile,
payload: {
file,
name,
},
})
setIsRenamingFile(false)
},
[handleFileAction],
)
const handleFileNameInput: FormEventHandler<HTMLInputElement> = useCallback((event) => {
setFileName((event.target as HTMLInputElement).value)
}, [])
const handleFileNameInputKeyDown: KeyboardEventHandler = useCallback(
(event) => {
if (fileName.length > 0 && event.key === KeyboardKey.Enter) {
itemRef.current?.focus()
}
},
[fileName.length],
)
const handleFileNameInputBlur = useCallback(() => {
renameFile(file, fileName).catch(console.error)
}, [file, fileName, renameFile])
const handleClick = useCallback(() => {
if (isRenamingFile) {
return
}
previewHandler(file)
}, [file, isRenamingFile, previewHandler])
return (
<div
ref={itemRef}
className="flex items-center justify-between p-3 focus:shadow-none"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
<div onClick={handleClick} className="flex cursor-pointer items-center">
{getFileIconComponent(getIconType(file.mimeType), 'w-8 h-8 flex-shrink-0')}
<div className="mx-4 flex flex-col">
{isRenamingFile ? (
<input
type="text"
className="text-input mb-1 border border-solid border-border bg-transparent px-1.5 py-1 text-foreground"
value={fileName}
ref={fileNameInputRef}
onInput={handleFileNameInput}
onKeyDown={handleFileNameInputKeyDown}
onBlur={handleFileNameInputBlur}
/>
) : (
<div className="break-word mb-1 text-mobile-menu-item md:text-sm">
<span className="align-middle">{file.name}</span>
{file.protected && (
<Icon type="lock-filled" className="ml-2 inline align-middle text-neutral" size="small" />
)}
</div>
)}
<div className="text-xs text-passive-0">
{file.created_at.toLocaleString()} · {formatSizeToReadableString(file.decryptedSize)}
</div>
</div>
</div>
<PopoverFileSubmenu
file={file}
isAttachedToNote={isAttachedToNote}
handleFileAction={handleFileAction}
setIsRenamingFile={setIsRenamingFile}
previewHandler={previewHandler}
/>
</div>
)
}
export default PopoverFileItem
@@ -1,20 +0,0 @@
import { IconType, FileItem } from '@standardnotes/snjs'
import { Dispatch, SetStateAction } from 'react'
import { PopoverFileItemAction } from './PopoverFileItemAction'
type CommonProps = {
file: FileItem
isAttachedToNote: boolean
handleFileAction: (action: PopoverFileItemAction) => Promise<{
didHandleAction: boolean
}>
previewHandler: (file: FileItem) => void
}
export type PopoverFileItemProps = CommonProps & {
getIconType(type: string): IconType
}
export type PopoverFileSubmenuProps = CommonProps & {
setIsRenamingFile: Dispatch<SetStateAction<boolean>>
}
@@ -1,154 +0,0 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import Switch from '@/Components/Switch/Switch'
import { PopoverFileSubmenuProps } from './PopoverFileItemProps'
import { PopoverFileItemActionType } from './PopoverFileItemAction'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import Popover from '../Popover/Popover'
const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
file,
isAttachedToNote,
handleFileAction,
setIsRenamingFile,
previewHandler,
}) => {
const menuContainerRef = useRef<HTMLDivElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [isFileProtected, setIsFileProtected] = useState(file.protected)
const closeMenu = useCallback(() => {
setIsOpen(false)
}, [])
const toggleMenu = useCallback(() => {
setIsOpen((isOpen) => !isOpen)
}, [])
return (
<div ref={menuContainerRef}>
<button
ref={menuButtonRef}
onClick={toggleMenu}
className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
>
<Icon type="more" className="text-neutral" />
</button>
<Popover anchorElement={menuButtonRef.current} open={isOpen} togglePopover={toggleMenu} className="py-2">
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={() => {
previewHandler(file)
closeMenu()
}}
>
<Icon type="file" className="mr-2 text-neutral" />
Preview file
</button>
{isAttachedToNote ? (
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DetachFileToNote,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link-off" className="mr-2 text-neutral" />
Detach from note
</button>
) : (
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.AttachFileToNote,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link" className="mr-2 text-neutral" />
Attach to note
</button>
)}
<HorizontalSeparator classes="my-1" />
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.ToggleFileProtection,
payload: { file },
callback: (isProtected: boolean) => {
setIsFileProtected(isProtected)
},
}).catch(console.error)
}}
>
<span className="flex items-center">
<Icon type="password" className="mr-2 text-neutral" />
Password protection
</span>
<Switch
className="pointer-events-none px-0"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
checked={isFileProtected}
/>
</button>
<HorizontalSeparator classes="my-1" />
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DownloadFile,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="download" className="mr-2 text-neutral" />
Download
</button>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={() => {
setIsRenamingFile(true)
closeMenu()
}}
>
<Icon type="pencil" className="mr-2 text-neutral" />
Rename
</button>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DeleteFile,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</button>
<div className="px-3 py-1 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">File ID:</span> {file.uuid}
</div>
<div>
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
</div>
</div>
</Popover>
</div>
)
}
export default PopoverFileSubmenu
@@ -1,4 +0,0 @@
export enum PopoverTabs {
AttachedFiles = 'attached-files-tab',
AllFiles = 'all-files-tab',
}
@@ -1,7 +0,0 @@
import { ICONS } from '@/Components/Icon/Icon'
export const getFileIconComponent = (iconType: string, className: string) => {
const IconComponent = ICONS[iconType as keyof typeof ICONS]
return <IconComponent className={className} />
}
@@ -1,32 +1,37 @@
import { FunctionComponent, MouseEventHandler } from 'react'
import { ForwardedRef, forwardRef, MouseEventHandler } from 'react'
import Icon from '@/Components/Icon/Icon'
import { IconType } from '@standardnotes/snjs'
type ButtonType = 'normal' | 'primary'
import { classNames } from '@/Utils/ConcatenateClassNames'
type Props = {
onClick: () => void
type: ButtonType
className?: string
icon: IconType
iconClassName?: string
label: string
id?: string
}
const RoundIconButton: FunctionComponent<Props> = ({ onClick, type, className, icon: iconType }) => {
const click: MouseEventHandler = (e) => {
e.preventDefault()
onClick()
}
const classes = type === 'primary' ? 'info ' : ''
return (
<button
className={`m-0 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-transparent bg-clip-padding text-neutral hover:bg-contrast hover:text-text focus:bg-contrast focus:text-text focus:outline-none focus:ring-info ${classes} ${
className ?? ''
}`}
onClick={click}
>
<Icon type={iconType} />
</button>
)
}
const RoundIconButton = forwardRef(
({ onClick, className, icon: iconType, iconClassName, id }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
const click: MouseEventHandler = (e) => {
e.preventDefault()
onClick()
}
return (
<button
className={classNames(
'bg-text-padding m-0 flex h-8.5 min-w-8.5 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-clip-padding text-neutral hover:bg-contrast hover:text-text focus:bg-contrast focus:text-text focus:outline-none focus:ring-info md:h-8 md:min-w-8',
className,
)}
onClick={click}
ref={ref}
id={id}
>
<Icon type={iconType} className={iconClassName} />
</button>
)
},
)
export default RoundIconButton
@@ -2,10 +2,9 @@ import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import ChangeEditorMenu from './ChangeEditorMenu'
import Popover from '../Popover/Popover'
import { classNames } from '@/Utils/ConcatenateClassNames'
import RoundIconButton from '../Button/RoundIconButton'
type Props = {
application: WebApplication
@@ -39,18 +38,13 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
return (
<div ref={containerRef}>
<button
className={classNames(
'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid text-neutral hover:bg-contrast focus:bg-contrast',
`border-accessory-tint-${selectedEditorIconTint}`,
)}
title="Change note type"
aria-label="Change note type"
<RoundIconButton
label="Change note type"
onClick={toggleMenu}
ref={buttonRef}
>
<Icon type={selectedEditorIcon} className={`text-accessory-tint-${selectedEditorIconTint}`} />
</button>
icon={selectedEditorIcon}
iconClassName={`text-accessory-tint-${selectedEditorIconTint}`}
/>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pt-2 md:pt-0">
<ChangeEditorMenu
application={application}
@@ -1,6 +1,5 @@
import { WebApplication } from '@/Application/Application'
import { KeyboardKey } from '@standardnotes/ui-services'
import { UuidString } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
@@ -22,7 +21,7 @@ type Props = {
navigationController: NavigationController
notesController: NotesController
selectionController: SelectedItemsController
selectedItems: Record<UuidString, ListableContentItem>
selectedUuids: SelectedItemsController['selectedUuids']
paginate: () => void
}
@@ -34,7 +33,7 @@ const ContentList: FunctionComponent<Props> = ({
navigationController,
notesController,
selectionController,
selectedItems,
selectedUuids,
paginate,
}) => {
const { selectPreviousItem, selectNextItem } = selectionController
@@ -82,7 +81,7 @@ const ContentList: FunctionComponent<Props> = ({
key={item.uuid}
application={application}
item={item}
selected={!!selectedItems[item.uuid]}
selected={selectedUuids.has(item.uuid)}
hideDate={hideDate}
hidePreview={hideNotePreview}
hideTags={hideTags}
@@ -1,7 +1,7 @@
import { KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application'
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
import { PrefKey, SystemViewId } from '@standardnotes/snjs'
import { FileItem, PrefKey, SystemViewId } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
import ContentList from '@/Components/ContentListView/ContentList'
@@ -24,6 +24,8 @@ import SearchBar from '../SearchBar/SearchBar'
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
import { LinkingController } from '@/Controllers/LinkingController'
type Props = {
accountMenuController: AccountMenuController
@@ -35,6 +37,7 @@ type Props = {
notesController: NotesController
selectionController: SelectedItemsController
searchOptionsController: SearchOptionsController
linkingController: LinkingController
}
const ContentListView: FunctionComponent<Props> = ({
@@ -47,12 +50,54 @@ const ContentListView: FunctionComponent<Props> = ({
notesController,
selectionController,
searchOptionsController,
linkingController,
}) => {
const { isNotesListVisibleOnTablets, toggleAppPane } = useResponsiveAppPane()
const fileInputRef = useRef<HTMLInputElement>(null)
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
const { addDragTarget, removeDragTarget } = useFileDragNDrop()
const fileDropCallback = useCallback(
async (files: FileItem[]) => {
const currentTag = navigationController.selected
if (!currentTag) {
return
}
if (navigationController.isInAnySystemView() || navigationController.isInSmartView()) {
console.error('Trying to link uploaded files to smart view')
return
}
files.forEach(async (file) => {
await linkingController.linkItems(file, currentTag)
})
},
[navigationController, linkingController],
)
useEffect(() => {
const target = itemsViewPanelRef.current
const currentTag = navigationController.selected
const shouldAddDropTarget = !navigationController.isInAnySystemView() && !navigationController.isInSmartView()
if (target && shouldAddDropTarget && currentTag) {
addDragTarget(target, {
tooltipText: `Drop your files to upload and link them to tag "${currentTag.title}"`,
callback: fileDropCallback,
})
}
return () => {
if (target) {
removeDragTarget(target)
}
}
}, [addDragTarget, fileDropCallback, navigationController, navigationController.selected, removeDragTarget])
const {
completedFullSync,
createNewNote,
@@ -64,7 +109,11 @@ const ContentListView: FunctionComponent<Props> = ({
searchBarElement,
} = itemListController
const { selectedItems, selectNextItem, selectPreviousItem } = selectionController
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
const { selected: selectedTag } = navigationController
const icon = selectedTag?.iconString
const isFilesSmartView = useMemo(
() => navigationController.selected?.uuid === SystemViewId.Files,
@@ -214,6 +263,7 @@ const ContentListView: FunctionComponent<Props> = ({
<ContentListHeader
application={application}
panelTitle={panelTitle}
icon={icon}
addButtonLabel={addButtonLabel}
addNewItem={addNewItem}
isFilesSmartView={isFilesSmartView}
@@ -231,7 +281,7 @@ const ContentListView: FunctionComponent<Props> = ({
{renderedItems.length ? (
<ContentList
items={renderedItems}
selectedItems={selectedItems}
selectedUuids={selectedUuids}
application={application}
paginate={paginate}
filesController={filesController}
@@ -1,7 +1,7 @@
import { FileItem } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef } from 'react'
import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent'
import { getFileIconComponent } from '../FilePreview/getFileIconComponent'
import ListItemConflictIndicator from './ListItemConflictIndicator'
import ListItemFlagIcons from './ListItemFlagIcons'
import ListItemTags from './ListItemTags'
@@ -5,13 +5,17 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
import Popover from '@/Components/Popover/Popover'
import DisplayOptionsMenu from './DisplayOptionsMenu'
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
import { IconType } from '@standardnotes/snjs'
import RoundIconButton from '@/Components/Button/RoundIconButton'
type Props = {
application: {
getPreference: WebApplication['getPreference']
setPreference: WebApplication['setPreference']
isNativeMobileWeb: WebApplication['isNativeMobileWeb']
}
panelTitle: string
icon?: IconType | string
addButtonLabel: string
addNewItem: () => void
isFilesSmartView: boolean
@@ -21,6 +25,7 @@ type Props = {
const ContentListHeader = ({
application,
panelTitle,
icon,
addButtonLabel,
addNewItem,
isFilesSmartView,
@@ -39,21 +44,29 @@ const ContentListHeader = ({
<div className="section-title-bar-header items-start gap-1 overflow-hidden">
<NavigationMenuButton />
<div className="flex min-w-0 flex-grow flex-col break-words">
<div className="text-lg font-semibold text-text">{panelTitle}</div>
{optionsSubtitle && <div className="text-xs text-passive-0">{optionsSubtitle}</div>}
<div className={`flex min-w-0 flex-grow flex-row ${!optionsSubtitle ? 'items-center' : ''}`}>
{icon && (
<Icon
type={icon as IconType}
size={'large'}
className={`ml-0.5 mr-1.5 text-neutral ${optionsSubtitle ? 'mt-1' : ''}`}
/>
)}
<div className="flex min-w-0 flex-grow flex-col break-words">
<div className="text-lg font-semibold text-text">{panelTitle}</div>
{optionsSubtitle && <div className="text-xs text-passive-0">{optionsSubtitle}</div>}
</div>
</div>
</div>
<div className="flex">
<div className="relative" ref={displayOptionsContainerRef}>
<button
className={classNames(
'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast',
showDisplayOptionsMenu && 'bg-contrast',
)}
<RoundIconButton
className={classNames(showDisplayOptionsMenu && 'bg-contrast')}
onClick={toggleDisplayOptionsMenu}
ref={displayOptionsButtonRef}
>
<Icon type="sort-descending" />
</button>
icon="sort-descending"
label="Display options menu"
/>
<Popover
open={showDisplayOptionsMenu}
anchorElement={displayOptionsButtonRef.current}
@@ -70,12 +83,12 @@ const ContentListHeader = ({
</Popover>
</div>
<button
className="ml-3 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125"
className="absolute bottom-6 right-6 ml-3 flex h-13 w-13 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125 md:static md:h-8 md:w-8"
title={addButtonLabel}
aria-label={addButtonLabel}
onClick={addNewItem}
>
<Icon type="add" />
<Icon type="add" size="custom" className="h-6 w-6 md:h-5 md:w-5" />
</button>
</div>
</div>
@@ -1,13 +0,0 @@
import { DisclosureButton } from '@reach/disclosure'
import styled from 'styled-components'
const StyledDisplayOptionsButton = styled(DisclosureButton).attrs(() => ({
className:
'flex justify-center items-center min-w-8 h-8 bg-text-padding hover:bg-contrast focus:bg-contrast text-neutral border border-solid border-border rounded-full cursor-pointer',
}))<{
$pressed: boolean
}>`
background-color: ${(props) => (props.$pressed ? 'var(--sn-stylekit-contrast-background-color)' : 'transparent')};
`
export default StyledDisplayOptionsButton
@@ -19,7 +19,7 @@ const ListItemTags: FunctionComponent<Props> = ({ hideTags, tags }) => {
className="inline-flex items-center rounded-sm bg-passive-4-opacity-variant py-1 px-1.5 text-foreground"
key={tag.uuid}
>
<Icon type="hashtag" className="mr-1 text-passive-1" size="small" />
<Icon type={tag.iconString} className="mr-1 text-passive-1" size="small" />
<span>{tag.title}</span>
</span>
))}
@@ -5,5 +5,6 @@ export type DisplayableListItemProps = AbstractListItemProps & {
tags: {
uuid: SNTag['uuid']
title: SNTag['title']
iconString: SNTag['iconString']
}[]
}
@@ -1,10 +1,10 @@
import Icon from '@/Components/Icon/Icon'
import { useCallback, useRef, useState } from 'react'
import { observer } from 'mobx-react-lite'
import FileMenuOptions from './FileMenuOptions'
import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import Popover from '../Popover/Popover'
import RoundIconButton from '../Button/RoundIconButton'
type Props = {
filesController: FilesController
@@ -19,15 +19,7 @@ const FilesOptionsPanel = ({ filesController, selectionController }: Props) => {
return (
<>
<button
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
title="File options menu"
aria-label="File options menu"
onClick={toggleMenu}
ref={buttonRef}
>
<Icon type="more" />
</button>
<RoundIconButton label="File options menu" onClick={toggleMenu} ref={buttonRef} icon="more" />
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
<FileMenuOptions
filesController={filesController}
@@ -2,19 +2,22 @@ import { WebApplication } from '@/Application/Application'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { FilesController } from '@/Controllers/FilesController'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { isHandlingFileDrag } from '@/Utils/DragTypeCheck'
import { StreamingFileReader } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEffect, useContext } from 'react'
import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs'
import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEffect, useContext, memo } from 'react'
import Portal from '../Portal/Portal'
type FilesDragInCallback = (tab: PopoverTabs) => void
type FilesDropCallback = (uploadedFiles: FileItem[]) => void
type FileDragTargetData = {
tooltipText: string
callback: (files: FileItem[]) => void
}
type FileDnDContextData = {
isDraggingFiles: boolean
addFilesDragInCallback: (callback: FilesDragInCallback) => void
addFilesDropCallback: (callback: FilesDropCallback) => void
addDragTarget: (target: HTMLElement, data: FileDragTargetData) => void
removeDragTarget: (target: HTMLElement) => void
}
export const FileDnDContext = createContext<FileDnDContextData | null>(null)
@@ -36,23 +39,57 @@ type Props = {
children: ReactNode
}
const FileDragOverlayClassName =
'overlay pointer-events-none absolute top-0 left-0 z-footer-bar h-full w-full border-2 border-info before:block before:h-full before:w-full before:bg-info before:opacity-20'
const MemoizedChildren = memo(({ children }: { children: ReactNode }) => {
return <>{children}</>
})
const FileDragNDropProvider = ({ application, children, featuresController, filesController }: Props) => {
const premiumModal = usePremiumModal()
const [isDraggingFiles, setIsDraggingFiles] = useState(false)
const [tooltipText, setTooltipText] = useState('')
const filesDragInCallbackRef = useRef<FilesDragInCallback>()
const filesDropCallbackRef = useRef<FilesDropCallback>()
const fileDragOverlayRef = useRef<HTMLDivElement>(null)
const addFilesDragInCallback = useCallback((callback: FilesDragInCallback) => {
filesDragInCallbackRef.current = callback
const addOverlayToElement = useCallback((target: Element) => {
if (fileDragOverlayRef.current) {
const targetBoundingRect = target.getBoundingClientRect()
fileDragOverlayRef.current.style.width = `${targetBoundingRect.width}px`
fileDragOverlayRef.current.style.height = `${targetBoundingRect.height}px`
fileDragOverlayRef.current.style.transform = `translate(${targetBoundingRect.x}px, ${targetBoundingRect.y}px)`
}
}, [])
const addFilesDropCallback = useCallback((callback: FilesDropCallback) => {
filesDropCallbackRef.current = callback
const removeOverlayFromElement = useCallback(() => {
if (fileDragOverlayRef.current) {
fileDragOverlayRef.current.style.width = ''
fileDragOverlayRef.current.style.height = ''
fileDragOverlayRef.current.style.transform = ''
}
}, [])
const dragTargets = useRef<Map<Element, FileDragTargetData>>(new Map())
const addDragTarget = useCallback((target: HTMLElement, data: FileDragTargetData) => {
target.setAttribute('data-file-drag-target', '')
dragTargets.current.set(target, data)
}, [])
const removeDragTarget = useCallback((target: HTMLElement) => {
target.removeAttribute('data-file-drag-target')
dragTargets.current.delete(target)
}, [])
const dragCounter = useRef(0)
const resetState = useCallback(() => {
setIsDraggingFiles(false)
setTooltipText('')
removeOverlayFromElement()
}, [removeOverlayFromElement])
const handleDrag = useCallback(
(event: DragEvent) => {
if (isHandlingFileDrag(event, application)) {
@@ -72,22 +109,31 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
event.preventDefault()
event.stopPropagation()
switch ((event.target as HTMLElement).id) {
case PopoverTabs.AllFiles:
filesDragInCallbackRef.current?.(PopoverTabs.AllFiles)
break
case PopoverTabs.AttachedFiles:
filesDragInCallbackRef.current?.(PopoverTabs.AttachedFiles)
break
removeOverlayFromElement()
let closestDragTarget: Element | null = null
if (event.target instanceof HTMLElement) {
closestDragTarget = event.target.closest('[data-file-drag-target]')
}
dragCounter.current = dragCounter.current + 1
if (event.dataTransfer?.items.length) {
setIsDraggingFiles(true)
if (closestDragTarget) {
addOverlayToElement(closestDragTarget)
const tooltipText = dragTargets.current.get(closestDragTarget)?.tooltipText
if (tooltipText) {
setTooltipText(tooltipText)
}
} else {
setTooltipText('')
removeOverlayFromElement()
}
}
},
[application],
[addOverlayToElement, application, removeOverlayFromElement],
)
const handleDragOut = useCallback(
@@ -105,22 +151,28 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
return
}
setIsDraggingFiles(false)
resetState()
},
[application],
[application, resetState],
)
const handleDrop = useCallback(
(event: DragEvent) => {
if (!isHandlingFileDrag(event, application)) {
setIsDraggingFiles(false)
resetState()
return
}
event.preventDefault()
event.stopPropagation()
setIsDraggingFiles(false)
let closestDragTarget: Element | null = null
if (event.target instanceof HTMLElement) {
closestDragTarget = event.target.closest('[data-file-drag-target]')
}
resetState()
if (!featuresController.hasFiles) {
premiumModal.activate('Files')
@@ -143,14 +195,16 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
return
}
filesDropCallbackRef.current?.(uploadedFiles)
if (closestDragTarget && dragTargets.current.has(closestDragTarget)) {
dragTargets.current.get(closestDragTarget)?.callback(uploadedFiles)
}
})
event.dataTransfer.clearData()
dragCounter.current = 0
}
},
[application, featuresController.hasFiles, filesController, premiumModal],
[application, featuresController.hasFiles, filesController, premiumModal, resetState],
)
useEffect(() => {
@@ -170,12 +224,29 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
const contextValue = useMemo(() => {
return {
isDraggingFiles,
addFilesDragInCallback,
addFilesDropCallback,
addDragTarget,
removeDragTarget,
}
}, [addFilesDragInCallback, addFilesDropCallback, isDraggingFiles])
}, [addDragTarget, isDraggingFiles, removeDragTarget])
return <FileDnDContext.Provider value={contextValue}>{children}</FileDnDContext.Provider>
return (
<FileDnDContext.Provider value={contextValue}>
<MemoizedChildren children={children} />
{isDraggingFiles ? (
<>
<div className="pointer-events-none absolute bottom-8 left-1/2 z-dropdown-menu -translate-x-1/2 rounded border-2 border-info bg-default px-5 py-3 shadow-main">
{tooltipText.length ? tooltipText : 'Drop your files to upload them'}
</div>
</>
) : null}
<Portal>
<div
className={classNames(FileDragOverlayClassName, isDraggingFiles ? 'visible' : 'invisible')}
ref={fileDragOverlayRef}
/>
</Portal>
</FileDnDContext.Provider>
)
}
export default FileDragNDropProvider
@@ -1,7 +1,7 @@
import { WebApplication } from '@/Application/Application'
import { DialogContent, DialogOverlay } from '@reach/dialog'
import { FunctionComponent, KeyboardEventHandler, useCallback, useMemo, useRef, useState } from 'react'
import { getFileIconComponent } from '@/Components/AttachedFilesPopover/getFileIconComponent'
import { getFileIconComponent } from './getFileIconComponent'
import Icon from '@/Components/Icon/Icon'
import FilePreviewInfoPanel from './FilePreviewInfoPanel'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
@@ -0,0 +1,7 @@
import { IconNameToSvgMapping } from '@/Components/Icon/IconNameToSvgMapping'
export const getFileIconComponent = (iconType: string, className: string) => {
const IconComponent = IconNameToSvgMapping[iconType as keyof typeof IconNameToSvgMapping]
return <IconComponent className={className} />
}
@@ -1,15 +1,16 @@
import { ElementIds } from '@/Constants/ElementIDs'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, useCallback, useRef, useState } from 'react'
import { ChangeEventHandler, useCallback, useEffect, useRef, useState } from 'react'
import FileOptionsPanel from '@/Components/FileContextMenu/FileOptionsPanel'
import FilePreview from '@/Components/FilePreview/FilePreview'
import { FileViewProps } from './FileViewProps'
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
import Icon from '../Icon/Icon'
import Popover from '../Popover/Popover'
import FilePreviewInfoPanel from '../FilePreview/FilePreviewInfoPanel'
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
import RoundIconButton from '../Button/RoundIconButton'
const SyncTimeoutNoDebounceMs = 100
const SyncTimeoutDebounceMs = 350
@@ -40,8 +41,33 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }:
[application, file],
)
const fileDragTargetRef = useRef<HTMLDivElement>(null)
const { addDragTarget, removeDragTarget } = useFileDragNDrop()
useEffect(() => {
const target = fileDragTargetRef.current
if (target) {
addDragTarget(target, {
tooltipText: 'Drop your files to upload and link them to the current file',
callback(files) {
files.forEach(async (uploadedFile) => {
await viewControllerManager.linkingController.linkItems(uploadedFile, file)
})
},
})
}
return () => {
if (target) {
removeDragTarget(target)
}
}
}, [addDragTarget, file, removeDragTarget, viewControllerManager.linkingController])
return (
<div className="sn-component section editor" aria-label="File">
<div className="sn-component section editor" aria-label="File" ref={fileDragTargetRef}>
<div className="flex flex-col">
<div
className="content-title-bar section-title-bar section-title-bar z-editor-title-bar w-full"
@@ -69,15 +95,12 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }:
filesController={viewControllerManager.filesController}
linkingController={viewControllerManager.linkingController}
/>
<button
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
title="File information panel"
aria-label="File information panel"
<RoundIconButton
label="File information panel"
onClick={toggleFileInfoPanel}
ref={fileInfoButtonRef}
>
<Icon type="info" />
</button>
icon="info"
/>
<Popover
open={isFileInfoPanelOpen}
togglePopover={toggleFileInfoPanel}
@@ -1,141 +1,77 @@
import { FunctionComponent, useMemo } from 'react'
import { IconType } from '@standardnotes/snjs'
import * as icons from '@standardnotes/icons'
export const ICONS = {
'account-circle': icons.AccountCircleIcon,
'arrow-left': icons.ArrowLeftIcon,
'arrow-right': icons.ArrowRightIcon,
'arrows-sort-down': icons.ArrowsSortDownIcon,
'arrows-sort-up': icons.ArrowsSortUpIcon,
'attachment-file': icons.AttachmentFileIcon,
'check-bold': icons.CheckBoldIcon,
'check-circle': icons.CheckCircleIcon,
'chevron-down': icons.ChevronDownIcon,
'chevron-left': icons.ChevronLeftIcon,
'chevron-right': icons.ChevronRightIcon,
'clear-circle-filled': icons.ClearCircleFilledIcon,
'cloud-off': icons.CloudOffIcon,
'diamond-filled': icons.DiamondFilledIcon,
'eye-off': icons.EyeOffIcon,
'file-doc': icons.FileDocIcon,
'file-image': icons.FileImageIcon,
'file-mov': icons.FileMovIcon,
'file-music': icons.FileMusicIcon,
'file-other': icons.FileOtherIcon,
'file-pdf': icons.FilePdfIcon,
'file-ppt': icons.FilePptIcon,
'file-xls': icons.FileXlsIcon,
'file-zip': icons.FileZipIcon,
'hashtag-off': icons.HashtagOffIcon,
'link-off': icons.LinkOffIcon,
'list-bulleted': icons.ListBulleted,
'lock-filled': icons.LockFilledIcon,
'menu-arrow-down-alt': icons.MenuArrowDownAlt,
'menu-arrow-down': icons.MenuArrowDownIcon,
'menu-arrow-right': icons.MenuArrowRightIcon,
'menu-close': icons.MenuCloseIcon,
'menu-variant': icons.MenuVariantIcon,
'notes-filled': icons.NotesFilledIcon,
'pencil-filled': icons.PencilFilledIcon,
'pencil-off': icons.PencilOffIcon,
'pin-filled': icons.PinFilledIcon,
'plain-text': icons.PlainTextIcon,
'premium-feature': icons.PremiumFeatureIcon,
'rich-text': icons.RichTextIcon,
'sort-descending': icons.SortDescendingIcon,
'star-circle-filled': icons.StarCircleFilled,
'star-filled': icons.StarFilledIcon,
'star-variant-filled': icons.StarVariantFilledIcon,
'trash-filled': icons.TrashFilledIcon,
'trash-sweep': icons.TrashSweepIcon,
'user-add': icons.UserAddIcon,
'user-switch': icons.UserSwitch,
accessibility: icons.AccessibilityIcon,
add: icons.AddIcon,
archive: icons.ArchiveIcon,
asterisk: icons.AsteriskIcon,
authenticator: icons.AuthenticatorIcon,
check: icons.CheckIcon,
close: icons.CloseIcon,
code: icons.CodeIcon,
copy: icons.CopyIcon,
dashboard: icons.DashboardIcon,
diamond: icons.DiamondIcon,
download: icons.DownloadIcon,
editor: icons.EditorIcon,
email: icons.EmailIcon,
eye: icons.EyeIcon,
file: icons.FileIcon,
folder: icons.FolderIcon,
hashtag: icons.HashtagIcon,
help: icons.HelpIcon,
history: icons.HistoryIcon,
info: icons.InfoIcon,
keyboard: icons.KeyboardIcon,
link: icons.LinkIcon,
listed: icons.ListedIcon,
lock: icons.LockIcon,
markdown: icons.MarkdownIcon,
more: icons.MoreIcon,
notes: icons.NotesIcon,
password: icons.PasswordIcon,
pencil: icons.PencilIcon,
pin: icons.PinIcon,
restore: icons.RestoreIcon,
search: icons.SearchIcon,
security: icons.SecurityIcon,
server: icons.ServerIcon,
settings: icons.SettingsIcon,
share: icons.ShareIcon,
signIn: icons.SignInIcon,
signOut: icons.SignOutIcon,
spreadsheets: icons.SpreadsheetsIcon,
star: icons.StarIcon,
subtract: icons.SubtractIcon,
sync: icons.SyncIcon,
tasks: icons.TasksIcon,
themes: icons.ThemesIcon,
trash: icons.TrashIcon,
tune: icons.TuneIcon,
unarchive: icons.UnarchiveIcon,
unpin: icons.UnpinIcon,
user: icons.UserIcon,
view: icons.ViewIcon,
warning: icons.WarningIcon,
window: icons.WindowIcon,
}
import { FunctionComponent } from 'react'
import { VectorIconNameOrEmoji } from '@standardnotes/snjs'
import { IconNameToSvgMapping } from './IconNameToSvgMapping'
import { classNames } from '@/Utils/ConcatenateClassNames'
type Props = {
type: IconType
type: VectorIconNameOrEmoji
className?: string
ariaLabel?: string
size?: 'small' | 'medium' | 'normal' | 'custom'
size?: 'small' | 'medium' | 'normal' | 'large' | 'custom'
}
const ContainerDimensions = {
small: 'w-3.5 h-3.5',
medium: 'w-4 h-4',
normal: 'w-5 h-5',
large: 'w-6 h-6',
custom: '',
}
const EmojiContainerDimensions = {
small: 'w-4 h-4',
medium: 'w-5 h-5',
normal: 'w-5 h-5',
large: 'w-7 h-6',
custom: '',
}
const EmojiOffset = {
small: '',
medium: '',
normal: '-mt-0.5',
large: '',
custom: '',
}
const EmojiSize = {
small: 'text-xs',
medium: 'text-sm',
normal: 'text-base',
large: 'text-lg',
custom: '',
}
const getIconComponent = (type: VectorIconNameOrEmoji) => {
return IconNameToSvgMapping[type as keyof typeof IconNameToSvgMapping]
}
export const isIconEmoji = (type: VectorIconNameOrEmoji): boolean => {
return getIconComponent(type) == undefined
}
const Icon: FunctionComponent<Props> = ({ type, className = '', ariaLabel, size = 'normal' }) => {
const IconComponent = ICONS[type as keyof typeof ICONS]
const dimensions = useMemo(() => {
switch (size) {
case 'small':
return 'w-3.5 h-3.5'
case 'medium':
return 'w-4 h-4'
case 'custom':
return ''
default:
return 'w-5 h-5'
}
}, [size])
const IconComponent = getIconComponent(type)
if (!IconComponent) {
return null
return (
<label
className={classNames(
'fill-current',
'text-center',
EmojiSize[size],
EmojiContainerDimensions[size],
EmojiOffset[size],
className,
)}
>
{type}
</label>
)
}
return (
<IconComponent
className={`${dimensions} fill-current ${className}`}
className={`${ContainerDimensions[size]} fill-current ${className}`}
role="img"
{...(ariaLabel ? { 'aria-label': ariaLabel } : { 'aria-hidden': true })}
/>
@@ -0,0 +1,106 @@
import * as icons from '@standardnotes/icons'
export const IconNameToSvgMapping = {
'account-circle': icons.AccountCircleIcon,
'arrow-left': icons.ArrowLeftIcon,
'arrow-right': icons.ArrowRightIcon,
'arrows-sort-down': icons.ArrowsSortDownIcon,
'arrows-sort-up': icons.ArrowsSortUpIcon,
'attachment-file': icons.AttachmentFileIcon,
'check-bold': icons.CheckBoldIcon,
'check-circle': icons.CheckCircleIcon,
'chevron-down': icons.ChevronDownIcon,
'chevron-left': icons.ChevronLeftIcon,
'chevron-right': icons.ChevronRightIcon,
'clear-circle-filled': icons.ClearCircleFilledIcon,
'cloud-off': icons.CloudOffIcon,
'diamond-filled': icons.DiamondFilledIcon,
'eye-off': icons.EyeOffIcon,
'file-doc': icons.FileDocIcon,
'file-image': icons.FileImageIcon,
'file-mov': icons.FileMovIcon,
'file-music': icons.FileMusicIcon,
'file-other': icons.FileOtherIcon,
'file-pdf': icons.FilePdfIcon,
'file-ppt': icons.FilePptIcon,
'file-xls': icons.FileXlsIcon,
'file-zip': icons.FileZipIcon,
'hashtag-off': icons.HashtagOffIcon,
'link-off': icons.LinkOffIcon,
'list-bulleted': icons.ListBulleted,
'lock-filled': icons.LockFilledIcon,
'menu-arrow-down-alt': icons.MenuArrowDownAlt,
'menu-arrow-down': icons.MenuArrowDownIcon,
'menu-arrow-right': icons.MenuArrowRightIcon,
'menu-close': icons.MenuCloseIcon,
'menu-variant': icons.MenuVariantIcon,
'notes-filled': icons.NotesFilledIcon,
'pencil-filled': icons.PencilFilledIcon,
'pencil-off': icons.PencilOffIcon,
'pin-filled': icons.PinFilledIcon,
'plain-text': icons.PlainTextIcon,
'premium-feature': icons.PremiumFeatureIcon,
'rich-text': icons.RichTextIcon,
'sort-descending': icons.SortDescendingIcon,
'star-circle-filled': icons.StarCircleFilled,
'star-filled': icons.StarFilledIcon,
'star-variant-filled': icons.StarVariantFilledIcon,
'trash-filled': icons.TrashFilledIcon,
'trash-sweep': icons.TrashSweepIcon,
'user-add': icons.UserAddIcon,
'user-switch': icons.UserSwitch,
'fullscreen-exit': icons.FullscreenExitIcon,
accessibility: icons.AccessibilityIcon,
add: icons.AddIcon,
archive: icons.ArchiveIcon,
asterisk: icons.AsteriskIcon,
authenticator: icons.AuthenticatorIcon,
check: icons.CheckIcon,
close: icons.CloseIcon,
code: icons.CodeIcon,
copy: icons.CopyIcon,
dashboard: icons.DashboardIcon,
diamond: icons.DiamondIcon,
download: icons.DownloadIcon,
editor: icons.EditorIcon,
email: icons.EmailIcon,
eye: icons.EyeIcon,
file: icons.FileIcon,
folder: icons.FolderIcon,
hashtag: icons.HashtagIcon,
help: icons.HelpIcon,
history: icons.HistoryIcon,
info: icons.InfoIcon,
keyboard: icons.KeyboardIcon,
link: icons.LinkIcon,
listed: icons.ListedIcon,
lock: icons.LockIcon,
markdown: icons.MarkdownIcon,
more: icons.MoreIcon,
notes: icons.NotesIcon,
password: icons.PasswordIcon,
pencil: icons.PencilIcon,
pin: icons.PinIcon,
restore: icons.RestoreIcon,
search: icons.SearchIcon,
security: icons.SecurityIcon,
server: icons.ServerIcon,
settings: icons.SettingsIcon,
share: icons.ShareIcon,
signIn: icons.SignInIcon,
signOut: icons.SignOutIcon,
spreadsheets: icons.SpreadsheetsIcon,
star: icons.StarIcon,
subtract: icons.SubtractIcon,
sync: icons.SyncIcon,
tasks: icons.TasksIcon,
themes: icons.ThemesIcon,
trash: icons.TrashIcon,
tune: icons.TuneIcon,
unarchive: icons.UnarchiveIcon,
unpin: icons.UnpinIcon,
user: icons.UserIcon,
view: icons.ViewIcon,
warning: icons.WarningIcon,
window: icons.WindowIcon,
}
@@ -0,0 +1,131 @@
import { EmojiString, Platform, VectorIconNameOrEmoji } from '@standardnotes/snjs'
import { FunctionComponent, useMemo, useRef, useState } from 'react'
import Dropdown from '../Dropdown/Dropdown'
import { DropdownItem } from '../Dropdown/DropdownItem'
import { isIconEmoji } from './Icon'
import { IconNameToSvgMapping } from './IconNameToSvgMapping'
import { IconPickerType } from './IconPickerType'
type Props = {
selectedValue: VectorIconNameOrEmoji
onIconChange: (value?: string) => void
platform: Platform
className?: string
}
const IconPicker = ({ selectedValue, onIconChange, platform, className }: Props) => {
const iconOptions = useMemo(
() =>
[...Object.keys(IconNameToSvgMapping)].map(
(value) =>
({
label: value,
value: value,
icon: value,
} as DropdownItem),
),
[],
)
const isSelectedEmoji = isIconEmoji(selectedValue)
const isMacOS = platform === Platform.MacWeb || platform === Platform.MacDesktop
const isWindows = platform === Platform.WindowsWeb || platform === Platform.WindowsDesktop
const emojiInputRef = useRef<HTMLInputElement>(null)
const [emojiInputFocused, setEmojiInputFocused] = useState(true)
const [currentType, setCurrentType] = useState<IconPickerType>(isSelectedEmoji ? 'emoji' : 'icon')
const [emojiInputValue, setEmojiInputValue] = useState(isSelectedEmoji ? selectedValue : '')
const selectTab = (type: IconPickerType | 'reset') => {
if (type === 'reset') {
onIconChange(undefined)
setEmojiInputValue('')
} else {
setCurrentType(type)
}
}
const TabButton: FunctionComponent<{
label: string
type: IconPickerType | 'reset'
}> = ({ type, label }) => {
const isSelected = currentType === type
return (
<button
className={`relative mr-2 cursor-pointer border-0 bg-default pb-1.5 text-sm focus:shadow-none ${
isSelected ? 'font-medium text-info' : 'text-text'
}`}
onClick={() => {
selectTab(type)
}}
>
{label}
</button>
)
}
const handleIconChange = (value: string) => {
onIconChange(value)
}
const handleEmojiChange = (value: EmojiString) => {
setEmojiInputValue(value)
const emojiLength = [...value].length
if (emojiLength === 1) {
onIconChange(value)
emojiInputRef.current?.blur()
setEmojiInputFocused(false)
} else {
setEmojiInputFocused(true)
}
}
return (
<div className={`flex h-full flex-grow flex-col overflow-auto ${className}`}>
<div className="flex">
<TabButton label="Icon" type={'icon'} />
<TabButton label="Emoji" type={'emoji'} />
<TabButton label="Reset" type={'reset'} />
</div>
<div className={'mt-2 h-full min-h-0 overflow-auto'}>
{currentType === 'icon' && (
<Dropdown
id="change-tag-icon-dropdown"
label="Change the icon for a tag"
items={iconOptions}
value={selectedValue}
onChange={handleIconChange}
/>
)}
{currentType === 'emoji' && (
<>
<div>
<input
ref={emojiInputRef}
autoComplete="off"
autoFocus={emojiInputFocused}
className="w-full flex-grow rounded border border-solid border-passive-3 bg-default px-2 py-1 text-base font-bold text-text focus:shadow-none focus:outline-none"
type="text"
value={emojiInputValue}
onChange={({ target: input }) => handleEmojiChange((input as HTMLInputElement)?.value)}
/>
</div>
<div className="mt-2 text-xs text-passive-0">
Use your keyboard to enter or paste in an emoji character.
</div>
{isMacOS && (
<div className="mt-2 text-xs text-passive-0">On macOS: + + Space bar to bring up emoji picker.</div>
)}
{isWindows && (
<div className="mt-2 text-xs text-passive-0">On Windows: Windows key + . to bring up emoji picker.</div>
)}
</>
)}
</div>
</div>
)
}
export default IconPicker
@@ -0,0 +1 @@
export type IconPickerType = 'icon' | 'emoji'
@@ -2,7 +2,7 @@ import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { observer } from 'mobx-react-lite'
import { useRef, useCallback } from 'react'
import Icon from '../Icon/Icon'
import RoundIconButton from '../Button/RoundIconButton'
import Popover from '../Popover/Popover'
import StyledTooltip from '../StyledTooltip/StyledTooltip'
import LinkedItemsPanel from './LinkedItemsPanel'
@@ -28,14 +28,7 @@ const LinkedItemsButton = ({ linkingController, filesController, onClickPreproce
return (
<>
<StyledTooltip label="Linked items panel">
<button
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
aria-label="Linked items panel"
onClick={toggleMenu}
ref={buttonRef}
>
<Icon type="link" />
</button>
<RoundIconButton label="Linked items panel" onClick={toggleMenu} ref={buttonRef} icon="link" />
</StyledTooltip>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isLinkingPanelOpen} className="pb-2">
<LinkedItemsPanel
@@ -5,7 +5,8 @@ import { SwitchProps } from '@/Components/Switch/SwitchProps'
import { IconType } from '@standardnotes/snjs'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { MenuItemType } from './MenuItemType'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
import { classNames } from '@/Utils/ConcatenateClassNames'
type MenuItemProps = {
children: ReactNode
@@ -40,7 +41,10 @@ const MenuItem = forwardRef(
<li className="list-none" role="none">
<button
ref={ref}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
className={classNames(
'flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5',
'text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none',
)}
onClick={() => {
onChange(!checked)
}}
@@ -59,7 +63,12 @@ const MenuItem = forwardRef(
ref={ref}
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
className={`flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item ${className}`}
className={classNames(
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left',
'text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground',
'focus:bg-info-backdrop focus:shadow-none md:text-menu-item',
className,
)}
onClick={onClick}
onBlur={onBlur}
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
@@ -5,10 +5,6 @@ import { WebApplication } from '@/Application/Application'
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
import Button from '../Button/Button'
import { useCallback } from 'react'
import AttachedFilesButton from '../AttachedFilesPopover/AttachedFilesButton'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController'
import { FilesController } from '@/Controllers/FilesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NotesController } from '@/Controllers/NotesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
@@ -17,9 +13,6 @@ import { LinkingController } from '@/Controllers/LinkingController'
type Props = {
application: WebApplication
featuresController: FeaturesController
filePreviewModalController: FilePreviewModalController
filesController: FilesController
navigationController: NavigationController
notesController: NotesController
selectionController: SelectedItemsController
@@ -29,9 +22,6 @@ type Props = {
const MultipleSelectedNotes = ({
application,
featuresController,
filePreviewModalController,
filesController,
navigationController,
notesController,
linkingController,
@@ -49,17 +39,6 @@ const MultipleSelectedNotes = ({
<div className="flex w-full items-center justify-between p-4">
<h1 className="m-0 text-lg font-bold">{count} selected notes</h1>
<div className="flex">
<div className="mr-3">
<AttachedFilesButton
application={application}
featuresController={featuresController}
filePreviewModalController={filePreviewModalController}
filesController={filesController}
navigationController={navigationController}
notesController={notesController}
selectionController={selectionController}
/>
</div>
<div className="mr-3">
<PinNoteButton notesController={notesController} />
</div>
@@ -9,10 +9,10 @@ import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
import { classNames } from '@/Utils/ConcatenateClassNames'
import Icon from '../Icon/Icon'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { isIOS } from '@/Utils'
import UpgradeNow from '../Footer/UpgradeNow'
import RoundIconButton from '../Button/RoundIconButton'
type Props = {
application: WebApplication
@@ -89,58 +89,48 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
<TagsSection viewControllerManager={viewControllerManager} />
</div>
<div className="flex items-center border-t border-border px-3.5 pt-2.5 md:hidden">
<button
className="mr-auto flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
<RoundIconButton
className="mr-auto bg-default"
onClick={() => {
toggleAppPane(AppPaneId.Items)
}}
title="Go to items list"
aria-label="Go to items list"
>
<Icon type="chevron-left" />
</button>
label="Go to items list"
icon="chevron-left"
/>
<UpgradeNow application={application} featuresController={viewControllerManager.featuresController} />
<button
className="ml-2.5 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
<RoundIconButton
className="ml-2.5 bg-default"
onClick={() => {
viewControllerManager.accountMenuController.toggleShow()
}}
title="Go to account menu"
aria-label="Go to account menu"
>
<Icon type="account-circle" />
</button>
label="Go to account menu"
icon="account-circle"
/>
{hasPasscode && (
<button
<RoundIconButton
id="lock-item"
onClick={() => application.lock()}
title="Locks application and wipes unencrypted data from memory."
aria-label="Locks application and wipes unencrypted data from memory."
className="ml-2.5 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
>
<Icon type="lock-filled" size="custom" className="h-4.5 w-4.5" />
</button>
label="Locks application and wipes unencrypted data from memory."
className="ml-2.5 bg-default"
icon="lock-filled"
/>
)}
<button
className="ml-2.5 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
<RoundIconButton
className="ml-2.5 bg-default"
onClick={() => {
viewControllerManager.preferencesController.openPreferences()
}}
title="Go to preferences"
aria-label="Go to preferences"
>
<Icon type="tune" />
</button>
<button
className="ml-2.5 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
label="Go to preferences"
icon="tune"
/>
<RoundIconButton
className="ml-2.5 bg-default"
onClick={() => {
viewControllerManager.quickSettingsMenuController.toggle()
}}
title="Go to quick settings menu"
aria-label="Go to quick settings menu"
>
<Icon type="themes" />
</button>
label="Go to quick settings menu"
icon="themes"
/>
</div>
</ResponsivePaneContent>
{panelElement && (

Some files were not shown because too many files have changed in this diff Show More