diff --git a/src/lib/bskyHelpers.ts b/src/lib/bskyHelpers.ts index 7cd14a3..056f2b0 100644 --- a/src/lib/bskyHelpers.ts +++ b/src/lib/bskyHelpers.ts @@ -1,19 +1,40 @@ import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs" +import { BSKY_USER_MATCH_TYPE } from "./constants" -export const isSimilarUser = (terms: string[], bskyProfile: ProfileView | undefined) => { - if(!bskyProfile) { return false } +export const isSimilarUser = (terms: string[], bskyProfile: ProfileView | undefined): { + isSimilar: boolean, + type: typeof BSKY_USER_MATCH_TYPE[keyof typeof BSKY_USER_MATCH_TYPE], +} => { + if (!bskyProfile) { + return { + isSimilar: false, + type: BSKY_USER_MATCH_TYPE.NONE, + } + } - return terms.some(term => { + for (const term of terms) { const lowerCaseName = term.toLocaleLowerCase() - if(lowerCaseName === bskyProfile?.handle.toLocaleLowerCase().replace("@", "").split('.')[0]) { - return true + if (lowerCaseName === bskyProfile?.handle.toLocaleLowerCase().replace("@", "").split('.')[0]) { + return { + isSimilar: true, + type: BSKY_USER_MATCH_TYPE.HANDLE, + } } - if(lowerCaseName === bskyProfile.displayName?.toLocaleLowerCase()) { - return true + if (lowerCaseName === bskyProfile.displayName?.toLocaleLowerCase()) { + return { + isSimilar: true, + type: BSKY_USER_MATCH_TYPE.DISPLAY_NAME, + } } - if(bskyProfile.description?.toLocaleLowerCase().includes(lowerCaseName)) { - return true + if (bskyProfile.description?.toLocaleLowerCase().includes(lowerCaseName)) { + return { + isSimilar: true, + type: BSKY_USER_MATCH_TYPE.DESCRIPTION, + } } - return false - }) + } + return { + isSimilar: false, + type: BSKY_USER_MATCH_TYPE.NONE, + } } diff --git a/src/lib/components/BskyUserCell.ts b/src/lib/components/BskyUserCell.ts index ec9d991..b57bf33 100644 --- a/src/lib/components/BskyUserCell.ts +++ b/src/lib/components/BskyUserCell.ts @@ -1,8 +1,11 @@ import type { ProfileView, ViewerState } from "@atproto/api/dist/client/types/app/bsky/actor/defs" +import { P, match } from "ts-pattern" import van from 'vanjs-core' +import { BSKY_USER_MATCH_TYPE } from "~lib/constants" const { a, div, p, img, button, span } = van.tags +const { svg, path } = van.tagsNS("http://www.w3.org/2000/svg") export type UserCellBtnLabel = { add: string, @@ -79,48 +82,76 @@ const Avatar = ({ avatar }: { avatar?: string }) => { return avatar ? img({ src: avatar, width: "40" }) : div({ class: "no-avatar" }) } +const MatchTypeLabel = ({ matchType }: { matchType: typeof BSKY_USER_MATCH_TYPE[keyof typeof BSKY_USER_MATCH_TYPE] }) => { + const [text, labelClass] = match(matchType) + .with( + BSKY_USER_MATCH_TYPE.HANDLE, + () => ["Same handle", "match-type__handle"] + ) + .with( + BSKY_USER_MATCH_TYPE.DISPLAY_NAME, + () => ["Same display name", "match-type__display-name"] + ) + .with( + BSKY_USER_MATCH_TYPE.DESCRIPTION, + () => ["Included handle or display name in description", "match-type__description"] + ) + .run() + + return div({ class: `match-type ${labelClass}` }, + svg({ fill: "none", width: "12", viewBox: "0 0 24 24", "stroke-width": "3", stroke: "currentColor", class: "w-6 h-6" }, + path({ "stroke-linecap": "round", "stroke-linejoin": "round", "d": "M4.5 12.75l6 6 9-13.5" }), + ), + text + ) +} + export const BskyUserCell = ({ profile, statusKey, btnLabel, + matchType, addAction, removeAction, }: { profile: ProfileView, statusKey: keyof ViewerState, btnLabel: UserCellBtnLabel, + matchType: typeof BSKY_USER_MATCH_TYPE[keyof typeof BSKY_USER_MATCH_TYPE], addAction: () => Promise, removeAction: () => Promise }) => { - return div({ class: "bsky-user-content" }, - div({ class: "icon-section" }, - a({ href: `https://bsky.app/profile/${profile.handle}`, target: "_blank", rel: "noopener" }, - Avatar({ avatar: profile.avatar }), + return div({ class: "bsky-user-content-wrapper" }, + MatchTypeLabel({ matchType }), + div({ class: "bsky-user-content" }, + div({ class: "icon-section" }, + a({ href: `https://bsky.app/profile/${profile.handle}`, target: "_blank", rel: "noopener" }, + Avatar({ avatar: profile.avatar }), + ), ), - ), - div({ class: "content" }, - div({ class: "name-and-controller" }, - div( - p({ class: "display-name" }, - a({ href: `https://bsky.app/profile/${profile.handle}`, target: "_blank", rel: "noopener" }, - profile.displayName ?? profile.handle, + div({ class: "content" }, + div({ class: "name-and-controller" }, + div( + p({ class: "display-name" }, + a({ href: `https://bsky.app/profile/${profile.handle}`, target: "_blank", rel: "noopener" }, + profile.displayName ?? profile.handle, + ), + ), + p({ class: "handle" }, + `@${profile.handle}`, ), ), - p({ class: "handle" }, - `@${profile.handle}`, + div( + ActionButton({ + profile, + statusKey, + btnLabel, + addAction, + removeAction, + }) ), ), - div( - ActionButton({ - profile, - statusKey, - btnLabel, - addAction, - removeAction, - }) - ), + profile.description ? p({ class: "description" }, profile.description) : "", ), - profile.description ? p({ class: "description" }, profile.description) : "", - ), - ) + )) } diff --git a/src/lib/components/NotFoundCell.ts b/src/lib/components/NotFoundCell.ts index c5cd0e6..1c53dae 100644 --- a/src/lib/components/NotFoundCell.ts +++ b/src/lib/components/NotFoundCell.ts @@ -4,7 +4,7 @@ import van from "vanjs-core" const { div, p } = van.tags const { svg, path } = van.tagsNS("http://www.w3.org/2000/svg") -const WarningIcon = () => svg( +const WarningIcon = () => svg( { fill: "none", "stroke-width": "1.5", @@ -19,11 +19,12 @@ const WarningIcon = () => svg( }), ) -export const NotFoundCell = () => div({ class: "bsky-user-content bsky-user-content__not-found" }, - WarningIcon(), - p({ - class: "not-found" - }, - "No similar users found." - ) -) +export const NotFoundCell = () => div({ class: "bsky-user-content-wrapper" }, + div({ class: "bsky-user-content bsky-user-content__not-found" }, + WarningIcon(), + p({ + class: "not-found" + }, + "No similar users found." + ) + )) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9a05a9d..e804178 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -23,3 +23,11 @@ export const VIEWER_STATE = { BLOCKING: "blocking", FOLLOWING: "following", } as const + + +export const BSKY_USER_MATCH_TYPE = { + HANDLE: "handle", + DISPLAY_NAME: "display_name", + DESCRIPTION: "description", + NONE: "none", +} as const diff --git a/src/lib/domHelpers.ts b/src/lib/domHelpers.ts index b1b1b2e..832666a 100644 --- a/src/lib/domHelpers.ts +++ b/src/lib/domHelpers.ts @@ -3,6 +3,7 @@ import van from "vanjs-core" import { ReloadButton } from "./components/ReloadBtn" import { NotFoundCell } from "./components/NotFoundCell" import { BskyUserCell, type UserCellBtnLabel } from "./components/BskyUserCell" +import type { BSKY_USER_MATCH_TYPE } from "./constants" export const getUserCells = ({ queryParam, filterInsertedElement }: { queryParam: string, filterInsertedElement: boolean }) => { const userCells = document.querySelectorAll(queryParam); @@ -12,7 +13,7 @@ export const getUserCells = ({ queryParam, filterInsertedElement }: { queryParam return Array.from(userCells).filter((userCell) => { const nextElement = userCell.nextElementSibling if (!nextElement) { return true } - return nextElement.classList.contains("bsky-user-content") === false + return nextElement.classList.contains("bsky-user-content-wrapper") === false }) } else { return Array.from(userCells) @@ -32,11 +33,12 @@ export const getAccountNameAndDisplayName = (userCell: Element) => { return { twAccountName, twDisplayName, twAccountNameRemoveUnderscore } } -export const insertBskyProfileEl = ({ dom, profile, statusKey, btnLabel, addAction, removeAction }: { +export const insertBskyProfileEl = ({ dom, profile, statusKey, btnLabel, matchType, addAction, removeAction }: { dom: Element, profile: ProfileView, statusKey: keyof ViewerState, btnLabel: UserCellBtnLabel, + matchType: typeof BSKY_USER_MATCH_TYPE[keyof typeof BSKY_USER_MATCH_TYPE], addAction: () => Promise, removeAction: () => Promise }) => { @@ -44,6 +46,7 @@ export const insertBskyProfileEl = ({ dom, profile, statusKey, btnLabel, addActi profile, statusKey, btnLabel, + matchType, addAction, removeAction, })) diff --git a/src/lib/searchAndInsertBskyUsers.ts b/src/lib/searchAndInsertBskyUsers.ts index 8305b38..fe393ac 100644 --- a/src/lib/searchAndInsertBskyUsers.ts +++ b/src/lib/searchAndInsertBskyUsers.ts @@ -55,6 +55,7 @@ export const searchAndInsertBskyUsers = async ( ] let targetAccount = null + let matchType = null // Loop over search parameters and break if a user is found for (const term of searchTerms) { @@ -63,7 +64,7 @@ export const searchAndInsertBskyUsers = async ( limit: 1, }) - const isUserFound = isSimilarUser([ + const { isSimilar: isUserFound, type } = isSimilarUser([ twAccountName, twAccountNameRemoveUnderscore, twDisplayName, @@ -71,6 +72,7 @@ export const searchAndInsertBskyUsers = async ( if (isUserFound) { targetAccount = searchResult + matchType = type break; // Stop searching when a user is found } } @@ -82,6 +84,7 @@ export const searchAndInsertBskyUsers = async ( profile: targetAccount, statusKey, btnLabel, + matchType, addAction: async () => { const result = await addQuery(targetAccount.did); bskyUserUrlMap.set(targetAccount.did, result.uri) diff --git a/src/style.content.css b/src/style.content.css index 47ef8ee..825bb56 100644 --- a/src/style.content.css +++ b/src/style.content.css @@ -6,6 +6,29 @@ --bsky-primary-hover-color: #2563eb; } +.bsky-user-content-wrapper .match-type { + color: var(--bsky-primary-color); + padding: 2px 14px 2px 14px; + font-size: 10px; + font-weight: bold; + width: fit-content; + display: flex; + gap: 4px; + border-radius: 10px 10px 0px 0px; +} + +.bsky-user-content-wrapper .match-type.match-type__handle { + background-color: #ffd700; +} + +.bsky-user-content-wrapper .match-type.match-type__display-name { + background-color: #FFA07A; +} + +.bsky-user-content-wrapper .match-type.match-type__description { + background-color: #D3D3D3; +} + .bsky-user-content { background: rgb(2,0,36); background: var(--bsky-primary-color);