diff --git a/package.json b/package.json
index 3abe721a..29486eea 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "impro",
- "version": "0.17.113",
+ "version": "0.17.114",
"type": "module",
"scripts": {
"start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve",
diff --git a/src/index.html b/src/index.html
index 27306289..c99fefe8 100644
--- a/src/index.html
+++ b/src/index.html
@@ -105,7 +105,7 @@
// Dev tools
if (
- window.env.environment === "development" ||
+ (window.env.environment === "development" && !window.env.playwright) ||
params.has("enable-debug")
) {
enableErrorLogs();
diff --git a/src/js/components/detected-rich-text.js b/src/js/components/detected-rich-text.js
index d2d38997..f481c360 100644
--- a/src/js/components/detected-rich-text.js
+++ b/src/js/components/detected-rich-text.js
@@ -1,6 +1,6 @@
import { render } from "/js/lib/lit-html.js";
import { Component } from "/js/components/component.js";
-import { Signal, effect } from "/js/signals.js";
+import { Signal, ReactiveStore, effect } from "/js/signals.js";
import { richTextTemplate } from "/js/templates/richText.template.js";
import {
getUnresolvedFacetsFromText,
@@ -16,33 +16,36 @@ class DetectedRichText extends Component {
if (this.initialized) return;
this.initialized = true;
- this.$text = new Signal.State(this.getAttribute("text") ?? "");
- this.$truncateUrls = new Signal.State(this.hasAttribute("truncate-urls"));
- this.$unresolvedFacets = new Signal.Computed(() =>
- getUnresolvedFacetsFromText(this.$text.get()),
+ this.state = new ReactiveStore("detected-rich-text");
+ this.state.$text = new Signal.State(this.getAttribute("text") ?? "");
+ this.state.$truncateUrls = new Signal.State(
+ this.hasAttribute("truncate-urls"),
);
- this.$resolvedFacets = new Signal.State(null);
+ this.state.$unresolvedFacets = new Signal.Computed(() =>
+ getUnresolvedFacetsFromText(this.state.$text.get()),
+ );
+ this.state.$resolvedFacets = new Signal.State(null);
// Resolve facets whenever unresolved facets change
this.disposeResolve = effect(() => {
if (!this.identityResolver) return;
- const unresolvedFacets = this.$unresolvedFacets.get();
+ const unresolvedFacets = this.state.$unresolvedFacets.get();
resolveFacets(unresolvedFacets, this.identityResolver).then(
(resolvedFacets) => {
- if (unresolvedFacets !== this.$unresolvedFacets.get()) {
+ if (unresolvedFacets !== this.state.$unresolvedFacets.get()) {
// If unresolved facets have changed since we started resolving, don't update
return;
}
- this.$resolvedFacets.set(resolvedFacets);
+ this.state.$resolvedFacets.set(resolvedFacets);
},
);
});
this.disposeRender = effect(() => {
- const text = this.$text.get();
- const unresolvedFacets = this.$unresolvedFacets.get();
- const resolvedFacets = this.$resolvedFacets.get();
- const truncateUrls = this.$truncateUrls.get();
+ const text = this.state.$text.get();
+ const unresolvedFacets = this.state.$unresolvedFacets.get();
+ const resolvedFacets = this.state.$resolvedFacets.get();
+ const truncateUrls = this.state.$truncateUrls.get();
let facets = resolvedFacets ?? unresolvedFacets;
if (!this.identityResolver) {
facets = facets.filter(
@@ -58,10 +61,10 @@ class DetectedRichText extends Component {
if (!this.initialized || oldValue === newValue) return;
if (name === "text") {
const text = newValue ?? "";
- this.$resolvedFacets.set(null);
- this.$text.set(text);
+ this.state.$resolvedFacets.set(null);
+ this.state.$text.set(text);
} else if (name === "truncate-urls") {
- this.$truncateUrls.set(newValue !== null);
+ this.state.$truncateUrls.set(newValue !== null);
}
}
diff --git a/src/js/components/plugin-posts-feed.js b/src/js/components/plugin-posts-feed.js
index 92ec6be3..b2fa40d0 100644
--- a/src/js/components/plugin-posts-feed.js
+++ b/src/js/components/plugin-posts-feed.js
@@ -1,7 +1,7 @@
import { html, render } from "/js/lib/lit-html.js";
import { Component } from "/js/components/component.js";
import { postFeedTemplate } from "/js/templates/postFeed.template.js";
-import { Signal, effect } from "/js/signals.js";
+import { Signal, ReactiveStore, effect } from "/js/signals.js";
class PluginPostsFeed extends Component {
static get observedAttributes() {
@@ -14,29 +14,27 @@ class PluginPostsFeed extends Component {
if (!this.postInteractionHandler) {
throw new Error("postInteractionHandler is required");
}
- this.attribs = {
- uris: new Signal.State(this.parseUris()),
- emptyMessage: new Signal.State(this.getAttribute("empty-message")),
- };
- this.state = {
- currentUser: this.dataLayer.derived.$currentUser,
- loaded: new Signal.State(false),
- posts: new Signal.Computed(() => {
- if (!this.state.loaded.get()) return null;
- const uris = this.attribs.uris.get();
- if (!uris) return null;
- return uris
- .map((uri) => this.dataLayer.derived.$hydratedPosts.get(uri))
- .filter(Boolean);
- }),
- error: new Signal.State(null),
- };
+ this.state = new ReactiveStore("plugin-posts-feed");
+ this.state.$uris = new Signal.State(this.parseUris());
+ this.state.$emptyMessage = new Signal.State(
+ this.getAttribute("empty-message"),
+ );
+ this.state.$loaded = new Signal.State(false);
+ this.state.$posts = new Signal.Computed(() => {
+ if (!this.state.$loaded.get()) return null;
+ const uris = this.state.$uris.get();
+ if (!uris) return null;
+ return uris
+ .map((uri) => this.dataLayer.derived.$hydratedPosts.get(uri))
+ .filter(Boolean);
+ });
+ this.state.$error = new Signal.State(null);
this._disposers = [
effect(() => {
- const error = this.state.error.get();
- const posts = this.state.posts.get();
- const currentUser = this.state.currentUser.get();
- const emptyMessage = this.attribs.emptyMessage.get();
+ const error = this.state.$error.get();
+ const posts = this.state.$posts.get();
+ const currentUser = this.dataLayer.derived.$currentUser.get();
+ const emptyMessage = this.state.$emptyMessage.get();
if (error) {
render(html`
${error}
`, this);
return;
@@ -60,7 +58,7 @@ class PluginPostsFeed extends Component {
);
}),
effect(() => {
- this.attribs.uris.get();
+ this.state.$uris.get();
this.load();
}),
];
@@ -75,8 +73,8 @@ class PluginPostsFeed extends Component {
attributeChangedCallback() {
if (this.initialized) {
// TODO - smarter updates?
- this.attribs.uris.set(this.parseUris());
- this.attribs.emptyMessage.set(this.getAttribute("empty-message"));
+ this.state.$uris.set(this.parseUris());
+ this.state.$emptyMessage.set(this.getAttribute("empty-message"));
}
}
@@ -92,14 +90,14 @@ class PluginPostsFeed extends Component {
const uris = this.parseUris();
const requestToken = Symbol();
this._requestToken = requestToken;
- this.state.error.set(null);
+ this.state.$error.set(null);
try {
await this.dataLayer.declarative.ensurePosts(uris);
if (this._requestToken !== requestToken) return;
- this.state.loaded.set(true);
+ this.state.$loaded.set(true);
} catch (error) {
if (this._requestToken !== requestToken) return;
- this.state.error.set(error.message ?? String(error));
+ this.state.$error.set(error.message ?? String(error));
}
}
}
diff --git a/src/js/components/plugin-profiles-list.js b/src/js/components/plugin-profiles-list.js
index ffd6900d..9a05428a 100644
--- a/src/js/components/plugin-profiles-list.js
+++ b/src/js/components/plugin-profiles-list.js
@@ -1,7 +1,7 @@
import { html, render } from "/js/lib/lit-html.js";
import { Component } from "/js/components/component.js";
import { profileFeedTemplate } from "/js/templates/profileFeed.template.js";
-import { Signal, effect } from "/js/signals.js";
+import { Signal, ReactiveStore, effect } from "/js/signals.js";
class PluginProfilesList extends Component {
static get observedAttributes() {
@@ -11,27 +11,26 @@ class PluginProfilesList extends Component {
connectedCallback() {
if (this.initialized) return;
this.initialized = true;
- this.attribs = {
- dids: new Signal.State(this.parseDids()),
- emptyMessage: new Signal.State(this.getAttribute("empty-message")),
- };
- this.state = {
- loaded: new Signal.State(false),
- profiles: new Signal.Computed(() => {
- if (!this.state.loaded.get()) return null;
- const dids = this.attribs.dids.get();
- return dids
- .map((did) => this.dataLayer.derived.$hydratedProfiles.get(did))
- .filter(Boolean);
- }),
- error: new Signal.State(null),
- };
+ this.state = new ReactiveStore("plugin-profiles-list");
+ this.state.$dids = new Signal.State(this.parseDids());
+ this.state.$emptyMessage = new Signal.State(
+ this.getAttribute("empty-message"),
+ );
+ this.state.$loaded = new Signal.State(false);
+ this.state.$profiles = new Signal.Computed(() => {
+ if (!this.state.$loaded.get()) return null;
+ const dids = this.state.$dids.get();
+ return dids
+ .map((did) => this.dataLayer.derived.$hydratedProfiles.get(did))
+ .filter(Boolean);
+ });
+ this.state.$error = new Signal.State(null);
this._disposers = [
effect(() => {
- const error = this.state.error.get();
- const profiles = this.state.profiles.get();
- const emptyMessage = this.attribs.emptyMessage.get();
- const dids = this.attribs.dids.get();
+ const error = this.state.$error.get();
+ const profiles = this.state.$profiles.get();
+ const emptyMessage = this.state.$emptyMessage.get();
+ const dids = this.state.$dids.get();
if (error) {
render(html`${error}
`, this);
return;
@@ -48,7 +47,7 @@ class PluginProfilesList extends Component {
);
}),
effect(() => {
- this.attribs.dids.get();
+ this.state.$dids.get();
this.load();
}),
];
@@ -62,8 +61,8 @@ class PluginProfilesList extends Component {
attributeChangedCallback() {
if (this.initialized) {
- this.attribs.dids.set(this.parseDids());
- this.attribs.emptyMessage.set(this.getAttribute("empty-message"));
+ this.state.$dids.set(this.parseDids());
+ this.state.$emptyMessage.set(this.getAttribute("empty-message"));
}
}
@@ -76,22 +75,22 @@ class PluginProfilesList extends Component {
}
async load() {
- const dids = this.attribs.dids.get();
+ const dids = this.state.$dids.get();
const requestToken = Symbol();
this._requestToken = requestToken;
- this.state.error.set(null);
+ this.state.$error.set(null);
if (dids.length === 0) {
- this.state.loaded.set(true);
+ this.state.$loaded.set(true);
return;
}
- this.state.loaded.set(false);
+ this.state.$loaded.set(false);
try {
await this.dataLayer.declarative.ensureDetailedProfiles(dids);
if (this._requestToken !== requestToken) return;
- this.state.loaded.set(true);
+ this.state.$loaded.set(true);
} catch (error) {
if (this._requestToken !== requestToken) return;
- this.state.error.set(error.message ?? String(error));
+ this.state.$error.set(error.message ?? String(error));
}
}
}
diff --git a/src/js/components/post-composer.js b/src/js/components/post-composer.js
index 4795ffa0..96168bfe 100644
--- a/src/js/components/post-composer.js
+++ b/src/js/components/post-composer.js
@@ -26,6 +26,7 @@ import {
import { LINK_CARD_SERVICE_URL } from "/js/config.js";
import { recordEmbedTemplate } from "/js/templates/postEmbed.template.js";
import { parseRecordLink, resolveRecordFromLink } from "/js/embedHelpers.js";
+import { Signal, ReactiveStore, effect, untrack } from "/js/signals.js";
import "/js/components/rich-text-input.js";
import "/js/components/image-alt-text-dialog.js";
import "/js/components/emoji-picker-dialog.js";
@@ -176,35 +177,68 @@ class PostComposer extends Component {
this.setAttribute("data-dialog-wrapper", "");
this.scrollLock = new ScrollLock(this);
this.innerHTML = "";
- this._postText = "";
this.initialText = this.initialText ?? null;
this.initialCursor = this.initialCursor ?? null;
- this._isSending = false;
this._unresolvedFacets = [];
this._quotedRecordUrl = null;
- this.quotedRecord = this.quotedRecord ?? null;
this._externalLinkUrl = null;
- this._externalLinkEmbedData = null;
this._rejectedLinkEmbeds = new Set();
- this._selectedImages = [];
- this._selectedVideo = null;
- this.render();
+ this._videoToken = null;
+ this.state = new ReactiveStore("postComposer");
+ this.state.$postText = new Signal.State("");
+ this.state.$isSending = new Signal.State(false);
+ this.state.$externalLinkEmbedData = new Signal.State(null);
+ this.state.$selectedImages = new Signal.State([]);
+ this.state.$selectedVideo = new Signal.State(null);
+ this.state.$quotedRecord = new Signal.State(
+ this._pendingQuotedRecord ?? null,
+ );
+ this._pendingQuotedRecord = null;
+ this._disposers = [
+ effect(() => {
+ this.render();
+ }),
+ ];
this.initialized = true;
}
+ disconnectedCallback() {
+ if (!this.initialized) return;
+ this._disposers?.forEach((dispose) => dispose());
+ this._disposers = null;
+ }
+
+ get quotedRecord() {
+ if (!this.state) return this._pendingQuotedRecord ?? null;
+ return untrack(() => this.state.$quotedRecord.get());
+ }
+
+ set quotedRecord(value) {
+ if (!this.state) {
+ this._pendingQuotedRecord = value;
+ return;
+ }
+ this.state.$quotedRecord.set(value);
+ }
+
render() {
const promptText = this.replyTo ? "Write your reply" : "What's up?";
- const currentCharCount = graphemeCount(this._postText);
+ const isSending = this.state.$isSending.get();
+ const externalLinkEmbedData = this.state.$externalLinkEmbedData.get();
+ const selectedImages = this.state.$selectedImages.get();
+ const selectedVideo = this.state.$selectedVideo.get();
+ const quotedRecord = this.state.$quotedRecord.get();
+ const currentCharCount = graphemeCount(this.state.$postText.get());
const charCountPercentage = Math.min(
Math.round((currentCharCount / 300) * 100),
100,
);
const isAboveCharLimit = currentCharCount > 300;
const isVideoUploading =
- this._selectedVideo &&
- (this._selectedVideo.status === "uploading" ||
- this._selectedVideo.status === "processing");
- const hasVideo = !!this._selectedVideo;
+ selectedVideo &&
+ (selectedVideo.status === "uploading" ||
+ selectedVideo.status === "processing");
+ const hasVideo = !!selectedVideo;
render(
html`