mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-04-23 20:12:22 -06:00
Merge pull request #6 from kawamataryo/feature/blocking
Add a blocking feature
This commit is contained in:
commit
6278a2077c
8619
package-lock.json
generated
8619
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@ -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": [
|
||||||
|
156
src/content.ts
156
src/content.ts
@ -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
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
129
src/lib/searchAndInsertBskyUsers.ts
Normal file
129
src/lib/searchAndInsertBskyUsers.ts
Normal 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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
124
src/popup.tsx
124
src/popup.tsx
@ -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>
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user