From d6bc1af25518cb0eac2c6115e657bdc71997b40d Mon Sep 17 00:00:00 2001 From: kawamataryo Date: Wed, 16 Aug 2023 22:15:48 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20use=20vanjs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 17 +++- package.json | 3 +- src/content.ts | 6 +- src/lib/components/BskyUserCell.ts | 125 +++++++++++++++++++++++++ src/lib/components/NotFoundCell.ts | 29 ++++++ src/lib/components/ReloadBtn.ts | 19 ++++ src/lib/domHelpers.ts | 140 ++++------------------------ src/lib/searchAndInsertBskyUsers.ts | 23 ++--- src/style.content.css | 15 +-- 9 files changed, 223 insertions(+), 154 deletions(-) create mode 100644 src/lib/components/BskyUserCell.ts create mode 100644 src/lib/components/NotFoundCell.ts create mode 100644 src/lib/components/ReloadBtn.ts diff --git a/package-lock.json b/package-lock.json index e171c36..fad5ff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sky-follower-bridge", - "version": "0.1.3", + "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sky-follower-bridge", - "version": "0.1.3", + "version": "0.2.0", "dependencies": { "@atproto/api": "^0.6.4", "@plasmohq/messaging": "^0.5.0", @@ -14,7 +14,8 @@ "plasmo": "^0.82.1", "react": "18.2.0", "react-dom": "18.2.0", - "ts-pattern": "^5.0.5" + "ts-pattern": "^5.0.5", + "vanjs-core": "^1.0.2" }, "devDependencies": { "@plasmohq/prettier-plugin-sort-imports": "4.0.1", @@ -9536,6 +9537,11 @@ "node": ">= 4" } }, + "node_modules/vanjs-core": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vanjs-core/-/vanjs-core-1.0.2.tgz", + "integrity": "sha512-c/ding4r+NRMobYJTLjVYyrqee51Zi1Er+8k1QV1vTZW4+e30/728DQOQh8yQc2VdZSrNJHYRWhW02C4NoiJgw==" + }, "node_modules/vue": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", @@ -15806,6 +15812,11 @@ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" }, + "vanjs-core": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vanjs-core/-/vanjs-core-1.0.2.tgz", + "integrity": "sha512-c/ding4r+NRMobYJTLjVYyrqee51Zi1Er+8k1QV1vTZW4+e30/728DQOQh8yQc2VdZSrNJHYRWhW02C4NoiJgw==" + }, "vue": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", diff --git a/package.json b/package.json index 4980573..9f1fbf6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "plasmo": "^0.82.1", "react": "18.2.0", "react-dom": "18.2.0", - "ts-pattern": "^5.0.5" + "ts-pattern": "^5.0.5", + "vanjs-core": "^1.0.2" }, "devDependencies": { "@plasmohq/prettier-plugin-sort-imports": "4.0.1", diff --git a/src/content.ts b/src/content.ts index 0e76362..7141561 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,8 +1,8 @@ -import { BskyClient, BskyLoginParams } from "./lib/bskyClient"; +import { BskyClient, type BskyLoginParams } from "./lib/bskyClient"; import type { PlasmoCSConfig } from "plasmo" import { MESSAGE_NAMES, VIEWER_STATE } from "~lib/constants"; import "./style.content.css" -import { initialize, searchBskyUsers } from '~lib/searchAndInsertBskyUsers'; +import { searchBskyUsers } from '~lib/searchAndInsertBskyUsers'; export const config: PlasmoCSConfig = { matches: ["https://twitter.com/*", "https://x.com/*"], @@ -54,7 +54,6 @@ const searchAndShowBskyUsers = async ({ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { if (Object.values(MESSAGE_NAMES).includes(message.name)) { - initialize() searchAndShowBskyUsers({ identifier: message.body.userId, password: message.body.password, @@ -64,6 +63,7 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { sendResponse({ hasError: false }) }) .catch((e) => { + console.error(e) sendResponse({ hasError: true, message: e.toString() }) }); return true diff --git a/src/lib/components/BskyUserCell.ts b/src/lib/components/BskyUserCell.ts new file mode 100644 index 0000000..90ebf1a --- /dev/null +++ b/src/lib/components/BskyUserCell.ts @@ -0,0 +1,125 @@ +import type { ProfileView, ViewerState } from "@atproto/api/dist/client/types/app/bsky/actor/defs" +import van from 'vanjs-core' + +const { a, div, p, img, button } = van.tags + +export type UserCellBtnLabel = { + add: string, + remove: string, + progressive: string, +} + +const ActionButton = ({ statusKey, profile, btnLabel, addAction, removeAction }: { + profile: ProfileView, + statusKey: keyof ViewerState, + btnLabel: UserCellBtnLabel, + addAction: () => Promise, + removeAction: () => Promise +}) => { + const label = van.state(`${profile.viewer[statusKey] ? btnLabel.progressive : btnLabel.add} on Bluesky`) + + const isStateOfBeing = van.state(profile.viewer[statusKey]) + const isProcessing = van.state(false) + const isJustApplied = van.state(false) + + const beingClass = van.derive(() => isStateOfBeing.val ? "action-button__being" : "") + const processingClass = van.derive(() => isProcessing.val ? "action-button__processing" : "") + const justAppliedClass = van.derive(() => isJustApplied.val ? "action-button__just-applied" : "") + + const onClick = async () => { + if (isProcessing.val) return + isProcessing.val = true + label.val = "Processing..." + + if (isStateOfBeing.val) { + await removeAction() + label.val = `${btnLabel.add} on Bluesky` + isStateOfBeing.val = false + } else { + await addAction() + label.val = `${btnLabel.progressive} on Bluesky` + isStateOfBeing.val = true + isJustApplied.val = true + } + + isProcessing.val = false + } + + const onMouseover = () => { + if( + isProcessing.val || + isJustApplied.val || + !isStateOfBeing.val + ) return + + label.val = `${btnLabel.remove} on Bluesky` + } + + const onMouseout = () => { + if (isJustApplied.val) { + isJustApplied.val = false + } + if(!isStateOfBeing.val) return + + label.val = `${btnLabel.progressive} on Bluesky` + } + + return () => button({ + class: `action-button ${beingClass.val} ${processingClass.val} ${justAppliedClass.val}`, + onclick: onClick, + onmouseover: onMouseover, + onmouseout: onMouseout, + }, + label.val, + ) +} + +const Avatar = ({ avatar }: { avatar?: string }) => { + return avatar ? img({ src: avatar, width: "40" }) : div({ class: "no-avatar" }) +} + +export const BskyUserCell = ({ + profile, + statusKey, + btnLabel, + addAction, + removeAction, +}: { + profile: ProfileView, + statusKey: keyof ViewerState, + btnLabel: UserCellBtnLabel, + addAction: () => Promise, + removeAction: () => Promise +}) => { + return div({ class: "bsky-user-content" }, + div({ class: "icon-section"}, + a({ href: `https://bsky.app/profile/${profile.handle}`, target: "_blank", rel: "noopener" }, + Avatar({ avatar: profile.avatar }), + ), + ), + div({ class: "content" }, + div({ class: "name-and-controller" }, + div( + p({ class: "display-name" }, + a({ href: `https://bsky.app/profile/${profile.handle}`, target: "_blank", rel: "noopener" }, + profile.displayName ?? profile.handle, + ), + ), + p({ class: "handle" }, + `@${profile.handle}`, + ), + ), + div( + ActionButton({ + profile, + statusKey, + btnLabel, + addAction, + removeAction, + }) + ), + ), + profile.description ? p({ class: "description" }, profile.description) : "", + ), + ) +} diff --git a/src/lib/components/NotFoundCell.ts b/src/lib/components/NotFoundCell.ts new file mode 100644 index 0000000..c5cd0e6 --- /dev/null +++ b/src/lib/components/NotFoundCell.ts @@ -0,0 +1,29 @@ + +import van from "vanjs-core" + +const { div, p } = van.tags +const { svg, path } = van.tagsNS("http://www.w3.org/2000/svg") + +const WarningIcon = () => svg( + { + fill: "none", + "stroke-width": "1.5", + stroke: "currentColor", + class: "w-6 h-6", + viewBox: "0 0 24 24" + }, + path({ + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" + }), +) + +export const NotFoundCell = () => div({ class: "bsky-user-content bsky-user-content__not-found" }, + WarningIcon(), + p({ + class: "not-found" + }, + "No similar users found." + ) +) diff --git a/src/lib/components/ReloadBtn.ts b/src/lib/components/ReloadBtn.ts new file mode 100644 index 0000000..cb3a6d3 --- /dev/null +++ b/src/lib/components/ReloadBtn.ts @@ -0,0 +1,19 @@ +import van from "vanjs-core" + +const { button, div } = van.tags + +export const ReloadButton = ({clickAction}: {clickAction: () => void}) => { + const deleted = van.state(false) + + return () => deleted.val ? null : div({ class: "bsky-reload-btn-wrapper" }, + button( + { + class: "bsky-reload-btn", + onclick: () => { + clickAction() + deleted.val = true + } + }, + "Find More" + )) +} diff --git a/src/lib/domHelpers.ts b/src/lib/domHelpers.ts index aab02ec..07494ce 100644 --- a/src/lib/domHelpers.ts +++ b/src/lib/domHelpers.ts @@ -1,10 +1,8 @@ import type { ProfileView, ViewerState } from "@atproto/api/dist/client/types/app/bsky/actor/defs" - -export type UserCellBtnLabel = { - add: string, - remove: string, - progressive: string, -} +import van from "vanjs-core" +import { ReloadButton } from "./components/ReloadBtn" +import { NotFoundCell } from "./components/NotFoundCell" +import { BskyUserCell, type UserCellBtnLabel } from "./components/BskyUserCell" export const getUserCells = ({ queryParam, filterInsertedElement }: { queryParam: string, filterInsertedElement: boolean }) => { const userCells = document.querySelectorAll(queryParam); @@ -23,27 +21,7 @@ export const getUserCells = ({ queryParam, filterInsertedElement }: { queryParam export const insertReloadEl = (clickAction: () => void) => { const lastInsertedEl = Array.from(document.querySelectorAll('.bsky-user-content')).at(-1) - lastInsertedEl.insertAdjacentHTML('afterend', ` -
- -
- `) - - const reloadBtn = document.querySelector(".bsky-reload-btn") as HTMLElement - reloadBtn.addEventListener("click", async (e) => { - const target = e.target as HTMLButtonElement - if (target.classList.contains('bsky-reload-btn__processing')) { - return - } - await clickAction() - }) -} - -export const removeReloadElIfExists = () => { - const reloadBtnWrapper = document.querySelector(".bsky-reload-btn-wrapper") as HTMLElement - reloadBtnWrapper?.remove() + van.add(lastInsertedEl.parentElement, ReloadButton({clickAction})) } export const getAccountNameAndDisplayName = (userCell: Element) => { @@ -53,111 +31,25 @@ export const getAccountNameAndDisplayName = (userCell: Element) => { return { twAccountName, twDisplayName } } -// TODO: vanjsを使ってdom操作を描き直したい -export const insertBskyProfileEl = ({ dom, profile, statusKey, btnLabel, abortController, followAction, unfollowAction }: { +export const insertBskyProfileEl = ({ dom, profile, statusKey, btnLabel, addAction, removeAction }: { dom: Element, profile: ProfileView, statusKey: keyof ViewerState, btnLabel: UserCellBtnLabel, - abortController: AbortController, - followAction: () => void, - unfollowAction: () => void + addAction: () => Promise, + removeAction: () => Promise }) => { - const avatarEl = profile.avatar ? `` : "
" - const actionBtnEl = profile.viewer[statusKey] ? `` : `` - dom.insertAdjacentHTML('afterend', ` -
- -
-
-
-

${profile.displayName ?? profile.handle}

-

@${profile.handle}

-
-
- ${actionBtnEl} -
-
- ${profile.description ? `

${profile.description}

` : ""} -
-
- `) - const bskyUserContentDom = dom.nextElementSibling as Element - - // register a click action - bskyUserContentDom?.addEventListener('click', async (e) => { - const target = e.target as Element - const classList = target.classList - - // follow action - if (classList.contains('follow-button') && !classList.contains('follow-button__following')) { - target.textContent = "processing..." - target.classList.add('follow-button__processing') - await followAction() - target.textContent = `${btnLabel.progressive} on Bluesky` - target.classList.remove('follow-button__processing') - target.classList.add('follow-button__following') - target.classList.add('follow-button__just-followed') - return - } - - // unfollow action - if (classList.contains('follow-button') && classList.contains('follow-button__following')) { - target.textContent = "processing..." - target.classList.add('follow-button__processing') - await unfollowAction() - target.textContent = `${btnLabel.add} on Bluesky` - target.classList.remove('follow-button__processing') - target.classList.remove('follow-button__following') - return - } - }, { - signal: abortController.signal - }) - - bskyUserContentDom?.addEventListener('mouseover', async (e) => { - const target = e.target as Element - const classList = target.classList - if (classList.contains('follow-button') && classList.contains('follow-button__following')) { - target.textContent = `${btnLabel.remove} on Bluesky` - } - }, { - signal: abortController.signal - }) - bskyUserContentDom?.addEventListener('mouseout', async (e) => { - const target = e.target as Element - const classList = target.classList - if (classList.contains('follow-button__just-followed')) { - target.classList.remove('follow-button__just-followed') - } - if (classList.contains('follow-button') && classList.contains('follow-button__following')) { - target.textContent = `${btnLabel.progressive} on Bluesky` - } - }, { - signal: abortController.signal - }) + van.add(dom.parentElement, BskyUserCell({ + profile, + statusKey, + btnLabel, + addAction, + removeAction, + })) } export const insertNotFoundEl = (dom: Element) => { - dom.insertAdjacentHTML('afterend', ` -
- -

No similar users found.

-
- `) -} - -export const cleanBskyUserElements = () => { - const bskyUserContent = document.querySelectorAll('.bsky-user-content'); - if (bskyUserContent.length > 0) { - bskyUserContent.forEach((el) => { - el.remove() - }) - } + van.add(dom.parentElement, NotFoundCell()) } export const isOutOfTopViewport = (el: Element) => { diff --git a/src/lib/searchAndInsertBskyUsers.ts b/src/lib/searchAndInsertBskyUsers.ts index 2ff86ea..475220c 100644 --- a/src/lib/searchAndInsertBskyUsers.ts +++ b/src/lib/searchAndInsertBskyUsers.ts @@ -1,22 +1,16 @@ -import { UserCellBtnLabel, isOutOfTopViewport } from './domHelpers'; -import { getAccountNameAndDisplayName, getUserCells, insertBskyProfileEl, insertNotFoundEl, insertReloadEl, removeReloadElIfExists } from "~lib/domHelpers"; +import { isOutOfTopViewport } from './domHelpers'; +import { getAccountNameAndDisplayName, getUserCells, insertBskyProfileEl, insertNotFoundEl, insertReloadEl } from "~lib/domHelpers"; import { isSimilarUser } from "~lib/bskyHelpers"; import { debugLog } from "~lib/utils"; import type { BskyClient } from './bskyClient'; import type { ViewerState } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; +import type { UserCellBtnLabel } from './components/BskyUserCell'; -let abortController = new AbortController(); - const notFoundUserCache = new Set() const followerUrlMap = new Map() -export const initialize = async () => { - abortController.abort() - abortController = new AbortController() -} - export const searchBskyUsers = async ( { agent, @@ -33,7 +27,6 @@ export const searchBskyUsers = async ( addQuery: (arg: string) => Promise, removeQuery: (arg: string) => Promise, }) => { - removeReloadElIfExists() const userCells = getUserCells({ queryParam: userCellQueryParam, @@ -65,12 +58,11 @@ export const searchBskyUsers = async ( profile: searchResultByAccountName, statusKey, btnLabel, - abortController, - followAction: async () => { + addAction: async () => { const result = await addQuery(searchResultByAccountName.did); followerUrlMap.set(searchResultByAccountName.did, result.uri) }, - unfollowAction: async () => { + removeAction: async () => { if (searchResultByAccountName?.viewer?.following) { await removeQuery(searchResultByAccountName?.viewer?.following); } else { @@ -88,14 +80,13 @@ export const searchBskyUsers = async ( insertBskyProfileEl({ dom: userCell, profile: searchResultByDisplayName, - abortController, statusKey, btnLabel, - followAction: async () => { + addAction: async () => { const result = await addQuery(searchResultByDisplayName.did); followerUrlMap.set(searchResultByDisplayName.did, result.uri) }, - unfollowAction: async () => { + removeAction: async () => { if (searchResultByDisplayName?.viewer?.following) { await removeQuery(searchResultByDisplayName?.viewer?.following); } else { diff --git a/src/style.content.css b/src/style.content.css index fb3bddd..47ef8ee 100644 --- a/src/style.content.css +++ b/src/style.content.css @@ -27,13 +27,14 @@ } .bsky-user-content .icon-section a{ text-decoration: none; + width: 40px; } .bsky-user-content .icon-section img { border-radius: 50%; } .bsky-user-content .icon-section .no-avatar { - width: 48px; - height: 48px; + width: 40px; + height: 40px; background: #ccc; border-radius: 50%; } @@ -63,7 +64,7 @@ justify-content: space-between; } -.name-and-controller .follow-button { +.name-and-controller .action-button { border: 1px solid #fff; padding: 6px 30px; font-size: 14px; @@ -74,27 +75,27 @@ cursor: pointer; } -.name-and-controller .follow-button__following { +.name-and-controller .action-button__being { background: transparent; color: #fff; cursor: pointer; } -.name-and-controller .follow-button__following:hover { +.name-and-controller .action-button__being:hover { background: rgba(255, 0, 0, 0.1); color: red; border: 1px solid red; cursor: pointer; } -.name-and-controller .follow-button__following.follow-button__just-followed:hover { +.name-and-controller .action-button__being.action-button__just-applied:hover { background: transparent !important; color: #fff !important; border: 1px solid #fff !important; cursor: pointer; } -.name-and-controller .follow-button__processing { +.name-and-controller .action-button__processing { background: rgb(255,255,255, 0.3) !important; color: #fff !important; border: 1px solid #fff !important;