refactor: import list

This commit is contained in:
kawamataryo 2024-11-28 21:42:43 +09:00
parent 93b4d2f8ae
commit d354030056
12 changed files with 200 additions and 56 deletions

View File

@ -0,0 +1,21 @@
import type { PlasmoMessaging } from "@plasmohq/messaging";
import { BskyClient } from "~lib/bskyClient";
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
const { session } = req.body;
const client = await BskyClient.createAgentFromSession(session);
try {
res.send({
result: await client.getMyProfile(),
});
} catch (e) {
res.send({
error: {
message: e.message,
},
});
}
};
export default handler;

View File

@ -71,12 +71,8 @@ const App = () => {
sendToBackground({ name: "openOptionPage" }); sendToBackground({ name: "openOptionPage" });
}; };
const stopAndShowDetectedUsers = async () => { const stopAndShowDetectedUsers = () => {
stopRetrieveLoop(); stopRetrieveLoop();
await chrome.storage.local.set({
users: JSON.stringify(users),
listName: listName,
});
openOptionPage(); openOptionPage();
}; };

View File

@ -169,4 +169,12 @@ export class BskyClient {
await this.addUserToList({ userDid, listUri }); await this.addUserToList({ userDid, listUri });
} }
}; };
public getMyProfile = async () => {
return {
pdsUrl: this.agent.pdsUrl,
did: this.agent.session.did,
handle: this.agent.session.handle,
};
};
} }

View File

@ -164,5 +164,19 @@ export class BskyServiceWorkerClient {
for (const userDid of userDids) { for (const userDid of userDids) {
await this.addUserToList({ userDid, listUri }); await this.addUserToList({ userDid, listUri });
} }
const listId = listUri.split("/").pop();
return listId;
};
public getMyProfile = async () => {
const { result, error } = await sendToBackground({
name: "getMyProfile",
body: {
session: this.session,
},
});
if (error) throw new Error(error.message);
return result;
}; };
} }

View File

@ -3,9 +3,10 @@ import React from "react";
type Props = { type Props = {
onClick: () => Promise<void>; onClick: () => Promise<void>;
label: string; label: string;
className?: string;
}; };
const AsyncButton = ({ onClick, label }: Props) => { const AsyncButton = ({ onClick, label, className }: Props) => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const handleClick = async () => { const handleClick = async () => {
@ -17,7 +18,7 @@ const AsyncButton = ({ onClick, label }: Props) => {
return ( return (
<button <button
type="button" type="button"
className="btn btn-primary btn-wide btn-sm mb-2" className={`btn btn-primary btn-wide btn-sm mb-2 ${className}`}
onClick={handleClick} onClick={handleClick}
disabled={loading} disabled={loading}
> >

View File

@ -31,8 +31,14 @@ export const Default: Story = {
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: 10, [BSKY_USER_MATCH_TYPE.DESCRIPTION]: 10,
[BSKY_USER_MATCH_TYPE.FOLLOWING]: 10, [BSKY_USER_MATCH_TYPE.FOLLOWING]: 10,
}, },
actionAll: async () => { importList: async () => {
console.log("actionAll"); console.log("importList");
},
followAll: async () => {
console.log("followAll");
},
blockAll: async () => {
console.log("blockAll");
}, },
actionMode: ACTION_MODE.FOLLOW, actionMode: ACTION_MODE.FOLLOW,
}, },
@ -56,8 +62,14 @@ export const NoDetections: Story = {
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: 0, [BSKY_USER_MATCH_TYPE.DESCRIPTION]: 0,
[BSKY_USER_MATCH_TYPE.FOLLOWING]: 0, [BSKY_USER_MATCH_TYPE.FOLLOWING]: 0,
}, },
actionAll: async () => { importList: async () => {
console.log("actionAll"); console.log("importList");
},
followAll: async () => {
console.log("followAll");
},
blockAll: async () => {
console.log("blockAll");
}, },
actionMode: ACTION_MODE.FOLLOW, actionMode: ACTION_MODE.FOLLOW,
}, },

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { match } from "ts-pattern";
import type { MatchType, MatchTypeFilterValue } from "../../types"; import type { MatchType, MatchTypeFilterValue } from "../../types";
import { import {
ACTION_MODE, ACTION_MODE,
@ -12,36 +13,30 @@ type Props = {
detectedCount: number; detectedCount: number;
filterValue: MatchTypeFilterValue; filterValue: MatchTypeFilterValue;
onChangeFilter: (key: MatchType) => void; onChangeFilter: (key: MatchType) => void;
actionAll: () => Promise<void>;
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE]; actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
matchTypeStats: Record<Exclude<MatchType, "none">, number>; matchTypeStats: Record<Exclude<MatchType, "none">, number>;
importList: () => Promise<void>;
followAll: () => Promise<void>;
blockAll: () => Promise<void>;
}; };
const Sidebar = ({ const Sidebar = ({
detectedCount, detectedCount,
filterValue, filterValue,
onChangeFilter, onChangeFilter,
actionAll,
actionMode, actionMode,
matchTypeStats, matchTypeStats,
importList,
followAll,
blockAll,
}: Props) => { }: Props) => {
const getActionLabel = () => {
switch (actionMode) {
case ACTION_MODE.FOLLOW:
return "Follow All";
case ACTION_MODE.BLOCK:
return "Block All";
case ACTION_MODE.IMPORT_LIST:
return "Import List";
default:
return "";
}
};
return ( return (
<aside className="bg-base-300 w-80 min-h-screen p-4 border-r border-base-300 flex flex-col"> <aside className="bg-base-300 w-80 min-h-screen p-4 border-r border-base-300 flex flex-col">
<div className="flex-grow"> <div className="flex-grow">
<div className="flex items-center gap-2"> <a
href="https://sky-follower-bridge.de"
className="flex items-center gap-2"
>
<svg <svg
className="w-5 h-5" className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -63,7 +58,7 @@ const Sidebar = ({
</g> </g>
</svg> </svg>
<span className="text-2xl font-bold">Sky Follower Bridge</span> <span className="text-2xl font-bold">Sky Follower Bridge</span>
</div> </a>
<div className="divider" /> <div className="divider" />
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<svg <svg
@ -157,7 +152,17 @@ const Sidebar = ({
</svg> </svg>
<p className="text-xl font-bold">Action</p> <p className="text-xl font-bold">Action</p>
</div> </div>
<AsyncButton onClick={actionAll} label={getActionLabel()} /> {match(actionMode)
.with(ACTION_MODE.FOLLOW, () => (
<AsyncButton onClick={followAll} label="Follow All" />
))
.with(ACTION_MODE.BLOCK, () => (
<AsyncButton onClick={blockAll} label="Block All" />
))
.with(ACTION_MODE.IMPORT_LIST, () => (
<AsyncButton onClick={importList} label="Import List" />
))
.otherwise(() => null)}
<p className="text-xs"> <p className="text-xs">
User detection is not perfect and may include false positives. User detection is not perfect and may include false positives.
</p> </p>

View File

@ -38,6 +38,7 @@ export const STORAGE_KEYS = {
BSKY_CLIENT_SESSION: `${STORAGE_PREFIX}_bsky_client_session`, BSKY_CLIENT_SESSION: `${STORAGE_PREFIX}_bsky_client_session`,
BSKY_MESSAGE_NAME: `${STORAGE_PREFIX}_bsky_message_name`, BSKY_MESSAGE_NAME: `${STORAGE_PREFIX}_bsky_message_name`,
DETECTED_BSKY_USERS: `${STORAGE_PREFIX}_detected_bsky_users`, DETECTED_BSKY_USERS: `${STORAGE_PREFIX}_detected_bsky_users`,
LIST_NAME: `${STORAGE_PREFIX}_list_name`,
} as const; } as const;
export const TARGET_URLS_REGEX = { export const TARGET_URLS_REGEX = {

View File

@ -125,20 +125,23 @@ export const useBskyUserManager = () => {
}); });
}, [users, matchTypeFilter, actionMode]); }, [users, matchTypeFilter, actionMode]);
const actionAll = React.useCallback(async () => { // Import list
const importList = React.useCallback(async () => {
if (!bskyClient.current) return;
const listUri = await bskyClient.current.createListAndAddUsers({
name: listName,
description: "List imported via Sky Follower Bridge",
userDids: filteredUsers.map((user) => user.did),
});
const myProfile = await bskyClient.current.getMyProfile();
return `https://bsky.app/profile/${myProfile.handle}/lists/${listUri}`;
}, [filteredUsers, listName]);
// Follow All
const followAll = React.useCallback(async () => {
if (!bskyClient.current) return; if (!bskyClient.current) return;
let actionCount = 0; let actionCount = 0;
if (actionMode === ACTION_MODE.IMPORT_LIST) {
const userDids = filteredUsers.map((user) => user.did);
await bskyClient.current.createListAndAddUsers({
name: listName,
description: "List imported via Sky Follower Bridge",
userDids,
});
return;
}
for (const user of filteredUsers) { for (const user of filteredUsers) {
let resultUri: string | null = null; let resultUri: string | null = null;
// follow // follow
@ -163,8 +166,17 @@ export const useBskyUserManager = () => {
await wait(300); await wait(300);
actionCount++; actionCount++;
} }
}
return actionCount;
}, [filteredUsers, actionMode, setUsers]);
// block // Block All
const blockAll = React.useCallback(async () => {
if (!bskyClient.current) return;
// block
let actionCount = 0;
for (const user of filteredUsers) {
let resultUri: string | null = null;
if (actionMode === ACTION_MODE.BLOCK) { if (actionMode === ACTION_MODE.BLOCK) {
if (user.isBlocking) { if (user.isBlocking) {
continue; continue;
@ -188,7 +200,7 @@ export const useBskyUserManager = () => {
} }
} }
return actionCount; return actionCount;
}, [filteredUsers, actionMode, setUsers, listName]); }, [filteredUsers, actionMode, setUsers]);
React.useEffect(() => { React.useEffect(() => {
chrome.storage.local.get( chrome.storage.local.get(
@ -227,7 +239,9 @@ export const useBskyUserManager = () => {
matchTypeFilter, matchTypeFilter,
changeMatchTypeFilter, changeMatchTypeFilter,
filteredUsers, filteredUsers,
actionAll,
matchTypeStats, matchTypeStats,
importList,
followAll,
blockAll,
}; };
}; };

View File

@ -46,6 +46,15 @@ export const useRetrieveBskyUsers = () => {
}, },
(v) => (v === undefined ? [] : v), (v) => (v === undefined ? [] : v),
); );
const [listName, setListName] = useStorage<string>(
{
key: STORAGE_KEYS.LIST_NAME,
instance: new Storage({
area: "local",
}),
},
(v) => (v === undefined ? "" : v),
);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [errorMessage, setErrorMessage] = React.useState(""); const [errorMessage, setErrorMessage] = React.useState("");
const [isBottomReached, setIsBottomReached] = React.useState(false); const [isBottomReached, setIsBottomReached] = React.useState(false);
@ -54,7 +63,6 @@ export const useRetrieveBskyUsers = () => {
session: AtpSessionData; session: AtpSessionData;
messageName: (typeof MESSAGE_NAMES)[keyof typeof MESSAGE_NAMES]; messageName: (typeof MESSAGE_NAMES)[keyof typeof MESSAGE_NAMES];
}>(null); }>(null);
const [listName, setListName] = React.useState<string>("");
const retrieveBskyUsers = React.useCallback( const retrieveBskyUsers = React.useCallback(
async (usersData: CrawledUserInfo[]) => { async (usersData: CrawledUserInfo[]) => {
@ -161,8 +169,7 @@ export const useRetrieveBskyUsers = () => {
bskyClient.current = new BskyServiceWorkerClient(session); bskyClient.current = new BskyServiceWorkerClient(session);
const listName = scrapeListNameFromPage(); setListName(scrapeListNameFromPage());
setListName(listName);
startRetrieveLoop(messageName).catch((e) => { startRetrieveLoop(messageName).catch((e) => {
console.error(e); console.error(e);

View File

@ -1,5 +1,5 @@
import { MESSAGE_NAMES } from "~lib/constants"; import { MESSAGE_NAMES } from "~lib/constants";
import { BSKY_DOMAIN, MESSAGE_NAME_TO_QUERY_PARAM_MAP } from "~lib/constants"; import { BSKY_DOMAIN } from "~lib/constants";
import { wait } from "~lib/utils"; import { wait } from "~lib/utils";
import type { CrawledUserInfo } from "~types"; import type { CrawledUserInfo } from "~types";
import { AbstractService } from "./abstractService"; import { AbstractService } from "./abstractService";

View File

@ -10,16 +10,20 @@ const Option = () => {
const { const {
users, users,
filteredUsers, filteredUsers,
listName,
matchTypeFilter, matchTypeFilter,
changeMatchTypeFilter, changeMatchTypeFilter,
handleClickAction, handleClickAction,
actionMode, actionMode,
actionAll,
matchTypeStats, matchTypeStats,
importList,
followAll,
blockAll,
} = useBskyUserManager(); } = useBskyUserManager();
const { confirm, ConfirmationDialog } = useConfirm({ const {
confirm: followAllConfirm,
ConfirmationDialog: FollowAllConfirmationDialog,
} = useConfirm({
title: "Proceed with Execution?", title: "Proceed with Execution?",
message: message:
"User detection is not perfect and may include false positives. Do you still want to proceed?", "User detection is not perfect and may include false positives. Do you still want to proceed?",
@ -27,13 +31,71 @@ const Option = () => {
okText: "OK", okText: "OK",
}); });
const handleActionAll = async () => { const {
if (!(await confirm())) { confirm: importListConfirm,
ConfirmationDialog: ImportListConfirmationDialog,
} = useConfirm({
title: "Proceed with Execution?",
message:
"Importing a list will create a new list and add all detected users to it. This feature is experimental and may not work as expected. Do you still want to proceed?",
cancelText: "Cancel",
okText: "OK",
});
const handleFollowAll = async () => {
if (!(await followAllConfirm())) {
return; return;
} }
toast.promise(followAll, {
pending: "Processing...",
success: {
render({ data }) {
return <span className="font-bold">Followed {data} users🎉</span>;
},
},
});
};
const result = await actionAll(); const handleBlockAll = async () => {
toast.success(`Followed ${result} users`); if (!(await followAllConfirm())) {
return;
}
toast.promise(blockAll, {
pending: "Processing...",
success: {
render({ data }) {
return <span className="font-bold">Blocked {data} users🎉</span>;
},
},
});
};
const handleImportList = async () => {
if (!(await importListConfirm())) {
return;
}
toast.promise(importList, {
pending: "Processing...",
success: {
render({ data }) {
return (
<>
<span className="font-bold">List imported successfully🎉</span>
<br />
<a href={data} target="_blank" rel="noreferrer" className="link">
View Imported List
</a>
</>
);
},
},
error: {
render({ data }) {
console.log(data);
return `Failed to import list: ${data}`;
},
},
});
}; };
return ( return (
@ -44,9 +106,11 @@ const Option = () => {
detectedCount={users.length} detectedCount={users.length}
filterValue={matchTypeFilter} filterValue={matchTypeFilter}
onChangeFilter={changeMatchTypeFilter} onChangeFilter={changeMatchTypeFilter}
actionAll={handleActionAll}
actionMode={actionMode} actionMode={actionMode}
matchTypeStats={matchTypeStats} matchTypeStats={matchTypeStats}
importList={handleImportList}
followAll={handleFollowAll}
blockAll={handleBlockAll}
/> />
</div> </div>
<div className="flex-1 ml-80 p-6 pt-0 overflow-y-auto"> <div className="flex-1 ml-80 p-6 pt-0 overflow-y-auto">
@ -72,7 +136,8 @@ const Option = () => {
autoClose={5000} autoClose={5000}
className="text-sm" className="text-sm"
/> />
<ConfirmationDialog /> <FollowAllConfirmationDialog />
<ImportListConfirmationDialog />
</div> </div>
</> </>
); );