diff --git a/src/lib/components/MatchTypeFilter.tsx b/src/lib/components/MatchTypeFilter.tsx index 0eb4b8b..19771a7 100644 --- a/src/lib/components/MatchTypeFilter.tsx +++ b/src/lib/components/MatchTypeFilter.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { BSKY_USER_MATCH_TYPE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants"; import type { MatchType } from "../../types"; +import { BSKY_USER_MATCH_TYPE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants"; export type MatchTypeFilterValue = { [BSKY_USER_MATCH_TYPE.DESCRIPTION]: boolean; diff --git a/src/lib/hooks/useRetrieveBskyUsers.ts b/src/lib/hooks/useRetrieveBskyUsers.ts index 08f3600..df98587 100644 --- a/src/lib/hooks/useRetrieveBskyUsers.ts +++ b/src/lib/hooks/useRetrieveBskyUsers.ts @@ -3,14 +3,9 @@ import { Storage } from "@plasmohq/storage"; import { useStorage } from "@plasmohq/storage/hook"; import React from "react"; import { BskyServiceWorkerClient } from "~lib/bskyServiceWorkerClient"; -import { - type MESSAGE_NAMES, - MESSAGE_NAME_TO_QUERY_PARAM_MAP, - STORAGE_KEYS, -} from "~lib/constants"; -import { extractUserData, getUserCells } from "~lib/domHelpers"; +import { type MESSAGE_NAMES, STORAGE_KEYS } from "~lib/constants"; import { searchBskyUser } from "~lib/searchBskyUsers"; -import { wait } from "~lib/utils"; +import { XService } from "~lib/services/x"; import type { CrawledUserInfo, MatchType } from "~types"; export type BskyUser = { @@ -26,21 +21,8 @@ export type BskyUser = { blockingUri: string | null; }; -const detectXUsers = (userCellQueryParam: string) => { - const userCells = getUserCells({ - queryParam: userCellQueryParam, - filterInsertedElement: true, - }); - return userCells.map((userCell) => { - return extractUserData(userCell); - }); -}; - export const useRetrieveBskyUsers = () => { const bskyClient = React.useRef(null); - const [detectedXUsers, setDetectedXUsers] = React.useState< - ReturnType - >([]); const [users, setUsers] = useStorage( { key: STORAGE_KEYS.DETECTED_BSKY_USERS, @@ -106,22 +88,7 @@ export const useRetrieveBskyUsers = () => { let index = 0; - const queryParam = MESSAGE_NAME_TO_QUERY_PARAM_MAP[messageName]; - - let scrollElement: HTMLElement | Window; - let modalScrollInterval: number; - - if (messageName === "search_bsky_user_on_list_members_page") { - // select the modal wrapper using viewport selector to avoid conflation with feed in the background - scrollElement = document.querySelector( - 'div[data-viewportview="true"]', - ) as HTMLElement; - // base interval off of intitial scroll height - modalScrollInterval = scrollElement.scrollHeight; - } else { - // for other cases, use the window, no need to cache a scroll interval due to different window scroll logic - scrollElement = window; - } + const xService = new XService(messageName); // loop until we get to the bottom while (!isBottomReached) { @@ -129,42 +96,15 @@ export const useRetrieveBskyUsers = () => { break; } - const data = detectXUsers(queryParam).filter((u) => { - return !detectedXUsers.some((t) => t.accountName === u.accountName); - }); - setDetectedXUsers((prev) => [...prev, ...data]); + const data = xService.getCrawledUsers(); await retrieveBskyUsers(data); - // handle scrolling pattern for both modal and window - if (scrollElement instanceof HTMLElement) { - scrollElement.scrollTop += modalScrollInterval; - } else { - window.scrollTo(0, document.body.scrollHeight); - } + const isEnd = await xService.performScrollAndCheckEnd(); - // wait for fetching data by x - await wait(3000); - - // break if bottom is reached - if (scrollElement instanceof HTMLElement) { - if ( - scrollElement.scrollTop + scrollElement.clientHeight >= - scrollElement.scrollHeight - ) { - setIsBottomReached(true); - setLoading(false); - break; - } - } else { - const documentElement = document.documentElement; - if ( - documentElement.scrollTop + documentElement.clientHeight >= - documentElement.scrollHeight - ) { - setIsBottomReached(true); - setLoading(false); - break; - } + if (isEnd) { + setIsBottomReached(true); + setLoading(false); + break; } index++; @@ -174,13 +114,9 @@ export const useRetrieveBskyUsers = () => { } } }, - [retrieveBskyUsers, detectedXUsers, isBottomReached], + [retrieveBskyUsers, isBottomReached], ); - React.useEffect(() => { - chrome.storage.local.set({ users: JSON.stringify(users) }); - }, [users]); - const stopRetrieveLoop = React.useCallback(() => { if (abortControllerRef.current) { abortControllerRef.current.abort(); diff --git a/src/lib/services/x.ts b/src/lib/services/x.ts index e69de29..6b049db 100644 --- a/src/lib/services/x.ts +++ b/src/lib/services/x.ts @@ -0,0 +1,80 @@ +import { MESSAGE_NAMES } from "~lib/constants"; +import { BSKY_DOMAIN, MESSAGE_NAME_TO_QUERY_PARAM_MAP } from "~lib/constants"; +import { wait } from "~lib/utils"; +import type { CrawledUserInfo, MessageName } from "~types"; + +export class XService { + // 対象のdomを取得する処理 + messageName: MessageName; + crawledUsers: Set; + + constructor(messageName: string) { + // TODO: add type check + this.messageName = messageName as MessageName; + this.crawledUsers = new Set(); + } + + private extractUserData(userCell: Element): CrawledUserInfo { + const anchors = Array.from(userCell.querySelectorAll("a")); + const [avatarEl, displayNameEl] = anchors; + const accountName = avatarEl?.getAttribute("href")?.replace("/", ""); + const accountNameRemoveUnderscore = accountName.replaceAll("_", ""); // bsky does not allow underscores in handle, so remove them. + const accountNameReplaceUnderscore = accountName.replaceAll("_", "-"); + const displayName = displayNameEl?.textContent; + const bskyHandle = + userCell.textContent?.match( + new RegExp(`([^/\\s]+\\.${BSKY_DOMAIN})`), + )?.[1] ?? + userCell.textContent + ?.match(/bsky\.app\/profile\/([^/\s]+)…?/)?.[1] + ?.replace("…", "") ?? + ""; + + return { + accountName, + displayName, + accountNameRemoveUnderscore, + accountNameReplaceUnderscore, + bskyHandle, + }; + } + + getCrawledUsers(): CrawledUserInfo[] { + const userCells = Array.from( + document.querySelectorAll( + MESSAGE_NAME_TO_QUERY_PARAM_MAP[this.messageName], + ), + ); + + const users = userCells + .map((userCell) => this.extractUserData(userCell)) + .filter((user) => !this.crawledUsers.has(user.accountName)); + + this.crawledUsers = new Set([ + ...this.crawledUsers, + ...users.map((user) => user.accountName), + ]); + + return users; + } + + async performScrollAndCheckEnd(): Promise { + const isListMembersPage = + this.messageName === MESSAGE_NAMES.SEARCH_BSKY_USER_ON_LIST_MEMBERS_PAGE; + + const scrollTarget = isListMembersPage + ? (document.querySelector('div[data-viewportview="true"]') as HTMLElement) + : document.documentElement; + + const initialScrollHeight = scrollTarget.scrollHeight; + scrollTarget.scrollTop += initialScrollHeight; + + await wait(3000); + + const hasReachedEnd = + scrollTarget.scrollTop + scrollTarget.clientHeight >= + scrollTarget.scrollHeight; + + return hasReachedEnd; + } +} diff --git a/src/types.ts b/src/types.ts index 4533069..aa5ef2a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,10 @@ -import type { BSKY_USER_MATCH_TYPE } from "~lib/constants"; +import type { BSKY_USER_MATCH_TYPE, MESSAGE_NAMES } from "~lib/constants"; export type MatchType = (typeof BSKY_USER_MATCH_TYPE)[keyof typeof BSKY_USER_MATCH_TYPE]; +export type MESSAGE_NAME = (typeof MESSAGE_NAMES)[keyof typeof MESSAGE_NAMES]; + export type BskyUser = { did: string; avatar: string;