diff --git a/package.json b/package.json
index b2218502..79b3a06a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "impro",
- "version": "0.18.66",
+ "version": "0.18.67",
"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 aca8a359..3a4d842e 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -1273,14 +1273,14 @@ tab-bar:not([full-width]) {
stroke: var(--text-color);
}
-plugin-icon {
+app-icon {
display: inline-flex;
width: 1em;
height: 1em;
color: inherit;
}
-plugin-icon svg {
+app-icon svg {
width: 100%;
height: 100%;
}
@@ -1302,7 +1302,7 @@ plugin-blob-image .blob-image-fallback {
color: var(--text-color-muted);
}
-.sidebar-nav-icon plugin-icon {
+.sidebar-nav-icon app-icon {
width: 25px;
height: 25px;
color: var(--text-color);
@@ -6681,6 +6681,27 @@ context-menu-item-group:not(:last-child) .context-menu-item-group {
flex: 1;
padding: 14px;
border-radius: var(--context-menu-button-radius);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.context-menu-item button > .icon,
+.context-menu-item button > .context-menu-item-icon {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ color: var(--text-color-muted);
+}
+
+.context-menu-item button > .context-menu-item-icon {
+ margin-left: auto;
+}
+
+.context-menu-item button > .context-menu-item-icon svg {
+ width: 100%;
+ height: 100%;
+ display: block;
}
.context-menu-item-group .context-menu-item button {
@@ -6719,10 +6740,11 @@ context-menu-item-group:not(:last-child) .context-menu-item-group {
.context-menu-item button {
background-color: transparent;
- padding: 14px 16px;
+ padding: 10px 12px;
border: none;
border-bottom: var(--hair) solid var(--post-border-color);
border-radius: 0;
+ font-size: 14px;
}
}
diff --git a/src/img/icons/custom/emoji-sad-line.svg b/src/img/icons/custom/emoji-sad-line.svg
new file mode 100644
index 00000000..2189fa88
--- /dev/null
+++ b/src/img/icons/custom/emoji-sad-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/img/icons/custom/quote-line.svg b/src/img/icons/custom/quote-line.svg
new file mode 100644
index 00000000..a9518827
--- /dev/null
+++ b/src/img/icons/custom/quote-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/img/icons/custom/speaker-slash-line.svg b/src/img/icons/custom/speaker-slash-line.svg
new file mode 100644
index 00000000..7a4167a4
--- /dev/null
+++ b/src/img/icons/custom/speaker-slash-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/img/icons/custom/speaker-volume-line.svg b/src/img/icons/custom/speaker-volume-line.svg
new file mode 100644
index 00000000..cc8bfc1f
--- /dev/null
+++ b/src/img/icons/custom/speaker-volume-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/img/icons/custom/user-check-line.svg b/src/img/icons/custom/user-check-line.svg
new file mode 100644
index 00000000..1da4158a
--- /dev/null
+++ b/src/img/icons/custom/user-check-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/img/icons/custom/user-minus-line.svg b/src/img/icons/custom/user-minus-line.svg
new file mode 100644
index 00000000..56e06060
--- /dev/null
+++ b/src/img/icons/custom/user-minus-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/img/icons/custom/user-plus-line.svg b/src/img/icons/custom/user-plus-line.svg
new file mode 100644
index 00000000..0741e8b8
--- /dev/null
+++ b/src/img/icons/custom/user-plus-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/img/icons/custom/user-x-line.svg b/src/img/icons/custom/user-x-line.svg
new file mode 100644
index 00000000..f3e88e8e
--- /dev/null
+++ b/src/img/icons/custom/user-x-line.svg
@@ -0,0 +1 @@
+
diff --git a/src/js/components/plugin-icon.js b/src/js/components/app-icon.js
similarity index 98%
rename from src/js/components/plugin-icon.js
rename to src/js/components/app-icon.js
index cf0ad91a..bf821a3f 100644
--- a/src/js/components/plugin-icon.js
+++ b/src/js/components/app-icon.js
@@ -7,9 +7,18 @@ const ICONS = {
"check",
"circle-check",
"crop-indicator",
+ "emoji-sad-line",
+ "emoji-smile-line",
"hamburger-menu",
+ "quote-line",
"reply",
"repost",
+ "speaker-slash-line",
+ "speaker-volume-line",
+ "user-check-line",
+ "user-minus-line",
+ "user-plus-line",
+ "user-x-line",
"verified-check",
"verifier-check",
]),
@@ -784,11 +793,11 @@ for (const [iconset, icons] of Object.entries(ICONS)) {
}
}
-class PluginIcon extends Component {
+class AppIcon extends Component {
static observedAttributes = ["icon"];
static cache = new Map();
- cache = PluginIcon.cache;
+ cache = AppIcon.cache;
attributeChangedCallback() {
this.render();
@@ -842,4 +851,4 @@ class PluginIcon extends Component {
}
}
-PluginIcon.register();
+AppIcon.register();
diff --git a/src/js/components/context-menu-item.js b/src/js/components/context-menu-item.js
index 4fa646a0..35566416 100644
--- a/src/js/components/context-menu-item.js
+++ b/src/js/components/context-menu-item.js
@@ -1,9 +1,10 @@
import { Component, getChildrenFragment } from "/js/components/component.js";
import { html, render } from "/js/lib/lit-html.js";
+import "/js/components/app-icon.js";
class ContextMenuItem extends Component {
static get observedAttributes() {
- return ["disabled"];
+ return ["disabled", "icon"];
}
connectedCallback() {
@@ -13,6 +14,7 @@ class ContextMenuItem extends Component {
this._children = getChildrenFragment(this);
this.innerHTML = "";
this.disabled = this.getAttribute("disabled") !== null;
+ this.icon = this.getAttribute("icon");
this.render();
this._initialized = true;
}
@@ -23,19 +25,27 @@ class ContextMenuItem extends Component {
}
if (name === "disabled") {
this.disabled = this.getAttribute("disabled") !== null;
- this.render();
+ } else if (name === "icon") {
+ this.icon = this.getAttribute("icon");
}
+ this.render();
}
render() {
render(
html`
`,
this,
);
- const button = this.querySelector("button");
- button.appendChild(this._children);
}
}
diff --git a/src/js/components/create-list-dialog.js b/src/js/components/create-list-dialog.js
index 96c4744f..e9192abd 100644
--- a/src/js/components/create-list-dialog.js
+++ b/src/js/components/create-list-dialog.js
@@ -199,6 +199,7 @@ class CreateListDialog extends Component {
this._pickImage()}
>
Upload from Files
@@ -208,6 +209,7 @@ class CreateListDialog extends Component {
? html`
{
this._newAvatarDataUrl = null;
this.render();
diff --git a/src/js/components/edit-list-details-dialog.js b/src/js/components/edit-list-details-dialog.js
index 27521059..0ee62950 100644
--- a/src/js/components/edit-list-details-dialog.js
+++ b/src/js/components/edit-list-details-dialog.js
@@ -216,6 +216,7 @@ class EditListDetailsDialog extends Component {
this._pickImage()}
>
Upload from Files
@@ -225,6 +226,7 @@ class EditListDetailsDialog extends Component {
? html`
{
this._newAvatarDataUrl = null;
this._removeAvatar = true;
diff --git a/src/js/components/edit-profile-dialog.js b/src/js/components/edit-profile-dialog.js
index e7b6378d..c21a598f 100644
--- a/src/js/components/edit-profile-dialog.js
+++ b/src/js/components/edit-profile-dialog.js
@@ -241,6 +241,7 @@ class EditProfileDialog extends Component {
this._pickImage("banner")}
>
Upload from Files
@@ -250,6 +251,7 @@ class EditProfileDialog extends Component {
? html`
{
this._newBannerDataUrl = null;
this._removeBanner = true;
@@ -266,6 +268,7 @@ class EditProfileDialog extends Component {
this._pickImage("avatar")}
>
Upload from Files
@@ -275,6 +278,7 @@ class EditProfileDialog extends Component {
? html`
{
this._newAvatarDataUrl = null;
this._removeAvatar = true;
diff --git a/src/js/plugins/pluginRendering.js b/src/js/plugins/pluginRendering.js
index 90048b66..4c23d50b 100644
--- a/src/js/plugins/pluginRendering.js
+++ b/src/js/plugins/pluginRendering.js
@@ -6,7 +6,7 @@ import {
import "/js/components/toggle-switch.js";
import "/js/components/plugin-profiles-list.js";
import "/js/components/plugin-posts-feed.js";
-import "/js/components/plugin-icon.js";
+import "/js/components/app-icon.js";
import "/js/components/plugin-blob-image.js";
function isExternalHref(href) {
@@ -444,7 +444,8 @@ export class PluginRenderer {
}
const ns = resolveChildNamespace(parentNs, node);
if (ns === SVG_NS) return this._createSvg(node);
- const tag = resolveTag(node, this.pluginId);
+ let tag = resolveTag(node, this.pluginId);
+ if (tag === "plugin-icon") tag = "app-icon";
const element = document.createElement(tag);
if (tag === "a") {
element.setAttribute("target", "_blank");
diff --git a/src/js/templates/postActionBar.template.js b/src/js/templates/postActionBar.template.js
index 8ec84785..2e1df9c6 100644
--- a/src/js/templates/postActionBar.template.js
+++ b/src/js/templates/postActionBar.template.js
@@ -74,6 +74,7 @@ function postContextMenuTemplate({
{
window.open(getBlueskyLinkForPost(post), "_blank");
}}
@@ -82,6 +83,7 @@ function postContextMenuTemplate({
{
navigator.clipboard.writeText(getPermalinkForPost(post));
showToast("Link copied to clipboard", { style: "success" });
@@ -93,6 +95,7 @@ function postContextMenuTemplate({
? html`
{
const postText = getFullPostText(post);
const targetLang = getBrowserLanguageCodes()[0] || "en";
@@ -103,6 +106,7 @@ function postContextMenuTemplate({
{
const postText = getFullPostText(post);
navigator.clipboard.writeText(postText);
@@ -123,12 +127,14 @@ function postContextMenuTemplate({
onClickShowMore(post, feedContext)}
>
Show more like this
onClickShowLess(post, feedContext)}
>
Show less like this
@@ -143,6 +149,7 @@ function postContextMenuTemplate({
onClickHidePost(post)}
>
Hide ${post.record?.reply ? "reply" : "post"} for me
@@ -156,6 +163,9 @@ function postContextMenuTemplate({
data-teststate=${post.author.viewer?.muted
? "muted"
: "unmuted"}
+ icon=${post.author.viewer?.muted
+ ? "speaker-volume-line"
+ : "speaker-slash-line"}
@click=${() =>
onClickMute(post.author, !post.author.viewer?.muted)}
>
@@ -168,6 +178,9 @@ function postContextMenuTemplate({
data-teststate=${post.author.viewer?.blocking
? "blocking"
: "not-blocking"}
+ icon=${post.author.viewer?.blocking
+ ? "user-check-line"
+ : "user-x-line"}
@click=${() =>
onClickBlock(post.author, !post.author.viewer?.blocking)}
>
@@ -177,6 +190,7 @@ function postContextMenuTemplate({
onClickReport(post)}
>
Report post
@@ -194,6 +208,7 @@ function postContextMenuTemplate({
data-teststate=${isPinnedToProfile
? "pinned"
: "unpinned"}
+ icon="pin-line"
@click=${() => onClickPin(post, !isPinnedToProfile)}
>
${isPinnedToProfile
@@ -204,6 +219,7 @@ function postContextMenuTemplate({
: null}
onClickDelete(post)}
>
Delete post
@@ -332,6 +348,7 @@ export function postActionBarTemplate({
{
if (!isAuthenticated) {
SignInModal.open();
@@ -345,6 +362,7 @@ export function postActionBarTemplate({
{
if (!isAuthenticated) {
SignInModal.open();
diff --git a/src/js/templates/profileCard.template.js b/src/js/templates/profileCard.template.js
index c57c0456..0d875378 100644
--- a/src/js/templates/profileCard.template.js
+++ b/src/js/templates/profileCard.template.js
@@ -114,6 +114,7 @@ function profileContextMenuTemplate({
{
window.open(getBlueskyLinkForProfile(profile), "_blank");
}}
@@ -122,6 +123,7 @@ function profileContextMenuTemplate({
{
navigator.clipboard.writeText(getPermalinkForProfile(profile));
showToast("Link copied to clipboard", { style: "success" });
@@ -134,6 +136,7 @@ function profileContextMenuTemplate({
? html`
{
router.go(linkToSearchPostsByProfile(profile));
}}
@@ -149,6 +152,7 @@ function profileContextMenuTemplate({
{
onClickFollow(profile, !isFollowing);
}}
@@ -160,6 +164,7 @@ function profileContextMenuTemplate({
{
onClickAddToLists(profile);
}}
@@ -169,6 +174,9 @@ function profileContextMenuTemplate({
{
onClickMute(profile, !profile.viewer?.muted);
}}
@@ -180,6 +188,9 @@ function profileContextMenuTemplate({
data-teststate=${profile.viewer?.blocking
? "blocking"
: "not-blocking"}
+ icon=${profile.viewer?.blocking
+ ? "user-check-line"
+ : "user-x-line"}
@click=${() => {
onClickBlock(profile, !profile.viewer?.blocking);
}}
@@ -188,6 +199,7 @@ function profileContextMenuTemplate({
{
onClickReport(profile);
}}
diff --git a/src/js/templates/sidebar.template.js b/src/js/templates/sidebar.template.js
index c601a360..cddbeb99 100644
--- a/src/js/templates/sidebar.template.js
+++ b/src/js/templates/sidebar.template.js
@@ -23,7 +23,7 @@ import {
linkToLogin,
} from "/js/navigation.js";
import "/js/components/animated-sidebar.js";
-import "/js/components/plugin-icon.js";
+import "/js/components/app-icon.js";
import { WelcomeModal } from "/js/modals/welcome.modal.js";
function pluginSidebarItemTemplate({ entry }) {
@@ -38,7 +38,7 @@ function pluginSidebarItemTemplate({ entry }) {
}}
>
diff --git a/src/js/views/chatDetail.view.js b/src/js/views/chatDetail.view.js
index 65c1929e..924bdebd 100644
--- a/src/js/views/chatDetail.view.js
+++ b/src/js/views/chatDetail.view.js
@@ -221,6 +221,7 @@ class ChatDetailView extends View {
return html`
setReply(message)}
>
Reply
@@ -1423,6 +1424,7 @@ class ChatDetailView extends View {
${groupDetails && canViewGroupDetails
? html` {
router.go(linkToGroupChatDetails(convoId));
}}
@@ -1432,6 +1434,7 @@ class ChatDetailView extends View {
: ""}
{
window.open(convoPermalink, "_blank");
}}
diff --git a/src/js/views/communityPluginListing.view.js b/src/js/views/communityPluginListing.view.js
index 343ef94c..c693cc3c 100644
--- a/src/js/views/communityPluginListing.view.js
+++ b/src/js/views/communityPluginListing.view.js
@@ -154,6 +154,7 @@ class CommunityPluginListingView extends View {
{
navigator.clipboard.writeText(pluginPermalink);
showToast("Link copied to clipboard", {
diff --git a/src/js/views/feedDetail.view.js b/src/js/views/feedDetail.view.js
index 46f315d5..83360314 100644
--- a/src/js/views/feedDetail.view.js
+++ b/src/js/views/feedDetail.view.js
@@ -73,6 +73,7 @@ class FeedDetailView extends View {
{
window.open(feedLink, "_blank");
}}
@@ -81,6 +82,7 @@ class FeedDetailView extends View {
{
navigator.clipboard.writeText(feedLink);
showToast("Link copied to clipboard", {
diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js
index c15426a3..d20c696b 100644
--- a/src/js/views/listDetail.view.js
+++ b/src/js/views/listDetail.view.js
@@ -88,12 +88,14 @@ class ListDetailView extends View {
listInteractionHandler.handleMuteList(list)}
>
Mute accounts
listInteractionHandler.handleBlockList(list)}
>
Block accounts
@@ -158,6 +160,7 @@ class ListDetailView extends View {
{
window.open(listPermalink, "_blank");
}}
@@ -166,6 +169,7 @@ class ListDetailView extends View {
{
navigator.clipboard.writeText(listPermalink);
showToast("Link copied to clipboard", {
@@ -178,18 +182,21 @@ class ListDetailView extends View {
${isCurrentUserList
? html` handleAddPeople(list)}
>
Add people to list
handleEditList(list)}
>
Edit list details
handleDeleteList(list)}
>
Delete list
diff --git a/src/js/views/settings.view.js b/src/js/views/settings.view.js
index b6e94830..9cb0ba03 100644
--- a/src/js/views/settings.view.js
+++ b/src/js/views/settings.view.js
@@ -150,6 +150,7 @@ class SettingsView extends View {
onRemove(account)}
>
Remove account
diff --git a/tests/unit/specs/components/plugin-icon.test.js b/tests/unit/specs/components/app-icon.test.js
similarity index 84%
rename from tests/unit/specs/components/plugin-icon.test.js
rename to tests/unit/specs/components/app-icon.test.js
index 15c7fe6a..5fb2b796 100644
--- a/tests/unit/specs/components/plugin-icon.test.js
+++ b/tests/unit/specs/components/app-icon.test.js
@@ -1,9 +1,9 @@
import { describe, it, beforeEach, mock } from "node:test";
import assert from "node:assert/strict";
import { MockFetch } from "../../testHelpers.js";
-import "/js/components/plugin-icon.js";
+import "/js/components/app-icon.js";
-describe("plugin-icon", () => {
+describe("app-icon", () => {
const SAMPLE_SVG =
'';
@@ -31,16 +31,16 @@ describe("plugin-icon", () => {
beforeEach(() => {
document.body.innerHTML = "";
- customElements.get("plugin-icon").cache = new Map();
+ customElements.get("app-icon").cache = new Map();
});
- describe("PluginIcon - iconset resolution", () => {
+ describe("AppIcon - iconset resolution", () => {
it("resolves an icon from the majesticons set", async () => {
const fetch = new MockFetch();
fetch.__intercept("/img/icons/", async () => okResponse(SAMPLE_SVG));
globalThis.fetch = fetch;
- const element = document.createElement("plugin-icon");
+ const element = document.createElement("app-icon");
element.setAttribute("icon", "bell");
document.body.appendChild(element);
await flush();
@@ -53,7 +53,7 @@ describe("plugin-icon", () => {
fetch.__intercept("/img/icons/", async () => okResponse(SAMPLE_SVG));
globalThis.fetch = fetch;
- const element = document.createElement("plugin-icon");
+ const element = document.createElement("app-icon");
element.setAttribute("icon", "verified-check");
document.body.appendChild(element);
await flush();
@@ -73,7 +73,7 @@ describe("plugin-icon", () => {
console.warn = warnMock;
try {
- const element = document.createElement("plugin-icon");
+ const element = document.createElement("app-icon");
element.setAttribute("icon", "not-a-real-icon");
document.body.appendChild(element);
await flush();
@@ -87,13 +87,13 @@ describe("plugin-icon", () => {
});
});
- describe("PluginIcon - rendering", () => {
+ describe("AppIcon - rendering", () => {
it("injects the fetched SVG markup", async () => {
const fetch = new MockFetch();
fetch.__intercept("/img/icons/", async () => okResponse(SAMPLE_SVG));
globalThis.fetch = fetch;
- const element = document.createElement("plugin-icon");
+ const element = document.createElement("app-icon");
element.setAttribute("icon", "cake");
document.body.appendChild(element);
await flush();
@@ -107,7 +107,7 @@ describe("plugin-icon", () => {
const fetch = new MockFetch();
globalThis.fetch = fetch;
- const element = document.createElement("plugin-icon");
+ const element = document.createElement("app-icon");
document.body.appendChild(element);
await flush();
@@ -125,7 +125,7 @@ describe("plugin-icon", () => {
);
globalThis.fetch = fetch;
- const element = document.createElement("plugin-icon");
+ const element = document.createElement("app-icon");
element.setAttribute("icon", "bus");
document.body.appendChild(element);
await flush();
@@ -137,17 +137,17 @@ describe("plugin-icon", () => {
});
});
- describe("PluginIcon - caching", () => {
+ describe("AppIcon - caching", () => {
it("only fetches once when the same icon is rendered twice", async () => {
const fetch = new MockFetch();
fetch.__intercept("/img/icons/", async () => okResponse(SAMPLE_SVG));
globalThis.fetch = fetch;
- const first = document.createElement("plugin-icon");
+ const first = document.createElement("app-icon");
first.setAttribute("icon", "chat");
document.body.appendChild(first);
- const second = document.createElement("plugin-icon");
+ const second = document.createElement("app-icon");
second.setAttribute("icon", "chat");
document.body.appendChild(second);
@@ -159,7 +159,7 @@ describe("plugin-icon", () => {
});
});
- describe("PluginIcon - error handling", () => {
+ describe("AppIcon - error handling", () => {
it("warns and renders nothing when the fetch 404s; does not retry", async () => {
const fetch = new MockFetch();
fetch.__intercept("/img/icons/", async () => notFoundResponse());
@@ -170,7 +170,7 @@ describe("plugin-icon", () => {
console.warn = warnMock;
try {
- const element = document.createElement("plugin-icon");
+ const element = document.createElement("app-icon");
element.setAttribute("icon", "moon");
document.body.appendChild(element);
await flush();
@@ -178,7 +178,7 @@ describe("plugin-icon", () => {
assert.deepEqual(element.innerHTML, "");
assert.deepEqual(warnMock.mock.callCount(), 1);
- const retry = document.createElement("plugin-icon");
+ const retry = document.createElement("app-icon");
retry.setAttribute("icon", "moon");
document.body.appendChild(retry);
await flush();
diff --git a/tests/unit/specs/plugins/pluginRendering.test.js b/tests/unit/specs/plugins/pluginRendering.test.js
index edd6b9f1..e3c526cc 100644
--- a/tests/unit/specs/plugins/pluginRendering.test.js
+++ b/tests/unit/specs/plugins/pluginRendering.test.js
@@ -369,14 +369,14 @@ describe("PluginRenderer:root reconciliation", () => {
});
describe("PluginRenderer:plugin-icon", () => {
- it("renders with the icon attribute passed through", () => {
+ it("renders as with the icon attribute passed through", () => {
const { bridge } = makeBridge();
const renderer = new PluginRenderer(bridge, "demo");
const element = renderer.createRoot().render({
tag: "plugin-icon",
attrs: { icon: "bell" },
});
- assert.deepEqual(element.tagName.toLowerCase(), "plugin-icon");
+ assert.deepEqual(element.tagName.toLowerCase(), "app-icon");
assert.deepEqual(element.getAttribute("icon"), "bell");
});
diff --git a/tests/unit/specs/templates/sidebar.template.test.js b/tests/unit/specs/templates/sidebar.template.test.js
index ae2b8555..1c3c7990 100644
--- a/tests/unit/specs/templates/sidebar.template.test.js
+++ b/tests/unit/specs/templates/sidebar.template.test.js
@@ -465,7 +465,7 @@ describe("sidebarTemplate - plugin sidebar items", () => {
assert(invoked);
});
- it("should render a with the entry's icon for each plugin item", () => {
+ it("should render an with the entry's icon for each plugin item", () => {
const result = sidebarTemplate({
isAuthenticated: true,
currentUser: mockUser,
@@ -477,7 +477,7 @@ describe("sidebarTemplate - plugin sidebar items", () => {
const container = document.createElement("div");
render(result, container);
const icons = container.querySelectorAll(
- ".sidebar-plugin-nav-item plugin-icon",
+ ".sidebar-plugin-nav-item app-icon",
);
assert.deepEqual(icons.length, 2);
assert.deepEqual(icons[0].getAttribute("icon"), "lightning-bolt");