diff --git a/packages/sdk/.codeclimate.yml b/packages/sdk/.codeclimate.yml new file mode 100644 index 00000000..c824599f --- /dev/null +++ b/packages/sdk/.codeclimate.yml @@ -0,0 +1,12 @@ +version: "2" +prepare: + fetch: + - url: 'https://raw.githubusercontent.com/getstation/tslint-config-station/master/tslint.json' + path: 'tslint.json' +checks: + return-statements: + enabled: false +plugins: + tslint: + enabled: true + config: tslint.json diff --git a/packages/sdk/.gitignore b/packages/sdk/.gitignore new file mode 100644 index 00000000..c4a6659a --- /dev/null +++ b/packages/sdk/.gitignore @@ -0,0 +1,65 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# IDE +.idea/ + +# Build +lib/ + diff --git a/packages/sdk/.npmignore b/packages/sdk/.npmignore new file mode 100644 index 00000000..6c3b48c6 --- /dev/null +++ b/packages/sdk/.npmignore @@ -0,0 +1,11 @@ +node_modules/ +npm-debug.log +yarn-error.log +.vscode/ +tests/ +src/ +.git* +.jshint* +.npmignore +.travis.yml +*.tar.gz diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md new file mode 100644 index 00000000..0594a44a --- /dev/null +++ b/packages/sdk/CHANGELOG.md @@ -0,0 +1,64 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [UNRELEASED] +### Added +- Added `tabs.create` method +- Added ServicesConsumer + +## [0.8.0] - 2018-12-12 +### Added +- Added silent option for `tabs.navToTab` method +- Added `sdk.tabs.updateTab` +### Removed +- Removed `sdk.tabs.dispatchURLInTab` which was misleading and not used + +## [0.6.1] - 2018-08-27 +### Added +- Tabs API can now be observed +- Tabs API nav Observable + +## [0.6.0] - 2018-08-27 +### Added +- Session API + +## [0.5.0] - 2018-08-24 +### Added +- Activity API + +## [0.4.1] - 2018-08-16 +### Added +- Add context field to search result items + +## [0.4.0] - 2018-06-07 +### Added +- Multiple GDrive accounts support + +## [0.3.0] - 2018-06-04 +### Added +- IPC Consumer +- Add context field in SearchResultItem + +## [0.2.1] - 2018-05-31 +### Fixed +- add `id` to search consumer + +## [0.2.0] - 2018-05-29 +### Changed +- `storage` now follows [WebExtensions API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/local) + +## [0.1.0] - 2018-05-24 +### Changed +- SDK is now instanciated by browserX only + +## [0.0.9] - 2018-05-24 +### Added +- Tabs Consumer + +## [0.0.1] to [0.0.8] - 2018 +### Added +- Storage Consumer +- Search Consumer diff --git a/packages/sdk/README.md b/packages/sdk/README.md new file mode 100644 index 00000000..27a6f2c9 --- /dev/null +++ b/packages/sdk/README.md @@ -0,0 +1,4 @@ +⚠️ Work in progress + +# sdk +Station SDK diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 00000000..d9418369 --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,39 @@ +{ + "name": "@getstation/sdk", + "version": "0.16.0", + "author": "joel@getstation.com", + "license": "ISC", + "description": "Station SDK", + "main": "./lib/index", + "types": "./lib/index", + "bugs": { + "url": "https://github.com/getstation/sdk/issues" + }, + "homepage": "https://github.com/getstation/sdk#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/getstation/sdk.git" + }, + "scripts": { + "build": "tsc -p .", + "prepublishOnly": "yarn run lint && yarn run build", + "lint": "tslint -p ." + }, + "peerDependencies": { + "rxjs": "^6.0.0" + }, + "dependencies": { + "tslib": "^1.9.3" + }, + "devDependencies": { + "@types/react": "^16.3.14", + "@types/react-dom": "^16.0.5", + "rxjs": "^6.4.0", + "tslint": "^5.12.1", + "tslint-config-station": "^0.6.0", + "typescript": "^3.9.4" + }, + "toolchain": { + "node": "10.15.3" + } +} diff --git a/packages/sdk/src/activity/consumer.ts b/packages/sdk/src/activity/consumer.ts new file mode 100644 index 00000000..dd1e8f89 --- /dev/null +++ b/packages/sdk/src/activity/consumer.ts @@ -0,0 +1,54 @@ +import { from, Observable } from 'rxjs'; +import { flatMap } from 'rxjs/operators'; + +import { Consumer, DefaultWeakMap } from '../common'; + +import { activity } from './index'; + +import QueryArgs = activity.QueryArgs; +import ActivityEntry = activity.ActivityEntry; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class ActivityConsumer extends Consumer implements activity.ActivityConsumer { + public readonly namespace = 'activity'; + + push(resourceId: string, extraData?: object, type?: string, manifestURL?: string): Promise<{ activityEntryId: string }> { + return protectedProvidersWeakMap.get(this)!.push({ + type: type || '', + createdAt: Date.now(), + resourceId, + manifestURL: manifestURL, + extraData, + }); + } + + query(userQueryArgs: Partial = {}): Observable { + const queryArgs: QueryArgs = { + orderBy: 'createdAt', + ascending: false, + limit: 1, + global: false, + where: { + resourceIds: null, + manifestURLs: null, + types: null, + }, + whereNot: { + resourceIds: null, + manifestURLs: null, + types: null, + }, + ...userQueryArgs, + }; + + const consumer = protectedProvidersWeakMap.get(this)!; + + return from(consumer.query(queryArgs)) + .pipe(flatMap(c => c)); + } + + setProviderInterface(providerInterface: activity.ActivityProviderInterface) { + protectedProvidersWeakMap.set(this, providerInterface); + } +} diff --git a/packages/sdk/src/activity/index.ts b/packages/sdk/src/activity/index.ts new file mode 100644 index 00000000..13c7bc49 --- /dev/null +++ b/packages/sdk/src/activity/index.ts @@ -0,0 +1,65 @@ +import { Observable } from 'rxjs'; + +import { Consumer } from '../common'; + +export namespace activity { + export interface ActivityConsumer extends Consumer { + /** + * Will push an activity entry for the given search result item. + * + * @param resourceId the id of the SearchResultItem this activity corresponds to + * @param manifestURL the manifestURL corresponding to the resource id + * @param extraData a set of key-value that can be used for anything + * @param type of activity + * @returns a promise with the activityEntryId (which represents the id in sqlite db) + */ + push(resourceId: string, extraData?: any, type?: string, manifestURL?: string): Promise<{ activityEntryId: string }>; + + /** + * Query the last **n** activity logs of the plugin. + * + * @example + * // get the last 10 pushed activity entries + * sdk.activity.query({limit: 10, ascending: false}).subscribe(items => { + * doSomething(items); + * }) + * + * @param query a QueryArgs item + * @returns an observable of array of ActivityEntry + */ + query(query: QueryArgs): Observable; + + setProviderInterface(providerInterface: ActivityProviderInterface): void; + } + + export interface ActivityEntry { + resourceId: string, // the id of the SearchResultItem this activity corresponds to + manifestURL?: string, // the service id corresponding to the resource id + type: string, // type of activity entry + extraData?: any, // a set of key-value that can be used for anything + createdAt: number, // timestamp at which the activity was made. By default, at the time the function is called + } + + export type ScopeFilter = string[] | string | null; + + export type QueryArgsScope = { + resourceIds: ScopeFilter, + manifestURLs: ScopeFilter, + types: ScopeFilter, + }; + + export interface QueryArgs { + orderBy: 'createdAt', // get the last n results or the first n results + ascending: boolean, // ascending or descending (default to false) + limit: number, // limit to the n first result (default to 1) + limitByDate?: number // timestamp in ms, limit only newer activity form thid date + global: boolean, // global or plugin activity (default to false) + where: Partial, + whereNot: Partial, + } + + export interface ActivityProviderInterface { + push(activityEntry: ActivityEntry): Promise<{ activityEntryId: string }>; + query(query: QueryArgs): Promise>; + } +} diff --git a/packages/sdk/src/common.ts b/packages/sdk/src/common.ts new file mode 100644 index 00000000..d94066e5 --- /dev/null +++ b/packages/sdk/src/common.ts @@ -0,0 +1,27 @@ +export class Consumer { + public readonly namespace: string; + public readonly id: string; + + constructor(id: string) { + this.id = id; + } +} + +const defaultWeakMapValue: any = new Proxy({}, { + get(_obj: any, prop: string | symbol) { + console.warn(`${String(prop)} provider not implemented`); + return defaultWeakMapValue; + }, + apply() { + return () => defaultWeakMapValue; + }, +}); + +export class DefaultWeakMap extends WeakMap { + get(key: K): V | undefined { + if (super.has(key)) { + return super.get(key); + } + return defaultWeakMapValue; + } +} diff --git a/packages/sdk/src/config/consumer.ts b/packages/sdk/src/config/consumer.ts new file mode 100644 index 00000000..fee1cee4 --- /dev/null +++ b/packages/sdk/src/config/consumer.ts @@ -0,0 +1,21 @@ +import { Consumer, DefaultWeakMap } from '../common'; + +import { config } from './index'; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class ConfigConsumer extends Consumer implements config.ConfigConsumer { + public readonly namespace = 'config'; + + get configData() { + return protectedProvidersWeakMap.get(this)!.configData; + } + + setProviderInterface(providerInterface: config.ConfigProviderInterface) { + protectedProvidersWeakMap.set(this, providerInterface); + } + + setIcon(applicationId: string, url: string): void { + return protectedProvidersWeakMap.get(this)!.setIcon(applicationId, url); + } +} diff --git a/packages/sdk/src/config/index.ts b/packages/sdk/src/config/index.ts new file mode 100644 index 00000000..c039b642 --- /dev/null +++ b/packages/sdk/src/config/index.ts @@ -0,0 +1,39 @@ +import { Observable } from 'rxjs'; + +import { Consumer } from '../common'; + +export namespace config { + + export interface ConfigConsumer extends Consumer { + readonly id: string; + + /** + * Get configData for current application + * @example + * sdk.config.configData + * .subscribe(configData => { + * // ... configData.subdomain + * }); + */ + readonly configData: Observable; + + /** + * Update Dock icon for current application with given URL + * @example + * sdk.config.setIcon('https://domain.tld/myicon.png'); + */ + setIcon(applicationId: string, url: string): void; + + setProviderInterface(providerInterface: config.ConfigProviderInterface): void; + } + + export interface ConfigProviderInterface { + configData: Observable; + setIcon(applicationId: string, url: string): void; + } + + export type ConfigData = { + applicationId: string, + subdomain?: string, + }; +} diff --git a/packages/sdk/src/history/consumer.ts b/packages/sdk/src/history/consumer.ts new file mode 100644 index 00000000..8362c7ba --- /dev/null +++ b/packages/sdk/src/history/consumer.ts @@ -0,0 +1,15 @@ +import { BehaviorSubject } from 'rxjs'; + +import { Consumer } from '../common'; + +import { history } from '.'; + +export class HistoryConsumer extends Consumer implements history.HistoryConsumer { + public readonly namespace = 'history'; + public entries: BehaviorSubject; + + constructor(id: string) { + super(id); + this.entries = new BehaviorSubject([]); + } +} diff --git a/packages/sdk/src/history/index.ts b/packages/sdk/src/history/index.ts new file mode 100644 index 00000000..2dd9b16a --- /dev/null +++ b/packages/sdk/src/history/index.ts @@ -0,0 +1,26 @@ +import { BehaviorSubject } from 'rxjs'; + +import { Consumer } from '../common'; +import { search } from '../index'; + +export namespace history { + + export interface HistoryConsumer extends Consumer { + readonly id: string; + + /** + * Push the history of the application usage + * @example + * sdk.activity + * .query({ limit: 10 }) + * .subscribe((activityEntries: activity.ActivityEntry[]) => { + * sdk.history.entries.next(activityAsHistory(activityEntries)); + * }); + */ + readonly entries: BehaviorSubject; + } + + export type HistoryEntry = search.SearchResultItem & { + date: Date, + }; +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts new file mode 100644 index 00000000..596551d7 --- /dev/null +++ b/packages/sdk/src/index.ts @@ -0,0 +1,120 @@ +// tslint:disable-next-line:no-import-side-effect +import 'rxjs'; + +import { ActivityConsumer } from './activity/consumer'; +import { ConfigConsumer } from './config/consumer'; +import { HistoryConsumer } from './history/consumer'; +import { IpcConsumer } from './ipc/consumer'; +import { ReactConsumer } from './react/consumer'; +import { SearchConsumer } from './search/consumer'; +import { SessionConsumer } from './session/consumer'; +import { StorageConsumer } from './storage/consumer'; +import { TabsConsumer } from './tabs/consumer'; +import { ResourcesConsumer } from './resources/consumer'; + +export * from './common'; +export * from './search'; +export * from './storage'; +export * from './tabs'; +export * from './session'; +export * from './ipc'; +export * from './react'; +export * from './activity'; +export * from './history'; +export * from './config'; +export * from './resources'; + +export type Consumers = + SearchConsumer | + StorageConsumer | + TabsConsumer | + SessionConsumer | + IpcConsumer | + ReactConsumer | + ActivityConsumer | + HistoryConsumer | + ConfigConsumer | + ResourcesConsumer; + +export type ConsumersNamespaces = Pick; + +export interface SDKOptions { + id: string, + name: string, +} + +export interface SDK { + readonly search: SearchConsumer, + readonly storage: StorageConsumer, + readonly tabs: TabsConsumer, + readonly session: SessionConsumer, + readonly ipc: IpcConsumer, + readonly react: ReactConsumer, + readonly activity: ActivityConsumer, + readonly history: HistoryConsumer, + readonly config: ConfigConsumer, + readonly resources: ResourcesConsumer, + register(consumer: Consumers): void, + unregister(consumer: Consumers): void, + close(): void, +} + +export interface Provider { + register(consumer: Consumers): void; + unregister(consumer: Consumers): void; +} + +export default function sdk(options: SDKOptions, provider: Provider): SDK { + const search = new SearchConsumer(options.id); + const storage = new StorageConsumer(options.id); + const tabs = new TabsConsumer(options.id); + const session = new SessionConsumer(options.id); + const ipc = new IpcConsumer(options.id); + const react = new ReactConsumer(options.id); + const activity = new ActivityConsumer(options.id); + const config = new ConfigConsumer(options.id); + const resources = new ResourcesConsumer(options.id); + provider.register(search); + provider.register(storage); + provider.register(tabs); + provider.register(session); + provider.register(ipc); + provider.register(react); + provider.register(activity); + provider.register(config); + provider.register(resources); + const bxsdk = { + search, + storage, + tabs, + session, + ipc, + react, + activity, + config, + resources, + register(consumer: Consumers) { + provider.register(consumer); + // tslint:disable-next-line:no-invalid-this + this[consumer.namespace] = consumer; + }, + unregister(consumer: Consumers) { + provider.unregister(consumer); + // tslint:disable-next-line:no-invalid-this + delete this[consumer.namespace]; + }, + close() { + provider.unregister(search); + provider.unregister(storage); + provider.unregister(tabs); + provider.unregister(session); + provider.unregister(ipc); + provider.unregister(react); + provider.unregister(activity); + provider.unregister(config); + provider.unregister(resources); + }, + }; + // @ts-ignore + return bxsdk; +} diff --git a/packages/sdk/src/ipc/consumer.ts b/packages/sdk/src/ipc/consumer.ts new file mode 100644 index 00000000..21f8d1e2 --- /dev/null +++ b/packages/sdk/src/ipc/consumer.ts @@ -0,0 +1,27 @@ +import { Observable, observable as Symbol_observable, Subscribable } from 'rxjs'; + +import { Consumer, DefaultWeakMap } from '../common'; + +import { ipc } from './index'; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class IpcConsumer extends Consumer implements ipc.IpcConsumer { + + public readonly namespace = 'ipc'; + public subscribe: Subscribable['subscribe']; + + // tslint:disable-next-line + [Symbol_observable](): Observable { + return protectedProvidersWeakMap.get(this)!.bxToPluginChannel; + } + + publish(args: any) { + protectedProvidersWeakMap.get(this)!.pluginToBxChannel.next(args); + } + + setProviderInterface(providerInterface: ipc.IpcProviderInterface) { + protectedProvidersWeakMap.set(this, providerInterface); + this.subscribe = providerInterface.bxToPluginChannel.subscribe.bind(providerInterface.bxToPluginChannel); + } +} diff --git a/packages/sdk/src/ipc/index.ts b/packages/sdk/src/ipc/index.ts new file mode 100644 index 00000000..5607a9d8 --- /dev/null +++ b/packages/sdk/src/ipc/index.ts @@ -0,0 +1,43 @@ +import { Observable, observable as Symbol_observable, Subject, Subscribable } from 'rxjs'; + +import { Consumer } from '../common'; + +export namespace ipc { + + /** + * Communicate with other processes of the current plugin + * @example + * // Code executed by a renderer process + * Observable.from(sdk.ipc).subscribe((message) => { + * console.log(message.message); // hello world + * }); + * + * // Code executed by the main process + * sdk.ipc.publish({ message: 'hello world' }); + */ + export interface IpcConsumer extends Consumer, Subscribable { + readonly id: string; + + // @ts-ignore: Typescript limitation until Symbol.observable is considered native + [Symbol_observable](): Observable; + + /** + * Sends a message to all other processes of the plugin + * @param args + */ + publish(args: any): void; + + /** + * Internal usage - Set the provider for this consumer + * @protected + * @param {ipc.IpcProviderInterface} providerInterface + */ + setProviderInterface(providerInterface: IpcProviderInterface): void; + } + + export interface IpcProviderInterface { + pluginToBxChannel: Subject; + bxToPluginChannel: Observable; + } + +} diff --git a/packages/sdk/src/react/consumer.ts b/packages/sdk/src/react/consumer.ts new file mode 100644 index 00000000..84b00001 --- /dev/null +++ b/packages/sdk/src/react/consumer.ts @@ -0,0 +1,20 @@ +import { ComponentClass } from 'react'; + +import { Consumer, DefaultWeakMap } from '../common'; + +import { react } from './index'; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class ReactConsumer extends Consumer implements react.ReactConsumer { + + public readonly namespace = 'react'; + + createPortal(children: ComponentClass, id: react.ValidPortalIds, position?: number) { + protectedProvidersWeakMap.get(this)!.createPortal(children, id, position); + } + + setProviderInterface(providerInterface: react.ReactProviderInterface) { + protectedProvidersWeakMap.set(this, providerInterface); + } +} diff --git a/packages/sdk/src/react/index.ts b/packages/sdk/src/react/index.ts new file mode 100644 index 00000000..6512a62e --- /dev/null +++ b/packages/sdk/src/react/index.ts @@ -0,0 +1,25 @@ +import { ComponentClass } from 'react'; + +import { Consumer } from '../common'; + +export namespace react { + + export interface ReactConsumer extends Consumer { + /** + * Will render the given children in specified destination + * @param {React.ComponentClass} children + * @param {react.ValidPortalIds} id + * @example + * sdk.react.createPortal(MyReactComponent, 'quickswitch'); + */ + createPortal(children: ComponentClass, id: react.ValidPortalIds, position?: number): void; + setProviderInterface(providerInterface: react.ReactProviderInterface): void; + } + + export interface ReactProviderInterface { + createPortal(children: ComponentClass, id: react.ValidPortalIds, position?: number): void; + } + + export type ValidPortalIds = 'quickswitch'; + +} diff --git a/packages/sdk/src/resources/consumer.ts b/packages/sdk/src/resources/consumer.ts new file mode 100644 index 00000000..85c4b3eb --- /dev/null +++ b/packages/sdk/src/resources/consumer.ts @@ -0,0 +1,25 @@ +import { Consumer, DefaultWeakMap } from '../common'; + +import { resources } from '.'; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class ResourcesConsumer extends Consumer implements resources.ResourcesConsumer { + public readonly namespace = 'resources'; + + constructor(manifestURL: string) { + super(manifestURL); + } + + setOpenHandler(handler: resources.OpenHandler) { + return protectedProvidersWeakMap.get(this)!.setOpenHandler(this.id, handler); + } + + setMetaDataHandler(handler: resources.MetaDataHandler) { + return protectedProvidersWeakMap.get(this)!.setMetaDataHandler(this.id, handler); + } + + setProviderInterface(providerInterface: resources.ResourcesProviderInterface): void { + protectedProvidersWeakMap.set(this, providerInterface); + } +} diff --git a/packages/sdk/src/resources/index.ts b/packages/sdk/src/resources/index.ts new file mode 100644 index 00000000..65d87c3f --- /dev/null +++ b/packages/sdk/src/resources/index.ts @@ -0,0 +1,28 @@ +import { Consumer } from '../common'; + +export namespace resources { + export type OpenHandler = (url: string, defaultOpen: () => Promise) => Promise; + export type MetaDataHandler = (url: string, defaultMetadata: ResourceMetaData) => Promise; + + export interface ResourcesConsumer extends Consumer { + setOpenHandler: (handler: OpenHandler) => void; + setMetaDataHandler: (handler: MetaDataHandler) => void; + setProviderInterface(providerInterface: resources.ResourcesProviderInterface): void + } + + export interface ResourcesProviderInterface { + setOpenHandler: (manifestURL: string, handler: OpenHandler) => void; + setMetaDataHandler: (manifestURL: string, handler: MetaDataHandler) => void; + } + + // http://ogp.me/#metadata + export interface ResourceMetaData { + bxResourceId: string, + manifestURL: string, + image: string, + title: string, + description?: string, + themeColor?: string, + url?: string, + } +} diff --git a/packages/sdk/src/search/consumer.ts b/packages/sdk/src/search/consumer.ts new file mode 100644 index 00000000..b656dc2a --- /dev/null +++ b/packages/sdk/src/search/consumer.ts @@ -0,0 +1,20 @@ +import { BehaviorSubject } from 'rxjs'; + +import { Consumer } from '../common'; + +import { search } from './index'; + +export class SearchConsumer extends Consumer implements search.SearchConsumer { + + public readonly namespace = 'search'; + + public query: BehaviorSubject; + public results: BehaviorSubject; + + constructor(id: string) { + super(id); + this.query = new BehaviorSubject({ value: '' }); + this.results = new BehaviorSubject({}); + } + +} diff --git a/packages/sdk/src/search/index.ts b/packages/sdk/src/search/index.ts new file mode 100644 index 00000000..ab51aac1 --- /dev/null +++ b/packages/sdk/src/search/index.ts @@ -0,0 +1,79 @@ +import { BehaviorSubject, Subject } from 'rxjs'; + +import { Consumer } from '../common'; + +export namespace search { + + export interface SearchConsumer extends Consumer { + readonly id: string; + + /** + * Receive query string updates + * @example + * sdk.search.query.subscribe(query => { + * // Query external provider + * fetch(`https://api.example.com/search/${encodeURIComponent(query.value)}`) + * .then(response => response.json()) + * .then(data => console.log(data)); + * }); + */ + readonly query: Subject; + + /** + * Push search results + * @example + * sdk.search.results.next({ + * loading: 'My Category', + * results: [ + * { + * id: 'search-result-1', + * category: 'My Category', + * label: 'Pizza list', + * url: 'https://my.example.com/pizza', + * imgUrl: 'https://my.example.com/pizza.png', + * }, + * { + * id: 'search-result-2', + * category: 'My Other Category', + * label: 'Message Georges', + * imgUrl: 'https://my.example.com/pizza.png', + * onSelect: async () => { + * const tabId = this.sdk.tabs.getTabs()[0].tabId; + * const code = ` + * var state = { page: '/messages/${id}' }; + * history.pushState(state, '', '${this.teamDomain}/messages/${id}'); + * var popStateEvent = new PopStateEvent('popstate', { state: state }); + * dispatchEvent(popStateEvent); + * `; + * this.sdk.tabs.executeJavaScript(tabId, code); + * await this.sdk.tabs.navToTab(tabId); + * } + * }, + * ..., + * ], + * }); + */ + readonly results: BehaviorSubject; + } + + export type SearchResultItem = { + resourceId: string, + category: string, + additionalSearchString?: string, + manifestURL?: string, + label: string, + context?: string, + url?: string, + imgUrl: string, + onSelect?: () => void, + }; + + export interface SearchResultWrapper { + results?: SearchResultItem[], + loading?: string, + } + + export interface SearchQuery { + value: string, + } +} diff --git a/packages/sdk/src/session/consumer.ts b/packages/sdk/src/session/consumer.ts new file mode 100644 index 00000000..f40e58ab --- /dev/null +++ b/packages/sdk/src/session/consumer.ts @@ -0,0 +1,22 @@ +import { Consumer, DefaultWeakMap } from '../common'; + +import { session } from './index'; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class SessionConsumer extends Consumer implements session.SessionConsumer { + + public readonly namespace = 'session'; + + getUserAgent() { + return protectedProvidersWeakMap.get(this)!.getUserAgent(); + } + + getCookies() { + return protectedProvidersWeakMap.get(this)!.getCookies(this.id); + } + + setProviderInterface(providerInterface: session.SessionProviderInterface) { + protectedProvidersWeakMap.set(this, providerInterface); + } +} diff --git a/packages/sdk/src/session/index.ts b/packages/sdk/src/session/index.ts new file mode 100644 index 00000000..85da6cf8 --- /dev/null +++ b/packages/sdk/src/session/index.ts @@ -0,0 +1,69 @@ +import { Consumer } from '../common'; + +export namespace session { + + export interface SessionConsumer extends Consumer { + readonly id: string; + /** + * Get Station User Agent + * @example + * const userAgent = await sdk.session.getUserAgent(); + */ + getUserAgent(): string; + /** + * List all cookies corresponding to the current service + * @example + * const cookies = await sdk.session.getCookies(); + */ + getCookies(): Promise; + setProviderInterface(providerInterface: session.SessionProviderInterface): void + } + + export interface SessionProviderInterface { + getUserAgent(): string; + getCookies(id: string): Promise; + } + + export type Cookie = { + // Docs: http://electron.atom.io/docs/api/structures/cookie + + /** + * The domain of the cookie. + */ + domain?: string; + /** + * The expiration date of the cookie as the number of seconds since the UNIX epoch. + * Not provided for session cookies. + */ + expirationDate?: number; + /** + * Whether the cookie is a host-only cookie. + */ + hostOnly?: boolean; + /** + * Whether the cookie is marked as HTTP only. + */ + httpOnly?: boolean; + /** + * The name of the cookie. + */ + name: string; + /** + * The path of the cookie. + */ + path?: string; + /** + * Whether the cookie is marked as secure. + */ + secure?: boolean; + /** + * Whether the cookie is a session cookie or a persistent cookie with an expiration + * date. + */ + session?: boolean; + /** + * The value of the cookie. + */ + value: string; + }; +} diff --git a/packages/sdk/src/storage/consumer.ts b/packages/sdk/src/storage/consumer.ts new file mode 100644 index 00000000..b9b571ec --- /dev/null +++ b/packages/sdk/src/storage/consumer.ts @@ -0,0 +1,29 @@ +import { Consumer, DefaultWeakMap } from '../common'; + +import { StorageEvent } from './event'; +import { storage } from './index'; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class StorageConsumer extends Consumer implements storage.StorageConsumer { + + public readonly namespace = 'storage'; + public onChanged: StorageEvent; + + constructor(id: string) { + super(id); + this.onChanged = new StorageEvent(); + } + + getItem(key: string) { + return protectedProvidersWeakMap.get(this)!.getItem(this.id, key); + } + + setItem(key: string, value: any) { + return protectedProvidersWeakMap.get(this)!.setItem(this.id, key, value); + } + + setProviderInterface(providerInterface: storage.StorageProviderInterface) { + protectedProvidersWeakMap.set(this, providerInterface); + } +} diff --git a/packages/sdk/src/storage/event.ts b/packages/sdk/src/storage/event.ts new file mode 100644 index 00000000..2fd8d84b --- /dev/null +++ b/packages/sdk/src/storage/event.ts @@ -0,0 +1,38 @@ +export class StorageEvent { + + protected listeners: Function[]; + + constructor() { + this.listeners = []; + } + + addListener(callback: (changes: StorageChanges) => void) { + this.listeners.push(callback); + } + + hasListener(callback: Function) { + return this.listeners.indexOf(callback) !== -1; + } + + removeListener(callback: Function) { + const index = this.listeners.indexOf(callback); + if (index !== -1) { + this.listeners.splice(index, 1); + } + } + + emit(...args: any[]) { + for (const listener of this.listeners) { + listener(...args); + } + } +} + +export interface StorageChange { + oldValue: T | undefined, + newValue: T | undefined, +} + +export interface StorageChanges { + [key: string]: StorageChange +} diff --git a/packages/sdk/src/storage/index.ts b/packages/sdk/src/storage/index.ts new file mode 100644 index 00000000..8ccbb63b --- /dev/null +++ b/packages/sdk/src/storage/index.ts @@ -0,0 +1,39 @@ +import { Consumer } from '../common'; + +import { StorageEvent } from './event'; + +export namespace storage { + + export interface StorageConsumer extends Consumer { + readonly id: string; + /** + * @event onChanged - Fired when one or more items change + * @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/onChanged + */ + readonly onChanged: StorageEvent; + + /** + * Retrieves one item from the storage. + * @param {string} key + * @returns A Promise that will be fulfilled with a results object + * containing the object that was found + */ + getItem(key: string): Promise; + + /** + * Stores one item in the storage area, or update existing item. + * @param {string} key + * @param value + * @fires onChanged + * @returns A Promise that will be fulfilled with no arguments if the operation succeeded + */ + setItem(key: string, value: any): Promise; + setProviderInterface(providerInterface: storage.StorageProviderInterface): void; + } + + export interface StorageProviderInterface { + setItem(consumerKey: string, key: string, value: any): Promise; + getItem(consumerKey: string, key: string): Promise; + } + +} diff --git a/packages/sdk/src/tabs/consumer.ts b/packages/sdk/src/tabs/consumer.ts new file mode 100644 index 00000000..79e03270 --- /dev/null +++ b/packages/sdk/src/tabs/consumer.ts @@ -0,0 +1,47 @@ +import { Observable } from 'rxjs'; + +import { Consumer, DefaultWeakMap } from '../common'; + +import { tabs } from './index'; + +const protectedProvidersWeakMap = new DefaultWeakMap(); + +export class TabsConsumer extends Consumer implements tabs.TabsConsumer { + public readonly namespace = 'tabs'; + + getTabs() { + return protectedProvidersWeakMap.get(this)!.getTabs(this.id); + } + + getTab(tabId: string): Observable { + return protectedProvidersWeakMap.get(this)!.getTab(tabId); + } + + updateTab(tabId: string, updatedTab: tabs.TabUpdate): void { + return protectedProvidersWeakMap.get(this)!.updateTab(tabId, updatedTab); + } + + navToTab(tabId: string, options: tabs.NavToTabOptions = { silent: false }) { + return protectedProvidersWeakMap.get(this)!.navToTab(tabId, options); + } + + nav() { + return protectedProvidersWeakMap.get(this)!.nav(); + } + + create(options: tabs.CreateOptions) { + return protectedProvidersWeakMap.get(this)!.create(options); + } + + getTabWebContentsState(tabId: string) { + return protectedProvidersWeakMap.get(this)!.getTabWebContentsState(tabId); + } + + executeJavaScript(tabId: string, code: string) { + return protectedProvidersWeakMap.get(this)!.executeJavaScript(tabId, code); + } + + setProviderInterface(providerInterface: tabs.TabsProviderInterface) { + protectedProvidersWeakMap.set(this, providerInterface); + } +} diff --git a/packages/sdk/src/tabs/index.ts b/packages/sdk/src/tabs/index.ts new file mode 100644 index 00000000..8bf08f09 --- /dev/null +++ b/packages/sdk/src/tabs/index.ts @@ -0,0 +1,126 @@ +import { Observable } from 'rxjs'; + +import { Consumer } from '../common'; + +export namespace tabs { + + export interface TabsConsumer extends Consumer { + readonly id: string; + + /** + * List all tabs + * @example + * const serviceTabs = sdk.tabs.getTabs(); + */ + getTabs(): Tab[]; + /** + * Receive tab updates + * @example + * sdk.tabs.getTab('mysaas-XXXXXXXXX/XXXXXXXXX').subscribe(tab => { + * ... + * }); + */ + getTab(id: string): Observable; + + /** + * Modifies the properties of a tab. + * + * Properties that are not specified in updatedTab are not modified. + * + * Modifying the url will trigger a navigation. + * @example + * sdk.tabs.updateTab('mysaas-XXXXXXXXX/XXXXXXXXX', { url: 'https://google.com' }). + */ + updateTab(tabId: string, updatedTab: TabUpdate): void; + /** + * Receive nav updates + * @example + * const nav = sdk.tabs.nav(); + */ + nav(): Observable; + /** + * Create a tab for given application and navigate to the given url + * @example` + * sdk.tabs.create({ applicationId: 'slack', url: 'https://google.fr' }); + */ + create(options: CreateOptions): void; + /** + * Navigate to given tabId. + * Silent option will notify activity api to allow + * plugins history recording to ignore automatic + * navigation (not triggered by the end user) + * @example + * sdk.tabs.navToTab('mysaas-XXXXXXXXX/XXXXXXXXX'); + */ + navToTab(tabId: string, options?: NavToTabOptions): void + /** + * Get tab webContents state + * @example + * sdk.tabs.getTabWebContentsState('mysaas-XXXXXXXXX/XXXXXXXXX'); + */ + getTabWebContentsState(tabId: string): TabWebContentsState + /** + * Execute javascript code in web view for given tabId + * @example + * const code = ` + * var state = { page: '/messages/${id}' }; + * history.pushState(state, '', 'https://mysaas.com/messages/${id}'); + * var popStateEvent = new PopStateEvent('popstate', { state: state }); + * dispatchEvent(popStateEvent); + * `; + * sdk.tabs.executeJavaScript('mysaas-XXXXXXXXX/XXXXXXXXX', code); + */ + executeJavaScript(tabId: string, code: string): Promise; + setProviderInterface(providerInterface: tabs.TabsProviderInterface): void + } + + export interface TabsProviderInterface { + getTabs(id: string): Tab[]; + getTab(tabId: string): Observable; + nav(): Observable; + create(options: CreateOptions): void; + updateTab(tabId: string, updatedTab: TabUpdate): void; + navToTab(tabId: string, options: NavToTabOptions): Promise + executeJavaScript(tabId: string, code: string): Promise; + getTabWebContentsState(tabId: string): TabWebContentsState + } + + export type NavToTabOptions = { + silent: boolean, + }; + + export interface CreateOptions { + applicationId: string, + url: string, + } + + export type Tab = { + applicationId: string, + badge: string, + canGoBack: boolean, + canGoForward: boolean, + favicons: string[], + isApplicationHome: boolean, + isLoading: boolean, + tabId: string, + title: string, + url: string, + }; + + export type TabUpdate = { + url?: string, + }; + + export type Nav = { + tabId: string, + previousTabId: string, + }; + + export enum TabWebContentsState { + notMounted, + waitingToAttach, + detaching, + mounted, + crashed, + } +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 00000000..00ca8124 --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "allowJs": false, + "declaration": true, + "preserveConstEnums": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "suppressImplicitAnyIndexErrors": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noImplicitThis": true, + "noUnusedParameters": true, + "noEmitHelpers": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "es2016", + "outDir": "./lib", + "jsx": "react", + "lib": [ + "es2017", + "dom" + ], + "typeRoots": ["./node_modules/@types"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} diff --git a/packages/sdk/tslint.json b/packages/sdk/tslint.json new file mode 100644 index 00000000..e1203cff --- /dev/null +++ b/packages/sdk/tslint.json @@ -0,0 +1,9 @@ +{ + "extends": [ + "tslint-config-station" + ], + "rules": { + "jsx-no-multiline-js": false, + "import-blacklist": false + } +} diff --git a/yarn.lock b/yarn.lock index b951e039..f77726ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2759,6 +2759,22 @@ __metadata: languageName: node linkType: hard +"@getstation/sdk@workspace:packages/sdk": + version: 0.0.0-use.local + resolution: "@getstation/sdk@workspace:packages/sdk" + dependencies: + "@types/react": "npm:^16.3.14" + "@types/react-dom": "npm:^16.0.5" + rxjs: "npm:^6.4.0" + tslib: "npm:^1.9.3" + tslint: "npm:^5.12.1" + tslint-config-station: "npm:^0.6.0" + typescript: "npm:^3.9.4" + peerDependencies: + rxjs: ^6.0.0 + languageName: unknown + linkType: soft + "@getstation/slack@npm:^13.0.0": version: 13.0.0 resolution: "@getstation/slack@npm:13.0.0" @@ -30663,7 +30679,7 @@ __metadata: languageName: node linkType: hard -"tslint@npm:^5.11.0": +"tslint@npm:^5.11.0, tslint@npm:^5.12.1": version: 5.20.1 resolution: "tslint@npm:5.20.1" dependencies: