diff --git a/bun.lockb b/bun.lockb index e2a4fb72..50c6d575 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/dist/index.js b/dist/index.js index 1c5aa0da..a05330b6 100644 --- a/dist/index.js +++ b/dist/index.js @@ -76,6 +76,9 @@ this._bindings.add(fn); fn(this._value); } + subscribeSilent(fn) { + this._bindings.add(fn); + } // stringify toString() { return JSON.stringify(this._value); @@ -171,6 +174,9 @@ ); return proxyState; } + function bulkSubscribe(statesToSubscibe, fn) { + statesToSubscibe.forEach((state) => state.subscribeSilent(fn)); + } function persistState(localStorageKey, state) { state.subscribe(() => { const stringifiedValue = state.toString(); @@ -619,7 +625,7 @@ id: UUID(), title, isoDateLastEdited: (/* @__PURE__ */ new Date()).toISOString(), - contentVersions: [] + contentVersions: [this.createObjectContent()] }; }; addObject = (messageObject) => { @@ -628,7 +634,12 @@ updateObject = (id, contents) => { const messageObject = this.objects.value.get(id); if (!messageObject) return; - messageObject.contentVersions.push(contents); + if (contents) { + messageObject.contentVersions.push(contents); + messageObject.contentVersions.sort( + (a, b) => a.isoDateVersionCreated > b.isoDateVersionCreated ? 0 : 1 + ); + } this.sendObject(messageObject); }; addObjectAndSend = (messageObject) => { @@ -652,12 +663,17 @@ this.objects.clear(); }; getLatestObjectContent = (messageObject) => { + return this.getObjectContentFromIndex(messageObject, 0); + }; + getObjectContentFromIndex = (messageObject, index) => { const versions = messageObject.contentVersions; - if (versions.length == 0) - return { - isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString() - }; - return versions[versions.length - 1]; + return versions[index] ?? this.createObjectContent(); + }; + createObjectContent = () => { + return { + id: UUID(), + isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString() + }; }; // channel setChannel = () => { @@ -980,6 +996,8 @@ objectTitle: "Object title", objectTitlePlaceholder: "My object", objectVersion: "Object version", + noteContent: "Note", + noteContentPlaceholder: "Take a note...", resendObjects: "Resend all objects", deleteObject: "Delete object" }; @@ -1192,27 +1210,66 @@ // src/Views/Objects/objectDetailModal.tsx function ObjectDetailModal(chat, messageObject, isPresented) { const editingTitle = new State(messageObject.title); + const selectedMessageObjectIndex = new State(0); + selectedMessageObjectIndex.subscribe(console.log); + const selectedMessageObject = createProxyState( + [selectedMessageObjectIndex], + () => chat.getObjectContentFromIndex( + messageObject, + selectedMessageObjectIndex.value + ) + ); + const didEditContent = new State(false); + const editingNoteContent = createProxyState( + [selectedMessageObject], + () => selectedMessageObject.value.noteContent ?? "" + ); + bulkSubscribe( + [editingNoteContent], + () => didEditContent.value = true + ); + function handleKeyDown(e) { + if (!e.metaKey && !e.ctrlKey) return; + switch (e.key) { + case "s": + e.preventDefault(); + saveAndClose(); + break; + } + } function closeModal() { isPresented.value = false; } function saveAndClose() { messageObject.title = editingTitle.value; - chat.updateObject(messageObject.id, { - isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString() - }); + chat.updateObject( + messageObject.id, + didEditContent.value == true ? { + isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString(), + id: UUID(), + noteContent: editingNoteContent.value + } : void 0 + ); closeModal(); } function deleteAndClose() { chat.deleteObject(messageObject); closeModal(); } - return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary" }, messageObject.id), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectTitle), /* @__PURE__ */ createElement( + return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented, "on:keydown": handleKeyDown }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary" }, messageObject.id), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectTitle), /* @__PURE__ */ createElement( "input", { "bind:value": editingTitle, placeholder: translation.objectTitlePlaceholder } - ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "history"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectVersion), /* @__PURE__ */ createElement("select", null, ...messageObject.contentVersions.map((content) => /* @__PURE__ */ createElement("option", null, new Date(content.isoDateVersionCreated).toLocaleString()))), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteAndClose }, translation.deleteObject, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete"))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 danger", "on:click": closeModal }, translation.discard), /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 primary", "on:click": saveAndClose }, translation.save, /* @__PURE__ */ createElement("span", { class: "icon" }, "save"))))); + ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "history"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectVersion), /* @__PURE__ */ createElement("select", { "bind:value": selectedMessageObjectIndex }, ...messageObject.contentVersions.map((content, index) => /* @__PURE__ */ createElement("option", { value: index }, new Date(content.isoDateVersionCreated).toLocaleString()))), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "sticky_note_2"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.noteContent), /* @__PURE__ */ createElement( + "textarea", + { + rows: "5", + "bind:value": editingNoteContent, + placeholder: translation.noteContentPlaceholder + } + ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger width-input", "on:click": deleteAndClose }, translation.deleteObject, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete"))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 danger", "on:click": closeModal }, translation.discard), /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 primary", "on:click": saveAndClose }, translation.save, /* @__PURE__ */ createElement("span", { class: "icon" }, "save"))))); } // src/Views/Objects/objectListEntry.tsx diff --git a/dist/styles.css b/dist/styles.css index 634da660..32951f47 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -602,7 +602,7 @@ label.tile:has(> span.icon) { padding-left: var(--control-padding); } -label.tile:has(> span.icon) :is(input, select) { +label.tile:has(> span.icon) :is(input, select, textarea) { padding-left: 0px; } @@ -735,8 +735,7 @@ button:not(.tile):not(header *), footer, input:not([type="checkbox"]):not([type="radio"]), select, -select + .icon, -textarea { +select + .icon { height: var(--control-height); } @@ -786,6 +785,10 @@ input[type="radio"] { height: 16px; } +label.tile textarea { + resize: vertical; +} + /* text */ diff --git a/package.json b/package.json index 0566e118..b5149311 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "bloatless-react": "^1.3.5", + "bloatless-react": "^1.3.7", "esbuild": "latest", "udn-frontend": "^1.0.7-beta1" }, diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 312c3ee6..318941fb 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -35,6 +35,9 @@ export interface MessageObject { export interface MessageObjectContent { isoDateVersionCreated: string; + id: string; + + noteContent?: string; } // chat @@ -300,7 +303,7 @@ export class Chat { id: React.UUID(), title, isoDateLastEdited: new Date().toISOString(), - contentVersions: [], + contentVersions: [this.createObjectContent()], }; }; @@ -308,10 +311,15 @@ export class Chat { this.objects.set(messageObject.id, messageObject); }; - updateObject = (id: string, contents: MessageObjectContent): void => { + updateObject = (id: string, contents?: MessageObjectContent): void => { const messageObject = this.objects.value.get(id); if (!messageObject) return; - messageObject.contentVersions.push(contents); + if (contents) { + messageObject.contentVersions.push(contents); + messageObject.contentVersions.sort((a, b) => + a.isoDateVersionCreated > b.isoDateVersionCreated ? 0 : 1 + ); + } this.sendObject(messageObject); }; @@ -342,13 +350,23 @@ export class Chat { getLatestObjectContent = ( messageObject: MessageObject + ): MessageObjectContent => { + return this.getObjectContentFromIndex(messageObject, 0); + }; + + getObjectContentFromIndex = ( + messageObject: MessageObject, + index: number ): MessageObjectContent => { const versions = messageObject.contentVersions; - if (versions.length == 0) - return { - isoDateVersionCreated: new Date().toISOString(), - }; - return versions[versions.length - 1]; + return versions[index] ?? this.createObjectContent(); + }; + + createObjectContent = () => { + return { + id: React.UUID(), + isoDateVersionCreated: new Date().toISOString(), + }; }; // channel diff --git a/src/Views/Objects/objectDetailModal.tsx b/src/Views/Objects/objectDetailModal.tsx index 36d634c6..621fd190 100644 --- a/src/Views/Objects/objectDetailModal.tsx +++ b/src/Views/Objects/objectDetailModal.tsx @@ -1,10 +1,6 @@ import * as React from "bloatless-react"; -import { - Chat, - MessageObject, - MessageObjectContent, -} from "../../Model/chatModel"; +import { Chat, MessageObject } from "../../Model/chatModel"; import { translation } from "../../translations"; @@ -15,20 +11,59 @@ export function ObjectDetailModal( ) { // state const editingTitle = new React.State(messageObject.title); - const selectedMessageObject = new React.State( - chat.getLatestObjectContent(messageObject) + + const selectedMessageObjectIndex = new React.State(0); + selectedMessageObjectIndex.subscribe(console.log); + const selectedMessageObject = React.createProxyState( + [selectedMessageObjectIndex], + () => + chat.getObjectContentFromIndex( + messageObject, + selectedMessageObjectIndex.value + ) + ); + + const didEditContent = new React.State(false); + const editingNoteContent = React.createProxyState( + [selectedMessageObject], + () => selectedMessageObject.value.noteContent ?? "" + ); + React.bulkSubscribe( + [editingNoteContent], + () => (didEditContent.value = true) ); // methods + function handleKeyDown(e: KeyboardEvent) { + if (!e.metaKey && !e.ctrlKey) return; + + switch (e.key) { + case "s": + e.preventDefault(); + saveAndClose(); + break; + } + } + function closeModal() { isPresented.value = false; } function saveAndClose() { messageObject.title = editingTitle.value; - chat.updateObject(messageObject.id, { - isoDateVersionCreated: new Date().toISOString(), - }); + + chat.updateObject( + messageObject.id, + didEditContent.value == true + ? { + isoDateVersionCreated: new Date().toISOString(), + id: React.UUID(), + + noteContent: editingNoteContent.value, + } + : undefined + ); + closeModal(); } @@ -38,7 +73,7 @@ export function ObjectDetailModal( } return ( -