Skip to content

Commit

Permalink
feat: add threads profile service
Browse files Browse the repository at this point in the history
  • Loading branch information
kawamataryo committed Jan 17, 2025
1 parent bfbfd98 commit 58ab247
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 3 deletions.
23 changes: 20 additions & 3 deletions src/contents/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import Modal from "~components/Modal";
import { ProfileDetectedUserListItem } from "~components/ProfileDetectedUserListItem";
import { useProfileSearch } from "~hooks/useProfileSearch";
import { getChromeStorage } from "~lib/chromeHelper";
import { STORAGE_KEYS } from "~lib/constants";
import { STORAGE_KEYS, PROFILE_TARGET_URLS_REGEX } from "~lib/constants";
import { XProfileService } from "~services/xProfileService";
import { ThreadsProfileService } from "~services/threadsProfileService";
import type { IProfileService } from "~types";
import { match, P } from "ts-pattern";

export const config: PlasmoCSConfig = {
matches: ["https://twitter.com/*", "https://x.com/*"],
matches: ["https://twitter.com/*", "https://x.com/*", "https://www.threads.net/*"],
all_frames: true,
};

Expand All @@ -20,14 +23,28 @@ export const getStyle = () => {
return style;
};

const getProfileService = (): IProfileService => {
const url = window.location.href;
return match(url)
.with(
P.when((url) => PROFILE_TARGET_URLS_REGEX.THREADS.test(url)),
() => new ThreadsProfileService(),
)
.with(
P.when((url) => PROFILE_TARGET_URLS_REGEX.X.test(url)),
() => new XProfileService(),
)
.otherwise(() => new XProfileService());
};

const Profile = () => {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const { bskyUsers, searchUser, initialize, handleClickAction } =
useProfileSearch();
const [isLoading, setIsLoading] = React.useState(false);

useEffect(() => {
const profileService = new XProfileService();
const profileService = getProfileService();

const checkAndAddButton = async () => {
const session = (
Expand Down
5 changes: 5 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export const TARGET_URLS_REGEX = {
INSTAGRAM: /^https:\/\/www\.instagram\.com\/[^/]+\/(followers|following)\/?/,
} as const;

export const PROFILE_TARGET_URLS_REGEX = {
THREADS: /^https:\/\/www\.threads\.net/,
X: /^https:\/\/(twitter|x)\.com/,
} as const;

export const MESSAGE_TYPE = {
ERROR: "error",
SUCCESS: "success",
Expand Down
101 changes: 101 additions & 0 deletions src/services/threadsProfileService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { CrawledUserInfo, IProfileService } from "~types";
import { BSKY_DOMAIN } from "~lib/constants";
import { debugLog } from "~lib/utils";

const SEARCH_BLUESKY_BUTTON_ID = "bsky-search-button";

const PROFILE_AREA_SELECTOR = '[aria-label="Column body"] > div > div';

export class ThreadsProfileService implements IProfileService {
isTargetPage() {
const url = window.location.href;
const profileAreaElement = document.querySelector<HTMLDivElement>(PROFILE_AREA_SELECTOR);
const profileAreaElementExists = profileAreaElement !== null && profileAreaElement.querySelector('h1') !== null;
debugLog(profileAreaElement);
return url.startsWith("https://www.threads.net/@") && profileAreaElementExists;
}

extractData(): Omit<CrawledUserInfo, "originalAvatar" | "originalProfileLink"> {
const profileAreaElement = document.querySelector<HTMLDivElement>(PROFILE_AREA_SELECTOR);
const profileAreaText = profileAreaElement?.innerText ?? "";
const [_, displayName, accountName] = profileAreaText.split("\n");
const bioText = profileAreaText.split("\n").slice(3).join("\n");
const accountNameRemoveUnderscore = accountName.replaceAll("_", "");
const accountNameReplaceUnderscore = accountName.replaceAll("_", "-");
const bskyHandleInDescription =
bioText?.match(new RegExp(`([^/\\s]+\\.${BSKY_DOMAIN})`))?.[1] ??
bioText?.match(/bsky\.app\/profile\/([^/\s]+)?/)?.[1]?.replace("…", "") ??
"";

return {
displayName,
accountName,
accountNameRemoveUnderscore,
accountNameReplaceUnderscore,
bskyHandleInDescription,
};
}

hasSearchBlueskyButton() {
return document.getElementById(SEARCH_BLUESKY_BUTTON_ID) !== null;
}

removeSearchBlueskyButton() {
const button = document.getElementById(SEARCH_BLUESKY_BUTTON_ID);
if (button) {
button.remove();
}
}

createSearchBlueskyButton() {
const button = document.createElement("button");
button.id = SEARCH_BLUESKY_BUTTON_ID;
button.textContent = chrome.i18n.getMessage("findSimilarBlueskyUsers");

// Set button styles
const buttonBackground = "oklch(0.488198 0.217165 264.376)";
const buttonHoverBackground = "oklab(0.439378 -0.0191538 -0.194508)";
Object.assign(button.style, {
padding: "4px 8px",
fontWeight: "bold",
width: "fit-content",
fontFamily:
"-apple-system, 'system-ui', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
cursor: "pointer",
borderRadius: "9999px",
background: buttonBackground,
transition: "background 0.3s ease",
fontSize: "12px",
color: "white",
border: "none",
marginBottom: "0px",
});

// Add hover effects
button.onmouseover = () => {
button.style.background = buttonHoverBackground;
};
button.onmouseout = () => {
button.style.background = buttonBackground;
};

return button;
}

mountSearchBlueskyButton({
clickAction,
}: {
clickAction: (
userData: Omit<CrawledUserInfo, "originalAvatar" | "originalProfileLink">,
) => void;
}) {
this.removeSearchBlueskyButton();
const button = this.createSearchBlueskyButton();
button.onclick = () => {
const userData = this.extractData();
clickAction(userData);
};
const userNameElement = document.querySelector(`${PROFILE_AREA_SELECTOR} > div`);
userNameElement?.parentElement?.insertBefore(button, userNameElement);
}
}

0 comments on commit 58ab247

Please sign in to comment.