From 6dcc0fc1f6ed8438c411d8fe4ce98a4e198d0110 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 2 Dec 2023 12:51:46 -1000 Subject: [PATCH] feat(familiar-chat): rewrite ui as preact --- packages/familiar-chat/index.js | 2 +- packages/familiar-chat/package.json | 4 +- packages/familiar-chat/src/index.js | 451 ++++++++++++++++------------ patches/preact+10.19.2.patch | 8 + yarn.lock | 5 + 5 files changed, 281 insertions(+), 189 deletions(-) create mode 100644 patches/preact+10.19.2.patch diff --git a/packages/familiar-chat/index.js b/packages/familiar-chat/index.js index 78a8410d3e..5824291e5c 100644 --- a/packages/familiar-chat/index.js +++ b/packages/familiar-chat/index.js @@ -1 +1 @@ -export { make } from './src/index.js'; \ No newline at end of file +export { make } from './src/index.js'; diff --git a/packages/familiar-chat/package.json b/packages/familiar-chat/package.json index 5cc499760a..e433e27edf 100644 --- a/packages/familiar-chat/package.json +++ b/packages/familiar-chat/package.json @@ -23,6 +23,7 @@ }, "scripts": { "start": "endo open familiar-chat ./index.js --powers SELF", + "dev": "nodemon --exec 'endo open familiar-chat ./index.js --powers SELF'", "test": "ava", "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js", "test:xs": "exit 0", @@ -67,6 +68,7 @@ "dependencies": { "@endo/cli": "^0.2.6", "@endo/daemon": "^0.2.6", - "@endo/far": "^0.2.22" + "@endo/far": "^0.2.22", + "preact": "^10.19.2" } } diff --git a/packages/familiar-chat/src/index.js b/packages/familiar-chat/src/index.js index 4232a05e83..56f7f7d986 100644 --- a/packages/familiar-chat/src/index.js +++ b/packages/familiar-chat/src/index.js @@ -2,6 +2,9 @@ import { E } from '@endo/far'; import { makeRefIterator } from '@endo/daemon/ref-reader.js'; +import 'preact/debug'; +import { h, render, Fragment } from 'preact'; +import { useState, useEffect } from 'preact/hooks'; /** @type any */ const { assert } = globalThis; @@ -11,213 +14,287 @@ const dateFormatter = new window.Intl.DateTimeFormat(undefined, { timeStyle: 'long', }); -const followMessagesComponent = async ($parent, $end, powers) => { - for await (const message of makeRefIterator(E(powers).followMessages())) { - const { number, who, when, dismissed } = message; - - const $error = document.createElement('span'); - $error.style.color = 'red'; - $error.innerText = ''; - // To be inserted later, but declared here for reference. - - const $message = document.createElement('div'); - $parent.insertBefore($message, $end); - - dismissed.then(() => { - $message.remove(); - }); - - const $number = document.createElement('span'); - $number.innerText = `${number}. `; - $message.appendChild($number); - - const $who = document.createElement('b'); - $who.innerText = `${who}:`; - $message.appendChild($who); - - if (message.type === 'request') { - const { what, settled } = message; - - const $what = document.createElement('span'); - $what.innerText = ` ${what} `; - $message.appendChild($what); - - const $when = document.createElement('i'); - $when.innerText = dateFormatter.format(Date.parse(when)); - $message.appendChild($when); - - const $input = document.createElement('span'); - $message.appendChild($input); - - const $pet = document.createElement('input'); - $input.appendChild($pet); +const arrayWithout = (array, value) => { + const newArray = array.slice(); + const index = newArray.indexOf(value); + if (index !== -1) { + newArray.splice(index, 1); + } + return newArray; +}; - const $resolve = document.createElement('button'); - $resolve.innerText = 'resolve'; - $input.appendChild($resolve); +const useAsync = (asyncFn, deps) => { + const [state, setState] = useState(); + useEffect(() => { + setState(undefined); + let shouldAbort = false; + const runAsync = async () => { + const result = await asyncFn(); + if (!shouldAbort) { + setState(result); + } + }; + runAsync(); + return () => { + shouldAbort = true; + }; + }, deps); + return state; +}; - const $reject = document.createElement('button'); - $reject.innerText = 'reject'; - $reject.onclick = () => { - E(powers).reject(number, $pet.value).catch(window.reportError); - }; - $input.appendChild($reject); +const useFollowReducer = (getSubFn, reducerFn, deps) => { + const [state, setState] = useState([]); + + useEffect(() => { + setState([]); + const sub = makeRefIterator(getSubFn()); + let shouldAbort = false; + const iterateChanges = async () => { + for await (const event of sub) { + // Check if we should abort iteration + if (shouldAbort) { + break; + } + reducerFn(event, setState); + } + }; + // start iteration + iterateChanges(); + // cleanup + return () => { + shouldAbort = true; + }; + }, deps); - $resolve.onclick = () => { - E(powers) - .resolve(number, $pet.value) - .catch(error => { - $error.innerText = ` ${error.message}`; - }); - }; + return state; +}; - settled.then(status => { - $input.innerText = ` ${status} `; +const useFollowMessages = (getSubFn, deps) => { + const reducerFn = (message, setState) => { + // apply change + setState(prevState => { + return [...prevState, message]; + }); + // listen for dismiss + message.dismissed.then(() => { + setState(prevState => { + return arrayWithout(prevState, message); }); - } else if (message.type === 'package') { - /** @type {{ strings: Array, names: Array }} */ - const { strings, names } = message; - assert(Array.isArray(strings)); - assert(Array.isArray(names)); - - $message.appendChild(document.createTextNode(' "')); - - let index = 0; - for ( - index = 0; - index < Math.min(strings.length, names.length); - index += 1 - ) { - assert.typeof(strings[index], 'string'); - const outer = JSON.stringify(strings[index]); - const inner = outer.slice(1, outer.length - 1); - $message.appendChild(document.createTextNode(inner)); - assert.typeof(names[index], 'string'); - const name = `@${names[index]}`; - const $name = document.createElement('b'); - $name.innerText = name; - $message.appendChild($name); - } - if (strings.length > names.length) { - const outer = JSON.stringify(strings[index]); - const inner = outer.slice(1, outer.length - 1); - $message.appendChild(document.createTextNode(inner)); - } - - $message.appendChild(document.createTextNode('" ')); + }); + }; - const $when = document.createElement('i'); - $when.innerText = dateFormatter.format(Date.parse(when)); - $message.appendChild($when); + const state = useFollowReducer(getSubFn, reducerFn, deps); + return state; +}; - $message.appendChild(document.createTextNode(' ')); +const useFollowNames = (getSubFn, deps) => { + const reducerFn = (change, setState) => { + // apply change + setState(prevState => { + if ('add' in change) { + const name = change.add; + return [...prevState, name]; + } else if ('remove' in change) { + const name = change.remove; + return arrayWithout(prevState, name); + } + return prevState; + }); + }; - if (names.length > 0) { - const $names = document.createElement('select'); - $message.appendChild($names); - for (const name of names) { - const $name = document.createElement('option'); - $name.innerText = name; - $names.appendChild($name); - } + const state = useFollowReducer(getSubFn, reducerFn, deps); + return state; +}; - $message.appendChild(document.createTextNode(' ')); - - const $as = document.createElement('input'); - $as.type = 'text'; - $message.appendChild($as); - - $message.appendChild(document.createTextNode(' ')); - - const $adopt = document.createElement('button'); - $adopt.innerText = 'Adopt'; - $message.appendChild($adopt); - $adopt.onclick = () => { - console.log($as.value, $as); - E(powers) - .adopt(number, $names.value, $as.value || $names.value) - .then( - () => { - $as.value = ''; - }, - error => { - $error.innerText = ` ${error.message}`; - }, - ); - }; - } +const packageMessageComponent = ({ message, actions }) => { + const { when, strings, names } = message; + assert(Array.isArray(strings)); + assert(Array.isArray(names)); + + const [asValue, setAsValue] = useState(''); + const [selectedName, setSelectedName] = useState(names[0]); + + const stringEntries = strings.map((string, index) => { + const name = names[index]; + if (name === undefined) { + // Special case for when there are more strings than names. + const textDisplay = JSON.stringify(string).slice(1, -1); + return textDisplay; + } else { + return h(Fragment, null, [string, h('b', null, `@${name}`)]); } + }); + + return h(Fragment, null, [ + ' "', + ...stringEntries, + '" ', + h('i', null, dateFormatter.format(Date.parse(when))), + ' ', + h( + 'select', + { + value: selectedName, + onchange: e => { + console.log(e.target.value); + setSelectedName(e.target.value); + }, + }, + names.map(name => h('option', { value: name }, name)), + ), + ' ', + h('input', { + type: 'text', + value: asValue, + oninput: e => setAsValue(e.target.value), + }), + h( + // @ts-ignore + 'button', + { + onclick: () => actions.adopt(selectedName, asValue), + }, + 'Adopt', + ), + ]); +}; - $message.appendChild(document.createTextNode(' ')); - - const $dismiss = document.createElement('button'); - $dismiss.innerText = 'Dismiss'; - $message.appendChild($dismiss); - $dismiss.onclick = () => { - E(powers) - .dismiss(number) - .catch(error => { - $error.innerText = ` ${error.message}`; - }); - }; - - $message.appendChild($error); - } +const requestMessageComponent = ({ message, actions }) => { + const [petName, setPetName] = useState(''); + const { what, when, settled } = message; + const status = useAsync(() => settled, [settled]); + const isUnsettled = status === undefined; + const statusText = isUnsettled ? '' : ` ${status} `; + + const makeControls = () => { + return h(Fragment, null, [ + h('input', { + type: 'text', + value: petName, + oninput: e => setPetName(e.target.value), + }), + h( + // @ts-ignore + 'button', + { + onclick: () => actions.resolve(petName), + }, + 'resolve', + ), + h( + // @ts-ignore + 'button', + { + onclick: () => actions.reject(petName), + }, + 'reject', + ), + ]); + }; + + return h(Fragment, null, [ + h('span', null, ` ${what} `), + h('i', null, dateFormatter.format(Date.parse(when))), + h('span', null, statusText), + isUnsettled && makeControls(), + ]); }; -const followNamesComponent = async ($parent, $end, powers) => { - const $title = document.createElement('h2'); - $title.innerText = 'Inventory'; - $parent.insertBefore($title, $end); - - const $ul = document.createElement('ul'); - $parent.insertBefore($ul, $end); - - const $names = new Map(); - for await (const change of makeRefIterator(E(powers).followNames())) { - if ('add' in change) { - const name = change.add; - - const $li = document.createElement('li'); - $ul.appendChild($li); - - const $name = document.createTextNode(`${name} `); - $li.appendChild($name); - $name.nodeValue = change.add; - - const $remove = document.createElement('button'); - $li.appendChild($remove); - $remove.innerText = 'Remove'; - $remove.onclick = () => E(powers).remove(name).catch(window.reportError); - - $names.set(name, $li); - } else if ('remove' in change) { - const $li = $names.get(change.remove); - if ($li !== undefined) { - $li.remove(); - $names.delete(change.remove); - } - } +const messageComponent = ({ message, powers }) => { + const { number, who } = message; + const [errorText, setErrorText] = useState(''); + + let messageBodyComponent; + if (message.type === 'request') { + messageBodyComponent = requestMessageComponent; + } else if (message.type === 'package') { + messageBodyComponent = packageMessageComponent; + } else { + throw new Error(`Unknown message type: ${message.type}`); } + + const reportError = error => { + setErrorText(error.message); + }; + const actions = { + dismiss: () => E(powers).dismiss(number).catch(reportError), + resolve: value => E(powers).resolve(number, value).catch(reportError), + reject: value => E(powers).reject(number, value).catch(reportError), + adopt: (selectedName, asValue) => + E(powers).adopt(number, selectedName, asValue).catch(reportError), + }; + + return h('div', null, [ + h('span', null, `${number}. `), + h('b', null, `${who}:`), + h(messageBodyComponent, { message, actions }), + ' ', + h( + // @ts-ignore + 'button', + { + onclick: () => actions.dismiss(), + }, + 'Dismiss', + ), + h( + 'span', + { + style: { + color: 'red', + }, + }, + errorText, + ), + ]); }; -const bodyComponent = ($parent, powers) => { - const $title = document.createElement('h1'); - $title.innerText = '🐈‍⬛'; - $parent.appendChild($title); +const followMessagesComponent = ({ powers }) => { + const messages = useFollowMessages(() => E(powers).followMessages(), []); + + const messageEntries = messages.map(message => { + return h(messageComponent, { message, powers }); + }); - const $endOfMessages = document.createTextNode(''); - $parent.appendChild($endOfMessages); - followMessagesComponent($parent, $endOfMessages, powers).catch( - window.reportError, - ); + return h(Fragment, null, [ + h('h2', null, 'Messages'), + h('div', null, messageEntries), + ]); +}; + +const followNamesComponent = ({ powers }) => { + const names = useFollowNames(() => E(powers).followNames(), []); + + const inventoryEntries = names.map(name => { + return h('li', null, [ + name, + h( + // @ts-ignore + 'button', + { + onclick: () => E(powers).remove(name).catch(window.reportError), + }, + 'Remove', + ), + ]); + }); + + return h(Fragment, null, [ + h('h2', null, 'Inventory'), + h('ul', null, inventoryEntries), + ]); +}; - const $endOfNames = document.createTextNode(''); - $parent.appendChild($endOfNames); - followNamesComponent($parent, $endOfNames, powers).catch(window.reportError); +const bodyComponent = ({ powers }) => { + return h(Fragment, null, [ + h('h1', {}, '🐈‍⬛'), + h(followMessagesComponent, { powers }), + h(followNamesComponent, { powers }), + ]); }; export const make = async powers => { document.body.innerHTML = ''; - bodyComponent(document.body, powers); + const app = h(bodyComponent, { powers }); + render(app, document.body); }; diff --git a/patches/preact+10.19.2.patch b/patches/preact+10.19.2.patch new file mode 100644 index 0000000000..f6d533f5e9 --- /dev/null +++ b/patches/preact+10.19.2.patch @@ -0,0 +1,8 @@ +diff --git a/node_modules/preact/dist/preact.mjs b/node_modules/preact/dist/preact.mjs +index e3ae6a4..e6d1797 100644 +--- a/node_modules/preact/dist/preact.mjs ++++ b/node_modules/preact/dist/preact.mjs +@@ -1,2 +1,2 @@ +-var n,l,u,t,i,o,r,f,e,c={},s=[],a=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,h=Array.isArray;function v(n,l){for(var u in l)n[u]=l[u];return n}function p(n){var l=n.parentNode;l&&l.removeChild(n)}function y(l,u,t){var i,o,r,f={};for(r in u)"key"==r?i=u[r]:"ref"==r?o=u[r]:f[r]=u[r];if(arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):t),"function"==typeof l&&null!=l.defaultProps)for(r in l.defaultProps)void 0===f[r]&&(f[r]=l.defaultProps[r]);return d(l,f,i,o,null)}function d(n,t,i,o,r){var f={type:n,props:t,key:i,ref:o,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,constructor:void 0,__v:null==r?++u:r,__i:-1,__u:0};return null==r&&null!=l.vnode&&l.vnode(f),f}function _(){return{current:null}}function g(n){return n.children}function b(n,l){this.props=n,this.context=l}function m(n,l){if(null==l)return n.__?m(n.__,n.__i+1):null;for(var u;lu&&i.sort(f));x.__r=0}function C(n,l,u,t,i,o,r,f,e,a,h){var v,p,y,d,_,g=t&&t.__k||s,b=l.length;for(u.__d=e,P(u,l,g),e=u.__d,v=0;v0?d(i.type,i.props,i.key,i.ref?i.ref:null,i.__v):i)?(i.__=n,i.__b=n.__b+1,f=H(i,u,r=t+a,s),i.__i=f,o=null,-1!==f&&(s--,(o=u[f])&&(o.__u|=131072)),null==o||null===o.__v?(-1==f&&a--,"function"!=typeof i.type&&(i.__u|=65536)):f!==r&&(f===r+1?a++:f>r?s>e-r?a+=f-r:a--:a=f(null!=e&&0==(131072&e.__u)?1:0))for(;r>=0||f=0){if((e=l[r])&&0==(131072&e.__u)&&i==e.key&&o===e.type)return r;r--}if(f2&&(e.children=arguments.length>3?n.call(arguments,2):t),d(l.type,e,i||l.key,o||l.ref,null)}function F(n,l){var u={__c:l="__cC"+e++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var u,t;return this.getChildContext||(u=[],(t={})[l]=this,this.getChildContext=function(){return t},this.shouldComponentUpdate=function(n){this.props.value!==n.value&&u.some(function(n){n.__e=!0,w(n)})},this.sub=function(n){u.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){u.splice(u.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Provider.__=u.Consumer.contextType=u}n=s.slice,l={__e:function(n,l,u,t){for(var i,o,r;l=l.__;)if((i=l.__c)&&!i.__)try{if((o=i.constructor)&&null!=o.getDerivedStateFromError&&(i.setState(o.getDerivedStateFromError(n)),r=i.__d),null!=i.componentDidCatch&&(i.componentDidCatch(n,t||{}),r=i.__d),r)return i.__E=i}catch(l){n=l}throw n}},u=0,t=function(n){return null!=n&&null==n.constructor},b.prototype.setState=function(n,l){var u;u=null!=this.__s&&this.__s!==this.state?this.__s:this.__s=v({},this.state),"function"==typeof n&&(n=n(v({},u),this.props)),n&&v(u,n),null!=n&&this.__v&&(l&&this._sb.push(l),w(this))},b.prototype.forceUpdate=function(n){this.__v&&(this.__e=!0,n&&this.__h.push(n),w(this))},b.prototype.render=g,i=[],r="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,f=function(n,l){return n.__v.__b-l.__v.__b},x.__r=0,e=0;export{b as Component,g as Fragment,E as cloneElement,F as createContext,y as createElement,_ as createRef,y as h,B as hydrate,t as isValidElement,l as options,q as render,$ as toChildArray}; ++var n,l,u,t,i,o,r,f,e,c={},s=[],a=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,h=Array.isArray;function v(n,l){for(var u in l)Reflect.defineProperty(n,u,{value:l[u],writable:true,configurable:true,enumerable:true});return n}function p(n){var l=n.parentNode;l&&l.removeChild(n)}function y(l,u,t){var i,o,r,f={};for(r in u)"key"==r?i=u[r]:"ref"==r?o=u[r]:f[r]=u[r];if(arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):t),"function"==typeof l&&null!=l.defaultProps)for(r in l.defaultProps)void 0===f[r]&&(f[r]=l.defaultProps[r]);return d(l,f,i,o,null)}function d(n,t,i,o,r){var f={type:n,props:t,key:i,ref:o,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,constructor:void 0,__v:null==r?++u:r,__i:-1,__u:0};return null==r&&null!=l.vnode&&l.vnode(f),f}function _(){return{current:null}}function g(n){return n.children}function b(n,l){this.props=n,this.context=l}function m(n,l){if(null==l)return n.__?m(n.__,n.__i+1):null;for(var u;lu&&i.sort(f));x.__r=0}function C(n,l,u,t,i,o,r,f,e,a,h){var v,p,y,d,_,g=t&&t.__k||s,b=l.length;for(u.__d=e,P(u,l,g),e=u.__d,v=0;v0?d(i.type,i.props,i.key,i.ref?i.ref:null,i.__v):i)?(i.__=n,i.__b=n.__b+1,f=H(i,u,r=t+a,s),i.__i=f,o=null,-1!==f&&(s--,(o=u[f])&&(o.__u|=131072)),null==o||null===o.__v?(-1==f&&a--,"function"!=typeof i.type&&(i.__u|=65536)):f!==r&&(f===r+1?a++:f>r?s>e-r?a+=f-r:a--:a=f(null!=e&&0==(131072&e.__u)?1:0))for(;r>=0||f=0){if((e=l[r])&&0==(131072&e.__u)&&i==e.key&&o===e.type)return r;r--}if(f2&&(e.children=arguments.length>3?n.call(arguments,2):t),d(l.type,e,i||l.key,o||l.ref,null)}function F(n,l){var u={__c:l="__cC"+e++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var u,t;return this.getChildContext||(u=[],(t={})[l]=this,this.getChildContext=function(){return t},this.shouldComponentUpdate=function(n){this.props.value!==n.value&&u.some(function(n){n.__e=!0,w(n)})},this.sub=function(n){u.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){u.splice(u.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Provider.__=u.Consumer.contextType=u}n=s.slice,l={__e:function(n,l,u,t){for(var i,o,r;l=l.__;)if((i=l.__c)&&!i.__)try{if((o=i.constructor)&&null!=o.getDerivedStateFromError&&(i.setState(o.getDerivedStateFromError(n)),r=i.__d),null!=i.componentDidCatch&&(i.componentDidCatch(n,t||{}),r=i.__d),r)return i.__E=i}catch(l){n=l}throw n}},u=0,t=function(n){return null!=n&&null==n.constructor},b.prototype.setState=function(n,l){var u;u=null!=this.__s&&this.__s!==this.state?this.__s:this.__s=v({},this.state),"function"==typeof n&&(n=n(v({},u),this.props)),n&&v(u,n),null!=n&&this.__v&&(l&&this._sb.push(l),w(this))},b.prototype.forceUpdate=function(n){this.__v&&(this.__e=!0,n&&this.__h.push(n),w(this))},b.prototype.render=g,i=[],r="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,f=function(n,l){return n.__v.__b-l.__v.__b},x.__r=0,e=0;export{b as Component,g as Fragment,E as cloneElement,F as createContext,y as createElement,_ as createRef,y as h,B as hydrate,t as isValidElement,l as options,q as render,$ as toChildArray}; + //# sourceMappingURL=preact.module.js.map diff --git a/yarn.lock b/yarn.lock index 6cd64a3b5a..0aa28e5c28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10041,6 +10041,11 @@ posthtml@^0.15.1: posthtml-parser "^0.7.2" posthtml-render "^1.3.1" +preact@^10.19.2: + version "10.19.2" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.2.tgz#841797620dba649aaac1f8be42d37c3202dcea8b" + integrity sha512-UA9DX/OJwv6YwP9Vn7Ti/vF80XL+YA5H2l7BpCtUr3ya8LWHFzpiO5R+N7dN16ujpIxhekRFuOOF82bXX7K/lg== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"