From b02cc9c12af580709fab890a41f53a6b88d4d4c4 Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Wed, 22 Jul 2026 21:46:40 -0500 Subject: [PATCH] Add list creation --- package.json | 2 +- src/css/style.css | 123 +++-- src/js/api.js | 15 + src/js/components/create-list-dialog.js | 465 ++++++++++++++++++ src/js/components/edit-list-details-dialog.js | 59 +-- src/js/components/edit-profile-dialog.js | 69 ++- src/js/dataLayer/mutations.js | 47 ++ src/js/views/lists.view.js | 40 +- src/js/views/settings/mutedWords.view.js | 10 +- .../e2e/specs/concerns/dragToDismiss.test.js | 21 +- .../concerns/listDialogsEdgeCases.test.js | 432 ++++++++++++++++ tests/e2e/specs/flows/editProfile.test.js | 18 +- tests/e2e/specs/views/lists.view.test.js | 30 ++ .../components/edit-profile-dialog.test.js | 26 +- 14 files changed, 1200 insertions(+), 157 deletions(-) create mode 100644 src/js/components/create-list-dialog.js create mode 100644 tests/e2e/specs/concerns/listDialogsEdgeCases.test.js diff --git a/package.json b/package.json index 3c73b551..e736f027 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.54", + "version": "0.18.55", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index a97e603e..d03eb76f 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -7349,21 +7349,21 @@ context-menu-item-group:not(:last-child) .context-menu-item-group { margin-bottom: 12px; } -.muted-word-field-label { +.field-caption { font-size: 13px; font-weight: 600; color: var(--text-color-muted); margin-bottom: 6px; } -.muted-word-radio-group { +.pill-radio-group { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; } -.muted-word-radio-group label { +.pill-radio-group label { flex: 1; display: flex; align-items: center; @@ -7378,11 +7378,11 @@ context-menu-item-group:not(:last-child) .context-menu-item-group { font-weight: 500; } -.muted-word-radio-group label:has(input:checked) { +.pill-radio-group label:has(input:checked) { background: color-mix(in srgb, var(--highlight-color) 15%, transparent); } -.muted-word-radio-group input[type="radio"] { +.pill-radio-group input[type="radio"] { accent-color: var(--highlight-color); background-color: transparent; margin: 0; @@ -9352,7 +9352,7 @@ tab-bar[full-width] .tab-bar-button { flex: 1; } -.edit-profile-dialog { +.form-dialog { background-color: var(--background-color); overflow: visible; display: flex; @@ -9360,33 +9360,44 @@ tab-bar[full-width] .tab-bar-button { } @media (min-width: 800px) { - .edit-profile-dialog { + .form-dialog { max-width: 600px; max-height: none; top: 50%; } } -.edit-profile-dialog-content { - padding: 16px 20px; +.form-dialog-content { + padding: 0; display: flex; flex-direction: column; - gap: 16px; } @media (min-width: 800px) { - .edit-profile-dialog-content { + .form-dialog-content { max-height: 80vh; } } -.edit-profile-dialog-header { +.form-dialog-header { display: flex; align-items: center; justify-content: space-between; + position: sticky; + top: 0; + z-index: 2; + background-color: var(--background-color); + padding: 16px; + border-radius: var(--sheet-border-radius) var(--sheet-border-radius) 0 0; +} + +@media (min-width: 800px) { + .form-dialog-header { + border-radius: var(--dialog-border-radius) var(--dialog-border-radius) 0 0; + } } -.edit-profile-dialog-header h2 { +.form-dialog-header h2 { margin: 0; font-size: 18px; position: absolute; @@ -9394,7 +9405,7 @@ tab-bar[full-width] .tab-bar-button { transform: translateX(-50%); } -.edit-profile-dialog-header-button { +.form-dialog-header-button { background: none; border: none; font-size: 16px; @@ -9403,32 +9414,46 @@ tab-bar[full-width] .tab-bar-button { padding: 4px 0; } -.edit-profile-dialog-header-button:disabled { +.form-dialog-header-button:disabled { opacity: 0.5; cursor: default; } -.edit-profile-dialog-save-button:enabled, -.edit-profile-dialog-save-button.saving { +.form-dialog-save-button:enabled, +.form-dialog-save-button.saving { color: var(--primary-button-color); } -.edit-profile-dialog-save-button { +.form-dialog-save-button { display: inline-flex; align-items: center; gap: 8px; } -.edit-profile-dialog-save-button .loading-spinner { +.form-dialog-save-button .loading-spinner { border-top-color: var(--primary-button-color); } -.edit-profile-dialog-body { +.form-dialog-body { display: flex; flex-direction: column; + padding: 0 20px 20px; } -.edit-profile-images-section { +.form-dialog-body > .form-dialog-field ~ .form-dialog-field, +.form-dialog-body > .form-dialog-field ~ .form-dialog-error { + margin-top: 16px; +} + +.form-dialog-body .field-caption { + margin-bottom: 0; +} + +.form-dialog-body .pill-radio-group { + margin-bottom: 0; +} + +.form-dialog-images-section { position: relative; padding-bottom: 40px; } @@ -9459,7 +9484,7 @@ tab-bar[full-width] .tab-bar-button { font-size: 14px; } -.edit-profile-image-overlay { +.form-dialog-image-overlay { position: absolute; inset: 0; display: flex; @@ -9474,13 +9499,13 @@ tab-bar[full-width] .tab-bar-button { } @media (hover: hover) { - .edit-profile-banner-preview:hover .edit-profile-image-overlay, - .edit-profile-avatar-wrapper:hover .edit-profile-image-overlay { + .edit-profile-banner-preview:hover .form-dialog-image-overlay, + .form-dialog-avatar-wrapper:hover .form-dialog-image-overlay { opacity: 1; } } -.edit-profile-camera-button { +.form-dialog-camera-button { position: absolute; width: 24px; height: 24px; @@ -9493,30 +9518,30 @@ tab-bar[full-width] .tab-bar-button { pointer-events: none; } -.edit-profile-camera-button .icon { +.form-dialog-camera-button .icon { width: 14px; height: 14px; display: flex; } -.edit-profile-camera-button-banner { +.form-dialog-camera-button-banner { bottom: 8px; right: 8px; } -.edit-profile-camera-button-avatar { +.form-dialog-camera-button-avatar { bottom: 0; right: 0; } -.edit-profile-avatar-wrapper { +.form-dialog-avatar-wrapper { position: absolute; bottom: 0; left: 12px; cursor: pointer; } -.edit-profile-avatar-preview { +.form-dialog-avatar-preview { position: relative; width: 80px; height: 80px; @@ -9526,32 +9551,31 @@ tab-bar[full-width] .tab-bar-button { border: 2px solid light-dark(var(--less-white), var(--dark-gray)); } -.edit-profile-avatar-preview img { +.form-dialog-avatar-preview img { width: 100%; height: 100%; object-fit: cover; } -.edit-profile-avatar-placeholder { +.form-dialog-avatar-placeholder { width: 100%; height: 100%; object-fit: cover; } -.edit-profile-field { +.form-dialog-field { display: flex; flex-direction: column; gap: 4px; - margin-top: 16px; } -.edit-profile-field label { +.form-dialog-field label { font-size: 14px; font-weight: 600; } -.edit-profile-input, -.edit-profile-textarea { +.form-dialog-input, +.form-dialog-textarea { padding: 10px 12px; border: var(--hair) solid var(--post-border-color); border-radius: var(--text-input-border-radius); @@ -9562,24 +9586,24 @@ tab-bar[full-width] .tab-bar-button { resize: vertical; } -.edit-profile-input:focus, -.edit-profile-textarea:focus { +.form-dialog-input:focus, +.form-dialog-textarea:focus { outline: 2px solid var(--highlight-color); outline-offset: -1px; } -.edit-profile-char-count { +.form-dialog-char-count { font-size: 12px; color: var(--text-color-muted); text-align: right; } -.edit-profile-char-count.overflow { +.form-dialog-char-count.overflow { color: var(--danger-color, #dc3545); font-weight: 600; } -.edit-profile-error { +.form-dialog-error { color: var(--danger-color, #dc3545); font-size: 14px; padding: 8px 12px; @@ -9587,19 +9611,22 @@ tab-bar[full-width] .tab-bar-button { background-color: light-dark(#fef2f2, #3b1111); } -.edit-list-details-dialog .edit-list-details-images-section { - padding: 8px 0; +.edit-list-details-dialog .form-dialog-images-section, +.create-list-dialog .form-dialog-images-section { + padding: 0; display: flex; justify-content: flex-start; } -.edit-list-details-dialog .edit-profile-avatar-wrapper { +.edit-list-details-dialog .form-dialog-avatar-wrapper, +.create-list-dialog .form-dialog-avatar-wrapper { position: relative; bottom: auto; left: auto; } -.edit-list-details-dialog .edit-profile-avatar-preview { +.edit-list-details-dialog .form-dialog-avatar-preview, +.create-list-dialog .form-dialog-avatar-preview { border-radius: var(--list-avatar-border-radius); } @@ -9661,11 +9688,11 @@ image-cropper { cursor: pointer; } -.edit-profile-cropper-content { +.form-dialog-cropper-content { max-height: calc(100vh - 56px); } -.edit-profile-cropper-container { +.form-dialog-cropper-container { border-radius: var(--image-preview-border-radius); overflow: hidden; } diff --git a/src/js/api.js b/src/js/api.js index c50ac94b..f4c7c74a 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -1390,6 +1390,21 @@ export class Api { return res.data; } + async createListRecord(record) { + const res = await this.request("com.atproto.repo.createRecord", { + method: "POST", + body: { + repo: this.session.did, + collection: "app.bsky.graph.list", + record: { + $type: "app.bsky.graph.list", + ...record, + }, + }, + }); + return res.data; + } + async getListRecord(rkey) { const res = await this.request("com.atproto.repo.getRecord", { query: { diff --git a/src/js/components/create-list-dialog.js b/src/js/components/create-list-dialog.js new file mode 100644 index 00000000..c9203fce --- /dev/null +++ b/src/js/components/create-list-dialog.js @@ -0,0 +1,465 @@ +import { html, render } from "/js/lib/lit-html.js"; +import { Component } from "/js/components/component.js"; +import { scrollLocks } from "/js/scrollLocks.js"; +import { + closeWithAnimation, + enableDragToDismiss, + resetScrollOnBlur, +} from "/js/dialogHelpers.js"; +import { classnames, graphemeCount, readFileAsDataUrl } from "/js/utils.js"; +import { ImageCompressor } from "/js/imageCompressor.js"; +import "/js/components/image-cropper.js"; +import "/js/components/context-menu.js"; +import "/js/components/context-menu-item.js"; +import "/js/components/context-menu-item-group.js"; +import { cameraIconTemplate } from "/js/templates/icons/cameraIcon.template.js"; +import { confirmModal } from "/js/modals/confirm.modal.js"; + +const MAX_NAME_LENGTH = 64; +const MAX_DESCRIPTION_LENGTH = 300; +const CURATE_LIST_PURPOSE = "app.bsky.graph.defs#curatelist"; +const MOD_LIST_PURPOSE = "app.bsky.graph.defs#modlist"; + +class CreateListDialog extends Component { + connectedCallback() { + if (this.initialized) { + return; + } + this.setAttribute("data-dialog-wrapper", ""); + this.scrollLock = null; + this._name = ""; + this._description = ""; + this._newAvatarDataUrl = null; + this._purpose = CURATE_LIST_PURPOSE; + this._saving = false; + this._error = null; + this._croppingImageSrc = null; + this._isOpen = false; + this.innerHTML = ""; + this.render(); + this.initialized = true; + } + + get _isDirty() { + return ( + this._name.length > 0 || + this._description.length > 0 || + this._newAvatarDataUrl !== null || + this._purpose !== CURATE_LIST_PURPOSE + ); + } + + get _isNameTooLong() { + return graphemeCount(this._name) > MAX_NAME_LENGTH; + } + + get _isNameEmpty() { + return this._name.trim().length === 0; + } + + get _isDescriptionTooLong() { + return graphemeCount(this._description) > MAX_DESCRIPTION_LENGTH; + } + + get _canSave() { + return ( + !this._saving && + !this._isNameEmpty && + !this._isNameTooLong && + !this._isDescriptionTooLong + ); + } + + render() { + const isCropping = !!this._croppingImageSrc; + + const nameCount = graphemeCount(this._name); + const descriptionCount = graphemeCount(this._description); + const avatarSrc = this._newAvatarDataUrl; + + render( + html` { + if (!isCropping && event.target.tagName === "DIALOG") { + if (await this.confirmClose()) { + this.close(); + } + } + }} + @cancel=${async (event) => { + event.preventDefault(); + if (isCropping) { + this._croppingImageSrc = null; + this.render(); + } else if (await this.confirmClose()) { + this.close(); + } + }} + @close=${() => { + this.scrollLock?.release(); + this.scrollLock = null; + this.dispatchEvent(new CustomEvent("create-list-closed")); + }} + > + ${isCropping + ? html`
+
+ +

Edit image

+ +
+
+ +
+
` + : html`
+
+ +

New list

+ +
+ +
+
+
List Avatar
+
+
this._openAvatarMenu()} + > +
+ ${avatarSrc + ? html`Avatar preview` + : html``} +
+
+
+ ${cameraIconTemplate()} +
+
+
+
+ + + + this._pickImage()} + > + Upload from Files + + + ${avatarSrc + ? html` + { + this._newAvatarDataUrl = null; + this.render(); + }} + > + Remove Avatar + + ` + : ""} + + +
+
List Type
+
{ + this._purpose = event.target.value; + this.render(); + }} + > + + +
+
+ +
+ + { + this._name = event.target.value; + this.render(); + }} + data-testid="create-list-name" + /> +
+ ${nameCount}/${MAX_NAME_LENGTH} +
+
+ +
+ + +
+ ${descriptionCount}/${MAX_DESCRIPTION_LENGTH} +
+
+ + ${this._error + ? html`
${this._error}
` + : ""} +
+
`} + + this._handleFileSelect(event)} + @cancel=${(event) => { + event.stopPropagation(); + }} + /> +
`, + this, + ); + + if (this._isOpen) { + const dialog = this.querySelector(".create-list-dialog"); + if (dialog && !dialog.open) { + dialog.showModal(); + } + } + } + + _openAvatarMenu() { + const menu = this.querySelector(".create-list-avatar-menu"); + const cameraButton = this.querySelector( + ".form-dialog-camera-button-avatar", + ); + if (menu && cameraButton) { + const rect = cameraButton.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.bottom; + menu.open(x, y); + } + } + + _pickImage() { + const input = this.querySelector(".create-list-file-input"); + if (input) { + input.click(); + } + } + + async _handleFileSelect(event) { + const file = event.target.files?.[0]; + if (!file || !file.type.startsWith("image/")) { + event.target.value = ""; + return; + } + + const dataUrl = await readFileAsDataUrl(file); + event.target.value = ""; + + this._croppingImageSrc = dataUrl; + this.render(); + } + + async _applyCrop() { + const cropper = this.querySelector("image-cropper"); + if (!cropper) return; + + const croppedDataUrl = cropper.cropImage(); + if (!croppedDataUrl) return; + + this._newAvatarDataUrl = croppedDataUrl; + this._croppingImageSrc = null; + this.render(); + } + + async _save() { + this._saving = true; + this._error = null; + this.render(); + + try { + let avatarBlob = null; + if (this._newAvatarDataUrl) { + const compressed = await new ImageCompressor().compressImage( + this._newAvatarDataUrl, + ); + avatarBlob = compressed.blob; + } + + const successCallback = () => { + this.close(); + }; + const errorCallback = (error) => { + console.error("Failed to create list:", error); + this._error = "Failed to create list. Please try again."; + this._saving = false; + this.render(); + }; + + this.dispatchEvent( + new CustomEvent("list-create", { + detail: { + listData: { + purpose: this._purpose, + name: this._name, + description: this._description, + avatarBlob, + }, + successCallback, + errorCallback, + }, + }), + ); + } catch (error) { + console.error("Error creating list:", error); + this._error = "Failed to create list. Please try again."; + this._saving = false; + this.render(); + } + } + + open() { + this._isOpen = true; + this.scrollLock ??= scrollLocks.acquire({ target: this }); + const dialog = this.querySelector(".create-list-dialog"); + if (dialog?.open) return; + if (dialog) { + dialog.showModal(); + enableDragToDismiss(dialog, { + confirmDismiss: () => this.confirmClose(), + onClose: () => this.close(), + scrollContainer: this.querySelector(".form-dialog-content"), + ignoreTouchTarget: (el) => + !!el.closest("button") || + el.tagName === "INPUT" || + el.tagName === "TEXTAREA" || + !!el.closest("image-cropper"), + disableWhenKeyboardOpen: true, + }); + + resetScrollOnBlur(dialog, this.querySelector(".form-dialog-content")); + } + } + + async confirmClose() { + if (!this._isDirty || !!this._croppingImageSrc || this._saving) return true; + return confirmModal("Are you sure you want to discard this list?", { + title: "Discard list?", + confirmButtonStyle: "danger", + confirmButtonText: "Discard", + }); + } + + close() { + this._isOpen = false; + return closeWithAnimation(this.querySelector(".create-list-dialog")); + } + + disconnectedCallback() { + this.scrollLock?.release(); + this.scrollLock = null; + } +} + +CreateListDialog.register(); diff --git a/src/js/components/edit-list-details-dialog.js b/src/js/components/edit-list-details-dialog.js index 8fb544d9..0dd6964e 100644 --- a/src/js/components/edit-list-details-dialog.js +++ b/src/js/components/edit-list-details-dialog.js @@ -96,7 +96,7 @@ class EditListDetailsDialog extends Component { render( html` { if (!isCropping && event.target.tagName === "DIALOG") { if (await this.confirmClose()) { @@ -121,11 +121,11 @@ class EditListDetailsDialog extends Component { > ${isCropping ? html`
-
+

Edit image

-
+
` - : html`
-
+ : html`
+
-
-
+
+
this._openAvatarMenu()} > -
+
${avatarSrc ? html`Avatar preview` : html``} -
+
${cameraIconTemplate()}
@@ -233,12 +231,12 @@ class EditListDetailsDialog extends Component { : ""} -
+
{ this._name = event.target.value; @@ -247,7 +245,7 @@ class EditListDetailsDialog extends Component { data-testid="edit-list-details-name" />
@@ -255,11 +253,11 @@ class EditListDetailsDialog extends Component {
-
+
@@ -278,7 +276,7 @@ class EditListDetailsDialog extends Component {
${this._error - ? html`
${this._error}
` + ? html`
${this._error}
` : ""}
`} @@ -308,7 +306,7 @@ class EditListDetailsDialog extends Component { _openAvatarMenu() { const menu = this.querySelector(".edit-list-details-avatar-menu"); const cameraButton = this.querySelector( - ".edit-profile-camera-button-avatar", + ".form-dialog-camera-button-avatar", ); if (menu && cameraButton) { const rect = cameraButton.getBoundingClientRect(); @@ -408,7 +406,7 @@ class EditListDetailsDialog extends Component { enableDragToDismiss(dialog, { confirmDismiss: () => this.confirmClose(), onClose: () => this.close(), - scrollContainer: this.querySelector(".edit-profile-dialog-content"), + scrollContainer: this.querySelector(".form-dialog-content"), ignoreTouchTarget: (el) => !!el.closest("button") || el.tagName === "INPUT" || @@ -417,10 +415,7 @@ class EditListDetailsDialog extends Component { disableWhenKeyboardOpen: true, }); - resetScrollOnBlur( - dialog, - this.querySelector(".edit-profile-dialog-content"), - ); + resetScrollOnBlur(dialog, this.querySelector(".form-dialog-content")); } } diff --git a/src/js/components/edit-profile-dialog.js b/src/js/components/edit-profile-dialog.js index 77afecbc..e7b6378d 100644 --- a/src/js/components/edit-profile-dialog.js +++ b/src/js/components/edit-profile-dialog.js @@ -107,7 +107,7 @@ class EditProfileDialog extends Component { render( html` { if (!isCropping && event.target.tagName === "DIALOG") { if (await this.confirmClose()) { @@ -133,11 +133,11 @@ class EditProfileDialog extends Component { > ${isCropping ? html`
-
+

Edit image

-
+
` - : html`
-
+ : html`
+
-
-
+
+
this._openImageMenu(event, "banner")} @@ -207,30 +207,30 @@ class EditProfileDialog extends Component { : html`
`} -
+
${cameraIconTemplate()}
this._openImageMenu(event, "avatar")} > -
+
${avatarSrc ? html`Avatar preview` : html``} -
+
${cameraIconTemplate()}
@@ -287,12 +287,12 @@ class EditProfileDialog extends Component { : ""} -
+
{ this._displayName = event.target.value; @@ -301,7 +301,7 @@ class EditProfileDialog extends Component { data-testid="edit-profile-display-name" />
@@ -309,11 +309,11 @@ class EditProfileDialog extends Component {
-
+
@@ -332,7 +332,7 @@ class EditProfileDialog extends Component {
${this._error - ? html`
${this._error}
` + ? html`
${this._error}
` : ""}
`} @@ -352,7 +352,7 @@ class EditProfileDialog extends Component { ); if (this._isOpen) { - const dialog = this.querySelector(".edit-profile-dialog"); + const dialog = this.querySelector(".form-dialog"); if (dialog && !dialog.open) { dialog.showModal(); } @@ -366,8 +366,8 @@ class EditProfileDialog extends Component { : ".edit-profile-banner-menu"; const buttonClass = target === "avatar" - ? ".edit-profile-camera-button-avatar" - : ".edit-profile-camera-button-banner"; + ? ".form-dialog-camera-button-avatar" + : ".form-dialog-camera-button-banner"; const menu = this.querySelector(menuClass); const cameraButton = this.querySelector(buttonClass); if (menu && cameraButton) { @@ -482,14 +482,14 @@ class EditProfileDialog extends Component { open() { this._isOpen = true; this.scrollLock ??= scrollLocks.acquire({ target: this }); - const dialog = this.querySelector(".edit-profile-dialog"); + const dialog = this.querySelector(".form-dialog"); if (dialog?.open) return; if (dialog) { dialog.showModal(); enableDragToDismiss(dialog, { confirmDismiss: () => this.confirmClose(), onClose: () => this.close(), - scrollContainer: this.querySelector(".edit-profile-dialog-content"), + scrollContainer: this.querySelector(".form-dialog-content"), ignoreTouchTarget: (el) => !!el.closest("button") || el.tagName === "INPUT" || @@ -498,10 +498,7 @@ class EditProfileDialog extends Component { disableWhenKeyboardOpen: true, }); - resetScrollOnBlur( - dialog, - this.querySelector(".edit-profile-dialog-content"), - ); + resetScrollOnBlur(dialog, this.querySelector(".form-dialog-content")); } } @@ -516,7 +513,7 @@ class EditProfileDialog extends Component { close() { this._isOpen = false; - return closeWithAnimation(this.querySelector(".edit-profile-dialog")); + return closeWithAnimation(this.querySelector(".form-dialog")); } disconnectedCallback() { diff --git a/src/js/dataLayer/mutations.js b/src/js/dataLayer/mutations.js index cffe108d..90e9b64e 100644 --- a/src/js/dataLayer/mutations.js +++ b/src/js/dataLayer/mutations.js @@ -921,6 +921,53 @@ export class Mutations { } } + async createList({ currentUser, purpose, name, description, avatarBlob }) { + const avatarRef = avatarBlob ? await this.api.uploadBlob(avatarBlob) : null; + const record = { + purpose, + name, + description, + createdAt: getCurrentTimestamp(), + }; + if (avatarRef) record.avatar = avatarRef; + + const res = await this.api.createListRecord(record); + + const creator = { + did: currentUser.did, + handle: currentUser.handle, + displayName: currentUser.displayName, + avatar: currentUser.avatar, + }; + const listView = { + $type: "app.bsky.graph.defs#listView", + uri: res.uri, + cid: res.cid, + name, + purpose, + description, + descriptionFacets: [], + avatar: avatarRef?.ref?.$link + ? buildCdnUrl("avatar", creator.did, avatarRef.ref.$link) + : undefined, + creator, + indexedAt: record.createdAt, + listItemCount: 0, + viewer: {}, + }; + this.dataStore.$lists.set(res.uri, listView); + const actorLists = untrack(() => + this.dataStore.$actorLists.get(creator.did), + ); + if (actorLists) { + this.dataStore.$actorLists.set(creator.did, { + ...actorLists, + lists: [listView, ...actorLists.lists], + }); + } + return listView; + } + async updateList(list, { name, description, avatarBlob, removeAvatar }) { const rkey = list.uri.split("/").pop(); const avatarRef = avatarBlob ? await this.api.uploadBlob(avatarBlob) : null; diff --git a/src/js/views/lists.view.js b/src/js/views/lists.view.js index ca26fcb4..862e3f4e 100644 --- a/src/js/views/lists.view.js +++ b/src/js/views/lists.view.js @@ -4,6 +4,9 @@ import { auth } from "/js/auth.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { listFeedTemplate } from "/js/templates/listFeed.template.js"; import { bindToPage, pageEffect } from "/js/router.js"; +import { showToast } from "/js/toasts.js"; +import { parseUri } from "/js/dataHelpers.js"; +import "/js/components/create-list-dialog.js"; class ListsView extends View { async render({ root, layout, context: { dataLayer } }) { @@ -29,7 +32,18 @@ class ListsView extends View { render( html`
- ${headerTemplate({ title: "Lists" })} + ${headerTemplate({ + title: "Lists", + rightItemTemplate: () => html` + + `, + })}
${listFeedTemplate({ lists: actorLists?.lists, @@ -46,6 +60,30 @@ class ListsView extends View { await dataLayer.requests.loadCurrentUserLists({ reload }); } + async function handleClickNew({ currentUser }) { + const dialog = document.createElement("create-list-dialog"); + dialog.addEventListener("list-create", async (event) => { + const { listData, successCallback, errorCallback } = event.detail; + try { + const list = await dataLayer.mutations.createList({ + ...listData, + currentUser, + }); + showToast("List created"); + successCallback(); + const { rkey } = parseUri(list.uri); + window.router.go(`/profile/${currentUser.handle}/lists/${rkey}`); + } catch (error) { + errorCallback(error); + } + }); + dialog.addEventListener("create-list-closed", () => { + dialog.remove(); + }); + root.querySelector("main").appendChild(dialog); + dialog.open(); + } + root.addEventListener("page-enter", async () => { window.scrollTo(0, 0); await loadLists(); diff --git a/src/js/views/settings/mutedWords.view.js b/src/js/views/settings/mutedWords.view.js index 1f1af605..71c74b2b 100644 --- a/src/js/views/settings/mutedWords.view.js +++ b/src/js/views/settings/mutedWords.view.js @@ -271,8 +271,8 @@ class SettingsMutedWordsView extends View { autocapitalize="none" /> -
Duration:
-
+
Duration:
+
-
Mute in:
-
+
Mute in:
+
-
Options:
+
Options: