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" });
};
const stopAndShowDetectedUsers = async () => {
const stopAndShowDetectedUsers = () => {
stopRetrieveLoop();
await chrome.storage.local.set({
users: JSON.stringify(users),
listName: listName,
});
openOptionPage();
};

View File

@ -169,4 +169,12 @@ export class BskyClient {
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) {
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 = {
onClick: () => Promise<void>;
label: string;
className?: string;
};
const AsyncButton = ({ onClick, label }: Props) => {
const AsyncButton = ({ onClick, label, className }: Props) => {
const [loading, setLoading] = React.useState(false);
const handleClick = async () => {
@ -17,7 +18,7 @@ const AsyncButton = ({ onClick, label }: Props) => {
return (
<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}
disabled={loading}
>

View File

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

View File

@ -1,4 +1,5 @@
import React from "react";
import { match } from "ts-pattern";
import type { MatchType, MatchTypeFilterValue } from "../../types";
import {
ACTION_MODE,
@ -12,36 +13,30 @@ type Props = {
detectedCount: number;
filterValue: MatchTypeFilterValue;
onChangeFilter: (key: MatchType) => void;
actionAll: () => Promise<void>;
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
matchTypeStats: Record<Exclude<MatchType, "none">, number>;
importList: () => Promise<void>;
followAll: () => Promise<void>;
blockAll: () => Promise<void>;
};
const Sidebar = ({
detectedCount,
filterValue,
onChangeFilter,
actionAll,
actionMode,
matchTypeStats,
importList,
followAll,
blockAll,
}: 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 (
<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 items-center gap-2">
<a
href="https://sky-follower-bridge.de"
className="flex items-center gap-2"
>
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
@ -63,7 +58,7 @@ const Sidebar = ({
</g>
</svg>
<span className="text-2xl font-bold">Sky Follower Bridge</span>
</div>
</a>
<div className="divider" />
<div className="flex items-center gap-2 mb-2">
<svg
@ -157,7 +152,17 @@ const Sidebar = ({
</svg>
<p className="text-xl font-bold">Action</p>
</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">
User detection is not perfect and may include false positives.
</p>

View File

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

View File

@ -125,20 +125,23 @@ export const useBskyUserManager = () => {
});
}, [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;
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) {
let resultUri: string | null = null;
// follow
@ -163,8 +166,17 @@ export const useBskyUserManager = () => {
await wait(300);
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 (user.isBlocking) {
continue;
@ -188,7 +200,7 @@ export const useBskyUserManager = () => {
}
}
return actionCount;
}, [filteredUsers, actionMode, setUsers, listName]);
}, [filteredUsers, actionMode, setUsers]);
React.useEffect(() => {
chrome.storage.local.get(
@ -227,7 +239,9 @@ export const useBskyUserManager = () => {
matchTypeFilter,
changeMatchTypeFilter,
filteredUsers,
actionAll,
matchTypeStats,
importList,
followAll,
blockAll,
};
};

View File

@ -46,6 +46,15 @@ export const useRetrieveBskyUsers = () => {
},
(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 [errorMessage, setErrorMessage] = React.useState("");
const [isBottomReached, setIsBottomReached] = React.useState(false);
@ -54,7 +63,6 @@ export const useRetrieveBskyUsers = () => {
session: AtpSessionData;
messageName: (typeof MESSAGE_NAMES)[keyof typeof MESSAGE_NAMES];
}>(null);
const [listName, setListName] = React.useState<string>("");
const retrieveBskyUsers = React.useCallback(
async (usersData: CrawledUserInfo[]) => {
@ -161,8 +169,7 @@ export const useRetrieveBskyUsers = () => {
bskyClient.current = new BskyServiceWorkerClient(session);
const listName = scrapeListNameFromPage();
setListName(listName);
setListName(scrapeListNameFromPage());
startRetrieveLoop(messageName).catch((e) => {
console.error(e);

View File

@ -1,5 +1,5 @@
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 type { CrawledUserInfo } from "~types";
import { AbstractService } from "./abstractService";

View File

@ -10,16 +10,20 @@ const Option = () => {
const {
users,
filteredUsers,
listName,
matchTypeFilter,
changeMatchTypeFilter,
handleClickAction,
actionMode,
actionAll,
matchTypeStats,
importList,
followAll,
blockAll,
} = useBskyUserManager();
const { confirm, ConfirmationDialog } = useConfirm({
const {
confirm: followAllConfirm,
ConfirmationDialog: FollowAllConfirmationDialog,
} = useConfirm({
title: "Proceed with Execution?",
message:
"User detection is not perfect and may include false positives. Do you still want to proceed?",
@ -27,13 +31,71 @@ const Option = () => {
okText: "OK",
});
const handleActionAll = async () => {
if (!(await confirm())) {
const {
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;
}
toast.promise(followAll, {
pending: "Processing...",
success: {
render({ data }) {
return <span className="font-bold">Followed {data} users🎉</span>;
},
},
});
};
const result = await actionAll();
toast.success(`Followed ${result} users`);
const handleBlockAll = async () => {
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 (
@ -44,9 +106,11 @@ const Option = () => {
detectedCount={users.length}
filterValue={matchTypeFilter}
onChangeFilter={changeMatchTypeFilter}
actionAll={handleActionAll}
actionMode={actionMode}
matchTypeStats={matchTypeStats}
importList={handleImportList}
followAll={handleFollowAll}
blockAll={handleBlockAll}
/>
</div>
<div className="flex-1 ml-80 p-6 pt-0 overflow-y-auto">
@ -72,7 +136,8 @@ const Option = () => {
autoClose={5000}
className="text-sm"
/>
<ConfirmationDialog />
<FollowAllConfirmationDialog />
<ImportListConfirmationDialog />
</div>
</>
);