diff --git a/.prettierrc b/.prettierrc index e671215d..bcea5cc2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,7 @@ { "singleQuote": true, "arrowParens": "avoid", + "useTabs": true, "plugins": ["@trivago/prettier-plugin-sort-imports"], "importOrder": ["", "^[./]"], "importOrderSeparation": true, diff --git a/eslint.config.js b/eslint.config.js index 81b335df..d6e13955 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,7 +10,8 @@ export default tseslint.config( eslintConfigPrettier, { rules: { - '@typescript-eslint/no-unused-vars': "warn" + '@typescript-eslint/no-unused-vars': "warn", + "@typescript-eslint/no-explicit-any": "warn" } } ); diff --git a/flake.lock b/flake.lock index ff7d2b3a..f781d850 100644 --- a/flake.lock +++ b/flake.lock @@ -1573,11 +1573,11 @@ "launcher_6": { "flake": false, "locked": { - "lastModified": 1712599324, - "narHash": "sha256-MNUTplS5O+w1G70kZMpFGfBTqOzzTRU6DYarl1bGVFY=", + "lastModified": 1713340250, + "narHash": "sha256-J8dcl4TiUB93/08oO2Bh0qG6Qi+udbm6JrmPB92NZyU=", "owner": "holochain", "repo": "launcher", - "rev": "597592253c9d2ca64a4e4ac4675ff5081c9dd9b5", + "rev": "f7b7aabd3c3ef16edd391b0b94c4223a2de98d5b", "type": "github" }, "original": { @@ -2444,11 +2444,11 @@ "scaffolding_8": { "flake": false, "locked": { - "lastModified": 1712841191, - "narHash": "sha256-96bq4Yo50p8Nu1CmsnmEOjGDHM8+ak8x+7mZ7vFtZHo=", + "lastModified": 1713363855, + "narHash": "sha256-Y9KsDAjlZZab07NL7pI1izxLOYT4BWYTx1h9DilW8Fk=", "owner": "holochain", "repo": "scaffolding", - "rev": "63a2c7966c21abff1c0397fbf4fde7329d0092cd", + "rev": "9ac485d52122b92bd2988a8fea1a8e4d9a18c3a1", "type": "github" }, "original": { @@ -2703,18 +2703,18 @@ }, "locked": { "dir": "versions/weekly", - "lastModified": 1713315590, - "narHash": "sha256-hWeNAq+F1rAoYulPFqpQOo0cjeMZVvKXLohnP0MOc9Y=", + "lastModified": 1713531171, + "narHash": "sha256-b8zxn8RWV7P2n/LH2gWZvLf44cXQmPbd2NygsYMvDIk=", "owner": "holochain", "repo": "holochain", - "rev": "d8715775f359211b7031f4bdca1cc89db679ed10", + "rev": "73e231beb85507ea0858eb914ac00f9538c23b15", "type": "github" }, "original": { "dir": "versions/weekly", "owner": "holochain", - "ref": "holochain-0.3.0-beta-dev.46", "repo": "holochain", + "rev": "73e231beb85507ea0858eb914ac00f9538c23b15", "type": "github" } } diff --git a/flake.nix b/flake.nix index 1b26373c..c351824d 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,8 @@ description = "Template for Holochain app development"; inputs = { - versions.url = "github:holochain/holochain/holochain-0.3.0-beta-dev.46?dir=versions/weekly"; + versions.url = + "github:holochain/holochain/73e231beb85507ea0858eb914ac00f9538c23b15?dir=versions/weekly"; holochain.url = "github:holochain/holochain"; holochain.inputs.versions.follows = "versions"; @@ -10,81 +11,66 @@ nixpkgs.follows = "holochain/nixpkgs"; flake-parts.follows = "holochain/flake-parts"; - hc-infra = { - url = "github:holochain-open-dev/infrastructure"; - }; - scaffolding = { - url = "github:holochain-open-dev/templates"; - }; + hc-infra = { url = "github:holochain-open-dev/infrastructure"; }; + scaffolding = { url = "github:holochain-open-dev/templates"; }; profiles.url = "github:holochain-open-dev/profiles/nixify"; }; - + nixConfig = { extra-substituters = [ "https://holochain-open-dev.cachix.org" "https://darksoil-studio.cachix.org" - ]; - extra-trusted-public-keys = [ - "holochain-open-dev.cachix.org-1:3Tr+9in6uo44Ga7qiuRIfOTFXog+2+YbyhwI/Z6Cp4U=" + ]; + extra-trusted-public-keys = [ + "holochain-open-dev.cachix.org-1:3Tr+9in6uo44Ga7qiuRIfOTFXog+2+YbyhwI/Z6Cp4U=" "darksoil-studio.cachix.org-1:UEi+aujy44s41XL/pscLw37KEVpTEIn8N/kn7jO8rkc=" ]; }; - outputs = inputs: - inputs.flake-parts.lib.mkFlake - { - inherit inputs; - specialArgs = { - # Special arguments for the flake parts of this repository - - rootPath = ./.; - }; - } - { - imports = [ - ./zomes/integrity/notifications/zome.nix - ./zomes/coordinator/notifications/zome.nix - # Just for testing purposes - ./workdir/dna.nix - ./workdir/happ.nix - ]; - - systems = builtins.attrNames inputs.holochain.devShells; - perSystem = - { inputs' - , config - , pkgs - , system - , ... - }: { - devShells.default = pkgs.mkShell { - inputsFrom = [ - inputs'.hc-infra.devShells.synchronized-pnpm - inputs'.holochain.devShells.holonix - ]; + inputs.flake-parts.lib.mkFlake { + inherit inputs; + specialArgs = { + # Special arguments for the flake parts of this repository - packages = [ - inputs'.scaffolding.packages.hc-scaffold-zome-template - ]; - }; + rootPath = ./.; + }; + } { + imports = [ + ./zomes/integrity/notifications/zome.nix + ./zomes/coordinator/notifications/zome.nix + # Just for testing purposes + ./workdir/dna.nix + ./workdir/happ.nix + ]; + + systems = builtins.attrNames inputs.holochain.devShells; + perSystem = { inputs', config, pkgs, system, ... }: { + devShells.default = pkgs.mkShell { + inputsFrom = [ + inputs'.hc-infra.devShells.synchronized-pnpm + inputs'.holochain.devShells.holonix + ]; + + packages = [ inputs'.scaffolding.packages.hc-scaffold-zome-template ]; + }; - packages.scaffold = pkgs.symlinkJoin { - name = "scaffold-remote-zome"; - paths = [ inputs'.hc-infra.packages.scaffold-remote-zome ]; - buildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram $out/bin/scaffold-remote-zome \ - --add-flags "notifications \ - --integrity-zome-name notifications_integrity \ - --coordinator-zome-name notifications \ - --remote-zome-git-url github:darksoil-studio/notifications \ - --remote-npm-package-name notifications \ - --remote-npm-package-path ui" \ - # --remote-zome-git-branch main - ''; - }; - }; + packages.scaffold = pkgs.symlinkJoin { + name = "scaffold-remote-zome"; + paths = [ inputs'.hc-infra.packages.scaffold-remote-zome ]; + buildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/scaffold-remote-zome \ + --add-flags "notifications \ + --integrity-zome-name notifications_integrity \ + --coordinator-zome-name notifications \ + --remote-zome-git-url github:darksoil-studio/notifications \ + --remote-npm-package-name notifications \ + --remote-npm-package-path ui" \ + # --remote-zome-git-branch main + ''; + }; }; + }; } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..ccb4a92c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ + +hard_tabs = true diff --git a/ui/src/context.ts b/ui/src/context.ts index 7c55e442..1457ef62 100644 --- a/ui/src/context.ts +++ b/ui/src/context.ts @@ -1,7 +1,7 @@ import { createContext } from '@lit/context'; + import { NotificationsStore } from './notifications-store.js'; export const notificationsStoreContext = createContext( - 'notifications/store' + 'notifications/store', ); - diff --git a/ui/src/elements/create-notification.ts b/ui/src/elements/create-notification.ts index a18fafee..0197a477 100644 --- a/ui/src/elements/create-notification.ts +++ b/ui/src/elements/create-notification.ts @@ -1,23 +1,35 @@ -import { LitElement, html } from 'lit'; -import { repeat } from "lit/directives/repeat.js"; -import { state, property, query, customElement } from 'lit/decorators.js'; -import { ActionHash, Record, DnaHash, AgentPubKey, EntryHash } from '@holochain/client'; +import { + hashProperty, + hashState, + notifyError, + onSubmit, + sharedStyles, + wrapPathInSvg, +} from '@holochain-open-dev/elements'; +import '@holochain-open-dev/elements/dist/elements/display-error.js'; import { EntryRecord } from '@holochain-open-dev/utils'; -import { hashProperty, notifyError, hashState, sharedStyles, onSubmit, wrapPathInSvg } from '@holochain-open-dev/elements'; +import { + ActionHash, + AgentPubKey, + DnaHash, + EntryHash, + Record, +} from '@holochain/client'; import { consume } from '@lit/context'; import { localized, msg } from '@lit/localize'; -import { mdiAlertCircleOutline, mdiDelete } from "@mdi/js"; - +import { mdiAlertCircleOutline, mdiDelete } from '@mdi/js'; +import SlAlert from '@shoelace-style/shoelace/dist/components/alert/alert.js'; +import '@shoelace-style/shoelace/dist/components/alert/alert.js'; +import '@shoelace-style/shoelace/dist/components/button/button.js'; import '@shoelace-style/shoelace/dist/components/card/card.js'; - -import '@holochain-open-dev/elements/dist/elements/display-error.js'; import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'; -import '@shoelace-style/shoelace/dist/components/button/button.js'; import '@shoelace-style/shoelace/dist/components/icon/icon.js'; -import SlAlert from '@shoelace-style/shoelace/dist/components/alert/alert.js'; -import '@shoelace-style/shoelace/dist/components/alert/alert.js'; -import { NotificationsStore } from '../notifications-store.js'; +import { LitElement, html } from 'lit'; +import { customElement, property, query, state } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; + import { notificationsStoreContext } from '../context.js'; +import { NotificationsStore } from '../notifications-store.js'; import { Notification } from '../types.js'; /** @@ -27,110 +39,119 @@ import { Notification } from '../types.js'; @localized() @customElement('create-notification') export class CreateNotification extends LitElement { - /** - * REQUIRED. The notification type for this Notification - */ - @property() - notificationType!: string; - - /** - * OPTIONAl. The notification group for this Notification - */ - @property() - notificationGroup: string | undefined; - - /** - * REQUIRED. The persistent for this Notification - */ - @property() - persistent!: boolean; - - /** - * REQUIRED. The recipients for this Notification - */ - @property() - recipients!: Array; - - /** - * REQUIRED. The content for this Notification - */ - @property() - content!: string; - - - /** - * @internal - */ - @consume({ context: notificationsStoreContext, subscribe: true }) - notificationsStore!: NotificationsStore; - - /** - * @internal - */ - @state() - committing = false; - - /** - * @internal - */ - @query('#create-form') - form!: HTMLFormElement; - - - async createNotification(fields: Partial) { - if (this.notificationType === undefined) throw new Error('Cannot create a new Notification without its notification_type field'); - if (this.persistent === undefined) throw new Error('Cannot create a new Notification without its persistent field'); - if (this.recipients === undefined) throw new Error('Cannot create a new Notification without its recipients field'); - if (this.content === undefined) throw new Error('Cannot create a new Notification without its content field'); - - const notification: Notification = { - notification_type: this.notificationType!, - notification_group: this.notificationGroup!, - persistent: this.persistent!, - recipients: this.recipients!, - content: this.content!, - }; - - try { - this.committing = true; - const record: EntryRecord = await this.notificationsStore.client.createNotification(notification); - - this.dispatchEvent(new CustomEvent('notification-created', { - composed: true, - bubbles: true, - detail: { - notificationHash: record.actionHash - } - })); - - this.form.reset(); - } catch (e: unknown) { - console.error(e); - notifyError(msg("Error creating the notification")); - } - this.committing = false; - } - - render() { - return html` - - ${msg("Create Notification")} - -
this.createNotification(fields))} - > - - ${msg("Create Notification")} -
-
`; - } - - static styles = [sharedStyles]; + /** + * REQUIRED. The notification type for this Notification + */ + @property() + notificationType!: string; + + /** + * OPTIONAl. The notification group for this Notification + */ + @property() + notificationGroup: string | undefined; + + /** + * REQUIRED. The persistent for this Notification + */ + @property() + persistent!: boolean; + + /** + * REQUIRED. The recipients for this Notification + */ + @property() + recipients!: Array; + + /** + * REQUIRED. The content for this Notification + */ + @property() + content!: string; + + /** + * @internal + */ + @consume({ context: notificationsStoreContext, subscribe: true }) + notificationsStore!: NotificationsStore; + + /** + * @internal + */ + @state() + committing = false; + + /** + * @internal + */ + @query('#create-form') + form!: HTMLFormElement; + + async createNotification(fields: Partial) { + if (this.notificationType === undefined) + throw new Error( + 'Cannot create a new Notification without its notification_type field', + ); + if (this.persistent === undefined) + throw new Error( + 'Cannot create a new Notification without its persistent field', + ); + if (this.recipients === undefined) + throw new Error( + 'Cannot create a new Notification without its recipients field', + ); + if (this.content === undefined) + throw new Error( + 'Cannot create a new Notification without its content field', + ); + + const notification: Notification = { + notification_type: this.notificationType!, + notification_group: this.notificationGroup!, + persistent: this.persistent!, + recipients: this.recipients!, + content: this.content!, + }; + + try { + this.committing = true; + const record: EntryRecord = + await this.notificationsStore.client.createNotification(notification); + + this.dispatchEvent( + new CustomEvent('notification-created', { + composed: true, + bubbles: true, + detail: { + notificationHash: record.actionHash, + }, + }), + ); + + this.form.reset(); + } catch (e: unknown) { + console.error(e); + notifyError(msg('Error creating the notification')); + } + this.committing = false; + } + + render() { + return html` + ${msg('Create Notification')} + +
this.createNotification(fields))} + > + ${msg('Create Notification')} +
+
`; + } + + static styles = [sharedStyles]; } diff --git a/ui/src/elements/notification-detail.ts b/ui/src/elements/notification-detail.ts index 66a5e3b6..cdfaf2a9 100644 --- a/ui/src/elements/notification-detail.ts +++ b/ui/src/elements/notification-detail.ts @@ -1,24 +1,28 @@ -import { LitElement, html } from 'lit'; -import { state, property, customElement } from 'lit/decorators.js'; -import { EntryHash, Record, ActionHash } from '@holochain/client'; -import { EntryRecord } from '@holochain-open-dev/utils'; +import { + hashProperty, + notifyError, + renderAsyncStatus, + sharedStyles, + wrapPathInSvg, +} from '@holochain-open-dev/elements'; +import '@holochain-open-dev/elements/dist/elements/display-error.js'; import { subscribe } from '@holochain-open-dev/stores'; -import { renderAsyncStatus, sharedStyles, hashProperty, wrapPathInSvg, notifyError } from '@holochain-open-dev/elements'; +import { EntryRecord } from '@holochain-open-dev/utils'; +import { ActionHash, EntryHash, Record } from '@holochain/client'; import { consume } from '@lit/context'; import { localized, msg } from '@lit/localize'; -import { mdiAlertCircleOutline, mdiPencil, mdiDelete } from '@mdi/js'; - - -import '@shoelace-style/shoelace/dist/components/card/card.js'; +import { mdiAlertCircleOutline, mdiDelete, mdiPencil } from '@mdi/js'; import '@shoelace-style/shoelace/dist/components/alert/alert.js'; -import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'; -import '@holochain-open-dev/elements/dist/elements/display-error.js'; -import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'; import SlAlert from '@shoelace-style/shoelace/dist/components/alert/alert.js'; import '@shoelace-style/shoelace/dist/components/button/button.js'; +import '@shoelace-style/shoelace/dist/components/card/card.js'; +import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'; +import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'; +import { LitElement, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; -import { NotificationsStore } from '../notifications-store.js'; import { notificationsStoreContext } from '../context.js'; +import { NotificationsStore } from '../notifications-store.js'; import { Notification } from '../types.js'; /** @@ -28,71 +32,77 @@ import { Notification } from '../types.js'; @localized() @customElement('notification-detail') export class NotificationDetail extends LitElement { + /** + * REQUIRED. The hash of the Notification to show + */ + @property(hashProperty('notification-hash')) + notificationHash!: ActionHash; + + /** + * @internal + */ + @consume({ context: notificationsStoreContext, subscribe: true }) + notificationsStore!: NotificationsStore; - /** - * REQUIRED. The hash of the Notification to show - */ - @property(hashProperty('notification-hash')) - notificationHash!: ActionHash; + async deleteNotification() { + try { + await this.notificationsStore.client.deleteNotification( + this.notificationHash, + ); - /** - * @internal - */ - @consume({ context: notificationsStoreContext, subscribe: true }) - notificationsStore!: NotificationsStore; + this.dispatchEvent( + new CustomEvent('notification-deleted', { + bubbles: true, + composed: true, + detail: { + notificationHash: this.notificationHash, + }, + }), + ); + } catch (e: unknown) { + console.error(e); + notifyError(msg('Error deleting the notification')); + } + } + renderDetail(entryRecord: EntryRecord) { + return html` + +
+ ${msg('Notification')} - async deleteNotification() { - try { - await this.notificationsStore.client.deleteNotification(this.notificationHash); - - this.dispatchEvent(new CustomEvent('notification-deleted', { - bubbles: true, - composed: true, - detail: { - notificationHash: this.notificationHash - } - })); - } catch (e: unknown) { - console.error(e); - notifyError(msg("Error deleting the notification")); - } - } + this.deleteNotification()} + > +
- renderDetail(entryRecord: EntryRecord) { - return html` - -
- ${msg("Notification")} +
+ + `; + } - this.deleteNotification()}> -
+ render() { + return html`${subscribe( + this.notificationsStore.notifications.get(this.notificationHash).entry, + renderAsyncStatus({ + complete: notification => { + return this.renderDetail(notification); + }, + pending: () => + html`
+ +
`, + error: e => + html``, + }), + )}`; + } -
- -
-
- `; - } - - render() { - return html`${subscribe(this.notificationsStore.notifications.get(this.notificationHash).entry, - renderAsyncStatus({ - complete: notification => { - return this.renderDetail(notification); - }, - pending: () => html`
- -
`, - error: e => html`` - }) - )}`; - } - - static styles = [sharedStyles]; + static styles = [sharedStyles]; } diff --git a/ui/src/elements/notification-summary.ts b/ui/src/elements/notification-summary.ts index e8ce08d6..96020c00 100644 --- a/ui/src/elements/notification-summary.ts +++ b/ui/src/elements/notification-summary.ts @@ -1,19 +1,21 @@ -import { LitElement, html } from 'lit'; -import { state, property, customElement } from 'lit/decorators.js'; -import { EntryHash, Record, ActionHash } from '@holochain/client'; -import { EntryRecord } from '@holochain-open-dev/utils'; +import { + hashProperty, + renderAsyncStatus, + sharedStyles, +} from '@holochain-open-dev/elements'; +import '@holochain-open-dev/elements/dist/elements/display-error.js'; import { subscribe } from '@holochain-open-dev/stores'; -import { renderAsyncStatus, hashProperty, sharedStyles } from '@holochain-open-dev/elements'; +import { EntryRecord } from '@holochain-open-dev/utils'; +import { ActionHash, EntryHash, Record } from '@holochain/client'; import { consume } from '@lit/context'; - import { localized, msg } from '@lit/localize'; - - -import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'; import '@shoelace-style/shoelace/dist/components/card/card.js'; -import '@holochain-open-dev/elements/dist/elements/display-error.js'; -import { NotificationsStore } from '../notifications-store.js'; +import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'; +import { LitElement, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + import { notificationsStoreContext } from '../context.js'; +import { NotificationsStore } from '../notifications-store.js'; import { Notification } from '../types.js'; /** @@ -23,56 +25,59 @@ import { Notification } from '../types.js'; @localized() @customElement('notification-summary') export class NotificationSummary extends LitElement { + /** + * REQUIRED. The hash of the Notification to show + */ + @property(hashProperty('notification-hash')) + notificationHash!: ActionHash; - /** - * REQUIRED. The hash of the Notification to show - */ - @property(hashProperty('notification-hash')) - notificationHash!: ActionHash; + /** + * @internal + */ + @consume({ context: notificationsStoreContext, subscribe: true }) + notificationsStore!: NotificationsStore; - /** - * @internal - */ - @consume({ context: notificationsStoreContext, subscribe: true }) - notificationsStore!: NotificationsStore; + renderSummary(entryRecord: EntryRecord) { + return html`
`; + } - renderSummary(entryRecord: EntryRecord) { - return html` -
+ renderNotification() { + return html`${subscribe( + this.notificationsStore.notifications.get(this.notificationHash).entry, + renderAsyncStatus({ + complete: notification => this.renderSummary(notification), + pending: () => + html`
+ +
`, + error: e => + html``, + }), + )}`; + } -
- `; - } - - renderNotification() { - return html`${subscribe(this.notificationsStore.notifications.get(this.notificationHash).entry, - renderAsyncStatus({ - complete: notification => this.renderSummary(notification), - pending: () => html`
- -
`, - error: e => html`` - }) - )}`; - } - - render() { - return html` this.dispatchEvent(new CustomEvent('notification-selected', { - composed: true, - bubbles: true, - detail: { - notificationHash: this.notificationHash - } - }))}> - ${this.renderNotification()} - `; - } + render() { + return html` + this.dispatchEvent( + new CustomEvent('notification-selected', { + composed: true, + bubbles: true, + detail: { + notificationHash: this.notificationHash, + }, + }), + )} + > + ${this.renderNotification()} + `; + } - - static styles = [sharedStyles]; + static styles = [sharedStyles]; } diff --git a/ui/src/elements/notifications-context.ts b/ui/src/elements/notifications-context.ts index 2ffdc641..8c6d4261 100644 --- a/ui/src/elements/notifications-context.ts +++ b/ui/src/elements/notifications-context.ts @@ -1,5 +1,5 @@ -import { css, html, LitElement } from 'lit'; import { provide } from '@lit/context'; +import { LitElement, css, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { notificationsStoreContext } from '../context.js'; @@ -7,17 +7,17 @@ import { NotificationsStore } from '../notifications-store.js'; @customElement('notifications-context') export class NotificationsContext extends LitElement { - @provide({ context: notificationsStoreContext }) - @property({ type: Object }) - store!: NotificationsStore; + @provide({ context: notificationsStoreContext }) + @property({ type: Object }) + store!: NotificationsStore; - render() { - return html``; - } + render() { + return html``; + } - static styles = css` - :host { - display: contents; - } - `; + static styles = css` + :host { + display: contents; + } + `; } diff --git a/ui/src/elements/notifications-for-recipient.ts b/ui/src/elements/notifications-for-recipient.ts index 58568c14..ae6b24eb 100644 --- a/ui/src/elements/notifications-for-recipient.ts +++ b/ui/src/elements/notifications-for-recipient.ts @@ -1,80 +1,88 @@ - -import { LitElement, html } from 'lit'; -import { state, customElement, property } from 'lit/decorators.js'; -import { Record, EntryHash, ActionHash, AgentPubKey } from '@holochain/client'; +import { + hashProperty, + renderAsyncStatus, + sharedStyles, + wrapPathInSvg, +} from '@holochain-open-dev/elements'; +import '@holochain-open-dev/elements/dist/elements/display-error.js'; +import '@holochain-open-dev/profiles/dist/elements/agent-avatar.js'; import { pipe, subscribe } from '@holochain-open-dev/stores'; -import { EntryRecord, slice} from '@holochain-open-dev/utils'; -import { renderAsyncStatus, hashProperty, sharedStyles, wrapPathInSvg } from '@holochain-open-dev/elements'; +import { EntryRecord, slice } from '@holochain-open-dev/utils'; +import { ActionHash, AgentPubKey, EntryHash, Record } from '@holochain/client'; import { consume } from '@lit/context'; import { localized, msg } from '@lit/localize'; import { mdiInformationOutline } from '@mdi/js'; - -import '@holochain-open-dev/elements/dist/elements/display-error.js'; -import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'; import '@shoelace-style/shoelace/dist/components/icon/icon.js'; +import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'; +import { LitElement, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; -import '@holochain-open-dev/profiles/dist/elements/agent-avatar.js'; - -import { NotificationsStore } from '../notifications-store.js'; import { notificationsStoreContext } from '../context.js'; +import { NotificationsStore } from '../notifications-store.js'; import { Notification } from '../types.js'; - /** * @element notifications-for-recipient */ @localized() @customElement('notifications-for-recipient') export class NotificationsForRecipient extends LitElement { + /** + * REQUIRED. The Recipient for which the Notifications should be fetched + */ + @property(hashProperty('recipient')) + recipient!: AgentPubKey; - /** - * REQUIRED. The Recipient for which the Notifications should be fetched - */ - @property(hashProperty('recipient')) - recipient!: AgentPubKey; + /** + * @internal + */ + @consume({ context: notificationsStoreContext, subscribe: true }) + notificationsStore!: NotificationsStore; - /** - * @internal - */ - @consume({ context: notificationsStoreContext, subscribe: true }) - notificationsStore!: NotificationsStore; - + renderList(hashes: Array) { + if (hashes.length === 0) + return html`
+ + ${msg('No notifications found for this recipient')} +
`; - renderList(hashes: Array) { - if (hashes.length === 0) - return html`
- - ${msg("No notifications found for this recipient")} -
`; + return html` +
+ ${hashes.map( + hash => + html``, + )} +
+ `; + } - return html` -
- ${hashes.map(hash => - html`` - )} -
- `; - } + render() { + return html`${subscribe( + this.notificationsStore.notificationsForRecipient.get(this.recipient) + .live, + renderAsyncStatus({ + complete: map => this.renderList(Array.from(map.keys())), + pending: () => + html`
+ +
`, + error: e => + html``, + }), + )}`; + } - render() { - return html`${subscribe(this.notificationsStore.notificationsForRecipient.get(this.recipient).live, - renderAsyncStatus({ - complete: map => this.renderList(Array.from(map.keys())), - pending: () => html`
- -
`, - error: e => html`` - }) - )}`; - } - - static styles = [sharedStyles]; + static styles = [sharedStyles]; } diff --git a/ui/src/mocks.ts b/ui/src/mocks.ts index 43eb6948..edaa9707 100644 --- a/ui/src/mocks.ts +++ b/ui/src/mocks.ts @@ -1,119 +1,143 @@ -import { Notification } from './types.js'; - import { - AgentPubKeyMap, - decodeEntry, - fakeEntry, - fakeCreateAction, - fakeUpdateEntry, - fakeDeleteEntry, - fakeRecord, - pickBy, - ZomeMock, - RecordBag, - entryState, - HoloHashMap, - HashType, - hash -} from "@holochain-open-dev/utils"; + AgentPubKeyMap, + HashType, + HoloHashMap, + RecordBag, + ZomeMock, + decodeEntry, + entryState, + fakeCreateAction, + fakeDeleteEntry, + fakeEntry, + fakeRecord, + fakeUpdateEntry, + hash, + pickBy, +} from '@holochain-open-dev/utils'; import { - decodeHashFromBase64, - NewEntryAction, - AgentPubKey, - ActionHash, - EntryHash, - Delete, - AppAgentClient, - fakeAgentPubKey, - fakeDnaHash, - Link, - fakeActionHash, - SignedActionHashed, - fakeEntryHash, - Record, -} from "@holochain/client"; -import { NotificationsClient } from './notifications-client.js' + ActionHash, + AgentPubKey, + AppAgentClient, + Delete, + EntryHash, + Link, + NewEntryAction, + Record, + SignedActionHashed, + decodeHashFromBase64, + fakeActionHash, + fakeAgentPubKey, + fakeDnaHash, + fakeEntryHash, +} from '@holochain/client'; import { encode } from '@msgpack/msgpack'; -export class NotificationsZomeMock extends ZomeMock implements AppAgentClient { - constructor( - myPubKey?: AgentPubKey - ) { - super("notifications_test", "notifications", myPubKey); - } - /** Notification */ - notifications = new HoloHashMap>; - revisions: Array; - }>(); - notificationsForRecipient = new HoloHashMap(); - - async create_notification(notification: Notification): Promise { - const entryHash = hash(notification, HashType.ENTRY); - const record = await fakeRecord(await fakeCreateAction(entryHash), fakeEntry(notification)); - - this.notifications.set(record.signed_action.hashed.hash, { - deletes: [], - revisions: [record] - }); - - await Promise.all(notification.recipients.map(async recipients => { - const existingRecipients = this.notificationsForRecipient.get(recipients) || []; - this.notificationsForRecipient.set(recipients, [...existingRecipients, { - target: record.signed_action.hashed.hash, - author: this.myPubKey, - timestamp: Date.now() * 1000, - zome_index: 0, - link_type: 0, - tag: new Uint8Array(), - create_link_hash: await fakeActionHash() - }]); - })); - - return record; - } - - async get_notification(notificationHash: ActionHash): Promise { - const notification = this.notifications.get(notificationHash); - return notification ? notification.revisions[0] : undefined; - } - - async get_all_deletes_for_notification(notificationHash: ActionHash): Promise> | undefined> { - const notification = this.notifications.get(notificationHash); - return notification ? notification.deletes : undefined; - } - - async get_oldest_delete_for_notification(notificationHash: ActionHash): Promise | undefined> { - const notification = this.notifications.get(notificationHash); - return notification ? notification.deletes[0] : undefined; - } - async delete_notification(original_notification_hash: ActionHash): Promise { - const record = await fakeRecord(await fakeDeleteEntry(original_notification_hash)); - - this.notifications.get(original_notification_hash).deletes.push(record.signed_action as SignedActionHashed); - - return record.signed_action.hashed.hash; - } - - - async get_notifications_for_recipient(recipient: AgentPubKey): Promise> { - return this.notificationsForRecipient.get(recipient) || []; - } - +import { NotificationsClient } from './notifications-client.js'; +import { Notification } from './types.js'; +export class NotificationsZomeMock extends ZomeMock implements AppAgentClient { + constructor(myPubKey?: AgentPubKey) { + super('notifications_test', 'notifications', myPubKey); + } + /** Notification */ + notifications = new HoloHashMap< + ActionHash, + { + deletes: Array>; + revisions: Array; + } + >(); + notificationsForRecipient = new HoloHashMap(); + + async create_notification(notification: Notification): Promise { + const entryHash = hash(notification, HashType.ENTRY); + const record = await fakeRecord( + await fakeCreateAction(entryHash), + fakeEntry(notification), + ); + + this.notifications.set(record.signed_action.hashed.hash, { + deletes: [], + revisions: [record], + }); + + await Promise.all( + notification.recipients.map(async recipients => { + const existingRecipients = + this.notificationsForRecipient.get(recipients) || []; + this.notificationsForRecipient.set(recipients, [ + ...existingRecipients, + { + target: record.signed_action.hashed.hash, + author: this.myPubKey, + timestamp: Date.now() * 1000, + zome_index: 0, + link_type: 0, + tag: new Uint8Array(), + create_link_hash: await fakeActionHash(), + }, + ]); + }), + ); + + return record; + } + + async get_notification( + notificationHash: ActionHash, + ): Promise { + const notification = this.notifications.get(notificationHash); + return notification ? notification.revisions[0] : undefined; + } + + async get_all_deletes_for_notification( + notificationHash: ActionHash, + ): Promise> | undefined> { + const notification = this.notifications.get(notificationHash); + return notification ? notification.deletes : undefined; + } + + async get_oldest_delete_for_notification( + notificationHash: ActionHash, + ): Promise | undefined> { + const notification = this.notifications.get(notificationHash); + return notification ? notification.deletes[0] : undefined; + } + async delete_notification( + original_notification_hash: ActionHash, + ): Promise { + const record = await fakeRecord( + await fakeDeleteEntry(original_notification_hash), + ); + + this.notifications + .get(original_notification_hash) + .deletes.push(record.signed_action as SignedActionHashed); + + return record.signed_action.hashed.hash; + } + + async get_notifications_for_recipient( + recipient: AgentPubKey, + ): Promise> { + return this.notificationsForRecipient.get(recipient) || []; + } } -export async function sampleNotification(client: NotificationsClient, partialNotification: Partial = {}): Promise { - return { - ...{ - notification_type: "Lorem ipsum 2", - notification_group: "Lorem ipsum 2", - persistent: true, - recipients: [client.client.myPubKey], - content: encode({ - hi: "hi2" - }), - }, - ...partialNotification - }; +export async function sampleNotification( + client: NotificationsClient, + partialNotification: Partial = {}, +): Promise { + return { + ...{ + notification_type: 'Lorem ipsum 2', + notification_group: 'Lorem ipsum 2', + persistent: true, + recipients: [client.client.myPubKey], + content: encode({ + hi: 'hi2', + }), + }, + ...partialNotification, + }; } diff --git a/ui/src/notifications-client.ts b/ui/src/notifications-client.ts index 45d1cfe1..3f4cda4e 100644 --- a/ui/src/notifications-client.ts +++ b/ui/src/notifications-client.ts @@ -1,59 +1,82 @@ -import { Notification } from './types.js'; - import { - SignedActionHashed, - CreateLink, - Link, - DeleteLink, - Delete, - AppAgentClient, - Record, - ActionHash, - EntryHash, - AgentPubKey, + EntryRecord, + ZomeClient, + isSignalFromCellWithRole, +} from '@holochain-open-dev/utils'; +import { + ActionHash, + AgentPubKey, + AppAgentClient, + CreateLink, + Delete, + DeleteLink, + EntryHash, + Link, + Record, + SignedActionHashed, } from '@holochain/client'; -import { isSignalFromCellWithRole, EntryRecord, ZomeClient } from '@holochain-open-dev/utils'; +import { Notification } from './types.js'; import { NotificationsSignal } from './types.js'; export class NotificationsClient extends ZomeClient { - constructor(public client: AppAgentClient, public roleName: string, public zomeName = 'notifications') { - super(client, roleName, zomeName); - } - /** Notification */ - - async createNotification(notification: Notification): Promise> { - const record: Record = await this.callZome('create_notification', notification); - return new EntryRecord(record); - } + constructor( + public client: AppAgentClient, + public roleName: string, + public zomeName = 'notifications', + ) { + super(client, roleName, zomeName); + } + /** Notification */ - async getNotification(notificationHash: ActionHash): Promise | undefined> { - const record: Record = await this.callZome('get_notification', notificationHash); - return record ? new EntryRecord(record) : undefined; - } + async createNotification( + notification: Notification, + ): Promise> { + const record: Record = await this.callZome( + 'create_notification', + notification, + ); + return new EntryRecord(record); + } - markNotificationsAsRead(notificationsHashes: ActionHash[]): Promise { - return this.callZome('mark_notifications_as_read', notificationsHashes); - } + async getNotification( + notificationHash: ActionHash, + ): Promise | undefined> { + const record: Record = await this.callZome( + 'get_notification', + notificationHash, + ); + return record ? new EntryRecord(record) : undefined; + } - dismissNotifications(notificationsHashes: ActionHash[]): Promise { - return this.callZome('dismiss_notifications', notificationsHashes); - } + markNotificationsAsRead(notificationsHashes: ActionHash[]): Promise { + return this.callZome('mark_notifications_as_read', notificationsHashes); + } - getAllDeletesForNotification(originalNotificationHash: ActionHash): Promise>> { - return this.callZome('get_all_deletes_for_notification', originalNotificationHash); - } + dismissNotifications(notificationsHashes: ActionHash[]): Promise { + return this.callZome('dismiss_notifications', notificationsHashes); + } - async getUndismissedNotifications(): Promise> { - return this.callZome('get_undismissed_notifications', undefined); - } + getAllDeletesForNotification( + originalNotificationHash: ActionHash, + ): Promise>> { + return this.callZome( + 'get_all_deletes_for_notification', + originalNotificationHash, + ); + } - async getReadNotifications(): Promise> { - return this.callZome('get_read_notifications', undefined); - } + async getUndismissedNotifications(): Promise> { + return this.callZome('get_undismissed_notifications', undefined); + } - async getDismissedNotifications(): Promise, SignedActionHashed[]]>> { - return this.callZome('get_dismissed_notifications', undefined); - } + async getReadNotifications(): Promise> { + return this.callZome('get_read_notifications', undefined); + } + async getDismissedNotifications(): Promise< + Array<[SignedActionHashed, SignedActionHashed[]]> + > { + return this.callZome('get_dismissed_notifications', undefined); + } } diff --git a/ui/src/notifications-store.ts b/ui/src/notifications-store.ts index 0b4b2414..7c1990de 100644 --- a/ui/src/notifications-store.ts +++ b/ui/src/notifications-store.ts @@ -1,88 +1,118 @@ -import { slice, LazyHoloHashMap } from "@holochain-open-dev/utils"; +import { LazyHoloHashMap, slice } from '@holochain-open-dev/utils'; import { ActionHash, encodeHashToBase64 } from '@holochain/client'; import { decode } from '@msgpack/msgpack'; import { NotificationsClient } from './notifications-client.js'; -import { AsyncComputed, deletedLinksStore, deletesForEntryStore, immutableEntryStore, liveLinksStore, uniquify } from "./signals.js"; +import { + AsyncComputed, + deletedLinksStore, + deletesForEntryStore, + immutableEntryStore, + liveLinksStore, + uniquify, +} from './signals.js'; export class NotificationsStore { - - constructor(public client: NotificationsClient) { } - - /** Notification */ - - notifications = new LazyHoloHashMap((notificationHash: ActionHash) => ({ - entry: immutableEntryStore(() => this.client.getNotification(notificationHash)), - deletes: deletesForEntryStore(this.client, notificationHash, () => this.client.getAllDeletesForNotification(notificationHash)), - }) - ); - - private undismissedNotifications = liveLinksStore( - this.client, - this.client.client.myPubKey, - () => this.client.getUndismissedNotifications(), - 'RecipientToNotifications' - ); - - private readNotificationsLinks = liveLinksStore( - this.client, - this.client.client.myPubKey, - () => this.client.getReadNotifications(), - 'ReadNotifications' - ); - - readNotifications = new AsyncComputed(() => { - const undismissedNotifications = this.undismissedNotifications.get(); - if (undismissedNotifications.status !== 'completed') return undismissedNotifications; - - const readNotificationsLinks = this.readNotificationsLinks.get(); - if (readNotificationsLinks.status !== 'completed') return readNotificationsLinks; - - /** Aggregate the read notification hashes and filter them by whether they've been dismissed */ - - const allReadNotificationsHashes = uniquify(Array.from([] as ActionHash[]).concat(...readNotificationsLinks.value.map(link => decode(link.tag) as ActionHash[]))); - - const undismissedNotificationsHashes = undismissedNotifications.value.map(l => encodeHashToBase64(l.target)); - - const notificationsHashes = allReadNotificationsHashes.filter(hash => undismissedNotificationsHashes.includes(encodeHashToBase64(hash))); - const value = slice(this.notifications, notificationsHashes); - return { - status: 'completed', - value - }; - }); - - unreadNotifications = new AsyncComputed(() => { - const undismissedNotifications = this.undismissedNotifications.get(); - if (undismissedNotifications.status !== 'completed') return undismissedNotifications; - const readNotifications = this.readNotifications.get(); - if (readNotifications.status !== 'completed') return readNotifications; - const readNotificationsHashes = Array.from(readNotifications.value.keys()).map(h => encodeHashToBase64(h)); - - const links = undismissedNotifications.value.filter(link => !readNotificationsHashes.includes(encodeHashToBase64(link.target))); - const value = slice(this.notifications, uniquify(links.map(l => l.target))); - return { - status: "completed", - value - }; - }); - - deletedNotificationsLinks = deletedLinksStore( - this.client, - this.client.client.myPubKey, - () => this.client.getDismissedNotifications(), - 'RecipientToNotifications' - ); - - dismissedNotifications = new AsyncComputed(() => { - const deletedLinks = this.deletedNotificationsLinks.get(); - if (deletedLinks.status !== 'completed') return deletedLinks; - - const value = slice(this.notifications, deletedLinks.value.map(l => l[0].hashed.content.target_address)); - - return { - status: 'completed', - value - }; - }); + constructor(public client: NotificationsClient) {} + + /** Notification */ + + notifications = new LazyHoloHashMap((notificationHash: ActionHash) => ({ + entry: immutableEntryStore(() => + this.client.getNotification(notificationHash), + ), + deletes: deletesForEntryStore(this.client, notificationHash, () => + this.client.getAllDeletesForNotification(notificationHash), + ), + })); + + private undismissedNotifications = liveLinksStore( + this.client, + this.client.client.myPubKey, + () => this.client.getUndismissedNotifications(), + 'RecipientToNotifications', + ); + + private readNotificationsLinks = liveLinksStore( + this.client, + this.client.client.myPubKey, + () => this.client.getReadNotifications(), + 'ReadNotifications', + ); + + readNotifications = new AsyncComputed(() => { + const undismissedNotifications = this.undismissedNotifications.get(); + if (undismissedNotifications.status !== 'completed') + return undismissedNotifications; + + const readNotificationsLinks = this.readNotificationsLinks.get(); + if (readNotificationsLinks.status !== 'completed') + return readNotificationsLinks; + + /** Aggregate the read notification hashes and filter them by whether they've been dismissed */ + + const allReadNotificationsHashes = uniquify( + Array.from([] as ActionHash[]).concat( + ...readNotificationsLinks.value.map( + link => decode(link.tag) as ActionHash[], + ), + ), + ); + + const undismissedNotificationsHashes = undismissedNotifications.value.map( + l => encodeHashToBase64(l.target), + ); + + const notificationsHashes = allReadNotificationsHashes.filter(hash => + undismissedNotificationsHashes.includes(encodeHashToBase64(hash)), + ); + const value = slice(this.notifications, notificationsHashes); + return { + status: 'completed', + value, + }; + }); + + unreadNotifications = new AsyncComputed(() => { + const undismissedNotifications = this.undismissedNotifications.get(); + if (undismissedNotifications.status !== 'completed') + return undismissedNotifications; + const readNotifications = this.readNotifications.get(); + if (readNotifications.status !== 'completed') return readNotifications; + const readNotificationsHashes = Array.from( + readNotifications.value.keys(), + ).map(h => encodeHashToBase64(h)); + + const links = undismissedNotifications.value.filter( + link => + !readNotificationsHashes.includes(encodeHashToBase64(link.target)), + ); + const value = slice(this.notifications, uniquify(links.map(l => l.target))); + return { + status: 'completed', + value, + }; + }); + + deletedNotificationsLinks = deletedLinksStore( + this.client, + this.client.client.myPubKey, + () => this.client.getDismissedNotifications(), + 'RecipientToNotifications', + ); + + dismissedNotifications = new AsyncComputed(() => { + const deletedLinks = this.deletedNotificationsLinks.get(); + if (deletedLinks.status !== 'completed') return deletedLinks; + + const value = slice( + this.notifications, + deletedLinks.value.map(l => l[0].hashed.content.target_address), + ); + + return { + status: 'completed', + value, + }; + }); } diff --git a/ui/src/signals.ts b/ui/src/signals.ts index 25a64d69..351852ac 100644 --- a/ui/src/signals.ts +++ b/ui/src/signals.ts @@ -1,109 +1,120 @@ import { - ActionCommittedSignal, - EntryRecord, - getHashType, - HashType, - HoloHashMap, - LinkTypeForSignal, - retype, - ZomeClient, -} from "@holochain-open-dev/utils"; + ActionCommittedSignal, + EntryRecord, + HashType, + HoloHashMap, + LinkTypeForSignal, + ZomeClient, + getHashType, + retype, +} from '@holochain-open-dev/utils'; import { - Action, - ActionHash, - CreateLink, - decodeHashFromBase64, - Delete, - DeleteLink, - encodeHashToBase64, - HoloHash, - SignedActionHashed, - Link, -} from "@holochain/client"; -import { encode } from "@msgpack/msgpack"; -import cloneDeep from "lodash-es/cloneDeep.js"; - + Action, + ActionHash, + CreateLink, + Delete, + DeleteLink, + HoloHash, + Link, + SignedActionHashed, + decodeHashFromBase64, + encodeHashToBase64, +} from '@holochain/client'; +import { encode } from '@msgpack/msgpack'; +import cloneDeep from 'lodash-es/cloneDeep.js'; import { Signal } from 'signal-polyfill'; - -export type AsyncResult = { - status: 'pending'; -} | { - status: 'completed'; - value: T; -} | { - status: 'error'; - error: E; -}; - -export class AsyncState extends Signal.State> { -} -export class AsyncComputed extends Signal.Computed> { -} - -export type AsyncSignal = AsyncState | AsyncComputed; - +export type AsyncResult = + | { + status: 'pending'; + } + | { + status: 'completed'; + value: T; + } + | { + status: 'error'; + error: E; + }; + +export class AsyncState extends Signal.State< + AsyncResult +> {} +export class AsyncComputed extends Signal.Computed< + AsyncResult +> {} + +export type AsyncSignal = + | AsyncState + | AsyncComputed; // An effect effect Signal which evaluates to cb, which schedules a read of // itself on the microtask queue whenever one of its dependencies might change -export function effect(cb: (() => (() => void)) | (() => void)) { - // This function would usually live in a library/framework, not application code - // NOTE: This scheduling logic is too basic to be useful. Do not copy/paste. - let pending = false; - - let w = new Signal.subtle.Watcher(() => { - if (!pending) { - pending = true; - queueMicrotask(() => { - pending = false; - for (let s of w.getPending()) s.get(); - w.watch(); - }); - } - }); - let destructor: void | (() => void); - let c = new Signal.Computed(() => { destructor?.(); destructor = cb(); }); - w.watch(c); - c.get(); - return () => { destructor?.(); w.unwatch(c) }; +export function effect(cb: (() => () => void) | (() => void)) { + // This function would usually live in a library/framework, not application code + // NOTE: This scheduling logic is too basic to be useful. Do not copy/paste. + let pending = false; + + const w = new Signal.subtle.Watcher(() => { + if (!pending) { + pending = true; + queueMicrotask(() => { + pending = false; + for (const s of w.getPending()) s.get(); + w.watch(); + }); + } + }); + let destructor: void | (() => void); + const c = new Signal.Computed(() => { + destructor?.(); + destructor = cb(); + }); + w.watch(c); + c.get(); + return () => { + destructor?.(); + w.unwatch(c); + }; } -export function toPromise(asyncSignal: AsyncSignal): Promise { - return new Promise((resolve, reject) => { - const unsubs = effect(() => { - const result = asyncSignal.get(); - if (result.status === 'pending') return; - else if (result.status === 'completed') { - resolve(result.value); - unsubs(); - } - else if (result.status === 'error') { - reject(result.error); - unsubs(); - } - }); - }); +export function toPromise( + asyncSignal: AsyncSignal, +): Promise { + return new Promise((resolve, reject) => { + const unsubs = effect(() => { + const result = asyncSignal.get(); + if (result.status === 'pending') return; + else if (result.status === 'completed') { + resolve(result.value); + unsubs(); + } else if (result.status === 'error') { + reject(result.error); + unsubs(); + } + }); + }); } export function value(signal: AsyncSignal): T | undefined { - const result = signal.get(); - if (result.status === 'completed') return result.value; - return undefined + const result = signal.get(); + if (result.status === 'completed') return result.value; + return undefined; } const DEFAULT_POLL_INTERVAL_MS = 20_000; // 20 seconds export function createLinkToLink( - createLink: SignedActionHashed + createLink: SignedActionHashed, ): Link { - return { - author: createLink.hashed.content.author, - link_type: createLink.hashed.content.link_type, - tag: createLink.hashed.content.tag, - target: createLink.hashed.content.target_address, - timestamp: createLink.hashed.content.timestamp, - zome_index: createLink.hashed.content.zome_index, - create_link_hash: createLink.hashed.hash, - }; + return { + author: createLink.hashed.content.author, + link_type: createLink.hashed.content.link_type, + tag: createLink.hashed.content.tag, + target: createLink.hashed.content.target_address, + timestamp: createLink.hashed.content.timestamp, + zome_index: createLink.hashed.content.zome_index, + create_link_hash: createLink.hashed.hash, + }; } /** @@ -116,161 +127,172 @@ export function createLinkToLink( * Useful for collections */ export function collectionStore< - S extends ActionCommittedSignal & any + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - fetchCollection: () => Promise, - linkType: string, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + fetchCollection: () => Promise, + linkType: string, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal> { - let active = false; - let unsubs: () => void | undefined; - const signal = new Signal.State>>({ status: 'pending' }, { - [Signal.subtle.watched]: () => { - active = true; - let links: Link[]; - - const maybeSet = (newLinksValue: Link[]) => { - if (!active) return; - const orderedNewLinks = uniquifyLinks(newLinksValue).sort( - sortLinksByTimestampAscending - ); - if ( - links === undefined || - !areArrayHashesEqual( - orderedNewLinks.map((l) => l.create_link_hash), - links.map((l) => l.create_link_hash) - ) - ) { - links = orderedNewLinks; - signal.set({ - status: 'completed', - value: links - }); - } - }; - - const fetch = () => { - if (!active) return; - fetchCollection().then(maybeSet).catch(e => signal.set({ - status: 'error', - error: e - })).finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - }; - fetch(); - unsubs = client.onSignal((originalSignal) => { - if (!(originalSignal as ActionCommittedSignal).type) return; - const signal = originalSignal as ActionCommittedSignal; - - if (signal.type === "LinkCreated") { - if (linkType in signal.link_type) { - maybeSet([...links, createLinkToLink(signal.action)]); - } - } else if (signal.type === "LinkDeleted") { - if (linkType in signal.link_type) { - maybeSet( - links.filter( - (link) => - link.create_link_hash.toString() !== - signal.create_link_action.hashed.hash.toString() - ) - ); - } - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: 'pending' - }); - active = false; - unsubs(); - }, - }); - - - - return signal; + let active = false; + let unsubs: () => void | undefined; + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + let links: Link[]; + + const maybeSet = (newLinksValue: Link[]) => { + if (!active) return; + const orderedNewLinks = uniquifyLinks(newLinksValue).sort( + sortLinksByTimestampAscending, + ); + if ( + links === undefined || + !areArrayHashesEqual( + orderedNewLinks.map(l => l.create_link_hash), + links.map(l => l.create_link_hash), + ) + ) { + links = orderedNewLinks; + signal.set({ + status: 'completed', + value: links, + }); + } + }; + + const fetch = () => { + if (!active) return; + fetchCollection() + .then(maybeSet) + .catch(e => + signal.set({ + status: 'error', + error: e, + }), + ) + .finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + }; + fetch(); + unsubs = client.onSignal(originalSignal => { + if (!(originalSignal as ActionCommittedSignal).type) return; + const signal = originalSignal as ActionCommittedSignal; + + if (signal.type === 'LinkCreated') { + if (linkType in signal.link_type) { + maybeSet([...links, createLinkToLink(signal.action)]); + } + } else if (signal.type === 'LinkDeleted') { + if (linkType in signal.link_type) { + maybeSet( + links.filter( + link => + link.create_link_hash.toString() !== + signal.create_link_action.hashed.hash.toString(), + ), + ); + } + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + unsubs(); + }, + }, + ); + + return signal; } export class NotFoundError extends Error { - constructor() { - super("NOT_FOUND"); - } + constructor() { + super('NOT_FOUND'); + } } export class ConflictingUpdatesError extends Error { - constructor(public conflictingUpdates: Array>) { - super("CONFLICTING_UPDATES"); - } + constructor(public conflictingUpdates: Array>) { + super('CONFLICTING_UPDATES'); + } } /** * Fetches the given entry, retrying if there is a failure - * - * Makes requests only the first time it is subscribed to, + * + * Makes requests only the first time it is subscribed to, * and will stop after it succeeds in fetching the entry * - * Whenever it succeeds, it caches the value so that any subsequent requests are cached + * Whenever it succeeds, it caches the value so that any subsequent requests are cached * * Useful for entries that can't be updated */ export function immutableEntryStore( - fetch: () => Promise | undefined>, - pollInterval: number = 1000, - maxRetries = 4 + fetch: () => Promise | undefined>, + pollInterval: number = 1000, + maxRetries = 4, ): AsyncSignal> { - let cached = false; - const signal = new Signal.State>>({ status: 'pending' }, { - [Signal.subtle.watched]: () => { - if (cached) return; - let retries = 0; - - const tryFetch = () => { - retries += 1; - fetch().then(value => { - if (value) { - cached = true; - signal.set({ - status: 'completed', - value - }); - } else { - if (retries < maxRetries) { - setTimeout(() => tryFetch, pollInterval); - } else { - signal.set({ - status: 'error', - error: new NotFoundError() - }); - } - } - }).catch(error => { - if (retries < maxRetries) { - setTimeout(() => tryFetch, pollInterval); - } else { - signal.set({ - status: 'error', - error - }); - } - }); - } - tryFetch(); - }, - [Signal.subtle.unwatched]: () => { - if (cached) return; - signal.set({ - status: 'pending' - }); - } - }); - - return signal; + let cached = false; + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + if (cached) return; + let retries = 0; + + const tryFetch = () => { + retries += 1; + fetch() + .then(value => { + if (value) { + cached = true; + signal.set({ + status: 'completed', + value, + }); + } else { + if (retries < maxRetries) { + setTimeout(() => tryFetch, pollInterval); + } else { + signal.set({ + status: 'error', + error: new NotFoundError(), + }); + } + } + }) + .catch(error => { + if (retries < maxRetries) { + setTimeout(() => tryFetch, pollInterval); + } else { + signal.set({ + status: 'error', + error, + }); + } + }); + }; + tryFetch(); + }, + [Signal.subtle.unwatched]: () => { + if (cached) return; + signal.set({ + status: 'pending', + }); + }, + }, + ); + + return signal; } /** @@ -283,91 +305,94 @@ export function immutableEntryStore( * Useful for entries that can be updated */ export function latestVersionOfEntryStore< - T, - S extends ActionCommittedSignal & any + T, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - fetchLatestVersion: () => Promise | undefined>, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + fetchLatestVersion: () => Promise | undefined>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal> { - let active = false; - let unsubs: () => void | undefined; - let latestVersion: EntryRecord | undefined; - - const signal = new Signal.State>>({ status: 'pending' }, { - [Signal.subtle.watched]: () => { - active = true; - const fetch = async () => { - if (!active) return; - try { - const nlatestVersion = await fetchLatestVersion(); - if (nlatestVersion) { - if ( - latestVersion?.actionHash.toString() !== - nlatestVersion?.actionHash.toString() - ) { - latestVersion = nlatestVersion; - signal.set({ - status: 'completed', - value: latestVersion, - }); - } - } else { - signal.set({ - status: 'error', - error: new NotFoundError(), - }); - } - } catch (e) { - signal.set({ - status: 'error', - error: e, - }); - } finally { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - } - }; - fetch(); - unsubs = client.onSignal((originalSignal) => { - if (!active) return; - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if ( - hcSignal.type === "EntryUpdated" && - latestVersion && - latestVersion.actionHash.toString() === - hcSignal.action.hashed.content.original_action_address.toString() - ) { - latestVersion = new EntryRecord({ - entry: { - Present: { - entry_type: "App", - entry: encode(hcSignal.app_entry), - }, - }, - signed_action: hcSignal.action, - }); - signal.set({ - status: 'completed', - value: latestVersion, - }); - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: 'pending' - }); - active = false; - latestVersion = undefined; - unsubs(); - }, - }); - - return signal; + let active = false; + let unsubs: () => void | undefined; + let latestVersion: EntryRecord | undefined; + + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + const fetch = async () => { + if (!active) return; + try { + const nlatestVersion = await fetchLatestVersion(); + if (nlatestVersion) { + if ( + latestVersion?.actionHash.toString() !== + nlatestVersion?.actionHash.toString() + ) { + latestVersion = nlatestVersion; + signal.set({ + status: 'completed', + value: latestVersion, + }); + } + } else { + signal.set({ + status: 'error', + error: new NotFoundError(), + }); + } + } catch (e) { + signal.set({ + status: 'error', + error: e, + }); + } finally { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + } + }; + fetch(); + unsubs = client.onSignal(originalSignal => { + if (!active) return; + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if ( + hcSignal.type === 'EntryUpdated' && + latestVersion && + latestVersion.actionHash.toString() === + hcSignal.action.hashed.content.original_action_address.toString() + ) { + latestVersion = new EntryRecord({ + entry: { + Present: { + entry_type: 'App', + entry: encode(hcSignal.app_entry), + }, + }, + signed_action: hcSignal.action, + }); + signal.set({ + status: 'completed', + value: latestVersion, + }); + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + latestVersion = undefined; + unsubs(); + }, + }, + ); + + return signal; } /** @@ -380,89 +405,92 @@ export function latestVersionOfEntryStore< * Useful for entries that can be updated */ export function allRevisionsOfEntryStore< - T, - S extends ActionCommittedSignal & any + T, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - fetchAllRevisions: () => Promise>>, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + fetchAllRevisions: () => Promise>>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal>> { - let active = false; - let unsubs: () => void | undefined; - let allRevisions: Array> | undefined; - const signal = new Signal.State>>>({ status: 'pending' }, { - [Signal.subtle.watched]: () => { - active = true; - const fetch = async () => { - if (!active) return; - - const nAllRevisions = await fetchAllRevisions().finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - if ( - allRevisions === undefined || - !areArrayHashesEqual( - allRevisions.map((r) => r.actionHash), - nAllRevisions.map((r) => r.actionHash) - ) - ) { - allRevisions = nAllRevisions; - signal.set({ - status: 'completed', - value: allRevisions - }); - } - }; - fetch().catch(error => { - signal.set({ - status: 'error', - error - }) - }); - unsubs = client.onSignal(async (originalSignal) => { - if (!active) return; - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if ( - hcSignal.type === "EntryUpdated" && - allRevisions && - allRevisions.find( - (revision) => - revision.actionHash.toString() === - hcSignal.action.hashed.content.original_action_address.toString() - ) - ) { - const newRevision = new EntryRecord({ - entry: { - Present: { - entry_type: "App", - entry: encode(hcSignal.app_entry), - }, - }, - signed_action: hcSignal.action, - }); - allRevisions.push(newRevision); - signal.set({ - status: 'completed', - value: allRevisions - }); - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: 'pending' - }); - active = false; - allRevisions = undefined; - unsubs(); - }, - }); - - return signal; + let active = false; + let unsubs: () => void | undefined; + let allRevisions: Array> | undefined; + const signal = new Signal.State>>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + const fetch = async () => { + if (!active) return; + + const nAllRevisions = await fetchAllRevisions().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + if ( + allRevisions === undefined || + !areArrayHashesEqual( + allRevisions.map(r => r.actionHash), + nAllRevisions.map(r => r.actionHash), + ) + ) { + allRevisions = nAllRevisions; + signal.set({ + status: 'completed', + value: allRevisions, + }); + } + }; + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(async originalSignal => { + if (!active) return; + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if ( + hcSignal.type === 'EntryUpdated' && + allRevisions && + allRevisions.find( + revision => + revision.actionHash.toString() === + hcSignal.action.hashed.content.original_action_address.toString(), + ) + ) { + const newRevision = new EntryRecord({ + entry: { + Present: { + entry_type: 'App', + entry: encode(hcSignal.app_entry), + }, + }, + signed_action: hcSignal.action, + }); + allRevisions.push(newRevision); + signal.set({ + status: 'completed', + value: allRevisions, + }); + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + allRevisions = undefined; + unsubs(); + }, + }, + ); + + return signal; } /** @@ -475,129 +503,134 @@ export function allRevisionsOfEntryStore< * Useful for entries that can be deleted */ export function deletesForEntryStore< - S extends ActionCommittedSignal & any + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - originalActionHash: ActionHash, - fetchDeletes: () => Promise>>, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + originalActionHash: ActionHash, + fetchDeletes: () => Promise>>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal>> { - let active = false; - let unsubs: () => void | undefined; - let deletes: Array> | undefined; - const signal = new Signal.State>>>({ status: 'pending' }, { - [Signal.subtle.watched]: () => { - active = true; - const fetch = async () => { - if (!active) return; - - const ndeletes = await fetchDeletes().finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - if ( - deletes === undefined || - !areArrayHashesEqual( - deletes.map((d) => d.hashed.hash), - ndeletes.map((d) => d.hashed.hash) - ) - ) { - deletes = ndeletes; - signal.set({ - status: 'completed', - value: deletes - }); - } - }; - fetch().catch(error => { - signal.set({ - status: 'error', - error - }) - }); - unsubs = client.onSignal((originalSignal) => { - if (!active) return; - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if ( - hcSignal.type === "EntryDeleted" && - hcSignal.action.hashed.content.deletes_address.toString() === - originalActionHash.toString() - ) { - let lastDeletes = deletes ? deletes : []; - deletes = [...lastDeletes, hcSignal.action]; - signal.set({ - status: 'completed', - value: deletes - }); - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: 'pending' - }); - active = false; - deletes = undefined; - unsubs(); - } - }); - - return signal; + let active = false; + let unsubs: () => void | undefined; + let deletes: Array> | undefined; + const signal = new Signal.State< + AsyncResult>> + >( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + const fetch = async () => { + if (!active) return; + + const ndeletes = await fetchDeletes().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + if ( + deletes === undefined || + !areArrayHashesEqual( + deletes.map(d => d.hashed.hash), + ndeletes.map(d => d.hashed.hash), + ) + ) { + deletes = ndeletes; + signal.set({ + status: 'completed', + value: deletes, + }); + } + }; + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(originalSignal => { + if (!active) return; + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if ( + hcSignal.type === 'EntryDeleted' && + hcSignal.action.hashed.content.deletes_address.toString() === + originalActionHash.toString() + ) { + const lastDeletes = deletes ? deletes : []; + deletes = [...lastDeletes, hcSignal.action]; + signal.set({ + status: 'completed', + value: deletes, + }); + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + deletes = undefined; + unsubs(); + }, + }, + ); + + return signal; } export const sortLinksByTimestampAscending = (linkA: Link, linkB: Link) => - linkA.timestamp - linkB.timestamp; + linkA.timestamp - linkB.timestamp; export const sortDeletedLinksByTimestampAscending = ( - linkA: [SignedActionHashed, SignedActionHashed[]], - linkB: [SignedActionHashed, SignedActionHashed[]] + linkA: [SignedActionHashed, SignedActionHashed[]], + linkB: [SignedActionHashed, SignedActionHashed[]], ) => linkA[0].hashed.content.timestamp - linkB[0].hashed.content.timestamp; export const sortActionsByTimestampAscending = ( - actionA: SignedActionHashed, - actionB: SignedActionHashed + actionA: SignedActionHashed, + actionB: SignedActionHashed, ) => actionA.hashed.content.timestamp - actionB.hashed.content.timestamp; export function uniquify(array: Array): Array { - const strArray = array.map((h) => encodeHashToBase64(h)); - const uniqueArray = [...new Set(strArray)]; - return uniqueArray.map((h) => decodeHashFromBase64(h) as H); + const strArray = array.map(h => encodeHashToBase64(h)); + const uniqueArray = [...new Set(strArray)]; + return uniqueArray.map(h => decodeHashFromBase64(h) as H); } export function uniquifyLinks(links: Array): Array { - const map = new HoloHashMap(); - for (const link of links) { - map.set(link.create_link_hash, link); - } + const map = new HoloHashMap(); + for (const link of links) { + map.set(link.create_link_hash, link); + } - return Array.from(map.values()); + return Array.from(map.values()); } function areArrayHashesEqual( - array1: Array, - array2: Array + array1: Array, + array2: Array, ): boolean { - if (array1.length !== array2.length) return false; + if (array1.length !== array2.length) return false; - for (let i = 0; i < array1.length; i += 1) { - if (array1[i].toString() !== array2[i].toString()) { - return false; - } - } + for (let i = 0; i < array1.length; i += 1) { + if (array1[i].toString() !== array2[i].toString()) { + return false; + } + } - return true; + return true; } function uniquifyActions( - actions: Array> + actions: Array>, ): Array> { - const map = new HoloHashMap>(); - for (const a of actions) { - map.set(a.hashed.hash, a); - } + const map = new HoloHashMap>(); + for (const a of actions) { + map.set(a.hashed.hash, a); + } - return Array.from(map.values()); + return Array.from(map.values()); } /** @@ -610,106 +643,109 @@ function uniquifyActions( * Useful for link types */ export function liveLinksStore< - BASE extends HoloHash, - S extends ActionCommittedSignal & any + BASE extends HoloHash, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - baseAddress: BASE, - fetchLinks: () => Promise>, - linkType: LinkTypeForSignal, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + baseAddress: BASE, + fetchLinks: () => Promise>, + linkType: LinkTypeForSignal, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal> { - let innerBaseAddress = baseAddress; - if (getHashType(innerBaseAddress) === HashType.AGENT) { - innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; - } - - let active = false; - let unsubs: () => void | undefined; - - let links: Array | undefined; - const signal = new Signal.State>>({ status: 'pending' }, { - [Signal.subtle.watched]: () => { - active = true; - - const maybeSet = (newLinksValue: Link[]) => { - if (!active) return; - const orderedNewLinks = uniquifyLinks(newLinksValue).sort( - sortLinksByTimestampAscending - ); - - if ( - links === undefined || - !areArrayHashesEqual( - orderedNewLinks.map((l) => l.create_link_hash), - links.map((l) => l.create_link_hash) - ) - ) { - links = orderedNewLinks; - signal.set({ - status: 'completed', - value: links - }); - } - }; - const fetch = async () => { - if (!active) return; - const nlinks = await fetchLinks().finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - maybeSet(nlinks); - }; - - fetch().catch(error => { - signal.set({ - status: 'error', - error - }) - }); - unsubs = client.onSignal((originalSignal) => { - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if (hcSignal.type === "LinkCreated") { - if ( - linkType in hcSignal.link_type && - hcSignal.action.hashed.content.base_address.toString() === - innerBaseAddress.toString() - ) { - let lastLinks = links ? links : []; - maybeSet([...lastLinks, createLinkToLink(hcSignal.action)]); - } - } else if (hcSignal.type === "LinkDeleted") { - if ( - linkType in hcSignal.link_type && - hcSignal.create_link_action.hashed.content.base_address.toString() === - innerBaseAddress.toString() - ) { - maybeSet( - (links ? links : []).filter( - (link) => - link.create_link_hash.toString() !== - hcSignal.create_link_action.hashed.hash.toString() - ) - ); - } - } - }); - }, - - [Signal.subtle.unwatched]: () => { - signal.set({ - status: 'pending' - }); - active = false; - links = undefined; - unsubs(); - } - }); - - return signal; + let innerBaseAddress = baseAddress; + if (getHashType(innerBaseAddress) === HashType.AGENT) { + innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; + } + + let active = false; + let unsubs: () => void | undefined; + + let links: Array | undefined; + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + + const maybeSet = (newLinksValue: Link[]) => { + if (!active) return; + const orderedNewLinks = uniquifyLinks(newLinksValue).sort( + sortLinksByTimestampAscending, + ); + + if ( + links === undefined || + !areArrayHashesEqual( + orderedNewLinks.map(l => l.create_link_hash), + links.map(l => l.create_link_hash), + ) + ) { + links = orderedNewLinks; + signal.set({ + status: 'completed', + value: links, + }); + } + }; + const fetch = async () => { + if (!active) return; + const nlinks = await fetchLinks().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + maybeSet(nlinks); + }; + + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(originalSignal => { + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if (hcSignal.type === 'LinkCreated') { + if ( + linkType in hcSignal.link_type && + hcSignal.action.hashed.content.base_address.toString() === + innerBaseAddress.toString() + ) { + const lastLinks = links ? links : []; + maybeSet([...lastLinks, createLinkToLink(hcSignal.action)]); + } + } else if (hcSignal.type === 'LinkDeleted') { + if ( + linkType in hcSignal.link_type && + hcSignal.create_link_action.hashed.content.base_address.toString() === + innerBaseAddress.toString() + ) { + maybeSet( + (links ? links : []).filter( + link => + link.create_link_hash.toString() !== + hcSignal.create_link_action.hashed.hash.toString(), + ), + ); + } + } + }); + }, + + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + links = undefined; + unsubs(); + }, + }, + ); + + return signal; } /** @@ -722,137 +758,151 @@ export function liveLinksStore< * Useful for link types and collections with some form of archive retrieving functionality */ export function deletedLinksStore< - BASE extends HoloHash, - S extends ActionCommittedSignal & any + BASE extends HoloHash, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - baseAddress: BASE, - fetchDeletedLinks: () => Promise< - Array< - [SignedActionHashed, Array>] - > - >, - linkType: LinkTypeForSignal, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + baseAddress: BASE, + fetchDeletedLinks: () => Promise< + Array< + [SignedActionHashed, Array>] + > + >, + linkType: LinkTypeForSignal, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal< - Array<[SignedActionHashed, Array>]> + Array<[SignedActionHashed, Array>]> > { - let innerBaseAddress = baseAddress; - if (getHashType(innerBaseAddress) === HashType.AGENT) { - innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; - } - - let active = false; - let unsubs: () => void | undefined; - let deletedLinks: Array< - [SignedActionHashed, Array>] - > | undefined; - const signal = new Signal.State, Array>]>>>({ status: 'pending' }, { - [Signal.subtle.watched]: () => { - active = true; - - const maybeSet = ( - newDeletedLinks: Array< - [SignedActionHashed, Array>] - > - ) => { - if (!active) return; - - const orderedNewLinks = newDeletedLinks.sort( - sortDeletedLinksByTimestampAscending - ); - - for (let i = 0; i < orderedNewLinks.length; i += 1) { - orderedNewLinks[i][1] = orderedNewLinks[i][1].sort( - sortActionsByTimestampAscending - ); - if ( - deletedLinks !== undefined && - (!deletedLinks[i] || - !areArrayHashesEqual( - orderedNewLinks[i][1].map((d) => d.hashed.hash), - deletedLinks[i][1].map((d) => d.hashed.hash) - )) - ) - return; - } - if ( - deletedLinks === undefined || - !areArrayHashesEqual( - orderedNewLinks.map((l) => l[0].hashed.hash), - deletedLinks.map((l) => l[0].hashed.hash) - ) - ) { - deletedLinks = orderedNewLinks; - signal.set({ - status: 'completed', - value: deletedLinks - }); - } - }; - const fetch = async () => { - if (!active) return; - const ndeletedLinks = await fetchDeletedLinks().finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - maybeSet(ndeletedLinks); - }; - fetch().catch(error => { - signal.set({ - status: 'error', - error - }) - }); - unsubs = client.onSignal((originalSignal) => { - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if (hcSignal.type === "LinkDeleted") { - if ( - linkType in hcSignal.link_type && - hcSignal.create_link_action.hashed.content.base_address.toString() === - innerBaseAddress.toString() - ) { - const lastDeletedLinks = deletedLinks ? deletedLinks : [] - const alreadyDeletedTargetIndex = lastDeletedLinks.findIndex( - ([cl]) => - cl.hashed.hash.toString() === - hcSignal.create_link_action.hashed.hash.toString() - ); - - if (alreadyDeletedTargetIndex !== -1) { - if ( - !lastDeletedLinks[alreadyDeletedTargetIndex][1].find( - (dl) => - dl.hashed.hash.toString() === - hcSignal.action.hashed.hash.toString() - ) - ) { - const clone = cloneDeep(deletedLinks); - clone[alreadyDeletedTargetIndex][1].push(hcSignal.action); - maybeSet(clone); - } - } else { - maybeSet([ - ...lastDeletedLinks, - [hcSignal.create_link_action, [hcSignal.action]], - ]); - } - } - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: 'pending' - }); - active = false; - deletedLinks = undefined; - unsubs(); - } - }); - - return signal; + let innerBaseAddress = baseAddress; + if (getHashType(innerBaseAddress) === HashType.AGENT) { + innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; + } + + let active = false; + let unsubs: () => void | undefined; + let deletedLinks: + | Array< + [SignedActionHashed, Array>] + > + | undefined; + const signal = new Signal.State< + AsyncResult< + Array< + [SignedActionHashed, Array>] + > + > + >( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + + const maybeSet = ( + newDeletedLinks: Array< + [ + SignedActionHashed, + Array>, + ] + >, + ) => { + if (!active) return; + + const orderedNewLinks = newDeletedLinks.sort( + sortDeletedLinksByTimestampAscending, + ); + + for (let i = 0; i < orderedNewLinks.length; i += 1) { + orderedNewLinks[i][1] = orderedNewLinks[i][1].sort( + sortActionsByTimestampAscending, + ); + if ( + deletedLinks !== undefined && + (!deletedLinks[i] || + !areArrayHashesEqual( + orderedNewLinks[i][1].map(d => d.hashed.hash), + deletedLinks[i][1].map(d => d.hashed.hash), + )) + ) + return; + } + if ( + deletedLinks === undefined || + !areArrayHashesEqual( + orderedNewLinks.map(l => l[0].hashed.hash), + deletedLinks.map(l => l[0].hashed.hash), + ) + ) { + deletedLinks = orderedNewLinks; + signal.set({ + status: 'completed', + value: deletedLinks, + }); + } + }; + const fetch = async () => { + if (!active) return; + const ndeletedLinks = await fetchDeletedLinks().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + maybeSet(ndeletedLinks); + }; + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(originalSignal => { + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if (hcSignal.type === 'LinkDeleted') { + if ( + linkType in hcSignal.link_type && + hcSignal.create_link_action.hashed.content.base_address.toString() === + innerBaseAddress.toString() + ) { + const lastDeletedLinks = deletedLinks ? deletedLinks : []; + const alreadyDeletedTargetIndex = lastDeletedLinks.findIndex( + ([cl]) => + cl.hashed.hash.toString() === + hcSignal.create_link_action.hashed.hash.toString(), + ); + + if (alreadyDeletedTargetIndex !== -1) { + if ( + !lastDeletedLinks[alreadyDeletedTargetIndex][1].find( + dl => + dl.hashed.hash.toString() === + hcSignal.action.hashed.hash.toString(), + ) + ) { + const clone = cloneDeep(deletedLinks); + clone[alreadyDeletedTargetIndex][1].push(hcSignal.action); + maybeSet(clone); + } + } else { + maybeSet([ + ...lastDeletedLinks, + [hcSignal.create_link_action, [hcSignal.action]], + ]); + } + } + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + deletedLinks = undefined; + unsubs(); + }, + }, + ); + + return signal; } diff --git a/ui/src/types.ts b/ui/src/types.ts index e8e4430a..e50c5b36 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -1,36 +1,32 @@ +import { ActionCommittedSignal } from '@holochain-open-dev/utils'; import { - Record, - ActionHash, - DnaHash, - SignedActionHashed, - EntryHash, - AgentPubKey, - Create, - Update, - Delete, - CreateLink, - DeleteLink + ActionHash, + AgentPubKey, + Create, + CreateLink, + Delete, + DeleteLink, + DnaHash, + EntryHash, + Record, + SignedActionHashed, + Update, } from '@holochain/client'; -import { ActionCommittedSignal } from '@holochain-open-dev/utils'; export type NotificationsSignal = ActionCommittedSignal; -export type EntryTypes = - | ({ type: 'Notification'; } & Notification); +export type EntryTypes = { type: 'Notification' } & Notification; export type LinkTypes = string; - - export interface Notification { - notification_type: string; + notification_type: string; - notification_group: string | undefined; + notification_group: string | undefined; - persistent: boolean; + persistent: boolean; - recipients: Array; + recipients: Array; - content: Uint8Array; + content: Uint8Array; } - diff --git a/zomes/coordinator/notifications/src/lib.rs b/zomes/coordinator/notifications/src/lib.rs index c21cb0aa..c35a7b01 100644 --- a/zomes/coordinator/notifications/src/lib.rs +++ b/zomes/coordinator/notifications/src/lib.rs @@ -3,138 +3,130 @@ use hdk::prelude::*; use notifications_integrity::*; #[hdk_extern] pub fn init(_: ()) -> ExternResult { - Ok(InitCallbackResult::Pass) + Ok(InitCallbackResult::Pass) } #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type")] pub enum Signal { - LinkCreated { action: SignedActionHashed, link_type: LinkTypes }, - LinkDeleted { - action: SignedActionHashed, - create_link_action: SignedActionHashed, - link_type: LinkTypes, - }, - EntryCreated { action: SignedActionHashed, app_entry: EntryTypes }, - EntryUpdated { - action: SignedActionHashed, - app_entry: EntryTypes, - original_app_entry: EntryTypes, - }, - EntryDeleted { action: SignedActionHashed, original_app_entry: EntryTypes }, + LinkCreated { + action: SignedActionHashed, + link_type: LinkTypes, + }, + LinkDeleted { + action: SignedActionHashed, + create_link_action: SignedActionHashed, + link_type: LinkTypes, + }, + EntryCreated { + action: SignedActionHashed, + app_entry: EntryTypes, + }, + EntryUpdated { + action: SignedActionHashed, + app_entry: EntryTypes, + original_app_entry: EntryTypes, + }, + EntryDeleted { + action: SignedActionHashed, + original_app_entry: EntryTypes, + }, } #[hdk_extern(infallible)] pub fn post_commit(committed_actions: Vec) { - for action in committed_actions { - if let Err(err) = signal_action(action) { - error!("Error signaling new action: {:?}", err); - } - } + for action in committed_actions { + if let Err(err) = signal_action(action) { + error!("Error signaling new action: {:?}", err); + } + } } fn signal_action(action: SignedActionHashed) -> ExternResult<()> { - match action.hashed.content.clone() { - Action::CreateLink(create_link) => { - if let Ok(Some(link_type)) = LinkTypes::from_type( - create_link.zome_index, - create_link.link_type, - ) { - emit_signal(Signal::LinkCreated { - action, - link_type, - })?; - } - Ok(()) - } - Action::DeleteLink(delete_link) => { - let record = get( - delete_link.link_add_address.clone(), - GetOptions::default(), - )? - .ok_or( - wasm_error!( - WasmErrorInner::Guest("Failed to fetch CreateLink action" - .to_string()) - ), - )?; - match record.action() { - Action::CreateLink(create_link) => { - if let Ok(Some(link_type)) = LinkTypes::from_type( - create_link.zome_index, - create_link.link_type, - ) { - emit_signal(Signal::LinkDeleted { - action, - link_type, - create_link_action: record.signed_action.clone(), - })?; - } - Ok(()) - } - _ => { - Err( - wasm_error!( - WasmErrorInner::Guest("Create Link should exist".to_string()) - ), - ) - } - } - } - Action::Create(_create) => { - if let Ok(Some(app_entry)) = get_entry_for_action(&action.hashed.hash) { - emit_signal(Signal::EntryCreated { - action, - app_entry, - })?; - } - Ok(()) - } - Action::Update(update) => { - if let Ok(Some(app_entry)) = get_entry_for_action(&action.hashed.hash) { - if let Ok(Some(original_app_entry)) = get_entry_for_action( - &update.original_action_address, - ) { - emit_signal(Signal::EntryUpdated { - action, - app_entry, - original_app_entry, - })?; - } - } - Ok(()) - } - Action::Delete(delete) => { - if let Ok(Some(original_app_entry)) = get_entry_for_action( - &delete.deletes_address, - ) { - emit_signal(Signal::EntryDeleted { - action, - original_app_entry, - })?; - } - Ok(()) - } - _ => Ok(()), - } + match action.hashed.content.clone() { + Action::CreateLink(create_link) => { + if let Ok(Some(link_type)) = + LinkTypes::from_type(create_link.zome_index, create_link.link_type) + { + emit_signal(Signal::LinkCreated { action, link_type })?; + } + Ok(()) + } + Action::DeleteLink(delete_link) => { + let record = get(delete_link.link_add_address.clone(), GetOptions::default())?.ok_or( + wasm_error!(WasmErrorInner::Guest( + "Failed to fetch CreateLink action".to_string() + )), + )?; + match record.action() { + Action::CreateLink(create_link) => { + if let Ok(Some(link_type)) = + LinkTypes::from_type(create_link.zome_index, create_link.link_type) + { + emit_signal(Signal::LinkDeleted { + action, + link_type, + create_link_action: record.signed_action.clone(), + })?; + } + Ok(()) + } + _ => Err(wasm_error!(WasmErrorInner::Guest( + "Create Link should exist".to_string() + ))), + } + } + Action::Create(_create) => { + if let Ok(Some(app_entry)) = get_entry_for_action(&action.hashed.hash) { + emit_signal(Signal::EntryCreated { action, app_entry })?; + } + Ok(()) + } + Action::Update(update) => { + if let Ok(Some(app_entry)) = get_entry_for_action(&action.hashed.hash) { + if let Ok(Some(original_app_entry)) = + get_entry_for_action(&update.original_action_address) + { + emit_signal(Signal::EntryUpdated { + action, + app_entry, + original_app_entry, + })?; + } + } + Ok(()) + } + Action::Delete(delete) => { + if let Ok(Some(original_app_entry)) = get_entry_for_action(&delete.deletes_address) { + emit_signal(Signal::EntryDeleted { + action, + original_app_entry, + })?; + } + Ok(()) + } + _ => Ok(()), + } } fn get_entry_for_action(action_hash: &ActionHash) -> ExternResult> { - let record = match get_details(action_hash.clone(), GetOptions::default())? { - Some(Details::Record(record_details)) => record_details.record, - _ => { - return Ok(None); - } - }; - let entry = match record.entry().as_option() { - Some(entry) => entry, - None => { - return Ok(None); - } - }; - let (zome_index, entry_index) = match record.action().entry_type() { - Some(EntryType::App(AppEntryDef { zome_index, entry_index, .. })) => { - (zome_index, entry_index) - } - _ => { - return Ok(None); - } - }; - EntryTypes::deserialize_from_type(*zome_index, *entry_index, entry) + let record = match get_details(action_hash.clone(), GetOptions::default())? { + Some(Details::Record(record_details)) => record_details.record, + _ => { + return Ok(None); + } + }; + let entry = match record.entry().as_option() { + Some(entry) => entry, + None => { + return Ok(None); + } + }; + let (zome_index, entry_index) = match record.action().entry_type() { + Some(EntryType::App(AppEntryDef { + zome_index, + entry_index, + .. + })) => (zome_index, entry_index), + _ => { + return Ok(None); + } + }; + EntryTypes::deserialize_from_type(*zome_index, *entry_index, entry) } diff --git a/zomes/coordinator/notifications/src/notification.rs b/zomes/coordinator/notifications/src/notification.rs index 2ed4511e..7166502d 100644 --- a/zomes/coordinator/notifications/src/notification.rs +++ b/zomes/coordinator/notifications/src/notification.rs @@ -3,32 +3,32 @@ use notifications_integrity::*; #[hdk_extern] pub fn create_notification(notification: Notification) -> ExternResult { - let notification_hash = create_entry(&EntryTypes::Notification(notification.clone()))?; - for base in notification.recipients.clone() { - create_link( - base, - notification_hash.clone(), - LinkTypes::RecipientToNotifications, - (), - )?; - } - let record = get(notification_hash.clone(), GetOptions::default())?.ok_or(wasm_error!( - WasmErrorInner::Guest("Could not find the newly created Notification".to_string()) - ))?; - Ok(record) + let notification_hash = create_entry(&EntryTypes::Notification(notification.clone()))?; + for base in notification.recipients.clone() { + create_link( + base, + notification_hash.clone(), + LinkTypes::RecipientToNotifications, + (), + )?; + } + let record = get(notification_hash.clone(), GetOptions::default())?.ok_or(wasm_error!( + WasmErrorInner::Guest("Could not find the newly created Notification".to_string()) + ))?; + Ok(record) } #[hdk_extern] pub fn get_notification(notification_hash: ActionHash) -> ExternResult> { - let Some(details) = get_details(notification_hash, GetOptions::default())? else { - return Ok(None); - }; - match details { - Details::Record(details) => Ok(Some(details.record)), - _ => Err(wasm_error!(WasmErrorInner::Guest( - "Malformed get details response".to_string() - ))), - } + let Some(details) = get_details(notification_hash, GetOptions::default())? else { + return Ok(None); + }; + match details { + Details::Record(details) => Ok(Some(details.record)), + _ => Err(wasm_error!(WasmErrorInner::Guest( + "Malformed get details response".to_string() + ))), + } } #[derive(Serialize, Deserialize, Debug, SerializedBytes)] @@ -36,118 +36,118 @@ pub struct ReadNotifications(pub Vec); #[hdk_extern] pub fn mark_notifications_as_read(notifications_hashes: Vec) -> ExternResult<()> { - let read_notifications = ReadNotifications(notifications_hashes); - - let bytes = SerializedBytes::try_from(read_notifications).map_err(|err| { - wasm_error!(WasmErrorInner::Guest(format!( - "Failed to serialize ReadNotifications {err:?}" - ))) - })?; - - create_link( - agent_info()?.agent_latest_pubkey, - agent_info()?.agent_latest_pubkey, - LinkTypes::ReadNotifications, - bytes.bytes().to_vec(), - )?; - - Ok(()) + let read_notifications = ReadNotifications(notifications_hashes); + + let bytes = SerializedBytes::try_from(read_notifications).map_err(|err| { + wasm_error!(WasmErrorInner::Guest(format!( + "Failed to serialize ReadNotifications {err:?}" + ))) + })?; + + create_link( + agent_info()?.agent_latest_pubkey, + agent_info()?.agent_latest_pubkey, + LinkTypes::ReadNotifications, + bytes.bytes().to_vec(), + )?; + + Ok(()) } #[hdk_extern] pub fn dismiss_notifications(notifications_hashes: Vec) -> ExternResult<()> { - for hash in notifications_hashes.iter() { - dismiss_notification(hash.clone())?; - } - - let read_links = get_read_notifications(())?.into_iter(); - - let undismissed_notifications_hashes: HashSet = get_undismissed_notifications(())? - .into_iter() - .filter_map(|link| link.target.into_action_hash()) - .collect(); - - // Iterate over the read links - for link in read_links { - let sb = SerializedBytes::from(UnsafeBytes::from(link.tag.0)); - let read_notifications_hashes = ReadNotifications::try_from(sb).map_err(|err| { - wasm_error!(WasmErrorInner::Guest(format!( - "Failed to serialize ReadNotifications {err:?}" - ))) - })?; - - if read_notifications_hashes.0.iter().all(|hash| { - notifications_hashes.contains(hash) || !undismissed_notifications_hashes.contains(hash) - }) { - delete_link(link.create_link_hash)?; - } - } - - Ok(()) + for hash in notifications_hashes.iter() { + dismiss_notification(hash.clone())?; + } + + let read_links = get_read_notifications(())?.into_iter(); + + let undismissed_notifications_hashes: HashSet = get_undismissed_notifications(())? + .into_iter() + .filter_map(|link| link.target.into_action_hash()) + .collect(); + + // Iterate over the read links + for link in read_links { + let sb = SerializedBytes::from(UnsafeBytes::from(link.tag.0)); + let read_notifications_hashes = ReadNotifications::try_from(sb).map_err(|err| { + wasm_error!(WasmErrorInner::Guest(format!( + "Failed to serialize ReadNotifications {err:?}" + ))) + })?; + + if read_notifications_hashes.0.iter().all(|hash| { + notifications_hashes.contains(hash) || !undismissed_notifications_hashes.contains(hash) + }) { + delete_link(link.create_link_hash)?; + } + } + + Ok(()) } fn dismiss_notification(notification_hash: ActionHash) -> ExternResult { - let links = get_undismissed_notifications(())?; - - for link in links { - if let Some(action_hash) = link.target.into_action_hash() { - if action_hash.eq(¬ification_hash) { - delete_link(link.create_link_hash)?; - } - } - } - delete_entry(notification_hash) + let links = get_undismissed_notifications(())?; + + for link in links { + if let Some(action_hash) = link.target.into_action_hash() { + if action_hash.eq(¬ification_hash) { + delete_link(link.create_link_hash)?; + } + } + } + delete_entry(notification_hash) } #[hdk_extern] pub fn get_all_deletes_for_notification( - original_notification_hash: ActionHash, + original_notification_hash: ActionHash, ) -> ExternResult>> { - let Some(details) = get_details(original_notification_hash, GetOptions::default())? else { - return Ok(None); - }; - match details { - Details::Entry(_) => Err(wasm_error!(WasmErrorInner::Guest( - "Malformed details".into() - ))), - Details::Record(record_details) => Ok(Some(record_details.deletes)), - } + let Some(details) = get_details(original_notification_hash, GetOptions::default())? else { + return Ok(None); + }; + match details { + Details::Entry(_) => Err(wasm_error!(WasmErrorInner::Guest( + "Malformed details".into() + ))), + Details::Record(record_details) => Ok(Some(record_details.deletes)), + } } #[hdk_extern] pub fn get_undismissed_notifications() -> ExternResult> { - get_links( - GetLinksInputBuilder::try_new( - agent_info()?.agent_latest_pubkey, - LinkTypes::RecipientToNotifications, - )? - .build(), - ) + get_links( + GetLinksInputBuilder::try_new( + agent_info()?.agent_latest_pubkey, + LinkTypes::RecipientToNotifications, + )? + .build(), + ) } #[hdk_extern] pub fn get_read_notifications() -> ExternResult> { - get_links( - GetLinksInputBuilder::try_new( - agent_info()?.agent_latest_pubkey, - LinkTypes::ReadNotifications, - )? - .build(), - ) + get_links( + GetLinksInputBuilder::try_new( + agent_info()?.agent_latest_pubkey, + LinkTypes::ReadNotifications, + )? + .build(), + ) } #[hdk_extern] pub fn get_dismissed_notifications( ) -> ExternResult)>> { - let details = get_link_details( - agent_info()?.agent_latest_pubkey, - LinkTypes::RecipientToNotifications, - None, - GetOptions::default(), - )?; - Ok(details - .into_inner() - .into_iter() - .filter(|(_link, deletes)| !deletes.is_empty()) - .collect()) + let details = get_link_details( + agent_info()?.agent_latest_pubkey, + LinkTypes::RecipientToNotifications, + None, + GetOptions::default(), + )?; + Ok(details + .into_inner() + .into_iter() + .filter(|(_link, deletes)| !deletes.is_empty()) + .collect()) } diff --git a/zomes/coordinator/notifications/tests/common/mod.rs b/zomes/coordinator/notifications/tests/common/mod.rs index 7057200a..2d27164d 100644 --- a/zomes/coordinator/notifications/tests/common/mod.rs +++ b/zomes/coordinator/notifications/tests/common/mod.rs @@ -4,34 +4,34 @@ use holochain::sweettest::*; use notifications_integrity::*; pub async fn sample_notification_1(conductor: &SweetConductor, zome: &SweetZome) -> Notification { - Notification { - notification_type: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_string(), - notification_group: Some( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_string(), - ), - persistent: true, - recipients: vec![zome.cell_id().agent_pubkey().clone()], - content: SerializedBytes::from(UnsafeBytes::from(vec![])), - } + Notification { + notification_type: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_string(), + notification_group: Some( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_string(), + ), + persistent: true, + recipients: vec![zome.cell_id().agent_pubkey().clone()], + content: SerializedBytes::from(UnsafeBytes::from(vec![])), + } } pub async fn sample_notification_2(conductor: &SweetConductor, zome: &SweetZome) -> Notification { - Notification { - notification_type: "Lorem ipsum 2".to_string(), - notification_group: Some("Lorem ipsum 2".to_string()), - persistent: true, - recipients: vec![zome.cell_id().agent_pubkey().clone()], - content: SerializedBytes::from(UnsafeBytes::from(vec![])), - } + Notification { + notification_type: "Lorem ipsum 2".to_string(), + notification_group: Some("Lorem ipsum 2".to_string()), + persistent: true, + recipients: vec![zome.cell_id().agent_pubkey().clone()], + content: SerializedBytes::from(UnsafeBytes::from(vec![])), + } } pub async fn create_notification( - conductor: &SweetConductor, - zome: &SweetZome, - notification: Notification, + conductor: &SweetConductor, + zome: &SweetZome, + notification: Notification, ) -> Record { - let record: Record = conductor - .call(zome, "create_notification", notification) - .await; - record + let record: Record = conductor + .call(zome, "create_notification", notification) + .await; + record } diff --git a/zomes/coordinator/notifications/tests/notification.rs b/zomes/coordinator/notifications/tests/notification.rs index aac216ff..843edb5c 100644 --- a/zomes/coordinator/notifications/tests/notification.rs +++ b/zomes/coordinator/notifications/tests/notification.rs @@ -2,115 +2,132 @@ #![allow(unused_variables)] #![allow(unused_imports)] -use std::time::Duration; use hdk::prelude::*; use holochain::{conductor::config::ConductorConfig, sweettest::*}; +use std::time::Duration; use notifications_integrity::*; - mod common; use common::{create_notification, sample_notification_1, sample_notification_2}; - #[tokio::test(flavor = "multi_thread")] async fn create_notification_test() { - // Use prebuilt dna file - let dna_path = std::env::current_dir() - .unwrap() - .join(std::env::var("DNA_PATH").expect("DNA_PATH not set, must be run using nix flake check")); - let dna = SweetDnaFile::from_bundle(&dna_path).await.unwrap(); - - // Set up conductors - let mut conductors = SweetConductorBatch::from_config(2, ConductorConfig::default()).await; - let apps = conductors.setup_app("notifications_test", &[dna]).await.unwrap(); - conductors.exchange_peer_info().await; - - let ((alice,), (_bobbo,)) = apps.into_tuples(); - - let alice_zome = alice.zome("notifications"); - - let sample = sample_notification_1(&conductors[0], &alice_zome).await; - - // Alice creates a Notification - let record: Record = create_notification(&conductors[0], &alice_zome, sample.clone()).await; - let entry: Notification = record.entry().to_app_option().unwrap().unwrap(); - assert!(entry.eq(&sample)); + // Use prebuilt dna file + let dna_path = std::env::current_dir().unwrap().join( + std::env::var("DNA_PATH").expect("DNA_PATH not set, must be run using nix flake check"), + ); + let dna = SweetDnaFile::from_bundle(&dna_path).await.unwrap(); + + // Set up conductors + let mut conductors = SweetConductorBatch::from_config(2, ConductorConfig::default()).await; + let apps = conductors + .setup_app("notifications_test", &[dna]) + .await + .unwrap(); + conductors.exchange_peer_info().await; + + let ((alice,), (_bobbo,)) = apps.into_tuples(); + + let alice_zome = alice.zome("notifications"); + + let sample = sample_notification_1(&conductors[0], &alice_zome).await; + + // Alice creates a Notification + let record: Record = create_notification(&conductors[0], &alice_zome, sample.clone()).await; + let entry: Notification = record.entry().to_app_option().unwrap().unwrap(); + assert!(entry.eq(&sample)); } - #[tokio::test(flavor = "multi_thread")] async fn create_and_read_notification() { - // Use prebuilt dna file - let dna_path = std::env::current_dir() - .unwrap() - .join(std::env::var("DNA_PATH").expect("DNA_PATH not set, must be run using nix flake check")); - let dna = SweetDnaFile::from_bundle(&dna_path).await.unwrap(); - - // Set up conductors - let mut conductors = SweetConductorBatch::from_config(2, ConductorConfig::default()).await; - let apps = conductors.setup_app("notifications_test", &[dna]).await.unwrap(); - conductors.exchange_peer_info().await; - - let ((alice,), (bobbo,)) = apps.into_tuples(); - - let alice_zome = alice.zome("notifications"); - let bob_zome = bobbo.zome("notifications"); - - let sample = sample_notification_1(&conductors[0], &alice_zome).await; - - // Alice creates a Notification - let record: Record = create_notification(&conductors[0], &alice_zome, sample.clone()).await; - - await_consistency(Duration::from_secs(60), [&alice, &bobbo]) - .await - .expect("Timed out waiting for consistency"); - - let get_record: Option = conductors[1] - .call(&bob_zome, "get_notification", record.signed_action.action_address().clone()) - .await; - - assert_eq!(record, get_record.unwrap()); + // Use prebuilt dna file + let dna_path = std::env::current_dir().unwrap().join( + std::env::var("DNA_PATH").expect("DNA_PATH not set, must be run using nix flake check"), + ); + let dna = SweetDnaFile::from_bundle(&dna_path).await.unwrap(); + + // Set up conductors + let mut conductors = SweetConductorBatch::from_config(2, ConductorConfig::default()).await; + let apps = conductors + .setup_app("notifications_test", &[dna]) + .await + .unwrap(); + conductors.exchange_peer_info().await; + + let ((alice,), (bobbo,)) = apps.into_tuples(); + + let alice_zome = alice.zome("notifications"); + let bob_zome = bobbo.zome("notifications"); + + let sample = sample_notification_1(&conductors[0], &alice_zome).await; + + // Alice creates a Notification + let record: Record = create_notification(&conductors[0], &alice_zome, sample.clone()).await; + + await_consistency(Duration::from_secs(60), [&alice, &bobbo]) + .await + .expect("Timed out waiting for consistency"); + + let get_record: Option = conductors[1] + .call( + &bob_zome, + "get_notification", + record.signed_action.action_address().clone(), + ) + .await; + + assert_eq!(record, get_record.unwrap()); } - #[tokio::test(flavor = "multi_thread")] async fn create_and_delete_notification() { - // Use prebuilt dna file - let dna_path = std::env::current_dir() - .unwrap() - .join(std::env::var("DNA_PATH").expect("DNA_PATH not set, must be run using nix flake check")); - let dna = SweetDnaFile::from_bundle(&dna_path).await.unwrap(); - - // Set up conductors - let mut conductors = SweetConductorBatch::from_config(2, ConductorConfig::default()).await; - let apps = conductors.setup_app("notifications_test", &[dna]).await.unwrap(); - conductors.exchange_peer_info().await; - - let ((alice,), (bobbo,)) = apps.into_tuples(); - - let alice_zome = alice.zome("notifications"); - let bob_zome = bobbo.zome("notifications"); - - let sample_1 = sample_notification_1(&conductors[0], &alice_zome).await; - - // Alice creates a Notification - let record: Record = create_notification(&conductors[0], &alice_zome, sample_1.clone()).await; - let original_action_hash = record.signed_action.hashed.hash; - - // Alice deletes the Notification - let delete_action_hash: ActionHash = conductors[0] - .call(&alice_zome, "delete_notification", original_action_hash.clone()) - .await; - - await_consistency(Duration::from_secs(60), [&alice, &bobbo]) - .await - .expect("Timed out waiting for consistency"); - - let deletes: Vec = conductors[1] - .call(&bob_zome, "get_all_deletes_for_notification", original_action_hash.clone()) - .await; - - assert_eq!(deletes.len(), 1); - assert_eq!(deletes[0].hashed.hash, delete_action_hash); + // Use prebuilt dna file + let dna_path = std::env::current_dir().unwrap().join( + std::env::var("DNA_PATH").expect("DNA_PATH not set, must be run using nix flake check"), + ); + let dna = SweetDnaFile::from_bundle(&dna_path).await.unwrap(); + + // Set up conductors + let mut conductors = SweetConductorBatch::from_config(2, ConductorConfig::default()).await; + let apps = conductors + .setup_app("notifications_test", &[dna]) + .await + .unwrap(); + conductors.exchange_peer_info().await; + + let ((alice,), (bobbo,)) = apps.into_tuples(); + + let alice_zome = alice.zome("notifications"); + let bob_zome = bobbo.zome("notifications"); + + let sample_1 = sample_notification_1(&conductors[0], &alice_zome).await; + + // Alice creates a Notification + let record: Record = create_notification(&conductors[0], &alice_zome, sample_1.clone()).await; + let original_action_hash = record.signed_action.hashed.hash; + + // Alice deletes the Notification + let delete_action_hash: ActionHash = conductors[0] + .call( + &alice_zome, + "delete_notification", + original_action_hash.clone(), + ) + .await; + + await_consistency(Duration::from_secs(60), [&alice, &bobbo]) + .await + .expect("Timed out waiting for consistency"); + + let deletes: Vec = conductors[1] + .call( + &bob_zome, + "get_all_deletes_for_notification", + original_action_hash.clone(), + ) + .await; + + assert_eq!(deletes.len(), 1); + assert_eq!(deletes[0].hashed.hash, delete_action_hash); } diff --git a/zomes/integrity/notifications/src/lib.rs b/zomes/integrity/notifications/src/lib.rs index 1aa71105..4b584485 100644 --- a/zomes/integrity/notifications/src/lib.rs +++ b/zomes/integrity/notifications/src/lib.rs @@ -11,325 +11,313 @@ pub use read_notifications::*; #[hdk_entry_types] #[unit_enum(UnitEntryTypes)] pub enum EntryTypes { - Notification(Notification), + Notification(Notification), } #[derive(Serialize, Deserialize)] #[hdk_link_types] pub enum LinkTypes { - RecipientToNotifications, - ReadNotifications, + RecipientToNotifications, + ReadNotifications, } #[hdk_extern] pub fn genesis_self_check(_data: GenesisSelfCheckData) -> ExternResult { - Ok(ValidateCallbackResult::Valid) + Ok(ValidateCallbackResult::Valid) } pub fn validate_agent_joining( - _agent_pub_key: AgentPubKey, - _membrane_proof: &Option, + _agent_pub_key: AgentPubKey, + _membrane_proof: &Option, ) -> ExternResult { - Ok(ValidateCallbackResult::Valid) + Ok(ValidateCallbackResult::Valid) } #[hdk_extern] pub fn validate(op: Op) -> ExternResult { - match op.flattened::()? { - FlatOp::StoreEntry(store_entry) => match store_entry { - OpEntry::CreateEntry { app_entry, action } => match app_entry { - EntryTypes::Notification(notification) => { - validate_create_notification(EntryCreationAction::Create(action), notification) - } - }, - OpEntry::UpdateEntry { - app_entry, action, .. - } => match app_entry { - EntryTypes::Notification(notification) => { - validate_create_notification(EntryCreationAction::Update(action), notification) - } - }, - _ => Ok(ValidateCallbackResult::Valid), - }, - FlatOp::RegisterUpdate(update_entry) => match update_entry { - OpUpdate::Entry { - original_action, - original_app_entry, - app_entry, - action, - } => match (app_entry, original_app_entry) { - ( - EntryTypes::Notification(notification), - EntryTypes::Notification(original_notification), - ) => validate_update_notification( - action, - notification, - original_action, - original_notification, - ), - _ => Ok(ValidateCallbackResult::Invalid( - "Original and updated entry types must be the same".to_string(), - )), - }, - _ => Ok(ValidateCallbackResult::Valid), - }, - FlatOp::RegisterDelete(delete_entry) => match delete_entry { - OpDelete::Entry { - original_action, - original_app_entry, - action, - } => match original_app_entry { - EntryTypes::Notification(notification) => { - validate_delete_notification(action, original_action, notification) - } - }, - _ => Ok(ValidateCallbackResult::Valid), - }, - FlatOp::RegisterCreateLink { - link_type, - base_address, - target_address, - tag, - action, - } => match link_type { - LinkTypes::RecipientToNotifications => validate_create_link_recipient_to_notifications( - action, - base_address, - target_address, - tag, - ), - LinkTypes::ReadNotifications => { - validate_create_link_read_notifications(action, base_address, target_address, tag) - } - }, - FlatOp::RegisterDeleteLink { - link_type, - base_address, - target_address, - tag, - original_action, - action, - } => match link_type { - LinkTypes::RecipientToNotifications => validate_delete_link_recipient_to_notifications( - action, - original_action, - base_address, - target_address, - tag, - ), - LinkTypes::ReadNotifications => validate_delete_link_read_notifications( - action, - original_action, - base_address, - target_address, - tag, - ), - }, - FlatOp::StoreRecord(store_record) => match store_record { - OpRecord::CreateEntry { app_entry, action } => match app_entry { - EntryTypes::Notification(notification) => { - validate_create_notification(EntryCreationAction::Create(action), notification) - } - }, - OpRecord::UpdateEntry { - original_action_hash, - app_entry, - action, - .. - } => { - let original_record = must_get_valid_record(original_action_hash)?; - let original_action = original_record.action().clone(); - let original_action = match original_action { - Action::Create(create) => EntryCreationAction::Create(create), - Action::Update(update) => EntryCreationAction::Update(update), - _ => { - return Ok(ValidateCallbackResult::Invalid( - "Original action for an update must be a Create or Update action" - .to_string(), - )); - } - }; - match app_entry { - EntryTypes::Notification(notification) => { - let result = validate_create_notification( - EntryCreationAction::Update(action.clone()), - notification.clone(), - )?; - if let ValidateCallbackResult::Valid = result { - let original_notification: Option = original_record - .entry() - .to_app_option() - .map_err(|e| wasm_error!(e))?; - let original_notification = match original_notification { - Some(notification) => notification, - None => { - return Ok( - ValidateCallbackResult::Invalid( - "The updated entry type must be the same as the original entry type" - .to_string(), - ), - ); - } - }; - validate_update_notification( - action, - notification, - original_action, - original_notification, - ) - } else { - Ok(result) - } - } - } - } - OpRecord::DeleteEntry { - original_action_hash, - action, - .. - } => { - let original_record = must_get_valid_record(original_action_hash)?; - let original_action = original_record.action().clone(); - let original_action = match original_action { - Action::Create(create) => EntryCreationAction::Create(create), - Action::Update(update) => EntryCreationAction::Update(update), - _ => { - return Ok(ValidateCallbackResult::Invalid( - "Original action for a delete must be a Create or Update action" - .to_string(), - )); - } - }; - let app_entry_type = match original_action.entry_type() { - EntryType::App(app_entry_type) => app_entry_type, - _ => { - return Ok(ValidateCallbackResult::Valid); - } - }; - let entry = match original_record.entry().as_option() { - Some(entry) => entry, - None => { - if original_action.entry_type().visibility().is_public() { - return Ok( - ValidateCallbackResult::Invalid( - "Original record for a delete of a public entry must contain an entry" - .to_string(), - ), - ); - } else { - return Ok(ValidateCallbackResult::Valid); - } - } - }; - let original_app_entry = match EntryTypes::deserialize_from_type( - app_entry_type.zome_index, - app_entry_type.entry_index, - entry, - )? { - Some(app_entry) => app_entry, - None => { - return Ok( - ValidateCallbackResult::Invalid( - "Original app entry must be one of the defined entry types for this zome" - .to_string(), - ), - ); - } - }; - match original_app_entry { - EntryTypes::Notification(original_notification) => { - validate_delete_notification(action, original_action, original_notification) - } - } - } - OpRecord::CreateLink { - base_address, - target_address, - tag, - link_type, - action, - } => match link_type { - LinkTypes::RecipientToNotifications => { - validate_create_link_recipient_to_notifications( - action, - base_address, - target_address, - tag, - ) - } - LinkTypes::ReadNotifications => validate_create_link_read_notifications( - action, - base_address, - target_address, - tag, - ), - }, - OpRecord::DeleteLink { - original_action_hash, - base_address, - action, - } => { - let record = must_get_valid_record(original_action_hash)?; - let create_link = match record.action() { - Action::CreateLink(create_link) => create_link.clone(), - _ => { - return Ok(ValidateCallbackResult::Invalid( - "The action that a DeleteLink deletes must be a CreateLink".to_string(), - )); - } - }; - let link_type = - match LinkTypes::from_type(create_link.zome_index, create_link.link_type)? { - Some(lt) => lt, - None => { - return Ok(ValidateCallbackResult::Valid); - } - }; - match link_type { - LinkTypes::RecipientToNotifications => { - validate_delete_link_recipient_to_notifications( - action, - create_link.clone(), - base_address, - create_link.target_address, - create_link.tag, - ) - } - LinkTypes::ReadNotifications => validate_delete_link_read_notifications( - action, - create_link.clone(), - base_address, - create_link.target_address, - create_link.tag, - ), - } - } - OpRecord::CreatePrivateEntry { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::UpdatePrivateEntry { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::CreateCapClaim { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::CreateCapGrant { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::UpdateCapClaim { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::UpdateCapGrant { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::Dna { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::OpenChain { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::CloseChain { .. } => Ok(ValidateCallbackResult::Valid), - OpRecord::InitZomesComplete { .. } => Ok(ValidateCallbackResult::Valid), - _ => Ok(ValidateCallbackResult::Valid), - }, - FlatOp::RegisterAgentActivity(agent_activity) => match agent_activity { - OpActivity::CreateAgent { agent, action } => { - let previous_action = must_get_action(action.prev_action)?; - match previous_action.action() { - Action::AgentValidationPkg( - AgentValidationPkg { membrane_proof, .. }, - ) => validate_agent_joining(agent, membrane_proof), - _ => { - Ok( - ValidateCallbackResult::Invalid( - "The previous action for a `CreateAgent` action must be an `AgentValidationPkg`" - .to_string(), - ), - ) - } - } - } - _ => Ok(ValidateCallbackResult::Valid), - }, - } + match op.flattened::()? { + FlatOp::StoreEntry(store_entry) => match store_entry { + OpEntry::CreateEntry { app_entry, action } => match app_entry { + EntryTypes::Notification(notification) => { + validate_create_notification(EntryCreationAction::Create(action), notification) + } + }, + OpEntry::UpdateEntry { + app_entry, action, .. + } => match app_entry { + EntryTypes::Notification(notification) => { + validate_create_notification(EntryCreationAction::Update(action), notification) + } + }, + _ => Ok(ValidateCallbackResult::Valid), + }, + FlatOp::RegisterUpdate(update_entry) => match update_entry { + OpUpdate::Entry { + original_action, + original_app_entry, + app_entry, + action, + } => match (app_entry, original_app_entry) { + ( + EntryTypes::Notification(notification), + EntryTypes::Notification(original_notification), + ) => validate_update_notification( + action, + notification, + original_action, + original_notification, + ), + _ => Ok(ValidateCallbackResult::Invalid( + "Original and updated entry types must be the same".to_string(), + )), + }, + _ => Ok(ValidateCallbackResult::Valid), + }, + FlatOp::RegisterDelete(delete_entry) => match delete_entry { + OpDelete::Entry { + original_action, + original_app_entry, + action, + } => match original_app_entry { + EntryTypes::Notification(notification) => { + validate_delete_notification(action, original_action, notification) + } + }, + _ => Ok(ValidateCallbackResult::Valid), + }, + FlatOp::RegisterCreateLink { + link_type, + base_address, + target_address, + tag, + action, + } => match link_type { + LinkTypes::RecipientToNotifications => validate_create_link_recipient_to_notifications( + action, + base_address, + target_address, + tag, + ), + LinkTypes::ReadNotifications => { + validate_create_link_read_notifications(action, base_address, target_address, tag) + } + }, + FlatOp::RegisterDeleteLink { + link_type, + base_address, + target_address, + tag, + original_action, + action, + } => match link_type { + LinkTypes::RecipientToNotifications => validate_delete_link_recipient_to_notifications( + action, + original_action, + base_address, + target_address, + tag, + ), + LinkTypes::ReadNotifications => validate_delete_link_read_notifications( + action, + original_action, + base_address, + target_address, + tag, + ), + }, + FlatOp::StoreRecord(store_record) => match store_record { + OpRecord::CreateEntry { app_entry, action } => match app_entry { + EntryTypes::Notification(notification) => { + validate_create_notification(EntryCreationAction::Create(action), notification) + } + }, + OpRecord::UpdateEntry { + original_action_hash, + app_entry, + action, + .. + } => { + let original_record = must_get_valid_record(original_action_hash)?; + let original_action = original_record.action().clone(); + let original_action = match original_action { + Action::Create(create) => EntryCreationAction::Create(create), + Action::Update(update) => EntryCreationAction::Update(update), + _ => { + return Ok(ValidateCallbackResult::Invalid( + "Original action for an update must be a Create or Update action" + .to_string(), + )); + } + }; + match app_entry { + EntryTypes::Notification(notification) => { + let result = validate_create_notification( + EntryCreationAction::Update(action.clone()), + notification.clone(), + )?; + if let ValidateCallbackResult::Valid = result { + let original_notification: Option = original_record + .entry() + .to_app_option() + .map_err(|e| wasm_error!(e))?; + let original_notification = match original_notification { + Some(notification) => notification, + None => { + return Ok(ValidateCallbackResult::Invalid( + "The updated entry type must be the same as the original entry type" + .to_string(), + )); + } + }; + validate_update_notification( + action, + notification, + original_action, + original_notification, + ) + } else { + Ok(result) + } + } + } + } + OpRecord::DeleteEntry { + original_action_hash, + action, + .. + } => { + let original_record = must_get_valid_record(original_action_hash)?; + let original_action = original_record.action().clone(); + let original_action = match original_action { + Action::Create(create) => EntryCreationAction::Create(create), + Action::Update(update) => EntryCreationAction::Update(update), + _ => { + return Ok(ValidateCallbackResult::Invalid( + "Original action for a delete must be a Create or Update action" + .to_string(), + )); + } + }; + let app_entry_type = match original_action.entry_type() { + EntryType::App(app_entry_type) => app_entry_type, + _ => { + return Ok(ValidateCallbackResult::Valid); + } + }; + let entry = match original_record.entry().as_option() { + Some(entry) => entry, + None => { + if original_action.entry_type().visibility().is_public() { + return Ok(ValidateCallbackResult::Invalid( + "Original record for a delete of a public entry must contain an entry".to_string(), + )); + } else { + return Ok(ValidateCallbackResult::Valid); + } + } + }; + let original_app_entry = match EntryTypes::deserialize_from_type( + app_entry_type.zome_index, + app_entry_type.entry_index, + entry, + )? { + Some(app_entry) => app_entry, + None => { + return Ok(ValidateCallbackResult::Invalid( + "Original app entry must be one of the defined entry types for this zome".to_string(), + )); + } + }; + match original_app_entry { + EntryTypes::Notification(original_notification) => { + validate_delete_notification(action, original_action, original_notification) + } + } + } + OpRecord::CreateLink { + base_address, + target_address, + tag, + link_type, + action, + } => match link_type { + LinkTypes::RecipientToNotifications => { + validate_create_link_recipient_to_notifications( + action, + base_address, + target_address, + tag, + ) + } + LinkTypes::ReadNotifications => validate_create_link_read_notifications( + action, + base_address, + target_address, + tag, + ), + }, + OpRecord::DeleteLink { + original_action_hash, + base_address, + action, + } => { + let record = must_get_valid_record(original_action_hash)?; + let create_link = match record.action() { + Action::CreateLink(create_link) => create_link.clone(), + _ => { + return Ok(ValidateCallbackResult::Invalid( + "The action that a DeleteLink deletes must be a CreateLink".to_string(), + )); + } + }; + let link_type = + match LinkTypes::from_type(create_link.zome_index, create_link.link_type)? { + Some(lt) => lt, + None => { + return Ok(ValidateCallbackResult::Valid); + } + }; + match link_type { + LinkTypes::RecipientToNotifications => { + validate_delete_link_recipient_to_notifications( + action, + create_link.clone(), + base_address, + create_link.target_address, + create_link.tag, + ) + } + LinkTypes::ReadNotifications => validate_delete_link_read_notifications( + action, + create_link.clone(), + base_address, + create_link.target_address, + create_link.tag, + ), + } + } + OpRecord::CreatePrivateEntry { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::UpdatePrivateEntry { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::CreateCapClaim { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::CreateCapGrant { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::UpdateCapClaim { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::UpdateCapGrant { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::Dna { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::OpenChain { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::CloseChain { .. } => Ok(ValidateCallbackResult::Valid), + OpRecord::InitZomesComplete { .. } => Ok(ValidateCallbackResult::Valid), + _ => Ok(ValidateCallbackResult::Valid), + }, + FlatOp::RegisterAgentActivity(agent_activity) => match agent_activity { + OpActivity::CreateAgent { agent, action } => { + let previous_action = must_get_action(action.prev_action)?; + match previous_action.action() { + Action::AgentValidationPkg(AgentValidationPkg { membrane_proof, .. }) => { + validate_agent_joining(agent, membrane_proof) + } + _ => Ok(ValidateCallbackResult::Invalid( + "The previous action for a `CreateAgent` action must be an `AgentValidationPkg`" + .to_string(), + )), + } + } + _ => Ok(ValidateCallbackResult::Valid), + }, + } } diff --git a/zomes/integrity/notifications/src/notification.rs b/zomes/integrity/notifications/src/notification.rs index 98e0f053..63be4866 100644 --- a/zomes/integrity/notifications/src/notification.rs +++ b/zomes/integrity/notifications/src/notification.rs @@ -2,68 +2,66 @@ use hdi::prelude::*; #[hdk_entry_helper] #[derive(Clone, PartialEq)] pub struct Notification { - pub notification_type: String, - pub notification_group: Option, - pub persistent: bool, - pub recipients: Vec, - pub content: SerializedBytes, + pub notification_type: String, + pub notification_group: Option, + pub persistent: bool, + pub recipients: Vec, + pub content: SerializedBytes, } pub fn validate_create_notification( - _action: EntryCreationAction, - _notification: Notification, + _action: EntryCreationAction, + _notification: Notification, ) -> ExternResult { - Ok(ValidateCallbackResult::Valid) + Ok(ValidateCallbackResult::Valid) } pub fn validate_update_notification( - _action: Update, - _notification: Notification, - _original_action: EntryCreationAction, - _original_notification: Notification, + _action: Update, + _notification: Notification, + _original_action: EntryCreationAction, + _original_notification: Notification, ) -> ExternResult { - Ok(ValidateCallbackResult::Invalid(String::from("Notifications cannot be updated"))) + Ok(ValidateCallbackResult::Invalid(String::from( + "Notifications cannot be updated", + ))) } pub fn validate_delete_notification( - _action: Delete, - _original_action: EntryCreationAction, - _original_notification: Notification, + _action: Delete, + _original_action: EntryCreationAction, + _original_notification: Notification, ) -> ExternResult { - Ok(ValidateCallbackResult::Valid) + Ok(ValidateCallbackResult::Valid) } pub fn validate_create_link_recipient_to_notifications( - _action: CreateLink, - _base_address: AnyLinkableHash, - target_address: AnyLinkableHash, - _tag: LinkTag, + _action: CreateLink, + _base_address: AnyLinkableHash, + target_address: AnyLinkableHash, + _tag: LinkTag, ) -> ExternResult { - // Check the entry type for the given action hash - let action_hash = target_address - .into_action_hash() - .ok_or( - wasm_error!( - WasmErrorInner::Guest("No action hash associated with link".to_string()) - ), - )?; - let record = must_get_valid_record(action_hash)?; - let _notification: crate::Notification = record - .entry() - .to_app_option() - .map_err(|e| wasm_error!(e))? - .ok_or( - wasm_error!( - WasmErrorInner::Guest("Linked action must reference an entry" - .to_string()) - ), - )?; - // TODO: add the appropriate validation rules - Ok(ValidateCallbackResult::Valid) + // Check the entry type for the given action hash + let action_hash = + target_address + .into_action_hash() + .ok_or(wasm_error!(WasmErrorInner::Guest( + "No action hash associated with link".to_string() + )))?; + let record = must_get_valid_record(action_hash)?; + let _notification: crate::Notification = record + .entry() + .to_app_option() + .map_err(|e| wasm_error!(e))? + .ok_or(wasm_error!(WasmErrorInner::Guest( + "Linked action must reference an entry".to_string() + )))?; + // TODO: add the appropriate validation rules + Ok(ValidateCallbackResult::Valid) } pub fn validate_delete_link_recipient_to_notifications( - _action: DeleteLink, - _original_action: CreateLink, - _base: AnyLinkableHash, - _target: AnyLinkableHash, - _tag: LinkTag, + _action: DeleteLink, + _original_action: CreateLink, + _base: AnyLinkableHash, + _target: AnyLinkableHash, + _tag: LinkTag, ) -> ExternResult { - // TODO: add the appropriate validation rules - Ok(ValidateCallbackResult::Valid) + // TODO: add the appropriate validation rules + Ok(ValidateCallbackResult::Valid) } diff --git a/zomes/integrity/notifications/src/read_notifications.rs b/zomes/integrity/notifications/src/read_notifications.rs index d7933be6..a8a95699 100644 --- a/zomes/integrity/notifications/src/read_notifications.rs +++ b/zomes/integrity/notifications/src/read_notifications.rs @@ -1,51 +1,51 @@ use hdi::prelude::*; pub fn validate_create_link_read_notifications( - action: CreateLink, - base_address: AnyLinkableHash, - target_address: AnyLinkableHash, - _tag: LinkTag, + action: CreateLink, + base_address: AnyLinkableHash, + target_address: AnyLinkableHash, + _tag: LinkTag, ) -> ExternResult { - // Check the entry type for the given action hash - let base_agent = - base_address - .into_agent_pub_key() - .ok_or(wasm_error!(WasmErrorInner::Guest( - "Base of a ReadNotifications link must be an agent".to_string() - )))?; - let target_agent = - target_address - .into_agent_pub_key() - .ok_or(wasm_error!(WasmErrorInner::Guest( - "Target of a ReadNotifications link must be an agent".to_string() - )))?; + // Check the entry type for the given action hash + let base_agent = + base_address + .into_agent_pub_key() + .ok_or(wasm_error!(WasmErrorInner::Guest( + "Base of a ReadNotifications link must be an agent".to_string() + )))?; + let target_agent = + target_address + .into_agent_pub_key() + .ok_or(wasm_error!(WasmErrorInner::Guest( + "Target of a ReadNotifications link must be an agent".to_string() + )))?; - if !base_agent.eq(&target_agent) { - return Ok(ValidateCallbackResult::Invalid(String::from( - "Base and target must be the same for a ReadNotifications link.", - ))); - } + if !base_agent.eq(&target_agent) { + return Ok(ValidateCallbackResult::Invalid(String::from( + "Base and target must be the same for a ReadNotifications link.", + ))); + } - if !base_agent.eq(&action.author) { - return Ok(ValidateCallbackResult::Invalid(String::from( - "Only a given agent can mark their own notifications as read.", - ))); - } + if !base_agent.eq(&action.author) { + return Ok(ValidateCallbackResult::Invalid(String::from( + "Only a given agent can mark their own notifications as read.", + ))); + } - Ok(ValidateCallbackResult::Valid) + Ok(ValidateCallbackResult::Valid) } pub fn validate_delete_link_read_notifications( - action: DeleteLink, - original_action: CreateLink, - _base: AnyLinkableHash, - _target: AnyLinkableHash, - _tag: LinkTag, + action: DeleteLink, + original_action: CreateLink, + _base: AnyLinkableHash, + _target: AnyLinkableHash, + _tag: LinkTag, ) -> ExternResult { - if !action.author.eq(&original_action.author) { - return Ok(ValidateCallbackResult::Invalid(String::from( - "Only a given agent can delete their own ReadNotifications links.", - ))); - } + if !action.author.eq(&original_action.author) { + return Ok(ValidateCallbackResult::Invalid(String::from( + "Only a given agent can delete their own ReadNotifications links.", + ))); + } - Ok(ValidateCallbackResult::Valid) + Ok(ValidateCallbackResult::Valid) }