Merge pull request #6 from kawamataryo/feature/blocking

Add a blocking feature
This commit is contained in:
ryo 2023-08-16 12:27:29 +09:00 committed by GitHub
commit 6278a2077c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 5291 additions and 3909 deletions

8619
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,25 +12,26 @@
"package:firefox": "plasmo package --target=firefox-mv3" "package:firefox": "plasmo package --target=firefox-mv3"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.3.3", "@atproto/api": "^0.6.4",
"@plasmohq/messaging": "^0.3.0", "@plasmohq/messaging": "^0.5.0",
"@plasmohq/storage": "^1.6.0", "@plasmohq/storage": "^1.7.2",
"plasmo": "0.67.4", "plasmo": "^0.82.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0" "react-dom": "18.2.0",
"ts-pattern": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@plasmohq/prettier-plugin-sort-imports": "3.6.4", "@plasmohq/prettier-plugin-sort-imports": "4.0.1",
"@types/chrome": "0.0.210", "@types/chrome": "0.0.243",
"@types/node": "18.11.18", "@types/node": "20.5.0",
"@types/react": "18.0.27", "@types/react": "18.2.20",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.2.7",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.15",
"postcss": "^8.4.23", "daisyui": "^3.5.1",
"prettier": "2.8.3", "postcss": "^8.4.28",
"tailwindcss": "^3.3.2", "prettier": "3.0.2",
"daisyui": "^2.51.6", "tailwindcss": "^3.3.3",
"typescript": "4.9.4" "typescript": "5.1.6"
}, },
"manifest": { "manifest": {
"host_permissions": [ "host_permissions": [

View File

@ -1,135 +1,71 @@
import { isOutOfTopViewport } from './lib/domHelpers'; import { BskyClient, BskyLoginParams } from "./lib/bskyClient";
import { BskyClient } from "./lib/bskyClient";
import type { PlasmoCSConfig } from "plasmo" import type { PlasmoCSConfig } from "plasmo"
import { MESSAGE_NAMES } from "~lib/constants"; import { MESSAGE_NAMES, VIEWER_STATE } from "~lib/constants";
import { getAccountNameAndDisplayName, getUserCells, insertBskyProfileEl, insertNotFoundEl, insertReloadEl, removeReloadElIfExists } from "~lib/domHelpers";
import { isSimilarUser } from "~lib/bskyHelpers";
import "./style.content.css" import "./style.content.css"
import { debugLog } from "~lib/utils"; import { initialize, searchBskyUsers } from '~lib/searchAndInsertBskyUsers';
export const config: PlasmoCSConfig = { export const config: PlasmoCSConfig = {
matches: ["https://twitter.com/*", "https://x.com/*"], matches: ["https://twitter.com/*", "https://x.com/*"],
all_frames: true all_frames: true
} }
let abortController = new AbortController(); const searchAndShowBskyUsers = async ({
identifier,
const notFoundUserCache = new Set<string>() password,
messageName,
const followerUrlMap = new Map<string, string>() }: BskyLoginParams & { messageName: string }) => {
const initialize = async () => {
abortController.abort()
abortController = new AbortController()
}
const searchBskyUsers = async ({
userId,
password
}) => {
removeReloadElIfExists()
const agent = await BskyClient.createAgent({ const agent = await BskyClient.createAgent({
identifier: userId, identifier,
password: password, password,
});
const userCells = getUserCells()
debugLog(`userCells length: ${userCells.length}`)
let index = 0
for (const userCell of userCells) {
if(isOutOfTopViewport(userCell)) {
continue
}
const { twAccountName, twDisplayName } = getAccountNameAndDisplayName(userCell)
if (notFoundUserCache.has(twAccountName)) {
insertNotFoundEl(userCell)
continue
}
const [searchResultByAccountName] = await agent.searchUser({
term: twAccountName,
limit: 1,
}) })
switch (messageName) {
// TODO: Refactor, this is duplicated case MESSAGE_NAMES.SEARCH_BSKY_USER_ON_FOLLOW_PAGE:
// first, search by account name await searchBskyUsers({
if (isSimilarUser(twDisplayName, searchResultByAccountName) || isSimilarUser(twAccountName, searchResultByAccountName)) { agent,
insertBskyProfileEl({ btnLabel: {
dom: userCell, add: "Follow",
profile: searchResultByAccountName, remove: "Unfollow",
abortController, progressive: "Following",
followAction: async () => {
const result = await agent.follow(searchResultByAccountName.did);
followerUrlMap.set(searchResultByAccountName.did, result.uri)
},
unfollowAction: async () => {
if(searchResultByAccountName?.viewer?.following) {
await agent.unfollow(searchResultByAccountName?.viewer?.following);
} else {
await agent.unfollow(followerUrlMap.get(searchResultByAccountName.did));
}
}, },
statusKey: VIEWER_STATE.FOLLOWING,
userCellQueryParam: '[data-testid="primaryColumn"] [data-testid="UserCell"]',
addQuery: async (arg: string) => await agent.follow(arg),
removeQuery: async (arg: string) => await agent.unfollow(arg),
}) })
} else { break
// if not found, search by display name case MESSAGE_NAMES.SEARCH_BSKY_USER_ON_BLOCK_PAGE:
const [searchResultByDisplayName] = await agent.searchUser({ // TODO: If already blocked, don't show blocking state. because blocking user can't find.
term: twDisplayName, await searchBskyUsers({
limit: 1, agent,
}) btnLabel: {
if (isSimilarUser(twDisplayName, searchResultByDisplayName) || isSimilarUser(twAccountName, searchResultByDisplayName)) { add: "Block",
insertBskyProfileEl({ remove: "Unblock",
dom: userCell, progressive: "Blocking",
profile: searchResultByDisplayName,
abortController,
followAction: async () => {
const result = await agent.follow(searchResultByDisplayName.did);
followerUrlMap.set(searchResultByDisplayName.did, result.uri)
},
unfollowAction: async () => {
if(searchResultByDisplayName?.viewer?.following) {
await agent.unfollow(searchResultByDisplayName?.viewer?.following);
} else {
await agent.unfollow(followerUrlMap.get(searchResultByDisplayName.did));
}
}, },
statusKey: VIEWER_STATE.BLOCKING,
userCellQueryParam: '[data-testid="UserCell"]',
addQuery: async (arg: string) => await agent.block(arg),
removeQuery: async (arg: string) => await agent.unblock(arg),
}) })
} else {
insertNotFoundEl(userCell)
notFoundUserCache.add(twAccountName)
}
}
index++
if (process.env.NODE_ENV === "development" && index > 5) {
break break
} }
} }
// TODO: if there are more users, insert reload button
insertReloadEl(async () => {
await searchBskyUsers({
userId,
password,
})
})
}
chrome.runtime.onMessage.addListener((message, _, sendResponse) => { chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
if (message.name === MESSAGE_NAMES.SEARCH_BSKY_USER) { if (Object.values(MESSAGE_NAMES).includes(message.name)) {
initialize() initialize()
searchAndShowBskyUsers({
searchBskyUsers({ identifier: message.body.userId,
userId: message.body.userId, password: message.body.password,
password: message.body.password messageName: message.name,
}).then(() => {
sendResponse({ hasError: false })
}).catch((e) => {
sendResponse({ hasError: true, message: e.toString() })
}) })
.then(() => {
sendResponse({ hasError: false })
})
.catch((e) => {
sendResponse({ hasError: true, message: e.toString() })
});
return true return true
} }
return false return false

View File

@ -1,8 +1,19 @@
import { BskyAgent } from "@atproto/api"; import { AtUri, BskyAgent } from "@atproto/api";
export type BskyLoginParams = {
identifier: string;
password: string;
}
export class BskyClient { export class BskyClient {
private service = "https://bsky.social"; private service = "https://bsky.social";
me: {
did: string;
handle: string;
email: string;
};
agent: BskyAgent; agent: BskyAgent;
private constructor() { private constructor() {
this.agent = new BskyAgent({ service: this.service }); this.agent = new BskyAgent({ service: this.service });
} }
@ -10,12 +21,14 @@ export class BskyClient {
public static async createAgent({ public static async createAgent({
identifier, identifier,
password, password,
}: { }: BskyLoginParams): Promise<BskyClient> {
identifier: string;
password: string;
}): Promise<BskyClient> {
const client = new BskyClient(); const client = new BskyClient();
await client.agent.login({ identifier, password }); const {data} = await client.agent.login({ identifier, password });
client.me = {
did: data.did,
handle: data.handle,
email: data.email,
}
return client; return client;
} }
@ -40,4 +53,25 @@ export class BskyClient {
public unfollow = async (followUri: string) => { public unfollow = async (followUri: string) => {
return await this.agent.deleteFollow(followUri); return await this.agent.deleteFollow(followUri);
} }
public block = async (subjectDid: string) => {
return await this.agent.app.bsky.graph.block.create({
repo: this.me.did,
collection: "app.bsky.graph.block",
},
{
subject: subjectDid,
createdAt: new Date().toISOString(),
})
}
public unblock = async (blockUri: string) => {
// TODO: unblock is not working. Need to fix it.
const {rkey} = new AtUri(blockUri)
return await this.agent.app.bsky.graph.block.delete({
repo: this.me.did,
collection: "app.bsky.graph.block",
rkey,
});
}
} }

View File

@ -1,21 +1,25 @@
export const MESSAGE_NAMES = { export const MESSAGE_NAMES = {
SEARCH_BSKY_USER: "search_bsky_user" SEARCH_BSKY_USER_ON_FOLLOW_PAGE: "search_bsky_user_on_follow_page",
SEARCH_BSKY_USER_ON_BLOCK_PAGE: "search_bsky_user_on_block_page",
} }
const STORAGE_PREFIX = "sky_follower_bridge_storage" const STORAGE_PREFIX = "sky_follower_bridge_storage"
export const STORAGE_KEYS = { export const STORAGE_KEYS = {
BSKY_USER_ID: `${STORAGE_PREFIX}_bsky_password`, BSKY_USER_ID: `${STORAGE_PREFIX}_bsky_password`,
BSKY_PASSWORD: `${STORAGE_PREFIX}_bsky_user`, BSKY_PASSWORD: `${STORAGE_PREFIX}_bsky_user`,
} } as const
export const TARGET_URLS_REGEX = [ export const TARGET_URLS_REGEX = {
/^https:\/\/twitter\.com\/[^/]+\/following$/, FOLLOW: /^https:\/\/(twitter|x)\.com\/[^/]+\/follow/,
/^https:\/\/twitter\.com\/[^/]+\/followers$/, BLOCK: /^https:\/\/(twitter|x)\.com\/settings\/blocked/,
/^https:\/\/x\.com\/[^/]+\/following$/, } as const
/^https:\/\/x\.com\/[^/]+\/followers$/,
]
export const MESSAGE_TYPE = { export const MESSAGE_TYPE = {
ERROR: "error", ERROR: "error",
SUCCESS: "success", SUCCESS: "success",
} as const } as const
export const VIEWER_STATE = {
BLOCKING: "blocking",
FOLLOWING: "following",
} as const

View File

@ -1,7 +1,13 @@
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs" import type { ProfileView, ViewerState } from "@atproto/api/dist/client/types/app/bsky/actor/defs"
export const getUserCells = ({ filterInsertedElement }: { filterInsertedElement: boolean } = { filterInsertedElement: true }) => { export type UserCellBtnLabel = {
const userCells = document.querySelectorAll('[data-testid="primaryColumn"] [data-testid="UserCell"]'); add: string,
remove: string,
progressive: string,
}
export const getUserCells = ({ queryParam, filterInsertedElement }: { queryParam: string, filterInsertedElement: boolean }) => {
const userCells = document.querySelectorAll(queryParam);
// filter out already inserted elements // filter out already inserted elements
if (filterInsertedElement) { if (filterInsertedElement) {
@ -46,9 +52,19 @@ export const getAccountNameAndDisplayName = (userCell: Element) => {
const twDisplayName = displayNameEl?.textContent const twDisplayName = displayNameEl?.textContent
return { twAccountName, twDisplayName } return { twAccountName, twDisplayName }
} }
export const insertBskyProfileEl = ({ dom, profile, abortController, followAction, unfollowAction }: { dom: Element, profile: ProfileView, abortController: AbortController, followAction: () => void, unfollowAction: () => void }) => {
// TODO: vanjsを使ってdom操作を描き直したい
export const insertBskyProfileEl = ({ dom, profile, statusKey, btnLabel, abortController, followAction, unfollowAction }: {
dom: Element,
profile: ProfileView,
statusKey: keyof ViewerState,
btnLabel: UserCellBtnLabel,
abortController: AbortController,
followAction: () => void,
unfollowAction: () => void
}) => {
const avatarEl = profile.avatar ? `<img src="${profile.avatar}" width="48" />` : "<div class='no-avatar'></div>" const avatarEl = profile.avatar ? `<img src="${profile.avatar}" width="48" />` : "<div class='no-avatar'></div>"
const followButtonEl = profile.viewer?.following ? "<button class='follow-button follow-button__following'>Following on Bluesky</button>" : "<button class='follow-button'>Follow on Bluesky</button>" 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', ` dom.insertAdjacentHTML('afterend', `
<div class="bsky-user-content"> <div class="bsky-user-content">
<div class="icon-section"> <div class="icon-section">
@ -63,7 +79,7 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
<p class="handle">@${profile.handle}</p> <p class="handle">@${profile.handle}</p>
</div> </div>
<div> <div>
${followButtonEl} ${actionBtnEl}
</div> </div>
</div> </div>
${profile.description ? `<p class="description">${profile.description}</p>` : ""} ${profile.description ? `<p class="description">${profile.description}</p>` : ""}
@ -82,9 +98,10 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
target.textContent = "processing..." target.textContent = "processing..."
target.classList.add('follow-button__processing') target.classList.add('follow-button__processing')
await followAction() await followAction()
target.textContent = "Following on Bluesky" target.textContent = `${btnLabel.progressive} on Bluesky`
target.classList.remove('follow-button__processing') target.classList.remove('follow-button__processing')
target.classList.add('follow-button__following') target.classList.add('follow-button__following')
target.classList.add('follow-button__just-followed')
return return
} }
@ -93,22 +110,20 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
target.textContent = "processing..." target.textContent = "processing..."
target.classList.add('follow-button__processing') target.classList.add('follow-button__processing')
await unfollowAction() await unfollowAction()
target.textContent = "Follow on Bluesky" target.textContent = `${btnLabel.add} on Bluesky`
target.classList.remove('follow-button__processing') target.classList.remove('follow-button__processing')
target.classList.remove('follow-button__following') target.classList.remove('follow-button__following')
target.classList.add('follow-button__just-followed')
return return
} }
}, { }, {
signal: abortController.signal signal: abortController.signal
}) })
// register a hover action
bskyUserContentDom?.addEventListener('mouseover', async (e) => { bskyUserContentDom?.addEventListener('mouseover', async (e) => {
const target = e.target as Element const target = e.target as Element
const classList = target.classList const classList = target.classList
if (classList.contains('follow-button') && classList.contains('follow-button__following')) { if (classList.contains('follow-button') && classList.contains('follow-button__following')) {
target.textContent = "Unfollow on Bluesky" target.textContent = `${btnLabel.remove} on Bluesky`
} }
}, { }, {
signal: abortController.signal signal: abortController.signal
@ -120,7 +135,7 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
target.classList.remove('follow-button__just-followed') target.classList.remove('follow-button__just-followed')
} }
if (classList.contains('follow-button') && classList.contains('follow-button__following')) { if (classList.contains('follow-button') && classList.contains('follow-button__following')) {
target.textContent = "Following on Bluesky" target.textContent = `${btnLabel.progressive} on Bluesky`
} }
}, { }, {
signal: abortController.signal signal: abortController.signal

View File

@ -0,0 +1,129 @@
import { UserCellBtnLabel, isOutOfTopViewport } from './domHelpers';
import { getAccountNameAndDisplayName, getUserCells, insertBskyProfileEl, insertNotFoundEl, insertReloadEl, removeReloadElIfExists } 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';
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,
btnLabel,
userCellQueryParam,
statusKey,
addQuery,
removeQuery,
}: {
agent: BskyClient,
userCellQueryParam: string,
btnLabel: UserCellBtnLabel,
statusKey: keyof ViewerState,
addQuery: (arg: string) => Promise<any>,
removeQuery: (arg: string) => Promise<any>,
}) => {
removeReloadElIfExists()
const userCells = getUserCells({
queryParam: userCellQueryParam,
filterInsertedElement: true,
})
debugLog(`userCells length: ${userCells.length}`)
let index = 0
for (const userCell of userCells) {
if (isOutOfTopViewport(userCell)) {
continue
}
const { twAccountName, twDisplayName } = getAccountNameAndDisplayName(userCell)
if (notFoundUserCache.has(twAccountName)) {
insertNotFoundEl(userCell)
continue
}
const [searchResultByAccountName] = await agent.searchUser({
term: twAccountName,
limit: 1,
})
// TODO: Refactor, this is duplicated
// first, search by account name
if (isSimilarUser(twDisplayName, searchResultByAccountName) || isSimilarUser(twAccountName, searchResultByAccountName)) {
insertBskyProfileEl({
dom: userCell,
profile: searchResultByAccountName,
statusKey,
btnLabel,
abortController,
followAction: async () => {
const result = await addQuery(searchResultByAccountName.did);
followerUrlMap.set(searchResultByAccountName.did, result.uri)
},
unfollowAction: async () => {
if (searchResultByAccountName?.viewer?.following) {
await removeQuery(searchResultByAccountName?.viewer?.following);
} else {
await removeQuery(followerUrlMap.get(searchResultByAccountName.did));
}
},
})
} else {
// if not found, search by display name
const [searchResultByDisplayName] = await agent.searchUser({
term: twDisplayName,
limit: 1,
})
if (isSimilarUser(twDisplayName, searchResultByDisplayName) || isSimilarUser(twAccountName, searchResultByDisplayName)) {
insertBskyProfileEl({
dom: userCell,
profile: searchResultByDisplayName,
abortController,
statusKey,
btnLabel,
followAction: async () => {
const result = await addQuery(searchResultByDisplayName.did);
followerUrlMap.set(searchResultByDisplayName.did, result.uri)
},
unfollowAction: async () => {
if (searchResultByDisplayName?.viewer?.following) {
await removeQuery(searchResultByDisplayName?.viewer?.following);
} else {
await removeQuery(followerUrlMap.get(searchResultByDisplayName.did));
}
},
})
} else {
insertNotFoundEl(userCell)
notFoundUserCache.add(twAccountName)
}
}
index++
if (process.env.NODE_ENV === "development" && index > 5) {
break
}
}
// TODO: if there are more users, insert reload button
insertReloadEl(async () => {
await searchBskyUsers({
agent,
btnLabel,
userCellQueryParam,
statusKey,
addQuery,
removeQuery,
})
})
}

View File

@ -1,13 +1,18 @@
import { FormEvent, useState } from "react" import { FormEvent, useState } from "react"
import iconSrc from "data-base64:~assets/icon.popup.svg" import { P, match } from "ts-pattern"
import "./style.css" import "./style.css"
import { sendToContentScript } from "@plasmohq/messaging" import { sendToContentScript } from "@plasmohq/messaging"
import { MESSAGE_NAMES, MESSAGE_TYPE, STORAGE_KEYS, TARGET_URLS_REGEX } from "~lib/constants"
import { useStorage } from "@plasmohq/storage/hook" import { useStorage } from "@plasmohq/storage/hook"
import {
MESSAGE_NAMES,
MESSAGE_TYPE,
STORAGE_KEYS,
TARGET_URLS_REGEX
} from "~lib/constants"
import { debugLog } from "./lib/utils" import { debugLog } from "./lib/utils"
function IndexPopup() { function IndexPopup() {
@ -15,45 +20,67 @@ function IndexPopup() {
const [password, setPassword] = useStorage(STORAGE_KEYS.BSKY_PASSWORD, "") const [password, setPassword] = useStorage(STORAGE_KEYS.BSKY_PASSWORD, "")
const [userId, setUserId] = useStorage(STORAGE_KEYS.BSKY_USER_ID, "") const [userId, setUserId] = useStorage(STORAGE_KEYS.BSKY_USER_ID, "")
const [message, setMessage] = useState<null | { const [message, setMessage] = useState<null | {
type: typeof MESSAGE_TYPE[keyof typeof MESSAGE_TYPE] type: (typeof MESSAGE_TYPE)[keyof typeof MESSAGE_TYPE]
message: string message: string
}>(null) }>(null)
const isDisabled = !password || !userId || isLoading const isDisabled = !password || !userId || isLoading
const isShowErrorMessage = message?.type === MESSAGE_TYPE.ERROR const isShowErrorMessage = message?.type === MESSAGE_TYPE.ERROR
const isShowSuccessMessage = message?.type === MESSAGE_TYPE.SUCCESS const isShowSuccessMessage = message?.type === MESSAGE_TYPE.SUCCESS
const setErrorMessage = (message: string) => { setMessage({ type: MESSAGE_TYPE.ERROR, message }) } const setErrorMessage = (message: string) => {
setMessage({ type: MESSAGE_TYPE.ERROR, message })
const isExecutablePage = async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
return TARGET_URLS_REGEX.some((r) => r.test(tab.url))
} }
const searchBskyUser = async (e: FormEvent) => { const searchBskyUser = async (e: FormEvent) => {
e.preventDefault() e.preventDefault()
if(!await isExecutablePage()) { const [{ url: currentUrl }] = await chrome.tabs.query({
setErrorMessage("Error: Invalid page. please open the Twitter followers or following page.") active: true,
return; currentWindow: true
})
if (!Object.values(TARGET_URLS_REGEX).some((r) => r.test(currentUrl))) {
setErrorMessage(
"Error: Invalid page. please open the Twitter following or blocking page."
)
return
} }
const messageName = match(currentUrl)
.with(
P.when((url) => TARGET_URLS_REGEX.FOLLOW.test(url)),
() => MESSAGE_NAMES.SEARCH_BSKY_USER_ON_FOLLOW_PAGE
)
.with(
P.when((url) => TARGET_URLS_REGEX.BLOCK.test(url)),
() => MESSAGE_NAMES.SEARCH_BSKY_USER_ON_BLOCK_PAGE
)
.run()
setMessage(null) setMessage(null)
setIsLoading(true) setIsLoading(true)
try { try {
const res: { hasError: boolean, message: string } = await sendToContentScript({ const res: { hasError: boolean; message: string } =
name: MESSAGE_NAMES.SEARCH_BSKY_USER, await sendToContentScript({
name: messageName,
body: { body: {
password, password,
userId, userId
} }
}) })
if (res.hasError) { if (res.hasError) {
setErrorMessage(res.message) setErrorMessage(res.message)
} else { } else {
setMessage({ type: MESSAGE_TYPE.SUCCESS, message: "Completed. Try again if no results found.”" }) setMessage({
type: MESSAGE_TYPE.SUCCESS,
message: "Completed. Try again if no results found.”"
})
} }
} catch (e) { } catch (e) {
setErrorMessage('Error: Something went wrong. Please reload the web page and try again.') setErrorMessage(
"Error: Something went wrong. Please reload the web page and try again."
)
console.error(e) console.error(e)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@ -62,7 +89,27 @@ function IndexPopup() {
return ( return (
<div className="px-5 pt-3 pb-4 w-[380px]"> <div className="px-5 pt-3 pb-4 w-[380px]">
<h1 className="text-primary text-2xl font-thin flex gap-2 items-center"><svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4"><path stroke-linecap="round" d="M36 8H13c-3 0-9 2-9 8s6 8 9 8h22c3 0 9 2 9 8s-6 8-9 8H12"/><path d="M40 12a4 4 0 1 0 0-8a4 4 0 0 0 0 8ZM8 44a4 4 0 1 0 0-8a4 4 0 0 0 0 8Z"/></g></svg>Sky Follower Bridge</h1> <h1 className="text-primary text-2xl font-thin flex gap-2 items-center">
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 48 48">
<g
fill="none"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="4">
<path
strokeLinecap="round"
d="M36 8H13c-3 0-9 2-9 8s6 8 9 8h22c3 0 9 2 9 8s-6 8-9 8H12"
/>
<path d="M40 12a4 4 0 1 0 0-8a4 4 0 0 0 0 8ZM8 44a4 4 0 1 0 0-8a4 4 0 0 0 0 8Z" />
</g>
</svg>
Sky Follower Bridge
</h1>
<form onSubmit={searchBskyUser} className="mt-2"> <form onSubmit={searchBskyUser} className="mt-2">
<label className="input-group input-group-lg"> <label className="input-group input-group-lg">
<span> <span>
@ -114,25 +161,44 @@ function IndexPopup() {
</label> </label>
<button <button
type="submit" type="submit"
className={`disabled:text-gray-600 mt-3 normal-case btn btn-primary btn-sm w-full ${isLoading ? "loading" : ""}`} className={`disabled:text-gray-600 mt-3 normal-case btn btn-primary btn-sm w-full`}
disabled={isDisabled}> disabled={isDisabled}>
Find Bluesky Users { isLoading && <span className="w-4 loading loading-spinner"></span> }
{ isLoading ? "Finding Bluesky Users" : "Find Bluesky Users" }
</button> </button>
{isShowErrorMessage && ( {isShowErrorMessage && (
<div className="alert text-red-600 border border-red-600 p-2 rounded-md mt-2"> <div className="flex gap-2 items-center text-red-600 border border-red-600 p-2 rounded-md mt-2">
<div> <svg
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> xmlns="http://www.w3.org/2000/svg"
className="stroke-current flex-shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
stroke-linejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{message.message}</span> <span>{message.message}</span>
</div> </div>
</div>
)} )}
{isShowSuccessMessage && ( {isShowSuccessMessage && (
<div className="alert text-green-600 border border-green-600 p-1 rounded-md mt-2"> <div className="flex gap-2 items-center text-green-600 border border-green-600 p-1 rounded-md mt-2">
<div> <svg
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> xmlns="http://www.w3.org/2000/svg"
className="stroke-current flex-shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>Success. Try again if no results found.</span> <span>Success. Try again if no results found.</span>
</div> </div>
</div>
)} )}
</form> </form>
</div> </div>

View File

@ -10,7 +10,7 @@ module.exports = {
themes: [ themes: [
{ {
winter: { winter: {
...require("daisyui/src/colors/themes")["[data-theme=winter]"], ...require("daisyui/src/theming/themes")["[data-theme=winter]"],
primary: "#1D4ED8" primary: "#1D4ED8"
} }
}, },