From 716bf3459f5e84ddc556c5d3bab25cc0b3f6e522 Mon Sep 17 00:00:00 2001 From: ZainebPadilla Date: Thu, 15 May 2025 19:15:14 +0200 Subject: [PATCH 01/12] add notification effect on the floating button --- src/contents/plasmo-inline.tsx | 91 ++++++++++++++++--- src/graphql/src/apollo-subscription-client.ts | 62 +++++++++++++ src/hooks/useEventNotification.ts | 25 +++++ 3 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 src/graphql/src/apollo-subscription-client.ts create mode 100644 src/hooks/useEventNotification.ts diff --git a/src/contents/plasmo-inline.tsx b/src/contents/plasmo-inline.tsx index 63b466c2..4f9ffc10 100644 --- a/src/contents/plasmo-inline.tsx +++ b/src/contents/plasmo-inline.tsx @@ -2,6 +2,10 @@ import type { PlasmoCSConfig, PlasmoGetInlineAnchor } from "plasmo" import { useEffect, useRef, useState } from "react" import IntuitionSearchIcon from "~src/components/icons/IntuitionSearchBar" +import { ApolloProvider, useSubscription } from "@apollo/client" +import { apolloSubscriptionClient } from "~src/graphql/src/apollo-subscription-client" +import { EventsDocument } from "~src/graphql/src/generated/subscriptions" + export const config: PlasmoCSConfig = { matches: ["https://*/*"] } @@ -11,10 +15,31 @@ export const getInlineAnchor: PlasmoGetInlineAnchor = () => export const getShadowHostId = () => "plasmo-inline-example-unique-id" -function PlasmoInline() { +export default function Wrapper() { + return ( + + + + ) +} + +export function PlasmoInline() { const [positionY, setPositionY] = useState(50) + const [hasNotification, setHasNotification] = useState(false) const draggingRef = useRef(false) + const { data } = useSubscription(EventsDocument, { + variables: { addresses: [], limit: 1 } + }) + + useEffect(() => { + const latest = data?.events?.[0] + const type = latest?.type?.toLowerCase?.() + if (type?.includes("claim") || type?.includes("atom")) { + setHasNotification(true) + } + }, [data]) + const handleMouseDown = (e: React.MouseEvent) => { const startY = e.clientY const startPositionY = positionY @@ -25,7 +50,6 @@ function PlasmoInline() { if (Math.abs(deltaY) > 5) { draggingRef.current = true } - if (draggingRef.current) { const newY = startPositionY + (deltaY / window.innerHeight) * 100 setPositionY(Math.min(90, Math.max(0, newY))) @@ -35,7 +59,6 @@ function PlasmoInline() { const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("mouseup", handleMouseUp) - if (!draggingRef.current) { handleSidePanel() } @@ -46,6 +69,7 @@ function PlasmoInline() { } const handleSidePanel = () => { + setHasNotification(false) chrome.runtime.sendMessage({ type: "open_sidepanel" }) } @@ -74,15 +98,60 @@ function PlasmoInline() { e.currentTarget.style.opacity = "0.2" }} > - {}} - size={35} - position={{ x: 0, y: 0 }} - className="hover:opacity-80 transition-opacity" - /> +
+ {}} + size={35} + position={{ x: 0, y: 0 }} + className="hover:opacity-80 transition-opacity" + /> + {hasNotification && ( + + + + + )} +
+ + {/* Inline keyframe for the ping animation */} + ) } - -export default PlasmoInline diff --git a/src/graphql/src/apollo-subscription-client.ts b/src/graphql/src/apollo-subscription-client.ts new file mode 100644 index 00000000..4e30d9e5 --- /dev/null +++ b/src/graphql/src/apollo-subscription-client.ts @@ -0,0 +1,62 @@ +import { ApolloClient, InMemoryCache, split } from "@apollo/client" +import { GraphQLWsLink } from "@apollo/client/link/subscriptions" +import { createClient } from "graphql-ws" +import { getMainDefinition } from "@apollo/client/utilities" +import { HttpLink } from "@apollo/client" + + +const ENV = "dev" // "local" | "dev" | "prod" + +const ENDPOINTS = { + local: { + http: "http://localhost:8080/v1/graphql", + ws: "ws://localhost:8080/v1/graphql" + }, + dev: { + http: "https://prod.base-sepolia.intuition-api.com/v1/graphql", + ws: "wss://prod.base-sepolia.intuition-api.com/v1/graphql" + }, + prod: { + http: "https://prod.base.intuition-api.com/v1/graphql", + ws: "wss://prod.base.intuition-api.com/v1/graphql" + } +} + +const { http, ws } = ENDPOINTS[ENV] + +const httpLink = new HttpLink({ + uri: http, + headers: { + "Content-Type": "application/json" + + } +}) + +const wsLink = new GraphQLWsLink( + createClient({ + url: ws, + connectionParams: { + headers: { + + } + } + }) +) + + +const splitLink = split( + ({ query }) => { + const definition = getMainDefinition(query) + return ( + definition.kind === "OperationDefinition" && + definition.operation === "subscription" + ) + }, + wsLink, + httpLink +) + +export const apolloSubscriptionClient = new ApolloClient({ + link: splitLink, + cache: new InMemoryCache() +}) diff --git a/src/hooks/useEventNotification.ts b/src/hooks/useEventNotification.ts new file mode 100644 index 00000000..c7652ee3 --- /dev/null +++ b/src/hooks/useEventNotification.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from "react" +import { useSubscription } from "@apollo/client" +import { EventsDocument } from "~src/graphql/src/generated/subscriptions" + +export const useEventNotification = () => { + const [hasNotification, setHasNotification] = useState(false) + + const { data } = useSubscription(EventsDocument, { + variables: { + addresses: [], + limit: 1 + } + }) + + useEffect(() => { + const event = data?.events?.[0] + const type = event?.type?.toLowerCase?.() + if (type?.includes("claim") || type?.includes("atom")) { + + setHasNotification(true) + } + }, [data]) + + return { hasNotification, reset: () => setHasNotification(false) } +} From 6d49f42397c63d40f78f17c28c7a780474ab2406 Mon Sep 17 00:00:00 2001 From: ZainebPadilla Date: Fri, 16 May 2025 10:43:46 +0200 Subject: [PATCH 02/12] notification effect on the floating button fixed --- src/contents/plasmo-inline.tsx | 29 ++-- src/graphql/src/generated/index.ts | 129 +++++++++++++++ src/graphql/src/generated/subscriptions.ts | 155 ++++++++++++++++++ .../subscriptions/follower-activity.graphql | 11 ++ 4 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 src/graphql/src/subscriptions/follower-activity.graphql diff --git a/src/contents/plasmo-inline.tsx b/src/contents/plasmo-inline.tsx index 4f9ffc10..cdfd8f47 100644 --- a/src/contents/plasmo-inline.tsx +++ b/src/contents/plasmo-inline.tsx @@ -4,7 +4,7 @@ import IntuitionSearchIcon from "~src/components/icons/IntuitionSearchBar" import { ApolloProvider, useSubscription } from "@apollo/client" import { apolloSubscriptionClient } from "~src/graphql/src/apollo-subscription-client" -import { EventsDocument } from "~src/graphql/src/generated/subscriptions" +import { FollowerActivityDocument } from "~src/graphql/src/generated/subscriptions" export const config: PlasmoCSConfig = { matches: ["https://*/*"] @@ -15,6 +15,8 @@ export const getInlineAnchor: PlasmoGetInlineAnchor = () => export const getShadowHostId = () => "plasmo-inline-example-unique-id" +const FOLLOWER_IDS = ["0xd01cd97bf00bddfbccf0a79a2a579e8add6ac4f8"] + export default function Wrapper() { return ( @@ -28,14 +30,20 @@ export function PlasmoInline() { const [hasNotification, setHasNotification] = useState(false) const draggingRef = useRef(false) - const { data } = useSubscription(EventsDocument, { - variables: { addresses: [], limit: 1 } + const { data } = useSubscription(FollowerActivityDocument, { + variables: { limit: 1 } }) useEffect(() => { - const latest = data?.events?.[0] - const type = latest?.type?.toLowerCase?.() - if (type?.includes("claim") || type?.includes("atom")) { + const event = data?.events?.[0] + const actorId = event?.account?.id + const type = event?.type + + if ( + actorId && + FOLLOWER_IDS.includes(actorId) && + (type === "ClaimCreated" || type === "AtomCreated") + ) { setHasNotification(true) } }, [data]) @@ -143,13 +151,12 @@ export function PlasmoInline() { - {/* Inline keyframe for the ping animation */} diff --git a/src/graphql/src/generated/index.ts b/src/graphql/src/generated/index.ts index 75357c18..3789d7bb 100644 --- a/src/graphql/src/generated/index.ts +++ b/src/graphql/src/generated/index.ts @@ -13925,6 +13925,15 @@ export type EventsSubscription = { }> } +export type FollowerActivitySubscriptionVariables = Exact<{ + limit: Scalars["Int"]["input"] +}> + +export type FollowerActivitySubscription = { + __typename?: "subscription_root" + events: Array<{ __typename?: "events"; type: any; block_timestamp: any }> +} + export const AccountClaimsAggregateFragmentDoc = ` fragment AccountClaimsAggregate on accounts { claims_aggregate(order_by: {shares: desc}) { @@ -18627,6 +18636,18 @@ ${VaultDetailsWithFilteredPositionsFragmentDoc} ${VaultBasicDetailsFragmentDoc} ${VaultFilteredPositionsFragmentDoc} ${PositionFieldsFragmentDoc}` +export const FollowerActivityDocument = ` + subscription FollowerActivity($limit: Int!) { + events( + where: {type: {_in: ["ClaimCreated", "AtomCreated"]}} + order_by: [{block_number: desc}] + limit: $limit + ) { + type + block_timestamp + } +} + ` export const AccountClaimsAggregate = { kind: "Document", definitions: [ @@ -36603,3 +36624,111 @@ export const Events = { } ] } as unknown as DocumentNode +export const FollowerActivity = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "subscription", + name: { kind: "Name", value: "FollowerActivity" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "limit" } + }, + type: { + kind: "NonNullType", + type: { kind: "NamedType", name: { kind: "Name", value: "Int" } } + } + } + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "events" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "where" }, + value: { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "type" }, + value: { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "_in" }, + value: { + kind: "ListValue", + values: [ + { + kind: "StringValue", + value: "ClaimCreated", + block: false + }, + { + kind: "StringValue", + value: "AtomCreated", + block: false + } + ] + } + } + ] + } + } + ] + } + }, + { + kind: "Argument", + name: { kind: "Name", value: "order_by" }, + value: { + kind: "ListValue", + values: [ + { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "block_number" }, + value: { kind: "EnumValue", value: "desc" } + } + ] + } + ] + } + }, + { + kind: "Argument", + name: { kind: "Name", value: "limit" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "limit" } + } + } + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "type" } }, + { + kind: "Field", + name: { kind: "Name", value: "block_timestamp" } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode diff --git a/src/graphql/src/generated/subscriptions.ts b/src/graphql/src/generated/subscriptions.ts index 7893be46..e2964c13 100644 --- a/src/graphql/src/generated/subscriptions.ts +++ b/src/graphql/src/generated/subscriptions.ts @@ -13945,6 +13945,15 @@ export type EventsSubscription = { }> } +export type FollowerActivitySubscriptionVariables = Exact<{ + limit: Scalars["Int"]["input"] +}> + +export type FollowerActivitySubscription = { + __typename?: "subscription_root" + events: Array<{ __typename?: "events"; type: any; block_timestamp: any }> +} + export const AccountClaimsAggregateFragmentDoc = { kind: "Document", definitions: [ @@ -33994,3 +34003,149 @@ export type EventsSubscriptionHookResult = ReturnType< > export type EventsSubscriptionResult = Apollo.SubscriptionResult +export const FollowerActivityDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "subscription", + name: { kind: "Name", value: "FollowerActivity" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "limit" } + }, + type: { + kind: "NonNullType", + type: { kind: "NamedType", name: { kind: "Name", value: "Int" } } + } + } + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "events" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "where" }, + value: { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "type" }, + value: { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "_in" }, + value: { + kind: "ListValue", + values: [ + { + kind: "StringValue", + value: "ClaimCreated", + block: false + }, + { + kind: "StringValue", + value: "AtomCreated", + block: false + } + ] + } + } + ] + } + } + ] + } + }, + { + kind: "Argument", + name: { kind: "Name", value: "order_by" }, + value: { + kind: "ListValue", + values: [ + { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "block_number" }, + value: { kind: "EnumValue", value: "desc" } + } + ] + } + ] + } + }, + { + kind: "Argument", + name: { kind: "Name", value: "limit" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "limit" } + } + } + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "type" } }, + { + kind: "Field", + name: { kind: "Name", value: "block_timestamp" } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode + +/** + * __useFollowerActivitySubscription__ + * + * To run a query within a React component, call `useFollowerActivitySubscription` and pass it any options that fit your needs. + * When your component renders, `useFollowerActivitySubscription` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useFollowerActivitySubscription({ + * variables: { + * limit: // value for 'limit' + * }, + * }); + */ +export function useFollowerActivitySubscription( + baseOptions: Apollo.SubscriptionHookOptions< + FollowerActivitySubscription, + FollowerActivitySubscriptionVariables + > & + ( + | { variables: FollowerActivitySubscriptionVariables; skip?: boolean } + | { skip: boolean } + ) +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useSubscription< + FollowerActivitySubscription, + FollowerActivitySubscriptionVariables + >(FollowerActivityDocument, options) +} +export type FollowerActivitySubscriptionHookResult = ReturnType< + typeof useFollowerActivitySubscription +> +export type FollowerActivitySubscriptionResult = + Apollo.SubscriptionResult diff --git a/src/graphql/src/subscriptions/follower-activity.graphql b/src/graphql/src/subscriptions/follower-activity.graphql new file mode 100644 index 00000000..6318e607 --- /dev/null +++ b/src/graphql/src/subscriptions/follower-activity.graphql @@ -0,0 +1,11 @@ +subscription FollowerActivity($limit: Int!) { + events( + where: { type: { _in: ["ClaimCreated", "AtomCreated"] } } + order_by: [{ block_number: desc }] + limit: $limit + ) { + type + + block_timestamp + } +} From b46507c7881b963dd5c00214a1811c545d6a1396 Mon Sep 17 00:00:00 2001 From: ZainebPadilla Date: Fri, 16 May 2025 11:31:19 +0200 Subject: [PATCH 03/12] add comment to explain the plasmo-inline.ts --- src/contents/plasmo-inline.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/contents/plasmo-inline.tsx b/src/contents/plasmo-inline.tsx index cdfd8f47..db940161 100644 --- a/src/contents/plasmo-inline.tsx +++ b/src/contents/plasmo-inline.tsx @@ -6,17 +6,23 @@ import { ApolloProvider, useSubscription } from "@apollo/client" import { apolloSubscriptionClient } from "~src/graphql/src/apollo-subscription-client" import { FollowerActivityDocument } from "~src/graphql/src/generated/subscriptions" +// This config tells Plasmo to inject this content script on all HTTPS pages export const config: PlasmoCSConfig = { matches: ["https://*/*"] } +// Anchors the shadow DOM to the element export const getInlineAnchor: PlasmoGetInlineAnchor = () => document.querySelector("body") +// A unique shadow host ID for Plasmo to scope this component export const getShadowHostId = () => "plasmo-inline-example-unique-id" +// Static list of account IDs to treat as "followers" +// When any of these perform an action, a notification is triggered const FOLLOWER_IDS = ["0xd01cd97bf00bddfbccf0a79a2a579e8add6ac4f8"] +// Top-level wrapper that provides Apollo context to the floating UI export default function Wrapper() { return ( @@ -25,20 +31,24 @@ export default function Wrapper() { ) } +// Main floating button component export function PlasmoInline() { - const [positionY, setPositionY] = useState(50) - const [hasNotification, setHasNotification] = useState(false) + const [positionY, setPositionY] = useState(50) // Y-axis position of the button + const [hasNotification, setHasNotification] = useState(false) // Whether to show the badge const draggingRef = useRef(false) + // Subscribes to GraphQL real-time events using the generated subscription const { data } = useSubscription(FollowerActivityDocument, { variables: { limit: 1 } }) + // Reacts to incoming subscription data useEffect(() => { const event = data?.events?.[0] const actorId = event?.account?.id const type = event?.type + // Check if the actor is a follower and if they created a claim or atom if ( actorId && FOLLOWER_IDS.includes(actorId) && @@ -48,6 +58,7 @@ export function PlasmoInline() { } }, [data]) + // Handle mouse drag interaction for repositioning the button vertically const handleMouseDown = (e: React.MouseEvent) => { const startY = e.clientY const startPositionY = positionY @@ -67,6 +78,7 @@ export function PlasmoInline() { const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("mouseup", handleMouseUp) + if (!draggingRef.current) { handleSidePanel() } @@ -76,6 +88,7 @@ export function PlasmoInline() { window.addEventListener("mouseup", handleMouseUp) } + // Opens the side panel and clears the notification badge const handleSidePanel = () => { setHasNotification(false) chrome.runtime.sendMessage({ type: "open_sidepanel" }) @@ -107,12 +120,14 @@ export function PlasmoInline() { }} >
+ {/* Main button icon */} {}} size={35} position={{ x: 0, y: 0 }} className="hover:opacity-80 transition-opacity" /> + {/* Ping-style animated badge when a notification is active */} {hasNotification && (
+ {/* CSS animation for the ping effect */} + + ) +} + +export default FloatingIconStatus diff --git a/src/contents/plasmo-inline.tsx b/src/contents/plasmo-inline.tsx index 05fe5752..28c7c8d7 100644 --- a/src/contents/plasmo-inline.tsx +++ b/src/contents/plasmo-inline.tsx @@ -8,10 +8,9 @@ import { import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { apolloSubscriptionClient } from "~src/graphql/src/apollo-subscription-client" import { useGetFollowingsFromAddressQuery } from "~src/graphql/src" +import FloatingIconStatus from "~src/components/FloatingIconStatus" import IntuitionSearchIcon from "~src/components/icons/IntuitionSearchBar" -console.log("๐Ÿš€ plasmo-inline.tsx LOADED !") - export const config: PlasmoCSConfig = { matches: ["https://*/*"] } @@ -114,13 +113,14 @@ const FloatingButton = ({ address }: { address: string }) => { onMouseEnter={(e) => (e.currentTarget.style.opacity = "1")} onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.4")} > -
+
{}} size={35} position={{ x: 0, y: 0 }} className="hover:opacity-80 transition-opacity" /> + {hasNotification && ( { )}
-
) } @@ -184,6 +176,8 @@ const Wrapper = () => { }) }, []) + console.log("๐Ÿงช Rendering Wrapper with address:", address) + return ( diff --git a/src/hooks/useClaimDetection.ts b/src/hooks/useClaimDetection.ts new file mode 100644 index 00000000..09256ac1 --- /dev/null +++ b/src/hooks/useClaimDetection.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react" +import { useGetClaimsByUriQuery } from "~src/graphql/src" + +export type Status = "loading" | "found" | "not_found" + +export const useClaimDetection = ( + uri: string, + address?: string +): { status: Status } => { + const [status, setStatus] = useState("loading") + + const { data, isLoading } = useGetClaimsByUriQuery( + { uri, address }, + { enabled: Boolean(uri) } + ) + + useEffect(() => { + if (isLoading) { + setStatus("loading") + } else if (data?.atoms?.length > 0) { + setStatus("found") + } else { + setStatus("not_found") + } + }, [isLoading, data]) + + return { status } +} From d1d66337b76f85dc6aa44d391d3a172ac4b998c1 Mon Sep 17 00:00:00 2001 From: ZainebPadilla Date: Mon, 26 May 2025 16:14:28 +0200 Subject: [PATCH 11/12] colour status linked to real claim or atom --- src/contents/plasmo-inline.tsx | 66 ++++++++++++++++++++++++++++++---- src/hooks/useClaimDetection.ts | 21 ++++++++--- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/contents/plasmo-inline.tsx b/src/contents/plasmo-inline.tsx index 28c7c8d7..e8c8bc39 100644 --- a/src/contents/plasmo-inline.tsx +++ b/src/contents/plasmo-inline.tsx @@ -1,14 +1,14 @@ import type { PlasmoCSConfig, PlasmoGetInlineAnchor } from "plasmo" import { useEffect, useRef, useState } from "react" import { + ApolloProvider, gql, - useSubscription, - ApolloProvider + useSubscription } from "@apollo/client" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { apolloSubscriptionClient } from "~src/graphql/src/apollo-subscription-client" import { useGetFollowingsFromAddressQuery } from "~src/graphql/src" -import FloatingIconStatus from "~src/components/FloatingIconStatus" +import { useClaimDetection } from "~src/hooks/useClaimDetection" import IntuitionSearchIcon from "~src/components/icons/IntuitionSearchBar" export const config: PlasmoCSConfig = { @@ -50,7 +50,11 @@ const FloatingButton = ({ address }: { address: string }) => { variables: { limit: 1 } }) - const followingIds = followData?.following?.map((f) => f.id).filter(Boolean) ?? [] + const claimDetection = useClaimDetection(window.location.href, address) + const status = claimDetection?.status ?? "loading" + + const followingIds = + followData?.following?.map((f) => f.id).filter(Boolean) ?? [] useEffect(() => { const latestEvent = eventData?.events?.[0] @@ -93,6 +97,56 @@ const FloatingButton = ({ address }: { address: string }) => { window.addEventListener("mouseup", handleMouseUp) } + const renderStatusBadge = () => { + const isLoading = status === "loading" + const color = status === "found" + ? "#22c55e" + : status === "not_found" + ? "#ef4444" + : "#3b82f6" + + return ( + + {isLoading && ( + + )} + + + ) + } + return (
{ onMouseEnter={(e) => (e.currentTarget.style.opacity = "1")} onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.4")} > -
+
{}} size={35} position={{ x: 0, y: 0 }} className="hover:opacity-80 transition-opacity" /> - + {renderStatusBadge()} {hasNotification && ( { + + if (isLoading) { setStatus("loading") - } else if (data?.atoms?.length > 0) { - setStatus("found") } else { - setStatus("not_found") + const atoms = data?.atoms ?? [] + + const foundClaim = atoms.some((atom) => { + const subjectCount = + atom?.as_subject_claims_aggregate?.aggregate?.count ?? 0 + const objectCount = + atom?.as_object_claims_aggregate?.aggregate?.count ?? 0 + return subjectCount > 0 || objectCount > 0 + }) + + + setStatus(foundClaim ? "found" : "not_found") } - }, [isLoading, data]) + }, [isLoading, data, uri, address]) return { status } } From b8c83baaa933a4f8474f667c6c970647b3df9814 Mon Sep 17 00:00:00 2001 From: ZainebPadilla Date: Mon, 26 May 2025 16:45:02 +0200 Subject: [PATCH 12/12] Add auto-disappearing status badge on floating button based on claim detection --- ...IconStatus.tsx => FloatingStatusBadge.tsx} | 28 ++++++++-- src/contents/plasmo-inline.tsx | 56 +------------------ 2 files changed, 26 insertions(+), 58 deletions(-) rename src/components/{FloatingIconStatus.tsx => FloatingStatusBadge.tsx} (63%) diff --git a/src/components/FloatingIconStatus.tsx b/src/components/FloatingStatusBadge.tsx similarity index 63% rename from src/components/FloatingIconStatus.tsx rename to src/components/FloatingStatusBadge.tsx index ba977bf5..e03fe24c 100644 --- a/src/components/FloatingIconStatus.tsx +++ b/src/components/FloatingStatusBadge.tsx @@ -1,11 +1,29 @@ -import { useClaimDetection } from "~src/hooks/useClaimDetection" +import { useEffect, useState } from "react" +import type { Status } from "~src/hooks/useClaimDetection" -const FloatingIconStatus = () => { - const { status } = useClaimDetection(window.location.href) +const FloatingStatusBadge = ({ status }: { status: Status }) => { + const [visible, setVisible] = useState(true) + + useEffect(() => { + if (status === "loading") { + setVisible(true) + return + } + + setVisible(true) + const timeout = setTimeout(() => setVisible(false), 6000) + return () => clearTimeout(timeout) + }, [status]) + + if (!visible) return null const isLoading = status === "loading" const color = - status === "found" ? "#22c55e" : status === "not_found" ? "#ef4444" : "#3b82f6" + status === "found" + ? "#22c55e" + : status === "not_found" + ? "#ef4444" + : "#3b82f6" return ( { ) } -export default FloatingIconStatus +export default FloatingStatusBadge diff --git a/src/contents/plasmo-inline.tsx b/src/contents/plasmo-inline.tsx index e8c8bc39..62f685d8 100644 --- a/src/contents/plasmo-inline.tsx +++ b/src/contents/plasmo-inline.tsx @@ -10,6 +10,7 @@ import { apolloSubscriptionClient } from "~src/graphql/src/apollo-subscription-c import { useGetFollowingsFromAddressQuery } from "~src/graphql/src" import { useClaimDetection } from "~src/hooks/useClaimDetection" import IntuitionSearchIcon from "~src/components/icons/IntuitionSearchBar" +import FloatingStatusBadge from "~src/components/FloatingStatusBadge" export const config: PlasmoCSConfig = { matches: ["https://*/*"] @@ -50,8 +51,7 @@ const FloatingButton = ({ address }: { address: string }) => { variables: { limit: 1 } }) - const claimDetection = useClaimDetection(window.location.href, address) - const status = claimDetection?.status ?? "loading" + const { status } = useClaimDetection(window.location.href, address) const followingIds = followData?.following?.map((f) => f.id).filter(Boolean) ?? [] @@ -97,56 +97,6 @@ const FloatingButton = ({ address }: { address: string }) => { window.addEventListener("mouseup", handleMouseUp) } - const renderStatusBadge = () => { - const isLoading = status === "loading" - const color = status === "found" - ? "#22c55e" - : status === "not_found" - ? "#ef4444" - : "#3b82f6" - - return ( - - {isLoading && ( - - )} - - - ) - } - return (
{ position={{ x: 0, y: 0 }} className="hover:opacity-80 transition-opacity" /> - {renderStatusBadge()} + {hasNotification && (