mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-04-10 14:11:22 -06:00
feat: add search again button
This commit is contained in:
parent
4383913ed5
commit
29cf178791
63
src/components/ReSearchModal.tsx
Normal file
63
src/components/ReSearchModal.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
||||||
|
import Modal from "~lib/components/Modal";
|
||||||
|
import UserCardWithoutActionButton from "~lib/components/UserCardWithoutActionButton";
|
||||||
|
|
||||||
|
interface ReSearchModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
reSearchResults: {
|
||||||
|
sourceDid: string;
|
||||||
|
users: ProfileView[];
|
||||||
|
};
|
||||||
|
handleClickReSearchResult: ({
|
||||||
|
sourceDid,
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
sourceDid: string;
|
||||||
|
user: ProfileView;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReSearchModal: React.FC<ReSearchModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
reSearchResults,
|
||||||
|
handleClickReSearchResult,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal open={open} width={600} onClose={onClose}>
|
||||||
|
<h2 className="text-lg font-bold text-center py-2">Search Results</h2>
|
||||||
|
{reSearchResults.users.length === 0 && (
|
||||||
|
<div className="text-center flex justify-center items-center flex-col gap-4 mt-5">
|
||||||
|
<span className="loading loading-spinner loading-lg" />
|
||||||
|
<div className="text-center flex justify-center items-center text-sm">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{reSearchResults.users.length > 0 && (
|
||||||
|
<div className="divide-y divide-gray-500">
|
||||||
|
{reSearchResults.users.map((user) => (
|
||||||
|
<UserCardWithoutActionButton
|
||||||
|
key={user.handle}
|
||||||
|
onClick={() =>
|
||||||
|
handleClickReSearchResult({
|
||||||
|
sourceDid: reSearchResults.sourceDid,
|
||||||
|
user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
user={{
|
||||||
|
avatar: user.avatar,
|
||||||
|
handle: user.handle,
|
||||||
|
displayName: user.displayName,
|
||||||
|
description: user.description,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReSearchModal;
|
@ -1,5 +1,5 @@
|
|||||||
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
||||||
import { BSKY_USER_MATCH_TYPE } from "./constants";
|
import { BSKY_PROFILE_LABEL, BSKY_USER_MATCH_TYPE } from "./constants";
|
||||||
|
|
||||||
type xUserInfo = {
|
type xUserInfo = {
|
||||||
bskyHandleInDescription: string;
|
bskyHandleInDescription: string;
|
||||||
@ -80,3 +80,9 @@ export const isSimilarUser = (
|
|||||||
type: BSKY_USER_MATCH_TYPE.NONE,
|
type: BSKY_USER_MATCH_TYPE.NONE,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isImpersonationUser = (user: ProfileView) => {
|
||||||
|
return user.labels.some(
|
||||||
|
(label) => label.val === BSKY_PROFILE_LABEL.IMPERSONATION,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
35
src/lib/components/ActionButton.tsx
Normal file
35
src/lib/components/ActionButton.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type ActionButtonProps = {
|
||||||
|
loading: boolean;
|
||||||
|
actionBtnLabelAndClass: { label: string; class: string };
|
||||||
|
handleActionButtonClick: () => void;
|
||||||
|
setIsBtnHovered: (value: boolean) => void;
|
||||||
|
setIsJustClicked: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionButton = ({
|
||||||
|
loading,
|
||||||
|
actionBtnLabelAndClass,
|
||||||
|
handleActionButtonClick,
|
||||||
|
setIsBtnHovered,
|
||||||
|
setIsJustClicked,
|
||||||
|
}: ActionButtonProps) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-sm rounded-3xl ${
|
||||||
|
loading ? "" : actionBtnLabelAndClass.class
|
||||||
|
}`}
|
||||||
|
onClick={handleActionButtonClick}
|
||||||
|
onMouseEnter={() => setIsBtnHovered(true)}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setIsBtnHovered(false);
|
||||||
|
setIsJustClicked(false);
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Processing..." : actionBtnLabelAndClass.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ActionButton;
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
import type { BskyUser } from "~types";
|
import type { BskyUser } from "~types";
|
||||||
@ -8,9 +9,19 @@ export type Props = {
|
|||||||
user: BskyUser;
|
user: BskyUser;
|
||||||
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
|
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
|
||||||
clickAction: (user: BskyUser) => Promise<void>;
|
clickAction: (user: BskyUser) => Promise<void>;
|
||||||
|
reSearch: (user: {
|
||||||
|
sourceDid: string;
|
||||||
|
accountName: string;
|
||||||
|
displayName: string;
|
||||||
|
}) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DetectedUserListItem = ({ user, actionMode, clickAction }: Props) => {
|
const DetectedUserListItem = ({
|
||||||
|
user,
|
||||||
|
actionMode,
|
||||||
|
clickAction,
|
||||||
|
reSearch,
|
||||||
|
}: Props) => {
|
||||||
const [isBtnHovered, setIsBtnHovered] = React.useState(false);
|
const [isBtnHovered, setIsBtnHovered] = React.useState(false);
|
||||||
const [isJustClicked, setIsJustClicked] = React.useState(false);
|
const [isJustClicked, setIsJustClicked] = React.useState(false);
|
||||||
const actionBtnLabelAndClass = React.useMemo(
|
const actionBtnLabelAndClass = React.useMemo(
|
||||||
@ -81,6 +92,14 @@ const DetectedUserListItem = ({ user, actionMode, clickAction }: Props) => {
|
|||||||
setIsJustClicked(true);
|
setIsJustClicked(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReSearchClick = () => {
|
||||||
|
reSearch({
|
||||||
|
sourceDid: user.did,
|
||||||
|
accountName: user.originalHandle,
|
||||||
|
displayName: user.originalDisplayName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-base-100 w-full relative grid grid-cols-[22%_1fr] gap-5">
|
<div className="bg-base-100 w-full relative grid grid-cols-[22%_1fr] gap-5">
|
||||||
<DetectedUserSource user={user} />
|
<DetectedUserSource user={user} />
|
||||||
@ -91,6 +110,7 @@ const DetectedUserListItem = ({ user, actionMode, clickAction }: Props) => {
|
|||||||
handleActionButtonClick={handleActionButtonClick}
|
handleActionButtonClick={handleActionButtonClick}
|
||||||
setIsBtnHovered={setIsBtnHovered}
|
setIsBtnHovered={setIsBtnHovered}
|
||||||
setIsJustClicked={setIsJustClicked}
|
setIsJustClicked={setIsJustClicked}
|
||||||
|
handleReSearchClick={handleReSearchClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { BskyUser } from "~types";
|
import type { BskyUser } from "~types";
|
||||||
import { MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
|
import { MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
|
||||||
import { UserInfo, UserProfile } from "./UserCard";
|
import UserInfo from "./UserInfo";
|
||||||
|
import UserProfile from "./UserProfile";
|
||||||
|
|
||||||
type DetectedUserSourceProps = {
|
type DetectedUserSourceProps = {
|
||||||
user: BskyUser;
|
user: BskyUser;
|
||||||
|
@ -5,9 +5,10 @@ export type Props = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
width?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Modal = ({ children, open = false, onClose }: Props) => {
|
const Modal = ({ children, open = false, onClose, width = 500 }: Props) => {
|
||||||
const anchorRef = useRef<HTMLDialogElement>(null);
|
const anchorRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -25,7 +26,10 @@ const Modal = ({ children, open = false, onClose }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<dialog className="modal" ref={anchorRef} open={open}>
|
<dialog className="modal" ref={anchorRef} open={open}>
|
||||||
<div className="modal-box p-10 bg-base-100 w-[500px] max-w-none text-base-content">
|
<div
|
||||||
|
className="modal-box p-10 bg-base-100 max-w-none text-base-content"
|
||||||
|
style={{ width }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" className="modal-backdrop">
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
@ -1,81 +1,46 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { BskyUser } from "~types";
|
import type { BskyUser } from "~types";
|
||||||
import AvatarFallbackSvg from "./Icons/AvatarFallbackSvg";
|
import ActionButton from "./ActionButton";
|
||||||
|
import UserInfo from "./UserInfo";
|
||||||
|
import UserProfile from "./UserProfile";
|
||||||
|
|
||||||
type UserProfileProps = {
|
|
||||||
avatar: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserProfile = ({ avatar, url }: UserProfileProps) => (
|
|
||||||
<div className="avatar">
|
|
||||||
<div className="w-10 h-10 rounded-full border border-white">
|
|
||||||
<a href={url} target="_blank" rel="noreferrer">
|
|
||||||
{avatar ? <img src={avatar} alt="" /> : <AvatarFallbackSvg />}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type UserInfoProps = {
|
|
||||||
handle: string;
|
|
||||||
displayName: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserInfo = ({ handle, displayName, url }: UserInfoProps) => (
|
|
||||||
<div>
|
|
||||||
<h2 className="card-title break-all text-[1.1rem] font-bold">
|
|
||||||
<a href={url} target="_blank" rel="noreferrer">
|
|
||||||
{displayName}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<p className="w-fit break-all text-gray-500 dark:text-gray-400 text-sm">
|
|
||||||
<a href={url} target="_blank" rel="noreferrer" className="break-all">
|
|
||||||
@{handle}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type ActionButtonProps = {
|
|
||||||
loading: boolean;
|
|
||||||
actionBtnLabelAndClass: { label: string; class: string };
|
|
||||||
handleActionButtonClick: () => void;
|
|
||||||
setIsBtnHovered: (value: boolean) => void;
|
|
||||||
setIsJustClicked: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActionButton = ({
|
|
||||||
loading,
|
|
||||||
actionBtnLabelAndClass,
|
|
||||||
handleActionButtonClick,
|
|
||||||
setIsBtnHovered,
|
|
||||||
setIsJustClicked,
|
|
||||||
}: ActionButtonProps) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-sm rounded-3xl ${
|
|
||||||
loading ? "" : actionBtnLabelAndClass.class
|
|
||||||
}`}
|
|
||||||
onClick={handleActionButtonClick}
|
|
||||||
onMouseEnter={() => setIsBtnHovered(true)}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setIsBtnHovered(false);
|
|
||||||
setIsJustClicked(false);
|
|
||||||
}}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? "Processing..." : actionBtnLabelAndClass.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
export type UserCardProps = {
|
export type UserCardProps = {
|
||||||
user: BskyUser;
|
user: Pick<BskyUser, "avatar" | "handle" | "displayName" | "description">;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
actionBtnLabelAndClass: { label: string; class: string };
|
actionBtnLabelAndClass: { label: string; class: string };
|
||||||
handleActionButtonClick: () => void;
|
handleActionButtonClick: () => void;
|
||||||
setIsBtnHovered: (value: boolean) => void;
|
setIsBtnHovered: (value: boolean) => void;
|
||||||
setIsJustClicked: (value: boolean) => void;
|
setIsJustClicked: (value: boolean) => void;
|
||||||
|
handleReSearchClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReSearchButton = ({
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-outline w-7 h-7 border rounded-full flex items-center justify-center"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="h-4 w-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserCard = ({
|
const UserCard = ({
|
||||||
@ -85,32 +50,38 @@ const UserCard = ({
|
|||||||
handleActionButtonClick,
|
handleActionButtonClick,
|
||||||
setIsBtnHovered,
|
setIsBtnHovered,
|
||||||
setIsJustClicked,
|
setIsJustClicked,
|
||||||
}: UserCardProps) => (
|
handleReSearchClick,
|
||||||
<div className="relative py-3 pl-0 pr-2 grid grid-cols-[50px_1fr]">
|
}: UserCardProps) => {
|
||||||
<UserProfile
|
return (
|
||||||
avatar={user.avatar}
|
<div className="relative py-3 pl-0 pr-2 grid grid-cols-[50px_1fr]">
|
||||||
url={`https://bsky.app/profile/${user.handle}`}
|
<UserProfile
|
||||||
/>
|
avatar={user.avatar}
|
||||||
<div className="flex flex-col gap-2">
|
url={`https://bsky.app/profile/${user.handle}`}
|
||||||
<div className="flex justify-between items-center gap-2">
|
/>
|
||||||
<UserInfo
|
<div className="flex flex-col gap-2">
|
||||||
handle={user.handle}
|
<div className="flex justify-between items-center gap-2">
|
||||||
displayName={user.displayName}
|
<div className="flex items-start gap-4">
|
||||||
url={`https://bsky.app/profile/${user.handle}`}
|
<UserInfo
|
||||||
/>
|
handle={user.handle}
|
||||||
<div className="card-actions">
|
displayName={user.displayName}
|
||||||
<ActionButton
|
url={`https://bsky.app/profile/${user.handle}`}
|
||||||
loading={loading}
|
/>
|
||||||
actionBtnLabelAndClass={actionBtnLabelAndClass}
|
<ReSearchButton onClick={handleReSearchClick} />
|
||||||
handleActionButtonClick={handleActionButtonClick}
|
</div>
|
||||||
setIsBtnHovered={setIsBtnHovered}
|
<div className="card-actions flex items-center gap-4">
|
||||||
setIsJustClicked={setIsJustClicked}
|
<ActionButton
|
||||||
/>
|
loading={loading}
|
||||||
|
actionBtnLabelAndClass={actionBtnLabelAndClass}
|
||||||
|
handleActionButtonClick={handleActionButtonClick}
|
||||||
|
setIsBtnHovered={setIsBtnHovered}
|
||||||
|
setIsJustClicked={setIsJustClicked}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm break-all">{user.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm break-all">{user.description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export default UserCard;
|
export default UserCard;
|
||||||
|
31
src/lib/components/UserCardWithoutActionButton.tsx
Normal file
31
src/lib/components/UserCardWithoutActionButton.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { BskyUser } from "~types";
|
||||||
|
import UserInfo from "./UserInfo";
|
||||||
|
import UserProfile from "./UserProfile";
|
||||||
|
|
||||||
|
export type UserCardWithoutActionButtonProps = {
|
||||||
|
user: Pick<BskyUser, "avatar" | "handle" | "displayName" | "description">;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCardWithoutActionButton = ({
|
||||||
|
user,
|
||||||
|
onClick,
|
||||||
|
}: UserCardWithoutActionButtonProps) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="relative py-3 pl-0 pr-2 grid grid-cols-[50px_1fr] w-full"
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyUp={onClick}
|
||||||
|
>
|
||||||
|
<UserProfile avatar={user.avatar} />
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex justify-between items-center gap-2">
|
||||||
|
<UserInfo handle={user.handle} displayName={user.displayName} />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm break-all text-left">{user.description}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UserCardWithoutActionButton;
|
28
src/lib/components/UserInfo.tsx
Normal file
28
src/lib/components/UserInfo.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type UserInfoProps = {
|
||||||
|
handle: string;
|
||||||
|
displayName: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserInfo = ({ handle, displayName, url }: UserInfoProps) => (
|
||||||
|
<div>
|
||||||
|
<h2 className="card-title break-all text-[1.1rem] font-bold">
|
||||||
|
{url ? (
|
||||||
|
<a href={url} target="_blank" rel="noreferrer">
|
||||||
|
{displayName}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>{displayName}</>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<p className="w-fit break-all text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
<a href={url} target="_blank" rel="noreferrer" className="break-all">
|
||||||
|
@{handle}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UserInfo;
|
25
src/lib/components/UserProfile.tsx
Normal file
25
src/lib/components/UserProfile.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import AvatarFallbackSvg from "./Icons/AvatarFallbackSvg";
|
||||||
|
|
||||||
|
type UserProfileProps = {
|
||||||
|
avatar: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserProfile = ({ avatar, url }: UserProfileProps) => (
|
||||||
|
<div className="avatar">
|
||||||
|
<div className="w-10 h-10 rounded-full border border-white">
|
||||||
|
{url ? (
|
||||||
|
<a href={url} target="_blank" rel="noreferrer">
|
||||||
|
{avatar ? <img src={avatar} alt="" /> : <AvatarFallbackSvg />}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full border border-white">
|
||||||
|
{avatar ? <img src={avatar} alt="" /> : <AvatarFallbackSvg />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UserProfile;
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
||||||
import { Storage } from "@plasmohq/storage";
|
import { Storage } from "@plasmohq/storage";
|
||||||
import { useStorage } from "@plasmohq/storage/hook";
|
import { useStorage } from "@plasmohq/storage/hook";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -9,6 +10,7 @@ import {
|
|||||||
MESSAGE_NAME_TO_ACTION_MODE_MAP,
|
MESSAGE_NAME_TO_ACTION_MODE_MAP,
|
||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
} from "~lib/constants";
|
} from "~lib/constants";
|
||||||
|
import { reSearchBskyUser } from "~lib/reSearchBskyUsers";
|
||||||
import { wait } from "~lib/utils";
|
import { wait } from "~lib/utils";
|
||||||
import type { BskyUser, MatchType } from "~types";
|
import type { BskyUser, MatchType } from "~types";
|
||||||
|
|
||||||
@ -242,6 +244,58 @@ export const useBskyUserManager = () => {
|
|||||||
);
|
);
|
||||||
}, [users, matchTypeFilter]);
|
}, [users, matchTypeFilter]);
|
||||||
|
|
||||||
|
const [reSearchResults, setReSearchResults] = React.useState<{
|
||||||
|
sourceDid: string;
|
||||||
|
users: ProfileView[];
|
||||||
|
}>({ sourceDid: "", users: [] });
|
||||||
|
const reSearch = React.useCallback(
|
||||||
|
async ({
|
||||||
|
sourceDid,
|
||||||
|
accountName,
|
||||||
|
displayName,
|
||||||
|
}: {
|
||||||
|
sourceDid: string;
|
||||||
|
accountName: string;
|
||||||
|
displayName: string;
|
||||||
|
}) => {
|
||||||
|
const searchResults = await reSearchBskyUser({
|
||||||
|
client: bskyClient.current,
|
||||||
|
userData: {
|
||||||
|
accountName,
|
||||||
|
displayName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setReSearchResults({ sourceDid, users: searchResults });
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearReSearchResults = React.useCallback(() => {
|
||||||
|
setReSearchResults({
|
||||||
|
sourceDid: "",
|
||||||
|
users: [],
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changeDetectedUser = React.useCallback(
|
||||||
|
(fromDid: string, toUser: ProfileView) => {
|
||||||
|
setUsers((prev) =>
|
||||||
|
prev.map((prevUser) =>
|
||||||
|
prevUser.did === fromDid
|
||||||
|
? {
|
||||||
|
...prevUser,
|
||||||
|
...toUser,
|
||||||
|
isFollowing: !!toUser.viewer?.following,
|
||||||
|
followingUri: toUser.viewer?.following,
|
||||||
|
isBlocking: !!toUser.viewer?.blocking,
|
||||||
|
blockingUri: toUser.viewer?.blocking,
|
||||||
|
}
|
||||||
|
: prevUser,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setUsers],
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
handleClickAction,
|
handleClickAction,
|
||||||
users,
|
users,
|
||||||
@ -253,5 +307,9 @@ export const useBskyUserManager = () => {
|
|||||||
importList,
|
importList,
|
||||||
followAll,
|
followAll,
|
||||||
blockAll,
|
blockAll,
|
||||||
|
reSearch,
|
||||||
|
reSearchResults,
|
||||||
|
changeDetectedUser,
|
||||||
|
clearReSearchResults,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
55
src/lib/reSearchBskyUsers.ts
Normal file
55
src/lib/reSearchBskyUsers.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
||||||
|
import { isOneSymbol } from "~lib/utils";
|
||||||
|
import { isImpersonationUser } from "./bskyHelpers";
|
||||||
|
import type { BskyServiceWorkerClient } from "./bskyServiceWorkerClient";
|
||||||
|
|
||||||
|
export const reSearchBskyUser = async ({
|
||||||
|
client,
|
||||||
|
userData,
|
||||||
|
}: {
|
||||||
|
client: BskyServiceWorkerClient;
|
||||||
|
userData: {
|
||||||
|
accountName: string;
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
}): Promise<ProfileView[]> => {
|
||||||
|
const searchTerms = [
|
||||||
|
userData.accountName,
|
||||||
|
userData.displayName,
|
||||||
|
userData.accountName.replaceAll("_", ""),
|
||||||
|
userData.displayName.replaceAll("_", ""),
|
||||||
|
];
|
||||||
|
const uniqueSearchTerms = new Set(searchTerms);
|
||||||
|
|
||||||
|
const searchResultDidSet = new Set<string>();
|
||||||
|
const searchResults: ProfileView[] = [];
|
||||||
|
|
||||||
|
for (const term of uniqueSearchTerms) {
|
||||||
|
// one symbol is not a valid search term for bsky
|
||||||
|
if (!term || isOneSymbol(term)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const results = await client.searchUser({
|
||||||
|
term,
|
||||||
|
limit: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
// skip impersonation users
|
||||||
|
if (isImpersonationUser(result)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (searchResultDidSet.has(result.did)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
searchResultDidSet.add(result.did);
|
||||||
|
searchResults.push(result);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchResults;
|
||||||
|
};
|
@ -1,15 +1,8 @@
|
|||||||
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
|
||||||
import { isSimilarUser } from "~lib/bskyHelpers";
|
import { isSimilarUser } from "~lib/bskyHelpers";
|
||||||
import { isOneSymbol } from "~lib/utils";
|
import { isOneSymbol } from "~lib/utils";
|
||||||
import type { CrawledUserInfo } from "~types";
|
import type { CrawledUserInfo } from "~types";
|
||||||
|
import { isImpersonationUser } from "./bskyHelpers";
|
||||||
import type { BskyServiceWorkerClient } from "./bskyServiceWorkerClient";
|
import type { BskyServiceWorkerClient } from "./bskyServiceWorkerClient";
|
||||||
import { BSKY_PROFILE_LABEL } from "./constants";
|
|
||||||
|
|
||||||
const isImpersonationUser = (user: ProfileView) => {
|
|
||||||
return user.labels.some(
|
|
||||||
(label) => label.val === BSKY_PROFILE_LABEL.IMPERSONATION,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const searchBskyUser = async ({
|
export const searchBskyUser = async ({
|
||||||
client,
|
client,
|
||||||
|
@ -4,6 +4,9 @@ import { ToastContainer, toast } from "react-toastify";
|
|||||||
import useConfirm from "~lib/components/ConfirmDialog";
|
import useConfirm from "~lib/components/ConfirmDialog";
|
||||||
import Sidebar from "~lib/components/Sidebar";
|
import Sidebar from "~lib/components/Sidebar";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
|
||||||
|
import React from "react";
|
||||||
|
import ReSearchModal from "~components/ReSearchModal";
|
||||||
import DetectedUserListItem from "~lib/components/DetectedUserListItem";
|
import DetectedUserListItem from "~lib/components/DetectedUserListItem";
|
||||||
|
|
||||||
const Option = () => {
|
const Option = () => {
|
||||||
@ -18,6 +21,10 @@ const Option = () => {
|
|||||||
importList,
|
importList,
|
||||||
followAll,
|
followAll,
|
||||||
blockAll,
|
blockAll,
|
||||||
|
reSearch,
|
||||||
|
reSearchResults,
|
||||||
|
changeDetectedUser,
|
||||||
|
clearReSearchResults,
|
||||||
} = useBskyUserManager();
|
} = useBskyUserManager();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -98,6 +105,37 @@ const Option = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [showReSearchModal, setShowReSearchModal] = React.useState(false);
|
||||||
|
const handleReSearch = async (user: {
|
||||||
|
sourceDid: string;
|
||||||
|
accountName: string;
|
||||||
|
displayName: string;
|
||||||
|
}) => {
|
||||||
|
reSearch({
|
||||||
|
sourceDid: user.sourceDid,
|
||||||
|
accountName: user.accountName,
|
||||||
|
displayName: user.displayName,
|
||||||
|
});
|
||||||
|
setShowReSearchModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickReSearchResult = ({
|
||||||
|
sourceDid,
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
sourceDid: string;
|
||||||
|
user: ProfileView;
|
||||||
|
}) => {
|
||||||
|
changeDetectedUser(sourceDid, user);
|
||||||
|
setShowReSearchModal(false);
|
||||||
|
clearReSearchResults();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseReSearchModal = () => {
|
||||||
|
setShowReSearchModal(false);
|
||||||
|
clearReSearchResults();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
@ -126,11 +164,18 @@ const Option = () => {
|
|||||||
user={user}
|
user={user}
|
||||||
clickAction={handleClickAction}
|
clickAction={handleClickAction}
|
||||||
actionMode={actionMode}
|
actionMode={actionMode}
|
||||||
|
reSearch={handleReSearch}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ReSearchModal
|
||||||
|
open={showReSearchModal}
|
||||||
|
onClose={handleCloseReSearchModal}
|
||||||
|
reSearchResults={reSearchResults}
|
||||||
|
handleClickReSearchResult={handleClickReSearchResult}
|
||||||
|
/>
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="top-right"
|
position="top-right"
|
||||||
autoClose={5000}
|
autoClose={5000}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user