├── .nvmrc
├── .eslintignore
├── .gitignore
├── lib
├── Types
│ ├── ComponentData.ts
│ ├── MessagePayloadApi.ts
│ ├── Environment.ts
│ ├── KeyboardModifier.ts
│ ├── Component.ts
│ ├── ComponentRelayOptions.ts
│ ├── MessagePayload.ts
│ ├── ComponentAction.ts
│ └── ComponentRelayParams.ts
├── Logger.ts
├── Utils.ts
└── ComponentRelay.ts
├── dist
├── @types
│ ├── Types
│ │ ├── ComponentData.d.ts
│ │ ├── MessagePayloadApi.d.ts
│ │ ├── Environment.d.ts
│ │ ├── KeyboardModifier.d.ts
│ │ ├── Component.d.ts
│ │ ├── ComponentRelayOptions.d.ts
│ │ ├── MessagePayload.d.ts
│ │ ├── ComponentRelayParams.d.ts
│ │ └── ComponentAction.d.ts
│ ├── Logger.d.ts
│ ├── Utils.d.ts
│ └── ComponentRelay.d.ts
├── dist.js.LICENSE.txt
├── dist.js
└── dist.js.map
├── .browserslistrc
├── .prettierrc
├── .github
├── codeql
│ ├── custom-queries
│ │ └── javascript
│ │ │ └── qlpack.yml
│ └── codeql-config.yml
└── workflows
│ ├── test_pr.yml
│ ├── publish_docs.yml
│ └── codeql-analysis.yml
├── webpack.prod.js
├── webpack.dev.js
├── test
├── setup
│ ├── fakeHttpServer.ts
│ └── jsdom.ts
├── lib
│ ├── localStorage.ts
│ ├── componentManager.ts
│ ├── appFactory.ts
│ ├── snCrypto.ts
│ └── deviceInterface.ts
├── logger.test.ts
├── utils.test.ts
└── helpers.ts
├── .eslintrc.json
├── tsconfig.json
├── jest.config.json
├── webpack.config.js
├── README.md
├── eslintrc.json
├── package.json
└── LICENSE
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.15.1
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 | docs
5 | test
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .sass-cache
3 | .DS_Store
4 | yarn-error.log
5 | coverage
6 | docs
--------------------------------------------------------------------------------
/lib/Types/ComponentData.ts:
--------------------------------------------------------------------------------
1 | export type ComponentData = {
2 | [key: string]: any
3 | }
4 |
--------------------------------------------------------------------------------
/lib/Types/MessagePayloadApi.ts:
--------------------------------------------------------------------------------
1 | export enum MessagePayloadApi {
2 | Component = 'component',
3 | }
4 |
--------------------------------------------------------------------------------
/dist/@types/Types/ComponentData.d.ts:
--------------------------------------------------------------------------------
1 | export declare type ComponentData = {
2 | [key: string]: any;
3 | };
4 |
--------------------------------------------------------------------------------
/lib/Types/Environment.ts:
--------------------------------------------------------------------------------
1 | export enum Environment {
2 | Web = 1,
3 | Desktop = 2,
4 | Mobile = 3,
5 | }
6 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | Edge 16
2 | Firefox 53
3 | Chrome 57
4 | Safari 11
5 | Opera 44
6 | ios 11
7 | ChromeAndroid 84
8 |
--------------------------------------------------------------------------------
/dist/@types/Types/MessagePayloadApi.d.ts:
--------------------------------------------------------------------------------
1 | export declare enum MessagePayloadApi {
2 | Component = "component"
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 120,
5 | "semi": false
6 | }
7 |
--------------------------------------------------------------------------------
/dist/@types/Types/Environment.d.ts:
--------------------------------------------------------------------------------
1 | export declare enum Environment {
2 | Web = 1,
3 | Desktop = 2,
4 | Mobile = 3
5 | }
6 |
--------------------------------------------------------------------------------
/lib/Types/KeyboardModifier.ts:
--------------------------------------------------------------------------------
1 | export enum KeyboardModifier {
2 | Shift = 'Shift',
3 | Ctrl = 'Control',
4 | Meta = 'Meta',
5 | }
6 |
--------------------------------------------------------------------------------
/.github/codeql/custom-queries/javascript/qlpack.yml:
--------------------------------------------------------------------------------
1 | name: custom-javascript-queries
2 | version: 0.0.0
3 | libraryPathDependencies:
4 | - codeql-javascript
5 |
--------------------------------------------------------------------------------
/dist/@types/Types/KeyboardModifier.d.ts:
--------------------------------------------------------------------------------
1 | export declare enum KeyboardModifier {
2 | Shift = "Shift",
3 | Ctrl = "Control",
4 | Meta = "Meta"
5 | }
6 |
--------------------------------------------------------------------------------
/dist/@types/Logger.d.ts:
--------------------------------------------------------------------------------
1 | export default class Logger {
2 | static enabled: boolean;
3 | private static get isSupported();
4 | static get info(): any;
5 | static get error(): any;
6 | }
7 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge')
2 | const config = require('./webpack.config.js')
3 |
4 | module.exports = merge(config, {
5 | mode: 'production',
6 | devtool: 'source-map',
7 | })
8 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const config = require('./webpack.config.js');
3 |
4 | module.exports = merge(config, {
5 | mode: 'development',
6 | devtool: 'eval-cheap-module-source-map',
7 | stats: {
8 | colors: true
9 | }
10 | });
11 |
--------------------------------------------------------------------------------
/.github/codeql/codeql-config.yml:
--------------------------------------------------------------------------------
1 | name: "Custom CodeQL Config"
2 |
3 | queries:
4 | - uses: security-and-quality
5 | - uses: ./.github/codeql/custom-queries/javascript
6 |
7 | paths:
8 | - lib
9 |
10 | paths-ignore:
11 | - dist
12 | - docs
13 | - coverage
14 | - test
15 | - node_modules
16 |
--------------------------------------------------------------------------------
/test/setup/fakeHttpServer.ts:
--------------------------------------------------------------------------------
1 | import nock from 'nock';
2 | import { htmlTemplate } from '../helpers';
3 |
4 | nock('http://localhost')
5 | .persist()
6 | .get(/(^\/|extensions)(.*)/)
7 | .reply(200, htmlTemplate, { 'Content-Type': 'text/html; charset=UTF-8', })
8 | .get(/themes(.*)/)
9 | .reply(200, "");
10 |
--------------------------------------------------------------------------------
/lib/Types/Component.ts:
--------------------------------------------------------------------------------
1 | import { ComponentData } from './ComponentData'
2 |
3 | export type Component = {
4 | uuid?: string
5 | origin?: string
6 | data?: ComponentData
7 | sessionKey?: string
8 | environment?: string
9 | platform?: string
10 | isMobile?: boolean
11 | acceptsThemes: boolean
12 | activeThemes: string[]
13 | }
14 |
--------------------------------------------------------------------------------
/lib/Types/ComponentRelayOptions.ts:
--------------------------------------------------------------------------------
1 | export type ComponentRelayOptions = {
2 | coallesedSaving?: boolean
3 | coallesedSavingDelay?: number
4 | /**
5 | * Outputs debugging information to console.
6 | */
7 | debug?: boolean
8 | /**
9 | * Indicates whether or not the component accepts themes.
10 | */
11 | acceptsThemes?: boolean
12 | }
13 |
--------------------------------------------------------------------------------
/test/setup/jsdom.ts:
--------------------------------------------------------------------------------
1 | import { JSDOM } from 'jsdom';
2 | import { htmlTemplate } from './../helpers';
3 |
4 | const { window } = new JSDOM(htmlTemplate, {
5 | resources: "usable",
6 | url: 'http://localhost',
7 | });
8 |
9 | global.window.alert = jest.fn();
10 | global.window.confirm = jest.fn();
11 | global.window.open = jest.fn();
12 | global.document = window.document;
13 |
--------------------------------------------------------------------------------
/dist/@types/Types/Component.d.ts:
--------------------------------------------------------------------------------
1 | import { ComponentData } from './ComponentData';
2 | export declare type Component = {
3 | uuid?: string;
4 | origin?: string;
5 | data?: ComponentData;
6 | sessionKey?: string;
7 | environment?: string;
8 | platform?: string;
9 | isMobile?: boolean;
10 | acceptsThemes: boolean;
11 | activeThemes: string[];
12 | };
13 |
--------------------------------------------------------------------------------
/dist/@types/Types/ComponentRelayOptions.d.ts:
--------------------------------------------------------------------------------
1 | export declare type ComponentRelayOptions = {
2 | coallesedSaving?: boolean;
3 | coallesedSavingDelay?: number;
4 | /**
5 | * Outputs debugging information to console.
6 | */
7 | debug?: boolean;
8 | /**
9 | * Indicates whether or not the component accepts themes.
10 | */
11 | acceptsThemes?: boolean;
12 | };
13 |
--------------------------------------------------------------------------------
/dist/@types/Utils.d.ts:
--------------------------------------------------------------------------------
1 | import { Environment } from './Types/Environment';
2 | declare global {
3 | interface Window {
4 | msCrypto: unknown;
5 | }
6 | }
7 | export declare const generateUuid: () => string;
8 | export declare const isValidJsonString: (str: unknown) => boolean;
9 | export declare const environmentToString: (environment: Environment) => string;
10 | export declare const isNotUndefinedOrNull: (value: any) => boolean;
11 |
--------------------------------------------------------------------------------
/lib/Types/MessagePayload.ts:
--------------------------------------------------------------------------------
1 | import type { MessageData, UuidString, ComponentAction } from '@standardnotes/snjs'
2 | import { MessagePayloadApi } from './MessagePayloadApi'
3 | import { ComponentData } from './ComponentData'
4 |
5 | export type MessagePayload = {
6 | action: ComponentAction
7 | data: MessageData
8 | componentData?: ComponentData
9 | messageId?: UuidString
10 | sessionKey?: UuidString
11 | api: MessagePayloadApi
12 | original?: MessagePayload
13 | callback?: (...params: any) => void
14 | }
15 |
--------------------------------------------------------------------------------
/test/lib/localStorage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A simple localStorage implementation using in-memory storage.
3 | */
4 | export class LocalStorage {
5 | constructor(private storageObject) { }
6 |
7 | getItem (key: string) {
8 | return this.storageObject[key];
9 | }
10 |
11 | setItem (key: string, value: any) {
12 | this.storageObject[key] = value;
13 | }
14 |
15 | removeItem (key: string) {
16 | delete this.storageObject[key];
17 | }
18 |
19 | clear () {
20 | this.storageObject = {}
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/lib/Logger.ts:
--------------------------------------------------------------------------------
1 | const noop = () => undefined
2 |
3 | export default class Logger {
4 | static enabled = false
5 |
6 | private static get isSupported() {
7 | return window.console || console ? true : false
8 | }
9 |
10 | static get info(): any {
11 | if (!Logger.isSupported || !this.enabled) {
12 | return noop
13 | }
14 | return console.log.bind(console)
15 | }
16 |
17 | static get error(): any {
18 | if (!Logger.isSupported) {
19 | return noop
20 | }
21 | return console.error.bind(console)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/dist/@types/Types/MessagePayload.d.ts:
--------------------------------------------------------------------------------
1 | import type { MessageData, UuidString, ComponentAction } from '@standardnotes/snjs';
2 | import { MessagePayloadApi } from './MessagePayloadApi';
3 | import { ComponentData } from './ComponentData';
4 | export declare type MessagePayload = {
5 | action: ComponentAction;
6 | data: MessageData;
7 | componentData?: ComponentData;
8 | messageId?: UuidString;
9 | sessionKey?: UuidString;
10 | api: MessagePayloadApi;
11 | original?: MessagePayload;
12 | callback?: (...params: any) => void;
13 | };
14 |
--------------------------------------------------------------------------------
/dist/dist.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*! @license DOMPurify 2.4.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.4.0/LICENSE */
2 |
3 | /**
4 | * @license
5 | * Lodash
6 | * Copyright OpenJS Foundation and other contributors
7 | * Released under MIT license
8 | * Based on Underscore.js 1.8.3
9 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
10 | */
11 |
--------------------------------------------------------------------------------
/lib/Types/ComponentAction.ts:
--------------------------------------------------------------------------------
1 | export enum ComponentAction {
2 | SetSize = 'set-size',
3 | StreamItems = 'stream-items',
4 | StreamContextItem = 'stream-context-item',
5 | SaveItems = 'save-items',
6 | CreateItem = 'create-item',
7 | CreateItems = 'create-items',
8 | DeleteItems = 'delete-items',
9 | SetComponentData = 'set-component-data',
10 | RequestPermissions = 'request-permissions',
11 | DuplicateItem = 'duplicate-item',
12 | ComponentRegistered = 'component-registered',
13 | ActivateThemes = 'themes',
14 | Reply = 'reply',
15 | ThemesActivated = 'themes-activated',
16 | KeyDown = 'key-down',
17 | KeyUp = 'key-up',
18 | Click = 'click',
19 | }
20 |
--------------------------------------------------------------------------------
/lib/Types/ComponentRelayParams.ts:
--------------------------------------------------------------------------------
1 | import { ComponentRelayOptions } from './ComponentRelayOptions'
2 |
3 | export type ComponentRelayParams = {
4 | /**
5 | * Represents the window object that the component is running in.
6 | */
7 | targetWindow: Window
8 | /**
9 | * The options to initialize
10 | */
11 | options?: ComponentRelayOptions
12 | /**
13 | * A callback that is executed after the component has been registered.
14 | */
15 | onReady?: () => void
16 | /**
17 | * A callback that is executed after themes have been changed.
18 | */
19 | onThemesChange?: () => void
20 |
21 | handleRequestForContentHeight: () => number | undefined
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/test_pr.yml:
--------------------------------------------------------------------------------
1 | name: Test pull requests
2 | on:
3 | pull_request:
4 | branches: [ main, develop ]
5 | workflow_dispatch:
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Setup NodeJS Environment
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version-file: '.nvmrc'
16 | - name: Install project dependencies
17 | run: yarn install --pure-lockfile
18 | - name: Transpile files
19 | run: yarn build:types
20 | - name: Check code for linting error/warnings
21 | run: yarn lint
22 | - name: Run code coverage report
23 | run: yarn coverage
24 |
--------------------------------------------------------------------------------
/dist/@types/Types/ComponentRelayParams.d.ts:
--------------------------------------------------------------------------------
1 | import { ComponentRelayOptions } from './ComponentRelayOptions';
2 | export declare type ComponentRelayParams = {
3 | /**
4 | * Represents the window object that the component is running in.
5 | */
6 | targetWindow: Window;
7 | /**
8 | * The options to initialize
9 | */
10 | options?: ComponentRelayOptions;
11 | /**
12 | * A callback that is executed after the component has been registered.
13 | */
14 | onReady?: () => void;
15 | /**
16 | * A callback that is executed after themes have been changed.
17 | */
18 | onThemesChange?: () => void;
19 | handleRequestForContentHeight: () => number | undefined;
20 | };
21 |
--------------------------------------------------------------------------------
/dist/@types/Types/ComponentAction.d.ts:
--------------------------------------------------------------------------------
1 | export declare enum ComponentAction {
2 | SetSize = "set-size",
3 | StreamItems = "stream-items",
4 | StreamContextItem = "stream-context-item",
5 | SaveItems = "save-items",
6 | CreateItem = "create-item",
7 | CreateItems = "create-items",
8 | DeleteItems = "delete-items",
9 | SetComponentData = "set-component-data",
10 | RequestPermissions = "request-permissions",
11 | DuplicateItem = "duplicate-item",
12 | ComponentRegistered = "component-registered",
13 | ActivateThemes = "themes",
14 | Reply = "reply",
15 | ThemesActivated = "themes-activated",
16 | KeyDown = "key-down",
17 | KeyUp = "key-up",
18 | Click = "click"
19 | }
20 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": [
5 | "@typescript-eslint"
6 | ],
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended"
11 | ],
12 | "rules": {
13 | "@typescript-eslint/no-unused-vars" : ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false, "argsIgnorePattern": "^_" }],
14 | "@typescript-eslint/no-explicit-any": ["warn", { "ignoreRestArgs": true }],
15 | "@typescript-eslint/no-non-null-assertion": ["off"],
16 | "semi": ["error", "never"],
17 | "quotes": ["error", "single"],
18 | "object-curly-spacing": ["error", "always"]
19 | },
20 | "ignorePatterns": ["webpack.*"]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["lib/ComponentRelay.ts"],
3 | "exclude": ["node_modules", "test/**/*", "webpack.*.js"],
4 | "compilerOptions": {
5 | "target": "esnext",
6 | "module": "esnext",
7 | "allowJs": true,
8 | "declaration": true,
9 | "declarationDir": "dist/@types",
10 | "emitDeclarationOnly": true,
11 | "newLine": "lf",
12 | "outDir": "./dist",
13 | "isolatedModules": true,
14 | "strict": true,
15 | "noImplicitAny": true,
16 | "moduleResolution": "node",
17 | "esModuleInterop": true,
18 | "skipLibCheck": true,
19 | "forceConsistentCasingInFileNames": true
20 | },
21 | "typedocOptions": {
22 | "entryPoints": ["./lib/componentRelay.ts"],
23 | "name": "Component Relay API Reference",
24 | "includeVersion": true,
25 | "out": "docs"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "clearMocks": true,
3 | "collectCoverageFrom": [
4 | "lib/**/{!(index),}.ts"
5 | ],
6 | "coverageDirectory": "coverage",
7 | "coveragePathIgnorePatterns": [
8 | "/node_modules"
9 | ],
10 | "coverageReporters": [
11 | "text",
12 | "html"
13 | ],
14 | "globals": {
15 | "ts-jest": {
16 | "diagnostics": false,
17 | "isolatedModules": true
18 | }
19 | },
20 | "preset": "ts-jest",
21 | "resetMocks": true,
22 | "restoreMocks": true,
23 | "roots": [
24 | "/lib",
25 | "/test"
26 | ],
27 | "setupFiles": [
28 | "/test/setup/jsdom.ts",
29 | "/test/setup/fakeHttpServer.ts"
30 | ],
31 | "testEnvironment": "jsdom",
32 | "testMatch": [
33 | "/test/?(*.)+(test).+(ts)"
34 | ],
35 | "verbose": true
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/publish_docs.yml:
--------------------------------------------------------------------------------
1 | name: Publish docs
2 | on:
3 | push:
4 | branches: [ main ]
5 | workflow_dispatch:
6 |
7 | jobs:
8 | publish-docs:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Setup NodeJS Environment
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version-file: '.nvmrc'
16 | - name: Install project dependencies
17 | run: yarn install --pure-lockfile
18 | - name: Update gh-pages
19 | run: |
20 | git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git
21 | git config user.name github-actions
22 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com
23 | rm -f docs/.nojekyll
24 | yarn build:docs
25 | touch docs/.nojekyll
26 | yarn publish:docs -m 'chore: build docs' -t
27 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './lib/ComponentRelay.ts',
5 | resolve: {
6 | extensions: ['.ts']
7 | },
8 | output: {
9 | path: path.resolve(__dirname, 'dist'),
10 | filename: 'dist.js',
11 | sourceMapFilename: 'dist.js.map',
12 | library: 'ComponentRelay',
13 | libraryTarget: 'umd',
14 | libraryExport: 'default',
15 | umdNamedDefine: true
16 | },
17 | module: {
18 | rules: [
19 | {
20 | exclude: /node_modules/,
21 | test: /\.(ts)?$/,
22 | use: [
23 | {
24 | loader: "babel-loader",
25 | options: {
26 | cacheDirectory: true,
27 | presets: [
28 | ["@babel/preset-env"],
29 | ["@babel/preset-typescript"]
30 | ],
31 | plugins: [
32 | "@babel/plugin-proposal-class-properties"
33 | ]
34 | }
35 | }
36 | ]
37 | }
38 | ]
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/lib/Utils.ts:
--------------------------------------------------------------------------------
1 | import { Environment } from './Types/Environment'
2 | import { v4 as uuidv4 } from 'uuid'
3 |
4 | declare global {
5 | interface Window {
6 | msCrypto: unknown
7 | }
8 | }
9 |
10 | export const generateUuid = (): string => {
11 | return uuidv4()
12 | }
13 |
14 | export const isValidJsonString = (str: unknown): boolean => {
15 | if (typeof str !== 'string') {
16 | return false
17 | }
18 | try {
19 | const result = JSON.parse(str)
20 | const type = Object.prototype.toString.call(result)
21 | return type === '[object Object]' || type === '[object Array]'
22 | } catch (e) {
23 | return false
24 | }
25 | }
26 |
27 | export const environmentToString = (environment: Environment): string => {
28 | const map = {
29 | [Environment.Web]: 'web',
30 | [Environment.Desktop]: 'desktop',
31 | [Environment.Mobile]: 'mobile',
32 | }
33 | return map[environment] ?? map[Environment.Web]
34 | }
35 |
36 | export const isNotUndefinedOrNull = (value: any): boolean => {
37 | return value !== null && value !== undefined
38 | }
39 |
--------------------------------------------------------------------------------
/test/lib/componentManager.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PermissionDialog,
3 | SNComponent,
4 | SNComponentManager,
5 | SNTheme,
6 | } from '@standardnotes/snjs';
7 |
8 | export class WebComponentManager extends SNComponentManager {
9 | presentPermissionsDialog(dialog: PermissionDialog) {
10 | const permissions = JSON.stringify(dialog.permissions);
11 | const approved = window.confirm(permissions);
12 | dialog.callback(approved);
13 | }
14 | }
15 |
16 | export class MobileComponentManager extends SNComponentManager {
17 | private mobileActiveTheme?: SNTheme;
18 |
19 | presentPermissionsDialog(dialog: PermissionDialog) {
20 | const permissions = JSON.stringify(dialog.permissions);
21 | const approved = window.confirm(permissions);
22 | dialog.callback(approved);
23 | }
24 |
25 | /** @override */
26 | urlForComponent(component: SNComponent) {
27 | if (component.isTheme()) {
28 | const encoded = encodeURI(component.hosted_url);
29 | return `data:text/css;base64,${encoded}`;
30 | } else {
31 | return super.urlForComponent(component);
32 | }
33 | }
34 |
35 | public setMobileActiveTheme(theme: SNTheme) {
36 | this.mobileActiveTheme = theme;
37 | this.postActiveThemesToAllComponents();
38 | }
39 |
40 | /** @override */
41 | getActiveThemes() {
42 | if (this.mobileActiveTheme) {
43 | return [this.mobileActiveTheme];
44 | } else {
45 | return [];
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/logger.test.ts:
--------------------------------------------------------------------------------
1 | import Logger from './../lib/logger';
2 |
3 | describe("Logger", () => {
4 | let consoleLog;
5 | let consoleError;
6 |
7 | beforeEach(() => {
8 | consoleLog = jest.spyOn(console, 'log');
9 | consoleError = jest.spyOn(console, 'error');
10 | });
11 |
12 | it('should not output messages to console if not enabled', () => {
13 | Logger.enabled = false;
14 | Logger.info('A simple message.');
15 | expect(Logger.enabled).toBe(false);
16 | expect(consoleLog).not.toBeCalled();
17 | });
18 |
19 | it('should output messages to console if "enabled" is true', () => {
20 | Logger.enabled = true;
21 | Logger.info('A simple message.');
22 | expect(Logger.enabled).toBe(true);
23 | expect(consoleLog).toBeCalledTimes(1);
24 | expect(consoleLog).toBeCalledWith('A simple message.');
25 | });
26 |
27 | it('should output errors to console if "enabled" is false', () => {
28 | Logger.enabled = false;
29 | Logger.error('An error occured.');
30 | expect(Logger.enabled).toBe(false);
31 | expect(consoleError).toBeCalledTimes(1);
32 | expect(consoleError).toBeCalledWith('An error occured.');
33 | });
34 |
35 | it('should output errors to console if "enabled" is true', () => {
36 | Logger.enabled = true;
37 | Logger.error('An error occured.');
38 | expect(Logger.enabled).toBe(true);
39 | expect(consoleError).toBeCalledTimes(1);
40 | expect(consoleError).toBeCalledWith('An error occured.');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/lib/appFactory.ts:
--------------------------------------------------------------------------------
1 | import { SNApplication, Environment, Platform, SNLog, SNComponentManager } from '@standardnotes/snjs';
2 | import DeviceInterface from './deviceInterface';
3 | import SNCrypto from './snCrypto';
4 | import { WebComponentManager, MobileComponentManager } from './componentManager';
5 |
6 | const getSwappedClasses = (environment: Environment) => {
7 | const classMap = {
8 | swap: SNComponentManager,
9 | with: WebComponentManager
10 | };
11 | switch (environment) {
12 | case Environment.Mobile:
13 | classMap.with = MobileComponentManager;
14 | break;
15 | }
16 | return [classMap];
17 | };
18 |
19 | export const createApplication = async (identifier: string, environment: Environment, platform: Platform) => {
20 | const deviceInterface = new DeviceInterface(
21 | setTimeout.bind(window),
22 | setInterval.bind(window)
23 | );
24 | SNLog.onLog = (message) => {
25 | console.log(message);
26 | };
27 | SNLog.onError = (error) => {
28 | console.error(error);
29 | };
30 | const application = new SNApplication(
31 | environment,
32 | platform,
33 | deviceInterface,
34 | new SNCrypto(),
35 | {
36 | confirm: async () => true,
37 | alert: async () => {},
38 | blockingDialog: () => () => {},
39 | },
40 | identifier,
41 | getSwappedClasses(environment),
42 | 'http://syncing.localhost'
43 | );
44 | await application.prepareForLaunch({
45 | receiveChallenge: (_challenge) => {
46 | throw Error('Factory application shouldn\'t have challenges');
47 | }
48 | });
49 | await application.launch(true);
50 | return application;
51 | };
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Component Relay
2 |
3 | To get started using the Component Relay, read the [Intro to Extensions guide](https://docs.standardnotes.org/extensions/intro).
4 |
5 | ## Installation
6 |
7 | ```
8 | yarn add @standardnotes/component-relay
9 | ```
10 |
11 | ‐ _or_ ‐
12 |
13 | ```
14 | npm install --save @standardnotes/component-relay
15 | ```
16 |
17 | ### Manual
18 |
19 | Import the file `dist/dist.js` in your HTML file. For example:
20 |
21 | ```html
22 | ...
23 |
24 | ...
25 | ```
26 |
27 | ## Tests
28 |
29 | The `component-relay` uses [Jest](https://jestjs.io/) for tests. Tests are located in the [test](test) directory. Test files have the following name: `.test.js`.
30 |
31 | ### Run all tests
32 |
33 | Run `yarn test` or `npm test` to run all test suites.
34 |
35 | ### Coverage
36 |
37 | Coverage can be generated by running the `coverage` script.
38 |
39 | Then open the [coverage/index.html](coverage/index.html) file in a browser to view it.
40 |
41 | ## Documentation
42 |
43 | Documentation is generated using [Typedoc](https://typedoc.org/). The documentation is available at https://standardnotes.github.io/component-relay/. To update the docs or to view the latest version, run:
44 |
45 | ```
46 | yarn build:docs
47 | ```
48 |
49 | This will generate documentation and place it in the `docs/` directory. Create a localhost server using `http-server` and open the [docs/index.html](docs/index.html) file in a browser to view the docs.
50 |
51 | To deploy the latest version of the docs to the `gh-pages` branch to publish using [GitHub Pages](https://pages.github.com/), replace `2.0.1` with the latest version and run:
52 |
53 | ```
54 | yarn deploy-docs -m "build: v2.0.1"
55 | ```
56 |
--------------------------------------------------------------------------------
/eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "project": "./tsconfig.json"
6 | },
7 | "plugins": ["@typescript-eslint", "prettier"],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "prettier"
13 | ],
14 | "rules": {
15 | "@typescript-eslint/no-unused-vars": [
16 | "error",
17 | {
18 | "vars": "all",
19 | "args": "after-used",
20 | "ignoreRestSiblings": false,
21 | "argsIgnorePattern": "^_",
22 | "varsIgnorePattern": "^_"
23 | }
24 | ],
25 | "@typescript-eslint/no-explicit-any": ["error", { "ignoreRestArgs": true }],
26 | "@typescript-eslint/no-floating-promises": ["error"],
27 | "block-scoped-var": "error",
28 | "comma-dangle": ["error", "always-multiline"],
29 | "curly": ["error", "all"],
30 | "no-confusing-arrow": "error",
31 | "no-inline-comments": "warn",
32 | "no-invalid-this": "error",
33 | "no-return-assign": "warn",
34 | "no-constructor-return": "error",
35 | "no-duplicate-imports": "error",
36 | "no-self-compare": "error",
37 | "no-console": ["error", { "allow": ["warn", "error"] }],
38 | "no-unmodified-loop-condition": "error",
39 | "no-unused-private-class-members": "error",
40 | "object-curly-spacing": ["error", "always"],
41 | "quotes": ["error", "single", { "avoidEscape": true }],
42 | "semi": ["error", "never"],
43 | "prettier/prettier": [
44 | "error",
45 | {
46 | "singleQuote": true,
47 | "trailingComma": "all",
48 | "printWidth": 120,
49 | "semi": false
50 | },
51 | {
52 | "usePrettierrc": false
53 | }
54 | ]
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@standardnotes/component-relay",
3 | "version": "2.3.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "git://github.com/standardnotes/component-relay.git"
7 | },
8 | "main": "dist/dist.js",
9 | "types": "dist/@types/ComponentRelay.d.ts",
10 | "scripts": {
11 | "build:docs": "typedoc",
12 | "build:types": "tsc",
13 | "build": "yarn run build:types && webpack --config webpack.prod.js",
14 | "clean-cache:docs": "rm -rf node_modules/.cache/gh-pages",
15 | "publish:docs": "gh-pages -b gh-pages -d docs",
16 | "coverage": "yarn test --coverage --silent",
17 | "lint": "eslint lib --ext .ts",
18 | "lint:fix": "yarn run lint --fix",
19 | "start": "webpack -w --config webpack.dev.js",
20 | "noci:test": "jest"
21 | },
22 | "dependencies": {
23 | "@standardnotes/common": "^1.43.0",
24 | "@standardnotes/features": "1.54.0",
25 | "@standardnotes/models": "1.34.3",
26 | "@standardnotes/sncrypto-common": "1.13.0",
27 | "@standardnotes/snjs": "2.147.2"
28 | },
29 | "devDependencies": {
30 | "@babel/core": "^7.12.3",
31 | "@babel/plugin-proposal-class-properties": "^7.12.1",
32 | "@babel/preset-env": "^7.12.1",
33 | "@babel/preset-typescript": "^7.12.7",
34 | "@types/jest": "^26.0.19",
35 | "@types/jsdom": "^16.2.5",
36 | "@types/node": "^14.14.6",
37 | "@types/uuid": "^8.3.0",
38 | "@typescript-eslint/eslint-plugin": "^4.6.1",
39 | "@typescript-eslint/parser": "^4.6.1",
40 | "babel-loader": "^8.2.2",
41 | "eslint": "^7.13.0",
42 | "gh-pages": "^3.1.0",
43 | "jest": "^26.6.3",
44 | "jsdom": "^16.4.0",
45 | "nock": "^13.0.5",
46 | "ts-jest": "^26.4.4",
47 | "typedoc": "^0.20.30",
48 | "typescript": "^4.0.5",
49 | "uuid": "^8.3.2",
50 | "webpack": "^5.4.0",
51 | "webpack-bundle-analyzer": "^4.3.0",
52 | "webpack-cli": "^4.2.0",
53 | "webpack-merge": "^5.3.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/test/lib/snCrypto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Base64String,
3 | HexString,
4 | SNPureCrypto,
5 | timingSafeEqual,
6 | Utf8String,
7 | } from '@standardnotes/sncrypto-common';
8 | import { generateUuid } from '../../lib/utils';
9 |
10 | /**
11 | * An SNPureCrypto implementation. Required to create a new SNApplication instance.
12 | */
13 | export default class SNCrypto implements SNPureCrypto {
14 | constructor() {}
15 |
16 | public deinit() {}
17 |
18 | public timingSafeEqual(a: string, b: string) {
19 | return timingSafeEqual(a, b);
20 | }
21 |
22 | public async pbkdf2(
23 | password: Utf8String,
24 | salt: Utf8String,
25 | iterations: number,
26 | length: number
27 | ): Promise {
28 | return password;
29 | }
30 |
31 | public async generateRandomKey(bits: number): Promise {
32 | return "arandomkey";
33 | }
34 |
35 | public async aes256CbcEncrypt(
36 | plaintext: Utf8String,
37 | iv: HexString,
38 | key: HexString
39 | ): Promise {
40 | return plaintext;
41 | }
42 |
43 | public async aes256CbcDecrypt(
44 | ciphertext: Base64String,
45 | iv: HexString,
46 | key: HexString
47 | ): Promise {
48 | return ciphertext;
49 | }
50 |
51 | public async hmac256(
52 | message: Utf8String,
53 | key: HexString
54 | ): Promise {
55 | return message;
56 | }
57 |
58 | public async sha256(text: string): Promise {
59 | return text;
60 | }
61 |
62 | public async unsafeSha1(text: string): Promise {
63 | return text;
64 | }
65 |
66 | public async argon2(
67 | password: Utf8String,
68 | salt: string,
69 | iterations: number,
70 | bytes: number,
71 | length: number
72 | ): Promise {
73 | return password;
74 | }
75 |
76 | public async xchacha20Encrypt(
77 | plaintext: Utf8String,
78 | nonce: HexString,
79 | key: HexString,
80 | assocData: Utf8String
81 | ): Promise {
82 | return plaintext;
83 | }
84 |
85 | public async xchacha20Decrypt(
86 | ciphertext: Base64String,
87 | nonce: HexString,
88 | key: HexString,
89 | assocData: Utf8String
90 | ): Promise {
91 | return ciphertext;
92 | }
93 |
94 | public generateUUIDSync() {
95 | return generateUuid();
96 | }
97 |
98 | public async generateUUID() {
99 | return generateUuid();
100 | }
101 |
102 | public async base64Encode(text: Utf8String): Promise {
103 | return btoa(text);
104 | }
105 |
106 | public async base64Decode(base64String: Base64String): Promise {
107 | return atob(base64String);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main, develop ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '24 20 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | config-file: ./.github/codeql/codeql-config.yml
50 | # If you wish to specify custom queries, you can do so here or in a config file.
51 | # By default, queries listed here will override any specified in a config file.
52 | # Prefix the list here with "+" to use these queries and those in the config file.
53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
54 |
55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
56 | # If this step fails, then you should remove it and run the build manually (see below)
57 | - name: Autobuild
58 | uses: github/codeql-action/autobuild@v1
59 |
60 | # ℹ️ Command-line programs to run using the OS shell.
61 | # 📚 https://git.io/JvXDl
62 |
63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
64 | # and modify them (or add more) to build your code if your project
65 | # uses a compiled language
66 |
67 | #- run: |
68 | # make bootstrap
69 | # make release
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v1
73 |
--------------------------------------------------------------------------------
/test/lib/deviceInterface.ts:
--------------------------------------------------------------------------------
1 | import { DeviceInterface as SNDeviceInterface } from '@standardnotes/snjs';
2 | import { LocalStorage } from './localStorage';
3 |
4 | const KEYCHAIN_STORAGE_KEY = 'keychain';
5 |
6 | /**
7 | * The DeviceInterface implementation to handle storage and keychain operations.
8 | */
9 | export default class DeviceInterface extends SNDeviceInterface {
10 | private storage = {};
11 | private localStorage: LocalStorage;
12 |
13 | constructor(timeout: any, interval: any) {
14 | super(timeout, interval);
15 | this.localStorage = new LocalStorage(this.storage);
16 | }
17 |
18 | async getRawStorageValue(key) {
19 | return this.localStorage.getItem(key);
20 | }
21 |
22 | async getAllRawStorageKeyValues() {
23 | const results = [];
24 | for (const key of Object.keys(this.storage)) {
25 | results.push({
26 | key: key,
27 | value: this.storage[key]
28 | });
29 | }
30 | return results;
31 | }
32 |
33 | async setRawStorageValue(key, value) {
34 | this.localStorage.setItem(key, value);
35 | }
36 |
37 | async removeRawStorageValue(key) {
38 | this.localStorage.removeItem(key);
39 | }
40 |
41 | async removeAllRawStorageValues() {
42 | this.localStorage.clear();
43 | }
44 |
45 | async openDatabase(_identifier) {
46 | return {};
47 | }
48 |
49 | getDatabaseKeyPrefix(identifier) {
50 | if (identifier) {
51 | return `${identifier}-item-`;
52 | } else {
53 | return 'item-';
54 | }
55 | }
56 |
57 | keyForPayloadId(id, identifier) {
58 | return `${this.getDatabaseKeyPrefix(identifier)}${id}`;
59 | }
60 |
61 | async getAllRawDatabasePayloads(identifier) {
62 | const models = [];
63 | for (const key in this.storage) {
64 | if (key.startsWith(this.getDatabaseKeyPrefix(identifier))) {
65 | models.push(JSON.parse(this.storage[key]));
66 | }
67 | }
68 | return models;
69 | }
70 |
71 | async saveRawDatabasePayload(payload, identifier) {
72 | this.localStorage.setItem(
73 | this.keyForPayloadId(payload.uuid, identifier),
74 | JSON.stringify(payload)
75 | );
76 | }
77 |
78 | async saveRawDatabasePayloads(payloads, identifier) {
79 | for (const payload of payloads) {
80 | await this.saveRawDatabasePayload(payload, identifier);
81 | }
82 | }
83 |
84 | async removeRawDatabasePayloadWithId(id, identifier) {
85 | this.localStorage.removeItem(this.keyForPayloadId(id, identifier));
86 | }
87 |
88 | async removeAllRawDatabasePayloads(identifier) {
89 | for (const key in this.storage) {
90 | if (key.startsWith(this.getDatabaseKeyPrefix(identifier))) {
91 | delete this.storage[key];
92 | }
93 | }
94 | }
95 |
96 | async getNamespacedKeychainValue(identifier) {
97 | const keychain = await this.getRawKeychainValue();
98 | if (!keychain) {
99 | return;
100 | }
101 | return keychain[identifier];
102 | }
103 |
104 | async setNamespacedKeychainValue(value, identifier) {
105 | let keychain = await this.getRawKeychainValue();
106 | if (!keychain) {
107 | keychain = {};
108 | }
109 | this.localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify({
110 | ...keychain,
111 | [identifier]: value,
112 | }));
113 | }
114 |
115 | async clearNamespacedKeychainValue(identifier) {
116 | const keychain = await this.getRawKeychainValue();
117 | if (!keychain) {
118 | return;
119 | }
120 | delete keychain[identifier];
121 | this.localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(keychain));
122 | }
123 |
124 | /** Allows unit tests to set legacy keychain structure as it was <= 003 */
125 | async legacy_setRawKeychainValue(value) {
126 | this.localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value));
127 | }
128 |
129 | async getRawKeychainValue() {
130 | const keychain = this.localStorage.getItem(KEYCHAIN_STORAGE_KEY);
131 | return JSON.parse(keychain);
132 | }
133 |
134 | async clearRawKeychainValue() {
135 | this.localStorage.removeItem(KEYCHAIN_STORAGE_KEY);
136 | }
137 |
138 | async openUrl(url) {
139 | window.open(url);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/test/utils.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { Environment } from '@standardnotes/snjs';
3 | import {
4 | isValidJsonString,
5 | generateUuid,
6 | environmentToString
7 | } from './../lib/utils';
8 |
9 | const uuidFormat = /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
10 |
11 | describe("Utils", () => {
12 | describe('generateUuid', () => {
13 | test("length should be 36 characters", () => {
14 | const uuid = generateUuid();
15 | expect(uuid.length).toEqual(36);
16 | });
17 |
18 | it("should have a valid format", () => {
19 | const uuid = generateUuid();
20 | expect(uuid).toEqual(expect.stringMatching(uuidFormat));
21 | });
22 | });
23 |
24 | describe('isValidJsonString', () => {
25 | test("anything other than string should return false", () => {
26 | let result = isValidJsonString(1);
27 | expect(result).toBe(false);
28 |
29 | result = isValidJsonString(false);
30 | expect(result).toBe(false);
31 |
32 | result = isValidJsonString(1000000000000);
33 | expect(result).toBe(false);
34 |
35 | result = isValidJsonString({});
36 | expect(result).toBe(false);
37 |
38 | result = isValidJsonString([]);
39 | expect(result).toBe(false);
40 |
41 | result = isValidJsonString(() => true);
42 | expect(result).toBe(false);
43 |
44 | result = isValidJsonString(undefined);
45 | expect(result).toBe(false);
46 |
47 | result = isValidJsonString(null);
48 | expect(result).toBe(false);
49 | });
50 |
51 | test("an invalid JSON string should return false", () => {
52 | let result = isValidJsonString("{???}");
53 | expect(result).toBe(false);
54 |
55 | result = isValidJsonString("");
56 | expect(result).toBe(false);
57 |
58 | result = isValidJsonString("{");
59 | expect(result).toBe(false);
60 | });
61 |
62 | test("stringified objects should return true", () => {
63 | let objToStr = JSON.stringify({})
64 | let result = isValidJsonString(objToStr);
65 | expect(result).toBe(true);
66 |
67 | objToStr = JSON.stringify({ test: 1234, foo: "bar", testing: true })
68 | result = isValidJsonString(objToStr);
69 | expect(result).toBe(true);
70 | });
71 |
72 | test("stringified arrays should return true", () => {
73 | let arrToStr = JSON.stringify([])
74 | let result = isValidJsonString(arrToStr);
75 | expect(result).toBe(true);
76 |
77 | arrToStr = JSON.stringify([{ test: 1234, foo: "bar", testing: true }])
78 | result = isValidJsonString(arrToStr);
79 | expect(result).toBe(true);
80 | });
81 | });
82 |
83 | describe('environmentToString', () => {
84 | test('an invalid value should fallback to "web"', () => {
85 | let result = environmentToString(10000000);
86 | expect(result).toBe("web");
87 |
88 | result = environmentToString(-1);
89 | expect(result).toBe("web");
90 |
91 | result = environmentToString(null);
92 | expect(result).toBe("web");
93 |
94 | result = environmentToString(undefined);
95 | expect(result).toBe("web");
96 |
97 | result = environmentToString('');
98 | expect(result).toBe("web");
99 |
100 | result = environmentToString(0.01);
101 | expect(result).toBe("web");
102 |
103 | result = environmentToString({});
104 | expect(result).toBe("web");
105 |
106 | result = environmentToString([]);
107 | expect(result).toBe("web");
108 |
109 | result = environmentToString(true);
110 | expect(result).toBe("web");
111 |
112 | result = environmentToString(false);
113 | expect(result).toBe("web");
114 |
115 | result = environmentToString(() => true);
116 | expect(result).toBe("web");
117 | });
118 |
119 | test('Environment.Web should return "web"', () => {
120 | const result = environmentToString(Environment.Web);
121 | expect(result).toBe("web");
122 | });
123 |
124 | test('Environment.Desktop should return "desktop"', () => {
125 | const result = environmentToString(Environment.Desktop);
126 | expect(result).toBe("desktop");
127 | });
128 |
129 | test('Environment.Mobile should return "mobile"', () => {
130 | const result = environmentToString(Environment.Mobile);
131 | expect(result).toBe("mobile");
132 | });
133 | });
134 | });
135 |
--------------------------------------------------------------------------------
/test/helpers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ComponentAction,
3 | ComponentArea,
4 | ContentType,
5 | PayloadSource,
6 | SNApplication,
7 | SNComponent,
8 | DecryptedItem,
9 | SNNote,
10 | SNTag
11 | } from '@standardnotes/snjs';
12 | import { generateUuid } from './../lib/utils';
13 |
14 | export const htmlTemplate = `
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | `;
23 |
24 | export const testExtensionEditorPackage = {
25 | identifier: "test.standardnotes.my-test-extension",
26 | name: "My Test Extension",
27 | content_type: "SN|Component",
28 | area: "editor-editor",
29 | version: "1.0.0",
30 | url: "http://localhost"
31 | };
32 |
33 | export const testExtensionForTagsPackage = {
34 | identifier: "test.standardnotes.my-test-tags-extension",
35 | name: "My Test Tags Extension",
36 | content_type: "SN|Component",
37 | area: "note-tags",
38 | version: "1.0.0",
39 | url: "http://localhost"
40 | };
41 |
42 | export const testThemeDefaultPackage = {
43 | identifier: "test.standardnotes.default-theme",
44 | name: "Default Theme",
45 | content_type: "SN|Theme",
46 | area: "themes",
47 | version: "1.0.0",
48 | url: "http://localhost/themes/default-theme"
49 | };
50 |
51 | export const testThemeDarkPackage = {
52 | identifier: "test.standardnotes.dark-theme",
53 | name: "Dark Theme",
54 | content_type: "SN|Theme",
55 | area: "themes",
56 | version: "1.0.0",
57 | url: "http://localhost/themes/dark-theme"
58 | };
59 |
60 | export const getTestNoteItem = ({ title = 'Hello', text = 'World', dirty = true } = {}) => {
61 | return {
62 | uuid: generateUuid(),
63 | content_type: ContentType.Note,
64 | dirty,
65 | content: {
66 | title,
67 | text,
68 | references: []
69 | },
70 | references: []
71 | };
72 | };
73 |
74 | const copyObject = (object: any) => {
75 | const objectStr = JSON.stringify(object);
76 | return JSON.parse(objectStr);
77 | };
78 |
79 | export const getRawTestComponentItem = (componentPackage: any) => {
80 | const today = new Date();
81 | componentPackage = copyObject(componentPackage);
82 | return {
83 | content_type: componentPackage.content_type,
84 | content: {
85 | uuid: generateUuid(),
86 | identifier: componentPackage.identifier,
87 | componentData: {
88 | foo: "bar"
89 | },
90 | name: componentPackage.name,
91 | hosted_url: componentPackage.url,
92 | url: componentPackage.url,
93 | local_url: null,
94 | area: componentPackage.area,
95 | package_info: componentPackage,
96 | valid_until: new Date(today.setFullYear(today.getFullYear() + 5)),
97 | references: []
98 | }
99 | };
100 | };
101 |
102 | export const sleep = async (seconds: number) => {
103 | await new Promise(resolve => setTimeout(resolve, seconds * 1000));
104 | };
105 |
106 | /**
107 | * A short amount of time to wait for messages to propagate via the postMessage API.
108 | */
109 | export const SHORT_DELAY_TIME = 0.05;
110 |
111 | export const registerComponent = async (
112 | application: SNApplication,
113 | targetWindow: Window,
114 | component: SNComponent
115 | ) => {
116 | application.componentManager.registerComponentWindow(
117 | component,
118 | targetWindow
119 | );
120 |
121 | /**
122 | * componentManager.registerComponentWindow() calls targetWindow.parent.postMesasge()
123 | * We need to make sure that the event is dispatched properly by waiting a few ms.
124 | * See https://github.com/jsdom/jsdom/issues/2245#issuecomment-392556153
125 | */
126 | await sleep(SHORT_DELAY_TIME);
127 | };
128 |
129 | export const createNoteItem = async (
130 | application: SNApplication,
131 | overrides = {},
132 | needsSync = false
133 | ) => {
134 | const testNoteItem = getTestNoteItem(overrides);
135 | return await application.createManagedItem(
136 | testNoteItem.content_type as ContentType,
137 | testNoteItem,
138 | needsSync
139 | ) as SNNote;
140 | };
141 |
142 | export const createTagItem = async (
143 | application: SNApplication,
144 | title: string
145 | ) => {
146 | return await application.createManagedItem(
147 | ContentType.Tag,
148 | {
149 | title,
150 | references: []
151 | },
152 | false
153 | ) as SNTag;
154 | };
155 |
156 | export const createComponentItem = async (
157 | application: SNApplication,
158 | componentPackage: any,
159 | overrides = {}
160 | ) => {
161 | const rawTestComponentItem = getRawTestComponentItem(componentPackage);
162 | return await application.createManagedItem(
163 | rawTestComponentItem.content_type as ContentType,
164 | {
165 | ...overrides,
166 | ...rawTestComponentItem.content
167 | },
168 | false
169 | ) as SNComponent;
170 | };
171 |
172 | export const registerComponentHandler = (
173 | application: SNApplication,
174 | areas: ComponentArea[],
175 | itemInContext?: DecryptedItem,
176 | customActionHandler?: (currentComponent: SNComponent, action: ComponentAction, data: any) => void,
177 | ) => {
178 | application.componentManager.registerHandler({
179 | identifier: 'generic-view-' + Math.random(),
180 | areas,
181 | actionHandler: (currentComponent, action, data) => {
182 | customActionHandler && customActionHandler(currentComponent, action, data);
183 | },
184 | contextRequestHandler: () => itemInContext
185 | });
186 | };
187 |
188 | export const jsonForItem = (item: DecryptedItem, component: SNComponent) => {
189 | const isMetadatUpdate =
190 | item.payload.source === PayloadSource.RemoteSaved ||
191 | item.payload.source === PayloadSource.LocalSaved ||
192 | item.payload.source === PayloadSource.PreSyncSave;
193 |
194 | const componentData = item.getDomainData('org.standardnotes.sn') || {};
195 | const clientData = componentData[component.getClientDataKey()!] || {};
196 |
197 | return {
198 | uuid: item.uuid,
199 | content_type: item.content_type!,
200 | created_at: item.created_at!,
201 | updated_at: item.serverUpdatedAt!,
202 | deleted: item.deleted!,
203 | isMetadataUpdate: isMetadatUpdate,
204 | content: item.content,
205 | clientData: clientData,
206 | };
207 | }
208 |
--------------------------------------------------------------------------------
/dist/@types/ComponentRelay.d.ts:
--------------------------------------------------------------------------------
1 | import type { OutgoingItemMessagePayload, AppDataField, DecryptedTransferPayload, ItemContent, ContentType } from '@standardnotes/snjs';
2 | import { ComponentRelayParams } from './Types/ComponentRelayParams';
3 | import { ComponentAction } from './Types/ComponentAction';
4 | export default class ComponentRelay {
5 | private contentWindow;
6 | private component;
7 | private sentMessages;
8 | private messageQueue;
9 | private lastStreamedItem?;
10 | private pendingSaveItems?;
11 | private pendingSaveTimeout?;
12 | private pendingSaveParams?;
13 | private messageHandler?;
14 | private keyDownEventListener?;
15 | private keyUpEventListener?;
16 | private clickEventListener?;
17 | private concernTimeouts;
18 | private options;
19 | private params;
20 | constructor(params: ComponentRelayParams);
21 | deinit(): void;
22 | private registerMessageHandler;
23 | private registerKeyboardEventListeners;
24 | private registerMouseEventListeners;
25 | private handleMessage;
26 | private onReady;
27 | /**
28 | * Gets the component UUID.
29 | */
30 | getSelfComponentUUID(): string | undefined;
31 | /**
32 | * Checks if the component is running in a Desktop application.
33 | */
34 | isRunningInDesktopApplication(): boolean;
35 | /**
36 | * Checks if the component is running in a Mobile application.
37 | */
38 | isRunningInMobileApplication(): boolean;
39 | /**
40 | * Gets the component's data value for the specified key.
41 | * @param key The key for the data object.
42 | * @returns `undefined` if the value for the key does not exist. Returns the stored value otherwise.
43 | */
44 | getComponentDataValueForKey(key: string): any;
45 | /**
46 | * Sets the component's data value for the specified key.
47 | * @param key The key for the data object.
48 | * @param value The value to store under the specified key.
49 | */
50 | setComponentDataValueForKey(key: string, value: any): void;
51 | /**
52 | * Clears the component's data object.
53 | */
54 | clearComponentData(): void;
55 | private postMessage;
56 | private activateThemes;
57 | private themeElementForUrl;
58 | private deactivateTheme;
59 | private generateUUID;
60 | /**
61 | * Gets the current platform where the component is running.
62 | */
63 | get platform(): string | undefined;
64 | /**
65 | * Gets the current environment where the component is running.
66 | */
67 | get environment(): string | undefined;
68 | /**
69 | * Streams a collection of Items, filtered by content type.
70 | * New items are passed to the callback as they come.
71 | * @param contentTypes A collection of Content Types.
72 | * @param callback A callback to process the streamed items.
73 | */
74 | streamItems(contentTypes: ContentType[], callback: (data: any) => void): void;
75 | /**
76 | * Streams the current Item in context.
77 | * @param callback A callback to process the streamed item.
78 | */
79 | streamContextItem(callback: (data: any) => void): void;
80 | /**
81 | * Creates and stores an Item in the item store.
82 | * @param item The Item's payload content.
83 | * @param callback The callback to process the created Item.
84 | */
85 | createItem(item: DecryptedTransferPayload, callback: (data: any) => void): void;
86 | /**
87 | * Creates and stores a collection of Items in the item store.
88 | * @param items The Item(s) payload collection.
89 | * @param callback The callback to process the created Item(s).
90 | */
91 | createItems(items: DecryptedTransferPayload[], callback: (data: any) => void): void;
92 | /**
93 | * Deletes an Item from the item store.
94 | * @param item The Item to delete.
95 | * @param callback The callback with the result of the operation.
96 | */
97 | deleteItem(item: DecryptedTransferPayload, callback: (data: OutgoingItemMessagePayload) => void): void;
98 | /**
99 | * Deletes a collection of Items from the item store.
100 | * @param items The Item(s) to delete.
101 | * @param callback The callback with the result of the operation.
102 | */
103 | deleteItems(items: DecryptedTransferPayload[], callback: (data: OutgoingItemMessagePayload) => void): void;
104 | /**
105 | * Performs a custom action to the component manager.
106 | * @param action
107 | * @param data
108 | * @param callback The callback with the result of the operation.
109 | */
110 | sendCustomEvent(action: ComponentAction, data: any, callback?: (data: any) => void): void;
111 | /**
112 | * Saves an existing Item in the item store.
113 | * @param item An existing Item to be saved.
114 | * @param callback
115 | * @param skipDebouncer
116 | */
117 | saveItem(item: DecryptedTransferPayload, callback?: () => void, skipDebouncer?: boolean): void;
118 | /**
119 | * Runs a callback before saving an Item.
120 | * @param item An existing Item to be saved.
121 | * @param presave Allows clients to perform any actions last second before the save actually occurs (like setting previews).
122 | * Saves debounce by default, so if a client needs to compute a property on an item before saving, it's best to
123 | * hook into the debounce cycle so that clients don't have to implement their own debouncing.
124 | * @param callback
125 | */
126 | saveItemWithPresave(item: DecryptedTransferPayload, presave: any, callback?: () => void): void;
127 | /**
128 | * Runs a callback before saving a collection of Items.
129 | * @param items A collection of existing Items to be saved.
130 | * @param presave Allows clients to perform any actions last second before the save actually occurs (like setting previews).
131 | * Saves debounce by default, so if a client needs to compute a property on an item before saving, it's best to
132 | * hook into the debounce cycle so that clients don't have to implement their own debouncing.
133 | * @param callback
134 | */
135 | saveItemsWithPresave(items: DecryptedTransferPayload[], presave: any, callback?: () => void): void;
136 | private performSavingOfItems;
137 | /**
138 | * Saves a collection of existing Items.
139 | * @param items The items to be saved.
140 | * @param callback
141 | * @param skipDebouncer Allows saves to go through right away rather than waiting for timeout.
142 | * This should be used when saving items via other means besides keystrokes.
143 | * @param presave
144 | */
145 | saveItems(items: DecryptedTransferPayload[], callback?: () => void, skipDebouncer?: boolean, presave?: any): void;
146 | /**
147 | * Sets a new container size for the current component.
148 | * @param width The new width.
149 | * @param height The new height.
150 | */
151 | setSize(width: string | number, height: string | number): void;
152 | /**
153 | * Sends the KeyDown keyboard event to the Standard Notes parent application.
154 | * @param keyboardModifier The keyboard modifier that was pressed.
155 | */
156 | private keyDownEvent;
157 | /**
158 | * Sends the KeyUp keyboard event to the Standard Notes parent application.
159 | * @param keyboardModifier The keyboard modifier that was released.
160 | */
161 | private keyUpEvent;
162 | /**
163 | * Sends the Click mouse event to the Standard Notes parent application.
164 | */
165 | private mouseClickEvent;
166 | private jsonObjectForItem;
167 | /**
168 | * Gets the Item's appData value for the specified key.
169 | * Uses the default domain (org.standardnotes.sn).
170 | * This function is used with Items returned from streamContextItem() and streamItems()
171 | * @param item The Item to get the appData value from.
172 | * @param key The key to get the value from.
173 | */
174 | getItemAppDataValue(item: OutgoingItemMessagePayload | undefined, key: AppDataField | string): any;
175 | }
176 |
--------------------------------------------------------------------------------
/dist/dist.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("ComponentRelay",[],t):"object"==typeof exports?exports.ComponentRelay=t():e.ComponentRelay=t()}(self,(()=>(()=>{"use strict";var e={d:(t,n)=>{for(var s in n)e.o(n,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:n[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};let n;var s;e.d(t,{default:()=>w}),function(e){e[e.Web=1]="Web",e[e.Desktop=2]="Desktop",e[e.Mobile=3]="Mobile"}(n||(n={}));var i=new Uint8Array(16);function o(){if(!s&&!(s="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return s(i)}const a=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i,r=function(e){return"string"==typeof e&&a.test(e)};for(var m=[],c=0;c<256;++c)m.push((c+256).toString(16).substr(1));const h=function(e,t,n){var s=(e=e||{}).random||(e.rng||o)();if(s[6]=15&s[6]|64,s[8]=63&s[8]|128,t){n=n||0;for(var i=0;i<16;++i)t[n+i]=s[i];return t}return function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=(m[e[t+0]]+m[e[t+1]]+m[e[t+2]]+m[e[t+3]]+"-"+m[e[t+4]]+m[e[t+5]]+"-"+m[e[t+6]]+m[e[t+7]]+"-"+m[e[t+8]]+m[e[t+9]]+"-"+m[e[t+10]]+m[e[t+11]]+m[e[t+12]]+m[e[t+13]]+m[e[t+14]]+m[e[t+15]]).toLowerCase();if(!r(n))throw TypeError("Stringified UUID is invalid");return n}(s)},d=e=>{var t;const s={[n.Web]:"web",[n.Desktop]:"desktop",[n.Mobile]:"mobile"};return null!==(t=s[e])&&void 0!==t?t:s[n.Web]},p=()=>{};class l{static get isSupported(){return!(!window.console&&!console)}static get info(){return l.isSupported&&this.enabled?console.log.bind(console):p}static get error(){return l.isSupported?console.error.bind(console):p}}var u,v;let g,f,y;function b(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,s)}return n}function S(e){for(var t=1;t{if(l.info("Components API Message received:",e.data),document.referrer&&new URL(document.referrer).origin!==new URL(e.origin).origin)return;const{data:t}=e,n=(e=>{if("string"!=typeof e)return!1;try{const t=JSON.parse(e),n=Object.prototype.toString.call(t);return"[object Object]"===n||"[object Array]"===n}catch(e){return!1}})(t)?JSON.parse(t):t;if(n){if(void 0===this.component.origin&&n.action===y.ComponentRegistered)this.component.origin=e.origin;else if(e.origin!==this.component.origin)return;this.handleMessage(n)}else l.error("Invalid data received. Skipping...")},this.contentWindow.document.addEventListener("message",this.messageHandler,!1),this.contentWindow.addEventListener("message",this.messageHandler,!1),l.info("Waiting for messages...")}registerKeyboardEventListeners(){this.keyDownEventListener=e=>{l.info("A key has been pressed: ".concat(e.key)),e.ctrlKey?this.keyDownEvent(g.Ctrl):e.shiftKey?this.keyDownEvent(g.Shift):(e.metaKey||"Meta"===e.key)&&this.keyDownEvent(g.Meta)},this.keyUpEventListener=e=>{l.info("A key has been released: ".concat(e.key)),"Control"===e.key?this.keyUpEvent(g.Ctrl):"Shift"===e.key?this.keyUpEvent(g.Shift):"Meta"===e.key&&this.keyUpEvent(g.Meta)},this.contentWindow.addEventListener("keydown",this.keyDownEventListener,!1),this.contentWindow.addEventListener("keyup",this.keyUpEventListener,!1)}registerMouseEventListeners(){this.clickEventListener=e=>{l.info("A click has been performed."),this.mouseClickEvent()},this.contentWindow.addEventListener("click",this.clickEventListener,!1)}handleMessage(e){switch(e.action){case y.ComponentRegistered:this.component.sessionKey=e.sessionKey,e.componentData&&(this.component.data=e.componentData),this.onReady(e.data),l.info("Component successfully registered with payload:",e);break;case y.ActivateThemes:this.activateThemes(e.data.themes);break;default:{var t,n;if(!e.original)return;const s=null===(t=this.sentMessages)||void 0===t?void 0:t.filter((t=>{var n;return t.messageId===(null===(n=e.original)||void 0===n?void 0:n.messageId)}))[0];if(!s){const e=this.contentWindow.document.title,t=("The extension '".concat(e,"' is attempting to communicate with Standard Notes, ")+"but an error is preventing it from doing so. Please restart this extension and try again.").replace(" "," ");return void l.info(t)}null==s||null===(n=s.callback)||void 0===n||n.call(s,e.data);break}}}onReady(e){this.component.environment=e.environment,this.component.platform=e.platform,this.component.uuid=e.uuid;for(const e of this.messageQueue)this.postMessage(e.action,e.data,e.callback);this.messageQueue=[],l.info("Data passed to onReady:",e),this.activateThemes(e.activeThemeUrls||[]),this.postMessage(y.ThemesActivated,{}),this.params.onReady&&this.params.onReady()}getSelfComponentUUID(){return this.component.uuid}isRunningInDesktopApplication(){return this.component.environment===d(n.Desktop)}isRunningInMobileApplication(){return this.component.environment===d(n.Mobile)}getComponentDataValueForKey(e){if(this.component.data)return this.component.data[e]}setComponentDataValueForKey(e,t){if(!this.component.data)throw new Error("The component has not been initialized.");if(!e||e&&0===e.length)throw new Error("The key for the data value should be a valid string.");this.component.data=S(S({},this.component.data),{},{[e]:t}),this.postMessage(y.SetComponentData,{componentData:this.component.data})}clearComponentData(){this.component.data={},this.postMessage(y.SetComponentData,{componentData:this.component.data})}postMessage(e,t,n){if(!this.component.sessionKey)return void this.messageQueue.push({action:e,data:t,api:f.Component,callback:n});e===y.SaveItems&&(t.height=this.params.handleRequestForContentHeight());const s={action:e,data:t,messageId:this.generateUUID(),sessionKey:this.component.sessionKey,api:f.Component},i=JSON.parse(JSON.stringify(s));let o;i.callback=n,this.sentMessages.push(i),o=this.isRunningInMobileApplication()?JSON.stringify(s):s,l.info("Posting message:",o),this.contentWindow.parent.postMessage(o,this.component.origin)}activateThemes(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];if(!this.component.acceptsThemes)return;l.info("Incoming themes:",e);const{activeThemes:t}=this.component;if(t&&t.sort().toString()==e.sort().toString())return;let n=e;const s=[];for(const i of t)e.includes(i)?n=n.filter((e=>e!==i)):s.push(i);l.info("Deactivating themes:",s),l.info("Activating themes:",n);for(const e of s)this.deactivateTheme(e);this.component.activeThemes=e;for(const e of n){if(!e)continue;const t=this.contentWindow.document.createElement("link");t.id=btoa(e),t.href=e,t.type="text/css",t.rel="stylesheet",t.media="screen,print",t.className="custom-theme",this.contentWindow.document.getElementsByTagName("head")[0].appendChild(t)}this.params.onThemesChange&&this.params.onThemesChange()}themeElementForUrl(e){return Array.from(this.contentWindow.document.getElementsByClassName("custom-theme")).slice().find((t=>t.id==btoa(e)))}deactivateTheme(e){const t=this.themeElementForUrl(e);t&&t.parentNode&&(t.setAttribute("disabled","true"),t.parentNode.removeChild(t))}generateUUID(){return h()}get platform(){return this.component.platform}get environment(){return this.component.environment}streamItems(e,t){this.postMessage(y.StreamItems,{content_types:e},(e=>{t(e.items)}))}streamContextItem(e){this.postMessage(y.StreamContextItem,{},(t=>{const{item:n}=t;(!this.lastStreamedItem||this.lastStreamedItem.uuid!==n.uuid)&&this.pendingSaveTimeout&&(clearTimeout(this.pendingSaveTimeout),this.performSavingOfItems(this.pendingSaveParams),this.pendingSaveTimeout=void 0,this.pendingSaveParams=void 0),this.lastStreamedItem=n,e(this.lastStreamedItem)}))}createItem(e,t){this.postMessage(y.CreateItem,{item:this.jsonObjectForItem(e)},(e=>{let{item:n}=e;!n&&e.items&&e.items.length>0&&(n=e.items[0]),t&&t(n)}))}createItems(e,t){const n=e.map((e=>this.jsonObjectForItem(e)));this.postMessage(y.CreateItems,{items:n},(e=>{t&&t(e.items)}))}deleteItem(e,t){this.deleteItems([e],t)}deleteItems(e,t){const n={items:e.map((e=>this.jsonObjectForItem(e)))};this.postMessage(y.DeleteItems,n,(e=>{t&&t(e)}))}sendCustomEvent(e,t,n){this.postMessage(e,t,(e=>{n&&n(e)}))}saveItem(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this.saveItems([e],t,n)}saveItemWithPresave(e,t,n){this.saveItemsWithPresave([e],t,n)}saveItemsWithPresave(e,t,n){this.saveItems(e,n,!1,t)}performSavingOfItems(e){let{items:t,presave:n,callback:s}=e;const i=setTimeout((()=>{this.concernTimeouts.forEach((e=>clearTimeout(e))),alert("This editor is unable to communicate with Standard Notes. Your changes may not be saved. Please backup your changes, then restart the application and try again.")}),5e3);this.concernTimeouts.push(i),n&&n();const o=[];for(const e of t)o.push(this.jsonObjectForItem(e));this.postMessage(y.SaveItems,{items:o},(()=>{this.concernTimeouts.forEach((e=>clearTimeout(e))),null==s||s()}))}saveItems(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],s=arguments.length>3?arguments[3]:void 0;if(this.pendingSaveItems||(this.pendingSaveItems=[]),this.options.coallesedSaving&&!n){this.pendingSaveTimeout&&clearTimeout(this.pendingSaveTimeout);const n=e.map((e=>e.uuid)),i=this.pendingSaveItems.filter((e=>!n.includes(e.uuid)));this.pendingSaveItems=i.concat(e),this.pendingSaveParams={items:this.pendingSaveItems,presave:s,callback:t},this.pendingSaveTimeout=setTimeout((()=>{this.performSavingOfItems(this.pendingSaveParams),this.pendingSaveItems=[],this.pendingSaveTimeout=void 0,this.pendingSaveParams=null}),this.options.coallesedSavingDelay)}else this.performSavingOfItems({items:e,presave:s,callback:t})}setSize(e,t){this.postMessage(y.SetSize,{type:"container",width:e,height:t})}keyDownEvent(e){this.postMessage(y.KeyDown,{keyboardModifier:e})}keyUpEvent(e){this.postMessage(y.KeyUp,{keyboardModifier:e})}mouseClickEvent(){this.postMessage(y.Click,{})}jsonObjectForItem(e){const t=Object.assign({},e);return t.children=null,t.parent=null,t}getItemAppDataValue(e,t){var n,s;const i=null==e||null===(n=e.content)||void 0===n||null===(s=n.appData)||void 0===s?void 0:s["org.standardnotes.sn"];return null==i?void 0:i[t]}}return t.default})()));
2 | //# sourceMappingURL=dist.js.map
--------------------------------------------------------------------------------
/lib/ComponentRelay.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | OutgoingItemMessagePayload,
3 | MessageData,
4 | DecryptedItem,
5 | AppDataField,
6 | DecryptedTransferPayload,
7 | ItemContent,
8 | ContentType,
9 | } from '@standardnotes/snjs'
10 | import { environmentToString, generateUuid, isValidJsonString } from './Utils'
11 | import Logger from './Logger'
12 | import { KeyboardModifier } from './Types/KeyboardModifier'
13 | import { ComponentRelayParams } from './Types/ComponentRelayParams'
14 | import { MessagePayload } from './Types/MessagePayload'
15 | import { Component } from './Types/Component'
16 | import { MessagePayloadApi } from './Types/MessagePayloadApi'
17 | import { ComponentRelayOptions } from './Types/ComponentRelayOptions'
18 | import { ComponentAction } from './Types/ComponentAction'
19 | import { Environment } from './Types/Environment'
20 |
21 | const DEFAULT_COALLESED_SAVING_DELAY = 250
22 |
23 | export default class ComponentRelay {
24 | private contentWindow: Window
25 | private component: Component = { activeThemes: [], acceptsThemes: true }
26 | private sentMessages: MessagePayload[] = []
27 | private messageQueue: MessagePayload[] = []
28 | private lastStreamedItem?: DecryptedTransferPayload
29 | private pendingSaveItems?: DecryptedTransferPayload[]
30 | private pendingSaveTimeout?: NodeJS.Timeout
31 | private pendingSaveParams?: any
32 | private messageHandler?: (event: any) => void
33 | private keyDownEventListener?: (event: KeyboardEvent) => void
34 | private keyUpEventListener?: (event: KeyboardEvent) => void
35 | private clickEventListener?: (event: MouseEvent) => void
36 | private concernTimeouts: NodeJS.Timeout[] = []
37 | private options: ComponentRelayOptions
38 | private params: Omit
39 |
40 | constructor(params: ComponentRelayParams) {
41 | if (!params || !params.targetWindow) {
42 | throw new Error('contentWindow must be a valid Window object.')
43 | }
44 |
45 | this.params = params
46 |
47 | this.options = params.options || {}
48 |
49 | if (this.options.coallesedSaving == undefined) {
50 | this.options.coallesedSaving = true
51 | }
52 | if (this.options.coallesedSavingDelay == undefined) {
53 | this.options.coallesedSavingDelay = DEFAULT_COALLESED_SAVING_DELAY
54 | }
55 | if (this.options.acceptsThemes != undefined) {
56 | this.component.acceptsThemes = this.options.acceptsThemes ?? true
57 | }
58 |
59 | Logger.enabled = this.options.debug ?? false
60 |
61 | this.contentWindow = params.targetWindow
62 | this.registerMessageHandler()
63 | this.registerKeyboardEventListeners()
64 | this.registerMouseEventListeners()
65 | }
66 |
67 | public deinit(): void {
68 | this.params.onReady = undefined
69 | this.component = {
70 | acceptsThemes: true,
71 | activeThemes: [],
72 | }
73 | this.messageQueue = []
74 | this.sentMessages = []
75 | this.lastStreamedItem = undefined
76 | this.pendingSaveItems = undefined
77 | this.pendingSaveTimeout = undefined
78 | this.pendingSaveParams = undefined
79 |
80 | if (this.messageHandler) {
81 | this.contentWindow.document.removeEventListener('message', this.messageHandler)
82 | this.contentWindow.removeEventListener('message', this.messageHandler)
83 | }
84 |
85 | if (this.keyDownEventListener) {
86 | this.contentWindow.removeEventListener('keydown', this.keyDownEventListener)
87 | }
88 |
89 | if (this.keyUpEventListener) {
90 | this.contentWindow.removeEventListener('keyup', this.keyUpEventListener)
91 | }
92 |
93 | if (this.clickEventListener) {
94 | this.contentWindow.removeEventListener('click', this.clickEventListener)
95 | }
96 | }
97 |
98 | private registerMessageHandler() {
99 | this.messageHandler = (event: MessageEvent) => {
100 | Logger.info('Components API Message received:', event.data)
101 |
102 | /**
103 | * We don't have access to window.parent.origin due to cross-domain restrictions.
104 | * Check referrer if available, otherwise defer to checking for first-run value.
105 | * Craft URL objects so that example.com === example.com/
106 | */
107 | if (document.referrer) {
108 | const referrer = new URL(document.referrer).origin
109 | const eventOrigin = new URL(event.origin).origin
110 |
111 | if (referrer !== eventOrigin) {
112 | return
113 | }
114 | }
115 |
116 | // Mobile environment sends data as JSON string.
117 | const { data } = event
118 | const parsedData = isValidJsonString(data) ? JSON.parse(data) : data
119 |
120 | if (!parsedData) {
121 | Logger.error('Invalid data received. Skipping...')
122 | return
123 | }
124 |
125 | /**
126 | * The Component Registered message will be the most reliable one, so we won't change it after any subsequent events,
127 | * in case you receive an event from another window.
128 | */
129 | if (typeof this.component.origin === 'undefined' && parsedData.action === ComponentAction.ComponentRegistered) {
130 | this.component.origin = event.origin
131 | } else if (event.origin !== this.component.origin) {
132 | // If event origin doesn't match first-run value, return.
133 | return
134 | }
135 |
136 | this.handleMessage(parsedData)
137 | }
138 |
139 | /**
140 | * Mobile (React Native) uses `document`, web/desktop uses `window`.addEventListener
141 | * for postMessage API to work properly.
142 | * Update May 2019:
143 | * As part of transitioning React Native webview into the community package,
144 | * we'll now only need to use window.addEventListener.
145 | * However, we want to maintain backward compatibility for Mobile < v3.0.5, so we'll keep document.addEventListener
146 | * Also, even with the new version of react-native-webview, Android may still require document.addEventListener (while iOS still only requires window.addEventListener)
147 | * https://github.com/react-native-community/react-native-webview/issues/323#issuecomment-467767933
148 | */
149 | this.contentWindow.document.addEventListener('message', this.messageHandler, false)
150 | this.contentWindow.addEventListener('message', this.messageHandler, false)
151 |
152 | Logger.info('Waiting for messages...')
153 | }
154 |
155 | private registerKeyboardEventListeners() {
156 | this.keyDownEventListener = (event: KeyboardEvent) => {
157 | Logger.info(`A key has been pressed: ${event.key}`)
158 |
159 | if (event.ctrlKey) {
160 | this.keyDownEvent(KeyboardModifier.Ctrl)
161 | } else if (event.shiftKey) {
162 | this.keyDownEvent(KeyboardModifier.Shift)
163 | } else if (event.metaKey || event.key === 'Meta') {
164 | this.keyDownEvent(KeyboardModifier.Meta)
165 | }
166 | }
167 |
168 | this.keyUpEventListener = (event: KeyboardEvent) => {
169 | Logger.info(`A key has been released: ${event.key}`)
170 |
171 | /**
172 | * Checking using event.key instead of the corresponding boolean properties.
173 | */
174 | if (event.key === 'Control') {
175 | this.keyUpEvent(KeyboardModifier.Ctrl)
176 | } else if (event.key === 'Shift') {
177 | this.keyUpEvent(KeyboardModifier.Shift)
178 | } else if (event.key === 'Meta') {
179 | this.keyUpEvent(KeyboardModifier.Meta)
180 | }
181 | }
182 |
183 | this.contentWindow.addEventListener('keydown', this.keyDownEventListener, false)
184 | this.contentWindow.addEventListener('keyup', this.keyUpEventListener, false)
185 | }
186 |
187 | private registerMouseEventListeners() {
188 | this.clickEventListener = (_event: MouseEvent) => {
189 | Logger.info('A click has been performed.')
190 |
191 | this.mouseClickEvent()
192 | }
193 |
194 | this.contentWindow.addEventListener('click', this.clickEventListener, false)
195 | }
196 |
197 | private handleMessage(payload: MessagePayload) {
198 | switch (payload.action) {
199 | case ComponentAction.ComponentRegistered:
200 | this.component.sessionKey = payload.sessionKey
201 | if (payload.componentData) {
202 | this.component.data = payload.componentData
203 | }
204 | this.onReady(payload.data)
205 | Logger.info('Component successfully registered with payload:', payload)
206 | break
207 |
208 | case ComponentAction.ActivateThemes:
209 | this.activateThemes(payload.data.themes)
210 | break
211 |
212 | default: {
213 | if (!payload.original) {
214 | return
215 | }
216 |
217 | // Get the callback from queue.
218 | const originalMessage = this.sentMessages?.filter((message: MessagePayload) => {
219 | return message.messageId === payload.original?.messageId
220 | })[0]
221 |
222 | if (!originalMessage) {
223 | // Connection must have been reset. We should alert the user unless it's a reply,
224 | // in which case we may have been deallocated and reinitialized and lost the
225 | // original message
226 | const extensionName = this.contentWindow.document.title
227 | const alertMessage = (
228 | `The extension '${extensionName}' is attempting to communicate with Standard Notes, ` +
229 | 'but an error is preventing it from doing so. Please restart this extension and try again.'
230 | ).replace(' ', ' ')
231 |
232 | Logger.info(alertMessage)
233 | return
234 | }
235 |
236 | originalMessage?.callback?.(payload.data)
237 | break
238 | }
239 | }
240 | }
241 |
242 | private onReady(data: MessageData) {
243 | this.component.environment = data.environment
244 | this.component.platform = data.platform
245 | this.component.uuid = data.uuid
246 |
247 | for (const message of this.messageQueue) {
248 | this.postMessage(message.action as ComponentAction, message.data, message.callback)
249 | }
250 |
251 | this.messageQueue = []
252 |
253 | Logger.info('Data passed to onReady:', data)
254 |
255 | this.activateThemes(data.activeThemeUrls || [])
256 |
257 | // After activateThemes is done, we want to send a message with the ThemesActivated action.
258 | this.postMessage(ComponentAction.ThemesActivated, {})
259 |
260 | if (this.params.onReady) {
261 | this.params.onReady()
262 | }
263 | }
264 |
265 | /**
266 | * Gets the component UUID.
267 | */
268 | public getSelfComponentUUID(): string | undefined {
269 | return this.component.uuid
270 | }
271 |
272 | /**
273 | * Checks if the component is running in a Desktop application.
274 | */
275 | public isRunningInDesktopApplication(): boolean {
276 | return this.component.environment === environmentToString(Environment.Desktop)
277 | }
278 |
279 | /**
280 | * Checks if the component is running in a Mobile application.
281 | */
282 | public isRunningInMobileApplication(): boolean {
283 | return this.component.environment === environmentToString(Environment.Mobile)
284 | }
285 |
286 | /**
287 | * Gets the component's data value for the specified key.
288 | * @param key The key for the data object.
289 | * @returns `undefined` if the value for the key does not exist. Returns the stored value otherwise.
290 | */
291 | public getComponentDataValueForKey(key: string): any {
292 | if (!this.component.data) {
293 | return
294 | }
295 | return this.component.data[key]
296 | }
297 |
298 | /**
299 | * Sets the component's data value for the specified key.
300 | * @param key The key for the data object.
301 | * @param value The value to store under the specified key.
302 | */
303 | public setComponentDataValueForKey(key: string, value: any): void {
304 | if (!this.component.data) {
305 | throw new Error('The component has not been initialized.')
306 | }
307 | if (!key || (key && key.length === 0)) {
308 | throw new Error('The key for the data value should be a valid string.')
309 | }
310 | this.component.data = {
311 | ...this.component.data,
312 | [key]: value,
313 | }
314 | this.postMessage(ComponentAction.SetComponentData, {
315 | componentData: this.component.data,
316 | })
317 | }
318 |
319 | /**
320 | * Clears the component's data object.
321 | */
322 | public clearComponentData(): void {
323 | this.component.data = {}
324 | this.postMessage(ComponentAction.SetComponentData, {
325 | componentData: this.component.data,
326 | })
327 | }
328 |
329 | private postMessage(action: ComponentAction, data: MessageData, callback?: (...params: any) => void) {
330 | /**
331 | * If the sessionKey is not set, we push the message to queue
332 | * that will be processed later on.
333 | */
334 | if (!this.component.sessionKey) {
335 | this.messageQueue.push({
336 | action,
337 | data,
338 | api: MessagePayloadApi.Component,
339 | callback: callback,
340 | })
341 | return
342 | }
343 |
344 | if (action === ComponentAction.SaveItems) {
345 | data.height = this.params.handleRequestForContentHeight()
346 | }
347 |
348 | const message = {
349 | action,
350 | data,
351 | messageId: this.generateUUID(),
352 | sessionKey: this.component.sessionKey,
353 | api: MessagePayloadApi.Component,
354 | }
355 |
356 | const sentMessage = JSON.parse(JSON.stringify(message))
357 | sentMessage.callback = callback
358 | this.sentMessages.push(sentMessage)
359 |
360 | let postMessagePayload
361 |
362 | // Mobile (React Native) requires a string for the postMessage API.
363 | if (this.isRunningInMobileApplication()) {
364 | postMessagePayload = JSON.stringify(message)
365 | } else {
366 | postMessagePayload = message
367 | }
368 |
369 | Logger.info('Posting message:', postMessagePayload)
370 | this.contentWindow.parent.postMessage(postMessagePayload, this.component.origin!)
371 | }
372 |
373 | private activateThemes(incomingUrls: string[] = []) {
374 | if (!this.component.acceptsThemes) {
375 | return
376 | }
377 |
378 | Logger.info('Incoming themes:', incomingUrls)
379 |
380 | const { activeThemes } = this.component
381 |
382 | if (activeThemes && activeThemes.sort().toString() == incomingUrls.sort().toString()) {
383 | // Incoming theme URLs are same as active, do nothing.
384 | return
385 | }
386 |
387 | let themesToActivate = incomingUrls
388 | const themesToDeactivate = []
389 |
390 | for (const activeUrl of activeThemes) {
391 | if (!incomingUrls.includes(activeUrl)) {
392 | // Active not present in incoming, deactivate it.
393 | themesToDeactivate.push(activeUrl)
394 | } else {
395 | // Already present in active themes, remove it from themesToActivate.
396 | themesToActivate = themesToActivate.filter((candidate) => {
397 | return candidate !== activeUrl
398 | })
399 | }
400 | }
401 |
402 | Logger.info('Deactivating themes:', themesToDeactivate)
403 | Logger.info('Activating themes:', themesToActivate)
404 |
405 | for (const themeUrl of themesToDeactivate) {
406 | this.deactivateTheme(themeUrl)
407 | }
408 |
409 | this.component.activeThemes = incomingUrls
410 |
411 | for (const themeUrl of themesToActivate) {
412 | if (!themeUrl) {
413 | continue
414 | }
415 |
416 | const link = this.contentWindow.document.createElement('link')
417 | link.id = btoa(themeUrl)
418 | link.href = themeUrl
419 | link.type = 'text/css'
420 | link.rel = 'stylesheet'
421 | link.media = 'screen,print'
422 | link.className = 'custom-theme'
423 | this.contentWindow.document.getElementsByTagName('head')[0].appendChild(link)
424 | }
425 |
426 | this.params.onThemesChange && this.params.onThemesChange()
427 | }
428 |
429 | private themeElementForUrl(themeUrl: string) {
430 | const elements = Array.from(this.contentWindow.document.getElementsByClassName('custom-theme')).slice()
431 | return elements.find((element) => {
432 | // We used to search here by `href`, but on desktop, with local file:// urls, that didn't work for some reason.
433 | return element.id == btoa(themeUrl)
434 | })
435 | }
436 |
437 | private deactivateTheme(themeUrl: string) {
438 | const element = this.themeElementForUrl(themeUrl)
439 | if (element && element.parentNode) {
440 | element.setAttribute('disabled', 'true')
441 | element.parentNode.removeChild(element)
442 | }
443 | }
444 |
445 | private generateUUID() {
446 | return generateUuid()
447 | }
448 |
449 | /**
450 | * Gets the current platform where the component is running.
451 | */
452 | public get platform(): string | undefined {
453 | return this.component.platform
454 | }
455 |
456 | /**
457 | * Gets the current environment where the component is running.
458 | */
459 | public get environment(): string | undefined {
460 | return this.component.environment
461 | }
462 |
463 | /**
464 | * Streams a collection of Items, filtered by content type.
465 | * New items are passed to the callback as they come.
466 | * @param contentTypes A collection of Content Types.
467 | * @param callback A callback to process the streamed items.
468 | */
469 | public streamItems(contentTypes: ContentType[], callback: (data: any) => void): void {
470 | this.postMessage(ComponentAction.StreamItems, { content_types: contentTypes }, (data: any) => {
471 | callback(data.items)
472 | })
473 | }
474 |
475 | /**
476 | * Streams the current Item in context.
477 | * @param callback A callback to process the streamed item.
478 | */
479 | public streamContextItem(callback: (data: any) => void): void {
480 | this.postMessage(ComponentAction.StreamContextItem, {}, (data) => {
481 | const { item } = data
482 | /**
483 | * If this is a new context item than the context item the component was currently entertaining,
484 | * we want to immediately commit any pending saves, because if you send the new context item to the
485 | * component before it has commited its presave, it will end up first replacing the UI with new context item,
486 | * and when the debouncer executes to read the component UI, it will be reading the new UI for the previous item.
487 | */
488 | const isNewItem = !this.lastStreamedItem || this.lastStreamedItem.uuid !== item.uuid
489 |
490 | if (isNewItem && this.pendingSaveTimeout) {
491 | clearTimeout(this.pendingSaveTimeout)
492 | this.performSavingOfItems(this.pendingSaveParams)
493 | this.pendingSaveTimeout = undefined
494 | this.pendingSaveParams = undefined
495 | }
496 |
497 | this.lastStreamedItem = item
498 | callback(this.lastStreamedItem)
499 | })
500 | }
501 |
502 | /**
503 | * Creates and stores an Item in the item store.
504 | * @param item The Item's payload content.
505 | * @param callback The callback to process the created Item.
506 | */
507 | public createItem(item: DecryptedTransferPayload, callback: (data: any) => void): void {
508 | this.postMessage(ComponentAction.CreateItem, { item: this.jsonObjectForItem(item) }, (data: any) => {
509 | let { item } = data
510 | /**
511 | * A previous version of the SN app had an issue where the item in the reply to ComponentActions.CreateItems
512 | * would be nested inside "items" and not "item". So handle both cases here.
513 | */
514 | if (!item && data.items && data.items.length > 0) {
515 | item = data.items[0]
516 | }
517 |
518 | callback && callback(item)
519 | })
520 | }
521 |
522 | /**
523 | * Creates and stores a collection of Items in the item store.
524 | * @param items The Item(s) payload collection.
525 | * @param callback The callback to process the created Item(s).
526 | */
527 | public createItems(items: DecryptedTransferPayload[], callback: (data: any) => void): void {
528 | const mapped = items.map((item) => this.jsonObjectForItem(item))
529 | this.postMessage(ComponentAction.CreateItems, { items: mapped }, (data: any) => {
530 | callback && callback(data.items)
531 | })
532 | }
533 |
534 | /**
535 | * Deletes an Item from the item store.
536 | * @param item The Item to delete.
537 | * @param callback The callback with the result of the operation.
538 | */
539 | public deleteItem(item: DecryptedTransferPayload, callback: (data: OutgoingItemMessagePayload) => void): void {
540 | this.deleteItems([item], callback)
541 | }
542 |
543 | /**
544 | * Deletes a collection of Items from the item store.
545 | * @param items The Item(s) to delete.
546 | * @param callback The callback with the result of the operation.
547 | */
548 | public deleteItems(items: DecryptedTransferPayload[], callback: (data: OutgoingItemMessagePayload) => void): void {
549 | const params = {
550 | items: items.map((item) => {
551 | return this.jsonObjectForItem(item)
552 | }),
553 | }
554 | this.postMessage(ComponentAction.DeleteItems, params, (data) => {
555 | callback && callback(data)
556 | })
557 | }
558 |
559 | /**
560 | * Performs a custom action to the component manager.
561 | * @param action
562 | * @param data
563 | * @param callback The callback with the result of the operation.
564 | */
565 | public sendCustomEvent(action: ComponentAction, data: any, callback?: (data: any) => void): void {
566 | this.postMessage(action, data, (data: any) => {
567 | callback && callback(data)
568 | })
569 | }
570 |
571 | /**
572 | * Saves an existing Item in the item store.
573 | * @param item An existing Item to be saved.
574 | * @param callback
575 | * @param skipDebouncer
576 | */
577 | public saveItem(item: DecryptedTransferPayload, callback?: () => void, skipDebouncer = false): void {
578 | this.saveItems([item], callback, skipDebouncer)
579 | }
580 |
581 | /**
582 | * Runs a callback before saving an Item.
583 | * @param item An existing Item to be saved.
584 | * @param presave Allows clients to perform any actions last second before the save actually occurs (like setting previews).
585 | * Saves debounce by default, so if a client needs to compute a property on an item before saving, it's best to
586 | * hook into the debounce cycle so that clients don't have to implement their own debouncing.
587 | * @param callback
588 | */
589 | public saveItemWithPresave(
590 | item: DecryptedTransferPayload,
591 | presave: any,
592 | callback?: () => void,
593 | ): void {
594 | this.saveItemsWithPresave([item], presave, callback)
595 | }
596 |
597 | /**
598 | * Runs a callback before saving a collection of Items.
599 | * @param items A collection of existing Items to be saved.
600 | * @param presave Allows clients to perform any actions last second before the save actually occurs (like setting previews).
601 | * Saves debounce by default, so if a client needs to compute a property on an item before saving, it's best to
602 | * hook into the debounce cycle so that clients don't have to implement their own debouncing.
603 | * @param callback
604 | */
605 | public saveItemsWithPresave(items: DecryptedTransferPayload[], presave: any, callback?: () => void): void {
606 | this.saveItems(items, callback, false, presave)
607 | }
608 |
609 | private performSavingOfItems({
610 | items,
611 | presave,
612 | callback,
613 | }: {
614 | items: DecryptedTransferPayload[]
615 | presave: () => void
616 | callback?: () => void
617 | }) {
618 | const ConcernIntervalMS = 5000
619 | const concernTimeout = setTimeout(() => {
620 | this.concernTimeouts.forEach((timeout) => clearTimeout(timeout))
621 | alert(
622 | 'This editor is unable to communicate with Standard Notes. ' +
623 | 'Your changes may not be saved. Please backup your changes, then restart the ' +
624 | 'application and try again.',
625 | )
626 | }, ConcernIntervalMS)
627 |
628 | this.concernTimeouts.push(concernTimeout)
629 |
630 | /**
631 | * Presave block allows client to gain the benefit of performing something in the debounce cycle.
632 | */
633 | presave && presave()
634 |
635 | const mappedItems = []
636 | for (const item of items) {
637 | mappedItems.push(this.jsonObjectForItem(item))
638 | }
639 |
640 | const wrappedCallback = () => {
641 | this.concernTimeouts.forEach((timeout) => clearTimeout(timeout))
642 | callback?.()
643 | }
644 |
645 | this.postMessage(ComponentAction.SaveItems, { items: mappedItems }, wrappedCallback)
646 | }
647 |
648 | /**
649 | * Saves a collection of existing Items.
650 | * @param items The items to be saved.
651 | * @param callback
652 | * @param skipDebouncer Allows saves to go through right away rather than waiting for timeout.
653 | * This should be used when saving items via other means besides keystrokes.
654 | * @param presave
655 | */
656 | public saveItems(
657 | items: DecryptedTransferPayload[],
658 | callback?: () => void,
659 | skipDebouncer = false,
660 | presave?: any,
661 | ): void {
662 | /**
663 | * We need to make sure that when we clear a pending save timeout,
664 | * we carry over those pending items into the new save.
665 | */
666 | if (!this.pendingSaveItems) {
667 | this.pendingSaveItems = []
668 | }
669 |
670 | if (this.options.coallesedSaving && !skipDebouncer) {
671 | if (this.pendingSaveTimeout) {
672 | clearTimeout(this.pendingSaveTimeout)
673 | }
674 |
675 | const incomingIds = items.map((item) => item.uuid)
676 | /**
677 | * Replace any existing save items with incoming values.
678 | * Only keep items here who are not in incomingIds.
679 | */
680 | const preexistingItems = this.pendingSaveItems.filter((item) => {
681 | return !incomingIds.includes(item.uuid)
682 | })
683 |
684 | // Add new items, now that we've made sure it's cleared of incoming items.
685 | this.pendingSaveItems = preexistingItems.concat(items)
686 |
687 | // We'll potentially need to commit early if stream-context-item message comes in.
688 | this.pendingSaveParams = {
689 | items: this.pendingSaveItems,
690 | presave,
691 | callback,
692 | }
693 |
694 | this.pendingSaveTimeout = setTimeout(() => {
695 | this.performSavingOfItems(this.pendingSaveParams)
696 | this.pendingSaveItems = []
697 | this.pendingSaveTimeout = undefined
698 | this.pendingSaveParams = null
699 | }, this.options.coallesedSavingDelay)
700 | } else {
701 | this.performSavingOfItems({ items, presave, callback })
702 | }
703 | }
704 |
705 | /**
706 | * Sets a new container size for the current component.
707 | * @param width The new width.
708 | * @param height The new height.
709 | */
710 | public setSize(width: string | number, height: string | number): void {
711 | this.postMessage(ComponentAction.SetSize, {
712 | type: 'container',
713 | width,
714 | height,
715 | })
716 | }
717 |
718 | /**
719 | * Sends the KeyDown keyboard event to the Standard Notes parent application.
720 | * @param keyboardModifier The keyboard modifier that was pressed.
721 | */
722 | private keyDownEvent(keyboardModifier: KeyboardModifier): void {
723 | this.postMessage(ComponentAction.KeyDown, { keyboardModifier })
724 | }
725 |
726 | /**
727 | * Sends the KeyUp keyboard event to the Standard Notes parent application.
728 | * @param keyboardModifier The keyboard modifier that was released.
729 | */
730 | private keyUpEvent(keyboardModifier: KeyboardModifier): void {
731 | this.postMessage(ComponentAction.KeyUp, { keyboardModifier })
732 | }
733 |
734 | /**
735 | * Sends the Click mouse event to the Standard Notes parent application.
736 | */
737 | private mouseClickEvent(): void {
738 | this.postMessage(ComponentAction.Click, {})
739 | }
740 |
741 | private jsonObjectForItem(item: DecryptedItem | DecryptedTransferPayload) {
742 | const copy = Object.assign({}, item) as any
743 | copy.children = null
744 | copy.parent = null
745 | return copy
746 | }
747 |
748 | /**
749 | * Gets the Item's appData value for the specified key.
750 | * Uses the default domain (org.standardnotes.sn).
751 | * This function is used with Items returned from streamContextItem() and streamItems()
752 | * @param item The Item to get the appData value from.
753 | * @param key The key to get the value from.
754 | */
755 | public getItemAppDataValue(item: OutgoingItemMessagePayload | undefined, key: AppDataField | string): any {
756 | const defaultDomain = 'org.standardnotes.sn'
757 | const domainData = item?.content?.appData?.[defaultDomain]
758 | return domainData?.[key as AppDataField]
759 | }
760 | }
761 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/dist/dist.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dist.js","mappings":"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","sources":["webpack://ComponentRelay/webpack/universalModuleDefinition","webpack://ComponentRelay/webpack/bootstrap","webpack://ComponentRelay/webpack/runtime/define property getters","webpack://ComponentRelay/webpack/runtime/hasOwnProperty shorthand","webpack://ComponentRelay/./lib/Types/Environment.ts","webpack://ComponentRelay/./node_modules/uuid/dist/esm-browser/rng.js","webpack://ComponentRelay/./node_modules/uuid/dist/esm-browser/regex.js","webpack://ComponentRelay/./node_modules/uuid/dist/esm-browser/validate.js","webpack://ComponentRelay/./node_modules/uuid/dist/esm-browser/stringify.js","webpack://ComponentRelay/./node_modules/uuid/dist/esm-browser/v4.js","webpack://ComponentRelay/./lib/Utils.ts","webpack://ComponentRelay/./lib/Logger.ts","webpack://ComponentRelay/./lib/Types/KeyboardModifier.ts","webpack://ComponentRelay/./lib/Types/MessagePayloadApi.ts","webpack://ComponentRelay/./lib/Types/ComponentAction.ts","webpack://ComponentRelay/./lib/ComponentRelay.ts"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"ComponentRelay\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ComponentRelay\"] = factory();\n\telse\n\t\troot[\"ComponentRelay\"] = factory();\n})(self, () => {\nreturn ","// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","export enum Environment {\n Web = 1,\n Desktop = 2,\n Mobile = 3,\n}\n","// Unique ID creation requires a high quality random # generator. In the browser we therefore\n// require the crypto API and do not support built-in fallback to lower quality random number\n// generators (like Math.random()).\nvar getRandomValues;\nvar rnds8 = new Uint8Array(16);\nexport default function rng() {\n // lazy load so that environments that need to polyfill have a chance to do so\n if (!getRandomValues) {\n // getRandomValues needs to be invoked in a context where \"this\" is a Crypto implementation. Also,\n // find the complete implementation of crypto (msCrypto) on IE11.\n getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto);\n\n if (!getRandomValues) {\n throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');\n }\n }\n\n return getRandomValues(rnds8);\n}","export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;","import REGEX from './regex.js';\n\nfunction validate(uuid) {\n return typeof uuid === 'string' && REGEX.test(uuid);\n}\n\nexport default validate;","import validate from './validate.js';\n/**\n * Convert array of 16 byte values to UUID string format of the form:\n * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\n */\n\nvar byteToHex = [];\n\nfor (var i = 0; i < 256; ++i) {\n byteToHex.push((i + 0x100).toString(16).substr(1));\n}\n\nfunction stringify(arr) {\n var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;\n // Note: Be careful editing this code! It's been tuned for performance\n // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434\n var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one\n // of the following:\n // - One or more input array values don't map to a hex octet (leading to\n // \"undefined\" in the uuid)\n // - Invalid input values for the RFC `version` or `variant` fields\n\n if (!validate(uuid)) {\n throw TypeError('Stringified UUID is invalid');\n }\n\n return uuid;\n}\n\nexport default stringify;","import rng from './rng.js';\nimport stringify from './stringify.js';\n\nfunction v4(options, buf, offset) {\n options = options || {};\n var rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`\n\n rnds[6] = rnds[6] & 0x0f | 0x40;\n rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided\n\n if (buf) {\n offset = offset || 0;\n\n for (var i = 0; i < 16; ++i) {\n buf[offset + i] = rnds[i];\n }\n\n return buf;\n }\n\n return stringify(rnds);\n}\n\nexport default v4;","import { Environment } from './Types/Environment'\nimport { v4 as uuidv4 } from 'uuid'\n\ndeclare global {\n interface Window {\n msCrypto: unknown\n }\n}\n\nexport const generateUuid = (): string => {\n return uuidv4()\n}\n\nexport const isValidJsonString = (str: unknown): boolean => {\n if (typeof str !== 'string') {\n return false\n }\n try {\n const result = JSON.parse(str)\n const type = Object.prototype.toString.call(result)\n return type === '[object Object]' || type === '[object Array]'\n } catch (e) {\n return false\n }\n}\n\nexport const environmentToString = (environment: Environment): string => {\n const map = {\n [Environment.Web]: 'web',\n [Environment.Desktop]: 'desktop',\n [Environment.Mobile]: 'mobile',\n }\n return map[environment] ?? map[Environment.Web]\n}\n\nexport const isNotUndefinedOrNull = (value: any): boolean => {\n return value !== null && value !== undefined\n}\n","const noop = () => undefined\n\nexport default class Logger {\n static enabled = false\n\n private static get isSupported() {\n return window.console || console ? true : false\n }\n\n static get info(): any {\n if (!Logger.isSupported || !this.enabled) {\n return noop\n }\n return console.log.bind(console)\n }\n\n static get error(): any {\n if (!Logger.isSupported) {\n return noop\n }\n return console.error.bind(console)\n }\n}\n","export enum KeyboardModifier {\n Shift = 'Shift',\n Ctrl = 'Control',\n Meta = 'Meta',\n}\n","export enum MessagePayloadApi {\n Component = 'component',\n}\n","export enum ComponentAction {\n SetSize = 'set-size',\n StreamItems = 'stream-items',\n StreamContextItem = 'stream-context-item',\n SaveItems = 'save-items',\n CreateItem = 'create-item',\n CreateItems = 'create-items',\n DeleteItems = 'delete-items',\n SetComponentData = 'set-component-data',\n RequestPermissions = 'request-permissions',\n DuplicateItem = 'duplicate-item',\n ComponentRegistered = 'component-registered',\n ActivateThemes = 'themes',\n Reply = 'reply',\n ThemesActivated = 'themes-activated',\n KeyDown = 'key-down',\n KeyUp = 'key-up',\n Click = 'click',\n}\n","import type {\n OutgoingItemMessagePayload,\n MessageData,\n DecryptedItem,\n AppDataField,\n DecryptedTransferPayload,\n ItemContent,\n ContentType,\n} from '@standardnotes/snjs'\nimport { environmentToString, generateUuid, isValidJsonString } from './Utils'\nimport Logger from './Logger'\nimport { KeyboardModifier } from './Types/KeyboardModifier'\nimport { ComponentRelayParams } from './Types/ComponentRelayParams'\nimport { MessagePayload } from './Types/MessagePayload'\nimport { Component } from './Types/Component'\nimport { MessagePayloadApi } from './Types/MessagePayloadApi'\nimport { ComponentRelayOptions } from './Types/ComponentRelayOptions'\nimport { ComponentAction } from './Types/ComponentAction'\nimport { Environment } from './Types/Environment'\n\nconst DEFAULT_COALLESED_SAVING_DELAY = 250\n\nexport default class ComponentRelay {\n private contentWindow: Window\n private component: Component = { activeThemes: [], acceptsThemes: true }\n private sentMessages: MessagePayload[] = []\n private messageQueue: MessagePayload[] = []\n private lastStreamedItem?: DecryptedTransferPayload\n private pendingSaveItems?: DecryptedTransferPayload[]\n private pendingSaveTimeout?: NodeJS.Timeout\n private pendingSaveParams?: any\n private messageHandler?: (event: any) => void\n private keyDownEventListener?: (event: KeyboardEvent) => void\n private keyUpEventListener?: (event: KeyboardEvent) => void\n private clickEventListener?: (event: MouseEvent) => void\n private concernTimeouts: NodeJS.Timeout[] = []\n private options: ComponentRelayOptions\n private params: Omit\n\n constructor(params: ComponentRelayParams) {\n if (!params || !params.targetWindow) {\n throw new Error('contentWindow must be a valid Window object.')\n }\n\n this.params = params\n\n this.options = params.options || {}\n\n if (this.options.coallesedSaving == undefined) {\n this.options.coallesedSaving = true\n }\n if (this.options.coallesedSavingDelay == undefined) {\n this.options.coallesedSavingDelay = DEFAULT_COALLESED_SAVING_DELAY\n }\n if (this.options.acceptsThemes != undefined) {\n this.component.acceptsThemes = this.options.acceptsThemes ?? true\n }\n\n Logger.enabled = this.options.debug ?? false\n\n this.contentWindow = params.targetWindow\n this.registerMessageHandler()\n this.registerKeyboardEventListeners()\n this.registerMouseEventListeners()\n }\n\n public deinit(): void {\n this.params.onReady = undefined\n this.component = {\n acceptsThemes: true,\n activeThemes: [],\n }\n this.messageQueue = []\n this.sentMessages = []\n this.lastStreamedItem = undefined\n this.pendingSaveItems = undefined\n this.pendingSaveTimeout = undefined\n this.pendingSaveParams = undefined\n\n if (this.messageHandler) {\n this.contentWindow.document.removeEventListener('message', this.messageHandler)\n this.contentWindow.removeEventListener('message', this.messageHandler)\n }\n\n if (this.keyDownEventListener) {\n this.contentWindow.removeEventListener('keydown', this.keyDownEventListener)\n }\n\n if (this.keyUpEventListener) {\n this.contentWindow.removeEventListener('keyup', this.keyUpEventListener)\n }\n\n if (this.clickEventListener) {\n this.contentWindow.removeEventListener('click', this.clickEventListener)\n }\n }\n\n private registerMessageHandler() {\n this.messageHandler = (event: MessageEvent) => {\n Logger.info('Components API Message received:', event.data)\n\n /**\n * We don't have access to window.parent.origin due to cross-domain restrictions.\n * Check referrer if available, otherwise defer to checking for first-run value.\n * Craft URL objects so that example.com === example.com/\n */\n if (document.referrer) {\n const referrer = new URL(document.referrer).origin\n const eventOrigin = new URL(event.origin).origin\n\n if (referrer !== eventOrigin) {\n return\n }\n }\n\n // Mobile environment sends data as JSON string.\n const { data } = event\n const parsedData = isValidJsonString(data) ? JSON.parse(data) : data\n\n if (!parsedData) {\n Logger.error('Invalid data received. Skipping...')\n return\n }\n\n /**\n * The Component Registered message will be the most reliable one, so we won't change it after any subsequent events,\n * in case you receive an event from another window.\n */\n if (typeof this.component.origin === 'undefined' && parsedData.action === ComponentAction.ComponentRegistered) {\n this.component.origin = event.origin\n } else if (event.origin !== this.component.origin) {\n // If event origin doesn't match first-run value, return.\n return\n }\n\n this.handleMessage(parsedData)\n }\n\n /**\n * Mobile (React Native) uses `document`, web/desktop uses `window`.addEventListener\n * for postMessage API to work properly.\n * Update May 2019:\n * As part of transitioning React Native webview into the community package,\n * we'll now only need to use window.addEventListener.\n * However, we want to maintain backward compatibility for Mobile < v3.0.5, so we'll keep document.addEventListener\n * Also, even with the new version of react-native-webview, Android may still require document.addEventListener (while iOS still only requires window.addEventListener)\n * https://github.com/react-native-community/react-native-webview/issues/323#issuecomment-467767933\n */\n this.contentWindow.document.addEventListener('message', this.messageHandler, false)\n this.contentWindow.addEventListener('message', this.messageHandler, false)\n\n Logger.info('Waiting for messages...')\n }\n\n private registerKeyboardEventListeners() {\n this.keyDownEventListener = (event: KeyboardEvent) => {\n Logger.info(`A key has been pressed: ${event.key}`)\n\n if (event.ctrlKey) {\n this.keyDownEvent(KeyboardModifier.Ctrl)\n } else if (event.shiftKey) {\n this.keyDownEvent(KeyboardModifier.Shift)\n } else if (event.metaKey || event.key === 'Meta') {\n this.keyDownEvent(KeyboardModifier.Meta)\n }\n }\n\n this.keyUpEventListener = (event: KeyboardEvent) => {\n Logger.info(`A key has been released: ${event.key}`)\n\n /**\n * Checking using event.key instead of the corresponding boolean properties.\n */\n if (event.key === 'Control') {\n this.keyUpEvent(KeyboardModifier.Ctrl)\n } else if (event.key === 'Shift') {\n this.keyUpEvent(KeyboardModifier.Shift)\n } else if (event.key === 'Meta') {\n this.keyUpEvent(KeyboardModifier.Meta)\n }\n }\n\n this.contentWindow.addEventListener('keydown', this.keyDownEventListener, false)\n this.contentWindow.addEventListener('keyup', this.keyUpEventListener, false)\n }\n\n private registerMouseEventListeners() {\n this.clickEventListener = (_event: MouseEvent) => {\n Logger.info('A click has been performed.')\n\n this.mouseClickEvent()\n }\n\n this.contentWindow.addEventListener('click', this.clickEventListener, false)\n }\n\n private handleMessage(payload: MessagePayload) {\n switch (payload.action) {\n case ComponentAction.ComponentRegistered:\n this.component.sessionKey = payload.sessionKey\n if (payload.componentData) {\n this.component.data = payload.componentData\n }\n this.onReady(payload.data)\n Logger.info('Component successfully registered with payload:', payload)\n break\n\n case ComponentAction.ActivateThemes:\n this.activateThemes(payload.data.themes)\n break\n\n default: {\n if (!payload.original) {\n return\n }\n\n // Get the callback from queue.\n const originalMessage = this.sentMessages?.filter((message: MessagePayload) => {\n return message.messageId === payload.original?.messageId\n })[0]\n\n if (!originalMessage) {\n // Connection must have been reset. We should alert the user unless it's a reply,\n // in which case we may have been deallocated and reinitialized and lost the\n // original message\n const extensionName = this.contentWindow.document.title\n const alertMessage = (\n `The extension '${extensionName}' is attempting to communicate with Standard Notes, ` +\n 'but an error is preventing it from doing so. Please restart this extension and try again.'\n ).replace(' ', ' ')\n\n Logger.info(alertMessage)\n return\n }\n\n originalMessage?.callback?.(payload.data)\n break\n }\n }\n }\n\n private onReady(data: MessageData) {\n this.component.environment = data.environment\n this.component.platform = data.platform\n this.component.uuid = data.uuid\n\n for (const message of this.messageQueue) {\n this.postMessage(message.action as ComponentAction, message.data, message.callback)\n }\n\n this.messageQueue = []\n\n Logger.info('Data passed to onReady:', data)\n\n this.activateThemes(data.activeThemeUrls || [])\n\n // After activateThemes is done, we want to send a message with the ThemesActivated action.\n this.postMessage(ComponentAction.ThemesActivated, {})\n\n if (this.params.onReady) {\n this.params.onReady()\n }\n }\n\n /**\n * Gets the component UUID.\n */\n public getSelfComponentUUID(): string | undefined {\n return this.component.uuid\n }\n\n /**\n * Checks if the component is running in a Desktop application.\n */\n public isRunningInDesktopApplication(): boolean {\n return this.component.environment === environmentToString(Environment.Desktop)\n }\n\n /**\n * Checks if the component is running in a Mobile application.\n */\n public isRunningInMobileApplication(): boolean {\n return this.component.environment === environmentToString(Environment.Mobile)\n }\n\n /**\n * Gets the component's data value for the specified key.\n * @param key The key for the data object.\n * @returns `undefined` if the value for the key does not exist. Returns the stored value otherwise.\n */\n public getComponentDataValueForKey(key: string): any {\n if (!this.component.data) {\n return\n }\n return this.component.data[key]\n }\n\n /**\n * Sets the component's data value for the specified key.\n * @param key The key for the data object.\n * @param value The value to store under the specified key.\n */\n public setComponentDataValueForKey(key: string, value: any): void {\n if (!this.component.data) {\n throw new Error('The component has not been initialized.')\n }\n if (!key || (key && key.length === 0)) {\n throw new Error('The key for the data value should be a valid string.')\n }\n this.component.data = {\n ...this.component.data,\n [key]: value,\n }\n this.postMessage(ComponentAction.SetComponentData, {\n componentData: this.component.data,\n })\n }\n\n /**\n * Clears the component's data object.\n */\n public clearComponentData(): void {\n this.component.data = {}\n this.postMessage(ComponentAction.SetComponentData, {\n componentData: this.component.data,\n })\n }\n\n private postMessage(action: ComponentAction, data: MessageData, callback?: (...params: any) => void) {\n /**\n * If the sessionKey is not set, we push the message to queue\n * that will be processed later on.\n */\n if (!this.component.sessionKey) {\n this.messageQueue.push({\n action,\n data,\n api: MessagePayloadApi.Component,\n callback: callback,\n })\n return\n }\n\n if (action === ComponentAction.SaveItems) {\n data.height = this.params.handleRequestForContentHeight()\n }\n\n const message = {\n action,\n data,\n messageId: this.generateUUID(),\n sessionKey: this.component.sessionKey,\n api: MessagePayloadApi.Component,\n }\n\n const sentMessage = JSON.parse(JSON.stringify(message))\n sentMessage.callback = callback\n this.sentMessages.push(sentMessage)\n\n let postMessagePayload\n\n // Mobile (React Native) requires a string for the postMessage API.\n if (this.isRunningInMobileApplication()) {\n postMessagePayload = JSON.stringify(message)\n } else {\n postMessagePayload = message\n }\n\n Logger.info('Posting message:', postMessagePayload)\n this.contentWindow.parent.postMessage(postMessagePayload, this.component.origin!)\n }\n\n private activateThemes(incomingUrls: string[] = []) {\n if (!this.component.acceptsThemes) {\n return\n }\n\n Logger.info('Incoming themes:', incomingUrls)\n\n const { activeThemes } = this.component\n\n if (activeThemes && activeThemes.sort().toString() == incomingUrls.sort().toString()) {\n // Incoming theme URLs are same as active, do nothing.\n return\n }\n\n let themesToActivate = incomingUrls\n const themesToDeactivate = []\n\n for (const activeUrl of activeThemes) {\n if (!incomingUrls.includes(activeUrl)) {\n // Active not present in incoming, deactivate it.\n themesToDeactivate.push(activeUrl)\n } else {\n // Already present in active themes, remove it from themesToActivate.\n themesToActivate = themesToActivate.filter((candidate) => {\n return candidate !== activeUrl\n })\n }\n }\n\n Logger.info('Deactivating themes:', themesToDeactivate)\n Logger.info('Activating themes:', themesToActivate)\n\n for (const themeUrl of themesToDeactivate) {\n this.deactivateTheme(themeUrl)\n }\n\n this.component.activeThemes = incomingUrls\n\n for (const themeUrl of themesToActivate) {\n if (!themeUrl) {\n continue\n }\n\n const link = this.contentWindow.document.createElement('link')\n link.id = btoa(themeUrl)\n link.href = themeUrl\n link.type = 'text/css'\n link.rel = 'stylesheet'\n link.media = 'screen,print'\n link.className = 'custom-theme'\n this.contentWindow.document.getElementsByTagName('head')[0].appendChild(link)\n }\n\n this.params.onThemesChange && this.params.onThemesChange()\n }\n\n private themeElementForUrl(themeUrl: string) {\n const elements = Array.from(this.contentWindow.document.getElementsByClassName('custom-theme')).slice()\n return elements.find((element) => {\n // We used to search here by `href`, but on desktop, with local file:// urls, that didn't work for some reason.\n return element.id == btoa(themeUrl)\n })\n }\n\n private deactivateTheme(themeUrl: string) {\n const element = this.themeElementForUrl(themeUrl)\n if (element && element.parentNode) {\n element.setAttribute('disabled', 'true')\n element.parentNode.removeChild(element)\n }\n }\n\n private generateUUID() {\n return generateUuid()\n }\n\n /**\n * Gets the current platform where the component is running.\n */\n public get platform(): string | undefined {\n return this.component.platform\n }\n\n /**\n * Gets the current environment where the component is running.\n */\n public get environment(): string | undefined {\n return this.component.environment\n }\n\n /**\n * Streams a collection of Items, filtered by content type.\n * New items are passed to the callback as they come.\n * @param contentTypes A collection of Content Types.\n * @param callback A callback to process the streamed items.\n */\n public streamItems(contentTypes: ContentType[], callback: (data: any) => void): void {\n this.postMessage(ComponentAction.StreamItems, { content_types: contentTypes }, (data: any) => {\n callback(data.items)\n })\n }\n\n /**\n * Streams the current Item in context.\n * @param callback A callback to process the streamed item.\n */\n public streamContextItem(callback: (data: any) => void): void {\n this.postMessage(ComponentAction.StreamContextItem, {}, (data) => {\n const { item } = data\n /**\n * If this is a new context item than the context item the component was currently entertaining,\n * we want to immediately commit any pending saves, because if you send the new context item to the\n * component before it has commited its presave, it will end up first replacing the UI with new context item,\n * and when the debouncer executes to read the component UI, it will be reading the new UI for the previous item.\n */\n const isNewItem = !this.lastStreamedItem || this.lastStreamedItem.uuid !== item.uuid\n\n if (isNewItem && this.pendingSaveTimeout) {\n clearTimeout(this.pendingSaveTimeout)\n this.performSavingOfItems(this.pendingSaveParams)\n this.pendingSaveTimeout = undefined\n this.pendingSaveParams = undefined\n }\n\n this.lastStreamedItem = item\n callback(this.lastStreamedItem)\n })\n }\n\n /**\n * Creates and stores an Item in the item store.\n * @param item The Item's payload content.\n * @param callback The callback to process the created Item.\n */\n public createItem(item: DecryptedTransferPayload, callback: (data: any) => void): void {\n this.postMessage(ComponentAction.CreateItem, { item: this.jsonObjectForItem(item) }, (data: any) => {\n let { item } = data\n /**\n * A previous version of the SN app had an issue where the item in the reply to ComponentActions.CreateItems\n * would be nested inside \"items\" and not \"item\". So handle both cases here.\n */\n if (!item && data.items && data.items.length > 0) {\n item = data.items[0]\n }\n\n callback && callback(item)\n })\n }\n\n /**\n * Creates and stores a collection of Items in the item store.\n * @param items The Item(s) payload collection.\n * @param callback The callback to process the created Item(s).\n */\n public createItems(items: DecryptedTransferPayload[], callback: (data: any) => void): void {\n const mapped = items.map((item) => this.jsonObjectForItem(item))\n this.postMessage(ComponentAction.CreateItems, { items: mapped }, (data: any) => {\n callback && callback(data.items)\n })\n }\n\n /**\n * Deletes an Item from the item store.\n * @param item The Item to delete.\n * @param callback The callback with the result of the operation.\n */\n public deleteItem(item: DecryptedTransferPayload, callback: (data: OutgoingItemMessagePayload) => void): void {\n this.deleteItems([item], callback)\n }\n\n /**\n * Deletes a collection of Items from the item store.\n * @param items The Item(s) to delete.\n * @param callback The callback with the result of the operation.\n */\n public deleteItems(items: DecryptedTransferPayload[], callback: (data: OutgoingItemMessagePayload) => void): void {\n const params = {\n items: items.map((item) => {\n return this.jsonObjectForItem(item)\n }),\n }\n this.postMessage(ComponentAction.DeleteItems, params, (data) => {\n callback && callback(data)\n })\n }\n\n /**\n * Performs a custom action to the component manager.\n * @param action\n * @param data\n * @param callback The callback with the result of the operation.\n */\n public sendCustomEvent(action: ComponentAction, data: any, callback?: (data: any) => void): void {\n this.postMessage(action, data, (data: any) => {\n callback && callback(data)\n })\n }\n\n /**\n * Saves an existing Item in the item store.\n * @param item An existing Item to be saved.\n * @param callback\n * @param skipDebouncer\n */\n public saveItem(item: DecryptedTransferPayload, callback?: () => void, skipDebouncer = false): void {\n this.saveItems([item], callback, skipDebouncer)\n }\n\n /**\n * Runs a callback before saving an Item.\n * @param item An existing Item to be saved.\n * @param presave Allows clients to perform any actions last second before the save actually occurs (like setting previews).\n * Saves debounce by default, so if a client needs to compute a property on an item before saving, it's best to\n * hook into the debounce cycle so that clients don't have to implement their own debouncing.\n * @param callback\n */\n public saveItemWithPresave(\n item: DecryptedTransferPayload,\n presave: any,\n callback?: () => void,\n ): void {\n this.saveItemsWithPresave([item], presave, callback)\n }\n\n /**\n * Runs a callback before saving a collection of Items.\n * @param items A collection of existing Items to be saved.\n * @param presave Allows clients to perform any actions last second before the save actually occurs (like setting previews).\n * Saves debounce by default, so if a client needs to compute a property on an item before saving, it's best to\n * hook into the debounce cycle so that clients don't have to implement their own debouncing.\n * @param callback\n */\n public saveItemsWithPresave(items: DecryptedTransferPayload[], presave: any, callback?: () => void): void {\n this.saveItems(items, callback, false, presave)\n }\n\n private performSavingOfItems({\n items,\n presave,\n callback,\n }: {\n items: DecryptedTransferPayload[]\n presave: () => void\n callback?: () => void\n }) {\n const ConcernIntervalMS = 5000\n const concernTimeout = setTimeout(() => {\n this.concernTimeouts.forEach((timeout) => clearTimeout(timeout))\n alert(\n 'This editor is unable to communicate with Standard Notes. ' +\n 'Your changes may not be saved. Please backup your changes, then restart the ' +\n 'application and try again.',\n )\n }, ConcernIntervalMS)\n\n this.concernTimeouts.push(concernTimeout)\n\n /**\n * Presave block allows client to gain the benefit of performing something in the debounce cycle.\n */\n presave && presave()\n\n const mappedItems = []\n for (const item of items) {\n mappedItems.push(this.jsonObjectForItem(item))\n }\n\n const wrappedCallback = () => {\n this.concernTimeouts.forEach((timeout) => clearTimeout(timeout))\n callback?.()\n }\n\n this.postMessage(ComponentAction.SaveItems, { items: mappedItems }, wrappedCallback)\n }\n\n /**\n * Saves a collection of existing Items.\n * @param items The items to be saved.\n * @param callback\n * @param skipDebouncer Allows saves to go through right away rather than waiting for timeout.\n * This should be used when saving items via other means besides keystrokes.\n * @param presave\n */\n public saveItems(\n items: DecryptedTransferPayload[],\n callback?: () => void,\n skipDebouncer = false,\n presave?: any,\n ): void {\n /**\n * We need to make sure that when we clear a pending save timeout,\n * we carry over those pending items into the new save.\n */\n if (!this.pendingSaveItems) {\n this.pendingSaveItems = []\n }\n\n if (this.options.coallesedSaving && !skipDebouncer) {\n if (this.pendingSaveTimeout) {\n clearTimeout(this.pendingSaveTimeout)\n }\n\n const incomingIds = items.map((item) => item.uuid)\n /**\n * Replace any existing save items with incoming values.\n * Only keep items here who are not in incomingIds.\n */\n const preexistingItems = this.pendingSaveItems.filter((item) => {\n return !incomingIds.includes(item.uuid)\n })\n\n // Add new items, now that we've made sure it's cleared of incoming items.\n this.pendingSaveItems = preexistingItems.concat(items)\n\n // We'll potentially need to commit early if stream-context-item message comes in.\n this.pendingSaveParams = {\n items: this.pendingSaveItems,\n presave,\n callback,\n }\n\n this.pendingSaveTimeout = setTimeout(() => {\n this.performSavingOfItems(this.pendingSaveParams)\n this.pendingSaveItems = []\n this.pendingSaveTimeout = undefined\n this.pendingSaveParams = null\n }, this.options.coallesedSavingDelay)\n } else {\n this.performSavingOfItems({ items, presave, callback })\n }\n }\n\n /**\n * Sets a new container size for the current component.\n * @param width The new width.\n * @param height The new height.\n */\n public setSize(width: string | number, height: string | number): void {\n this.postMessage(ComponentAction.SetSize, {\n type: 'container',\n width,\n height,\n })\n }\n\n /**\n * Sends the KeyDown keyboard event to the Standard Notes parent application.\n * @param keyboardModifier The keyboard modifier that was pressed.\n */\n private keyDownEvent(keyboardModifier: KeyboardModifier): void {\n this.postMessage(ComponentAction.KeyDown, { keyboardModifier })\n }\n\n /**\n * Sends the KeyUp keyboard event to the Standard Notes parent application.\n * @param keyboardModifier The keyboard modifier that was released.\n */\n private keyUpEvent(keyboardModifier: KeyboardModifier): void {\n this.postMessage(ComponentAction.KeyUp, { keyboardModifier })\n }\n\n /**\n * Sends the Click mouse event to the Standard Notes parent application.\n */\n private mouseClickEvent(): void {\n this.postMessage(ComponentAction.Click, {})\n }\n\n private jsonObjectForItem(item: DecryptedItem | DecryptedTransferPayload) {\n const copy = Object.assign({}, item) as any\n copy.children = null\n copy.parent = null\n return copy\n }\n\n /**\n * Gets the Item's appData value for the specified key.\n * Uses the default domain (org.standardnotes.sn).\n * This function is used with Items returned from streamContextItem() and streamItems()\n * @param item The Item to get the appData value from.\n * @param key The key to get the value from.\n */\n public getItemAppDataValue(item: OutgoingItemMessagePayload | undefined, key: AppDataField | string): any {\n const defaultDomain = 'org.standardnotes.sn'\n const domainData = item?.content?.appData?.[defaultDomain]\n return domainData?.[key as AppDataField]\n }\n}\n"],"names":["root","factory","exports","module","define","amd","self","__webpack_require__","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Environment","getRandomValues","rnds8","Uint8Array","rng","crypto","bind","msCrypto","Error","uuid","byteToHex","i","push","toString","substr","options","buf","offset","rnds","random","arr","arguments","length","undefined","toLowerCase","TypeError","environmentToString","environment","map","Web","Desktop","Mobile","noop","Logger","isSupported","window","console","info","this","enabled","log","error","KeyboardModifier","MessagePayloadApi","ComponentAction","ComponentRelay","constructor","params","activeThemes","acceptsThemes","targetWindow","coallesedSaving","coallesedSavingDelay","component","debug","contentWindow","registerMessageHandler","registerKeyboardEventListeners","registerMouseEventListeners","deinit","onReady","messageQueue","sentMessages","lastStreamedItem","pendingSaveItems","pendingSaveTimeout","pendingSaveParams","messageHandler","document","removeEventListener","keyDownEventListener","keyUpEventListener","clickEventListener","event","data","referrer","URL","origin","parsedData","str","result","JSON","parse","type","e","isValidJsonString","action","ComponentRegistered","handleMessage","addEventListener","ctrlKey","keyDownEvent","Ctrl","shiftKey","Shift","metaKey","Meta","keyUpEvent","_event","mouseClickEvent","payload","sessionKey","componentData","ActivateThemes","activateThemes","themes","original","originalMessage","filter","message","messageId","extensionName","title","alertMessage","replace","callback","platform","postMessage","activeThemeUrls","ThemesActivated","getSelfComponentUUID","isRunningInDesktopApplication","isRunningInMobileApplication","getComponentDataValueForKey","setComponentDataValueForKey","value","SetComponentData","clearComponentData","api","Component","SaveItems","height","handleRequestForContentHeight","generateUUID","sentMessage","stringify","postMessagePayload","parent","incomingUrls","sort","themesToActivate","themesToDeactivate","activeUrl","includes","candidate","themeUrl","deactivateTheme","link","createElement","id","btoa","href","rel","media","className","getElementsByTagName","appendChild","onThemesChange","themeElementForUrl","Array","from","getElementsByClassName","slice","find","element","parentNode","setAttribute","removeChild","uuidv4","streamItems","contentTypes","StreamItems","content_types","items","streamContextItem","StreamContextItem","item","clearTimeout","performSavingOfItems","createItem","CreateItem","jsonObjectForItem","createItems","mapped","CreateItems","deleteItem","deleteItems","DeleteItems","sendCustomEvent","saveItem","skipDebouncer","saveItems","saveItemWithPresave","presave","saveItemsWithPresave","concernTimeout","setTimeout","concernTimeouts","forEach","timeout","alert","mappedItems","incomingIds","preexistingItems","concat","setSize","width","SetSize","keyboardModifier","KeyDown","KeyUp","Click","copy","assign","children","getItemAppDataValue","domainData","content","appData"],"sourceRoot":""}
--------------------------------------------------------------------------------