From 1bac2981117cfcb590551002fdff02c126b05f2e Mon Sep 17 00:00:00 2001 From: celine Date: Tue, 27 Jan 2026 22:12:54 -0500 Subject: [PATCH] oops forgot the new files --- .../[rkey]/Interactions/recommendAction.ts | 134 ++++++++++++++++++ components/Icons/RecommendTiny.tsx | 37 +++++ components/RecommendButton.tsx | 69 +++++++++ 3 files changed, 240 insertions(+) create mode 100644 app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts create mode 100644 components/Icons/RecommendTiny.tsx create mode 100644 components/RecommendButton.tsx diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts new file mode 100644 index 00000000..8f3a2eda --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts @@ -0,0 +1,134 @@ +"use server"; + +import { AtpBaseClient, PubLeafletInteractionsRecommend } from "lexicons/api"; +import { getIdentityData } from "actions/getIdentityData"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; +import { TID } from "@atproto/common"; +import { AtUri, Un$Typed } from "@atproto/api"; +import { supabaseServerClient } from "supabase/serverClient"; +import { Json } from "supabase/database.types"; + +type RecommendResult = + | { success: true; uri: string } + | { + success: false; + error: OAuthSessionError | { type: string; message: string }; + }; + +export async function recommendAction(args: { + document: string; +}): Promise { + console.log("recommend action..."); + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + console.log("recommended"); + + return { + success: false, + error: { + type: "oauth_session_expired", + message: "Not authenticated", + }, + }; + } + + const sessionResult = await restoreOAuthSession(identity.atp_did); + if (!sessionResult.ok) { + return { success: false, error: sessionResult.error }; + } + let credentialSession = sessionResult.value; + let agent = new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession), + ); + + let record: Un$Typed = { + subject: args.document, + createdAt: new Date().toISOString(), + }; + + let rkey = TID.nextStr(); + let uri = AtUri.make( + credentialSession.did!, + "pub.leaflet.interactions.recommend", + rkey, + ); + + await agent.pub.leaflet.interactions.recommend.create( + { rkey, repo: credentialSession.did! }, + record, + ); + + await supabaseServerClient.from("recommends_on_documents").upsert({ + uri: uri.toString(), + document: args.document, + recommender_did: credentialSession.did!, + record: { + $type: "pub.leaflet.interactions.recommend", + ...record, + } as unknown as Json, + }); + + return { + success: true, + uri: uri.toString(), + }; +} + +export async function unrecommendAction(args: { + document: string; +}): Promise { + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + return { + success: false, + error: { + type: "oauth_session_expired", + message: "Not authenticated", + }, + }; + } + + const sessionResult = await restoreOAuthSession(identity.atp_did); + if (!sessionResult.ok) { + return { success: false, error: sessionResult.error }; + } + let credentialSession = sessionResult.value; + let agent = new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession), + ); + + // Find the existing recommend record + const { data: existingRecommend } = await supabaseServerClient + .from("recommends_on_documents") + .select("uri") + .eq("document", args.document) + .eq("recommender_did", credentialSession.did!) + .single(); + + if (!existingRecommend) { + return { + success: false, + error: { + type: "not_found", + message: "Recommend not found", + }, + }; + } + + let uri = new AtUri(existingRecommend.uri); + + await agent.pub.leaflet.interactions.recommend.delete({ + rkey: uri.rkey, + repo: credentialSession.did!, + }); + + await supabaseServerClient + .from("recommends_on_documents") + .delete() + .eq("uri", existingRecommend.uri); + + return { + success: true, + uri: existingRecommend.uri, + }; +} diff --git a/components/Icons/RecommendTiny.tsx b/components/Icons/RecommendTiny.tsx new file mode 100644 index 00000000..7ec07c14 --- /dev/null +++ b/components/Icons/RecommendTiny.tsx @@ -0,0 +1,37 @@ +import { Props } from "./Props"; + +export const RecommendTinyFilled = (props: Props) => { + return ( + + + + ); +}; + +export const RecommendTinyEmpty = (props: Props) => { + return ( + + + + ); +}; diff --git a/components/RecommendButton.tsx b/components/RecommendButton.tsx new file mode 100644 index 00000000..1bab147e --- /dev/null +++ b/components/RecommendButton.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; +import { RecommendTinyEmpty, RecommendTinyFilled } from "./Icons/RecommendTiny"; +import { + recommendAction, + unrecommendAction, +} from "app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction"; + +export function RecommendButton(props: { + documentUri: string; + recommendsCount: number; + hasRecommended: boolean; + className?: string; + showCount?: boolean; +}) { + const [hasRecommended, setHasRecommended] = useState(props.hasRecommended); + const [count, setCount] = useState(props.recommendsCount); + const [isPending, setIsPending] = useState(false); + + const handleClick = async () => { + if (isPending) return; + + const currentlyRecommended = hasRecommended; + setIsPending(true); + setHasRecommended(!currentlyRecommended); + setCount((c) => (currentlyRecommended ? c - 1 : c + 1)); + + try { + if (currentlyRecommended) { + await unrecommendAction({ document: props.documentUri }); + } else { + await recommendAction({ document: props.documentUri }); + } + } catch (error) { + // Revert on error + setHasRecommended(currentlyRecommended); + setCount((c) => (currentlyRecommended ? c + 1 : c - 1)); + } finally { + setIsPending(false); + } + }; + + const showCount = props.showCount !== false; + + return ( + + ); +} -- 2.51.2