use vanjs

This commit is contained in:
kawamataryo 2023-08-16 22:15:48 +09:00
parent 782b75c8fc
commit d6bc1af255
9 changed files with 223 additions and 154 deletions

17
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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<void>,
removeAction: () => Promise<void>
}) => {
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<void>,
removeAction: () => Promise<void>
}) => {
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) : "",
),
)
}

View File

@ -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."
)
)

View File

@ -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"
))
}

View File

@ -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', `
<div class="bsky-reload-btn-wrapper">
<button class="bsky-reload-btn">
Find More
</button>
</div>
`)
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<void>,
removeAction: () => Promise<void>
}) => {
const avatarEl = profile.avatar ? `<img src="${profile.avatar}" width="48" />` : "<div class='no-avatar'></div>"
const actionBtnEl = profile.viewer[statusKey] ? `<button class='follow-button follow-button__following'>${btnLabel.progressive} on Bluesky</button>` : `<button class='follow-button'>${btnLabel.add} on Bluesky</button>`
dom.insertAdjacentHTML('afterend', `
<div class="bsky-user-content">
<div class="icon-section">
<a href="https://bsky.app/profile/${profile.handle}" target="_blank" rel="noopener">
${avatarEl}
</a>
</div>
<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}</a></p>
<p class="handle">@${profile.handle}</p>
</div>
<div>
${actionBtnEl}
</div>
</div>
${profile.description ? `<p class="description">${profile.description}</p>` : ""}
</div>
</div>
`)
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', `
<div class="bsky-user-content bsky-user-content__not-found">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><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" /></svg>
<p class="not-found">No similar users found.</p>
</div>
`)
}
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) => {

View File

@ -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<string>()
const followerUrlMap = new Map<string, string>()
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<any>,
removeQuery: (arg: string) => Promise<any>,
}) => {
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 {

View File

@ -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;