mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-04-10 14:11:22 -06:00
Merge pull request #112 from dtflowers/enhance-list-features
[RFC] Allow Users to Import List from Bridge
This commit is contained in:
commit
f637e48f3a
21
src/background/messages/addUserToList.ts
Normal file
21
src/background/messages/addUserToList.ts
Normal 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, userDid, listUri } = req.body;
|
||||
const client = await BskyClient.createAgentFromSession(session);
|
||||
|
||||
try {
|
||||
res.send({
|
||||
result: await client.addUserToList({ userDid, listUri }),
|
||||
});
|
||||
} catch (e) {
|
||||
res.send({
|
||||
error: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
21
src/background/messages/createList.ts
Normal file
21
src/background/messages/createList.ts
Normal 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, name, description } = req.body;
|
||||
const client = await BskyClient.createAgentFromSession(session);
|
||||
|
||||
try {
|
||||
res.send({
|
||||
uri: await client.createList({ name, description }),
|
||||
});
|
||||
} catch (e) {
|
||||
res.send({
|
||||
error: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
35
src/background/messages/createListAndAddUsers.ts
Normal file
35
src/background/messages/createListAndAddUsers.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { PlasmoMessaging } from "@plasmohq/messaging";
|
||||
import { BskyClient } from "~lib/bskyClient";
|
||||
import { STORAGE_KEYS } from "~lib/constants";
|
||||
|
||||
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
|
||||
const { name, description, userDids } = req.body;
|
||||
|
||||
const storage = await chrome.storage.local.get(
|
||||
STORAGE_KEYS.BSKY_CLIENT_SESSION,
|
||||
);
|
||||
const session = storage[STORAGE_KEYS.BSKY_CLIENT_SESSION];
|
||||
|
||||
if (!session || !session.did) {
|
||||
res.send({
|
||||
error: {
|
||||
message: "Invalid session data",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await BskyClient.createAgentFromSession(session);
|
||||
await client.createListAndAddUsers({ name, description, userDids });
|
||||
res.send({ success: true });
|
||||
} catch (e) {
|
||||
res.send({
|
||||
error: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
@ -29,6 +29,7 @@ const App = () => {
|
||||
restart,
|
||||
isBottomReached,
|
||||
errorMessage,
|
||||
listName,
|
||||
} = useRetrieveBskyUsers();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
@ -72,7 +73,10 @@ const App = () => {
|
||||
|
||||
const stopAndShowDetectedUsers = async () => {
|
||||
stopRetrieveLoop();
|
||||
await chrome.storage.local.set({ users: JSON.stringify(users) });
|
||||
await chrome.storage.local.set({
|
||||
users: JSON.stringify(users),
|
||||
listName: listName,
|
||||
});
|
||||
openOptionPage();
|
||||
};
|
||||
|
||||
|
@ -114,4 +114,59 @@ export class BskyClient {
|
||||
rkey,
|
||||
});
|
||||
};
|
||||
|
||||
public createList = async ({
|
||||
name,
|
||||
description,
|
||||
}: {
|
||||
name: string;
|
||||
description: string;
|
||||
}) => {
|
||||
const result = await this.agent.com.atproto.repo.createRecord({
|
||||
repo: this.me.did,
|
||||
collection: "app.bsky.graph.list",
|
||||
record: {
|
||||
$type: "app.bsky.graph.list",
|
||||
purpose: "app.bsky.graph.defs#curatelist",
|
||||
name,
|
||||
description,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
return result.data.uri;
|
||||
};
|
||||
|
||||
public addUserToList = async ({
|
||||
userDid,
|
||||
listUri,
|
||||
}: {
|
||||
userDid: string;
|
||||
listUri: string;
|
||||
}) => {
|
||||
return await this.agent.com.atproto.repo.createRecord({
|
||||
repo: this.me.did,
|
||||
collection: "app.bsky.graph.listitem",
|
||||
record: {
|
||||
$type: "app.bsky.graph.listitem",
|
||||
subject: userDid,
|
||||
list: listUri,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public createListAndAddUsers = async ({
|
||||
name,
|
||||
description,
|
||||
userDids,
|
||||
}: {
|
||||
name: string;
|
||||
description: string;
|
||||
userDids: string[];
|
||||
}) => {
|
||||
const listUri = await this.createList({ name, description });
|
||||
for (const userDid of userDids) {
|
||||
await this.addUserToList({ userDid, listUri });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -110,4 +110,59 @@ export class BskyServiceWorkerClient {
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
public createList = async ({
|
||||
name,
|
||||
description,
|
||||
}: {
|
||||
name: string;
|
||||
description: string;
|
||||
}) => {
|
||||
const { uri, error } = await sendToBackground({
|
||||
name: "createList",
|
||||
body: {
|
||||
session: this.session,
|
||||
name,
|
||||
description,
|
||||
},
|
||||
});
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
return uri;
|
||||
};
|
||||
|
||||
public addUserToList = async ({
|
||||
userDid,
|
||||
listUri,
|
||||
}: {
|
||||
userDid: string;
|
||||
listUri: string;
|
||||
}) => {
|
||||
const { result, error } = await sendToBackground({
|
||||
name: "addUserToList",
|
||||
body: {
|
||||
session: this.session,
|
||||
userDid,
|
||||
listUri,
|
||||
},
|
||||
});
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
public createListAndAddUsers = async ({
|
||||
name,
|
||||
description,
|
||||
userDids,
|
||||
}: {
|
||||
name: string;
|
||||
description: string;
|
||||
userDids: string[];
|
||||
}) => {
|
||||
const listUri = await this.createList({ name, description });
|
||||
for (const userDid of userDids) {
|
||||
await this.addUserToList({ userDid, listUri });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -25,6 +25,19 @@ const Sidebar = ({
|
||||
actionMode,
|
||||
matchTypeStats,
|
||||
}: 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">
|
||||
@ -144,10 +157,7 @@ const Sidebar = ({
|
||||
</svg>
|
||||
<p className="text-xl font-bold">Action</p>
|
||||
</div>
|
||||
<AsyncButton
|
||||
onClick={actionAll}
|
||||
label={actionMode === ACTION_MODE.FOLLOW ? "Follow All" : "Block All"}
|
||||
/>
|
||||
<AsyncButton onClick={actionAll} label={getActionLabel()} />
|
||||
<p className="text-xs">
|
||||
⚠️ User detection is not perfect and may include false positives.
|
||||
</p>
|
||||
|
@ -84,7 +84,7 @@ const UserCard = ({ user, actionMode, clickAction }: Props) => {
|
||||
const actionBtnLabelAndClass = React.useMemo(
|
||||
() =>
|
||||
match(actionMode)
|
||||
.with(ACTION_MODE.FOLLOW, () => {
|
||||
.with(ACTION_MODE.FOLLOW, ACTION_MODE.IMPORT_LIST, () => {
|
||||
const follow = {
|
||||
label: "Follow on Bluesky",
|
||||
class: "btn-primary",
|
||||
@ -109,7 +109,7 @@ const UserCard = ({ user, actionMode, clickAction }: Props) => {
|
||||
})
|
||||
.with(ACTION_MODE.BLOCK, () => {
|
||||
const block = {
|
||||
label: "block on Bluesky",
|
||||
label: "Block on Bluesky",
|
||||
class: "btn-primary",
|
||||
};
|
||||
const blocking = {
|
||||
|
@ -20,11 +20,13 @@ export const MESSAGE_NAME_TO_QUERY_PARAM_MAP = {
|
||||
export const ACTION_MODE = {
|
||||
FOLLOW: "follow",
|
||||
BLOCK: "block",
|
||||
IMPORT_LIST: "import_list",
|
||||
};
|
||||
|
||||
export const MESSAGE_NAME_TO_ACTION_MODE_MAP = {
|
||||
[MESSAGE_NAMES.SEARCH_BSKY_USER_ON_FOLLOW_PAGE]: ACTION_MODE.FOLLOW,
|
||||
[MESSAGE_NAMES.SEARCH_BSKY_USER_ON_LIST_MEMBERS_PAGE]: ACTION_MODE.FOLLOW,
|
||||
[MESSAGE_NAMES.SEARCH_BSKY_USER_ON_LIST_MEMBERS_PAGE]:
|
||||
ACTION_MODE.IMPORT_LIST,
|
||||
[MESSAGE_NAMES.SEARCH_BSKY_USER_ON_BLOCK_PAGE]: ACTION_MODE.BLOCK,
|
||||
};
|
||||
|
||||
|
@ -21,6 +21,14 @@ export const useBskyUserManager = () => {
|
||||
},
|
||||
(v) => (v === undefined ? [] : v),
|
||||
);
|
||||
const [listName, setListName] = React.useState<string>("");
|
||||
React.useEffect(() => {
|
||||
chrome.storage.local.get("listName", (result) => {
|
||||
const name = result.listName || "Imported List from X";
|
||||
setListName(name);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const bskyClient = React.useRef<BskyServiceWorkerClient | null>(null);
|
||||
const [actionMode, setActionMode] = React.useState<
|
||||
(typeof ACTION_MODE)[keyof typeof ACTION_MODE]
|
||||
@ -121,6 +129,19 @@ export const useBskyUserManager = () => {
|
||||
if (!bskyClient.current) return;
|
||||
let actionCount = 0;
|
||||
|
||||
if (actionMode === ACTION_MODE.IMPORT_LIST) {
|
||||
const userDids = filteredUsers.map((user) => user.did);
|
||||
await chrome.runtime.sendMessage({
|
||||
name: "createListAndAddUsers",
|
||||
body: {
|
||||
name: listName,
|
||||
description: "List imported via Sky Follower Bridge",
|
||||
userDids,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const user of filteredUsers) {
|
||||
let resultUri: string | null = null;
|
||||
// follow
|
||||
@ -170,7 +191,7 @@ export const useBskyUserManager = () => {
|
||||
}
|
||||
}
|
||||
return actionCount;
|
||||
}, [filteredUsers, actionMode, setUsers]);
|
||||
}, [filteredUsers, actionMode, setUsers, listName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
chrome.storage.local.get(
|
||||
@ -204,6 +225,7 @@ export const useBskyUserManager = () => {
|
||||
return {
|
||||
handleClickAction,
|
||||
users,
|
||||
listName,
|
||||
actionMode,
|
||||
matchTypeFilter,
|
||||
changeMatchTypeFilter,
|
||||
|
@ -25,6 +25,16 @@ const getService = (messageName: string): AbstractService => {
|
||||
.otherwise(() => new XService(messageName));
|
||||
};
|
||||
|
||||
const scrapeListNameFromPage = (): string => {
|
||||
const listNameElement = document.querySelector(
|
||||
'div[aria-label="Timeline: List"] span',
|
||||
);
|
||||
if (listNameElement) {
|
||||
return listNameElement.textContent.trim();
|
||||
}
|
||||
return "Imported List from X";
|
||||
};
|
||||
|
||||
export const useRetrieveBskyUsers = () => {
|
||||
const bskyClient = React.useRef<BskyServiceWorkerClient | null>(null);
|
||||
const [users, setUsers] = useStorage<BskyUser[]>(
|
||||
@ -44,6 +54,7 @@ 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[]) => {
|
||||
@ -120,6 +131,13 @@ export const useRetrieveBskyUsers = () => {
|
||||
[retrieveBskyUsers, isBottomReached],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
chrome.storage.local.set({
|
||||
users: JSON.stringify(users),
|
||||
listName: listName,
|
||||
});
|
||||
}, [users, listName]);
|
||||
|
||||
const stopRetrieveLoop = React.useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
@ -143,6 +161,9 @@ export const useRetrieveBskyUsers = () => {
|
||||
|
||||
bskyClient.current = new BskyServiceWorkerClient(session);
|
||||
|
||||
const listName = scrapeListNameFromPage();
|
||||
setListName(listName);
|
||||
|
||||
startRetrieveLoop(messageName).catch((e) => {
|
||||
console.error(e);
|
||||
setErrorMessage(e.message);
|
||||
@ -173,6 +194,7 @@ export const useRetrieveBskyUsers = () => {
|
||||
return {
|
||||
initialize,
|
||||
users,
|
||||
listName,
|
||||
loading,
|
||||
errorMessage,
|
||||
isRateLimitError,
|
||||
|
@ -10,6 +10,7 @@ const Option = () => {
|
||||
const {
|
||||
users,
|
||||
filteredUsers,
|
||||
listName,
|
||||
matchTypeFilter,
|
||||
changeMatchTypeFilter,
|
||||
handleClickAction,
|
||||
|
Loading…
x
Reference in New Issue
Block a user