From 5d7c1cec883e3ee3b604e7d639c23cc6862ebd0b Mon Sep 17 00:00:00 2001 From: Pouria Delfanazari Date: Wed, 13 Nov 2024 15:36:57 -0800 Subject: [PATCH] Add login support for custom PDSs --- package-lock.json | 130 ++++++++++++++++++ package.json | 1 + src/app/api/auth/identity/actions.ts | 10 ++ .../contentDisplay/feedHeader/FeedHeader.tsx | 9 +- .../contentDisplay/feedItem/FeedItem.tsx | 5 +- src/components/contentDisplay/lists/Lists.tsx | 6 +- .../notification/NotificationContent.tsx | 12 +- .../notification/NotificationPost.tsx | 8 +- src/components/forms/loginForm/LoginForm.tsx | 4 +- src/components/navigational/appBar/AppBar.tsx | 8 +- .../navigational/feedTabs/FeedTabs.tsx | 8 +- src/components/navigational/navbar/Navbar.tsx | 8 +- src/containers/lists/ListMembersContainer.tsx | 8 +- src/containers/lists/ListsContainer.tsx | 4 +- src/containers/posts/UserPostsContainer.tsx | 10 +- src/containers/search/PostSearchContainer.tsx | 10 +- src/containers/search/UserSearchContainer.tsx | 15 +- .../BlockedUsersContainer.tsx | 8 +- .../ContentFilteringContainer.tsx | 6 +- .../homeFeedContainer/HomeFeedContainer.tsx | 4 +- .../MutedUsersContainer.tsx | 8 +- .../myFeedsContainer/MyFeedsContainer.tsx | 8 +- .../ThreadPreferencesContainer.tsx | 4 +- src/containers/thread/LikedByContainer.tsx | 4 +- src/containers/thread/PostThreadContainer.tsx | 4 +- src/containers/thread/QuotesContainer.tsx | 6 +- src/containers/thread/RepostedByContainer.tsx | 5 +- src/containers/users/FollowersContainer.tsx | 8 +- src/containers/users/FollowingContainer.tsx | 9 +- .../users/KnownFollowersContainer.tsx | 4 - src/lib/api/auth/auth.ts | 37 ++--- src/lib/api/bsky/actor/index.ts | 36 ++--- src/lib/api/bsky/agent.ts | 66 ++++++--- src/lib/api/bsky/feed/index.ts | 28 ++-- src/lib/api/bsky/identity/index.ts | 27 ++++ src/lib/api/bsky/list/index.tsx | 10 +- src/lib/consts/general.ts | 1 + src/lib/hooks/bsky/actor/useBlockUser.tsx | 15 +- src/lib/hooks/bsky/actor/usePreferences.tsx | 5 +- src/lib/hooks/bsky/actor/useProfile.tsx | 5 +- src/lib/hooks/bsky/actor/useSearchUsers.tsx | 13 +- src/lib/hooks/bsky/actor/useUpdateProfile.tsx | 4 +- src/lib/hooks/bsky/feed/useDeletePost.tsx | 4 +- src/lib/hooks/bsky/feed/useFeed.tsx | 8 +- src/lib/hooks/bsky/feed/useFeedInfo.tsx | 7 +- src/lib/hooks/bsky/feed/useLike.tsx | 4 +- src/lib/hooks/bsky/feed/useMuteUser.tsx | 5 +- src/lib/hooks/bsky/feed/useProfilePosts.tsx | 9 +- src/lib/hooks/bsky/feed/usePublishPost.tsx | 4 +- src/lib/hooks/bsky/feed/useRepost.tsx | 4 +- src/lib/hooks/bsky/feed/useSaveFeed.tsx | 6 +- src/lib/hooks/bsky/list/useListInfo.tsx | 9 +- .../bsky/notification/useNotification.tsx | 4 +- .../hooks/bsky/social/useKnownFollowers.tsx | 4 +- src/lib/hooks/bsky/useAgent.tsx | 27 ---- src/lib/utils/general.ts | 1 + src/lib/utils/session.ts | 17 +++ types/next-auth.d.ts | 1 + 58 files changed, 455 insertions(+), 240 deletions(-) create mode 100644 src/app/api/auth/identity/actions.ts create mode 100644 src/lib/api/bsky/identity/index.ts delete mode 100644 src/lib/hooks/bsky/useAgent.tsx create mode 100644 src/lib/utils/general.ts create mode 100644 src/lib/utils/session.ts diff --git a/package-lock.json b/package-lock.json index 547f3a50..7ba5cd30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.7.0", "dependencies": { "@atproto/api": "^0.13.11", + "@atproto/identity": "^0.4.3", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", @@ -124,6 +125,26 @@ "zod": "^3.23.8" } }, + "node_modules/@atproto/crypto": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.2.tgz", + "integrity": "sha512-aeOfPQYCDbhn2hV06oBF2KXrWjf/BK4yL8lfANJKSmKl3tKWCkiW/moi643rUXXxSE72KtWtQeqvNFYnnFJ0ig==", + "dependencies": { + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "uint8arrays": "3.0.0" + } + }, + "node_modules/@atproto/identity": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.3.tgz", + "integrity": "sha512-DLXMWh57dHvIeBl+IvC+q20z0IdDZT1awOn84vDyxacL9DfhbiTy/zCUPFEzHyvfrilNG1tDA4zQzURubdFqNg==", + "dependencies": { + "@atproto/common-web": "^0.3.1", + "@atproto/crypto": "^0.4.2", + "axios": "^0.27.2" + } + }, "node_modules/@atproto/lexicon": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.2.tgz", @@ -1291,6 +1312,31 @@ "node": ">= 10" } }, + "node_modules/@noble/curves": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "dependencies": { + "@noble/hashes": "1.5.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3620,6 +3666,11 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/attr-accept": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", @@ -3694,6 +3745,15 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3971,6 +4031,17 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4197,6 +4268,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5128,6 +5207,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5152,6 +5250,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -6253,6 +6364,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index 9e001c71..a08bd9de 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@atproto/api": "^0.13.11", + "@atproto/identity": "^0.4.3", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", diff --git a/src/app/api/auth/identity/actions.ts b/src/app/api/auth/identity/actions.ts new file mode 100644 index 00000000..8126aa78 --- /dev/null +++ b/src/app/api/auth/identity/actions.ts @@ -0,0 +1,10 @@ +"use server"; + +import { getDidFromHandle, getPDS } from "@/lib/api/bsky/identity"; + +export async function getService(handle: string) { + const userDID = await getDidFromHandle(handle); + const service = await getPDS(userDID); + + return service; +} diff --git a/src/components/contentDisplay/feedHeader/FeedHeader.tsx b/src/components/contentDisplay/feedHeader/FeedHeader.tsx index 5f1097d6..0801214c 100644 --- a/src/components/contentDisplay/feedHeader/FeedHeader.tsx +++ b/src/components/contentDisplay/feedHeader/FeedHeader.tsx @@ -11,7 +11,6 @@ import { togglePinFeed, toggleSaveFeed, } from "@/lib/api/bsky/feed"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useRouter } from "next/navigation"; import FeedHeaderSkeleton from "./FeedHeaderSkeleton"; import { useQueryClient } from "@tanstack/react-query"; @@ -21,6 +20,7 @@ import { BiSolidBookmarkAlt } from "react-icons/bi"; import { BiPlus } from "react-icons/bi"; import { BiSolidHeart } from "react-icons/bi"; import Link from "next/link"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { feed: string; @@ -29,7 +29,6 @@ interface Props { export default function FeedHeader(props: Props) { const { feed } = props; const router = useRouter(); - const agent = useAgent(); const [isSaved, setIsSaved] = useState(null); const [isPinned, setIsPinned] = useState(null); const [isLiked, setIsLiked] = useState(null); @@ -54,9 +53,9 @@ export default function FeedHeader(props: Props) { }, [feedInfo]); const toggleSave = async () => { - if (!agent) return; setIsSaved((prev) => !prev); try { + const agent = await getAgentFromClient(); const response = await toggleSaveFeed(agent, feed); if (!response.success) { setIsSaved((prev) => !prev); @@ -70,9 +69,9 @@ export default function FeedHeader(props: Props) { }; const togglePin = async () => { - if (!agent) return; setIsPinned((prev) => !prev); try { + const agent = await getAgentFromClient(); const response = await togglePinFeed(agent, feed); if (!response.success) { setIsPinned((prev) => !prev); @@ -86,7 +85,7 @@ export default function FeedHeader(props: Props) { }; const toggleLike = async () => { - if (!agent) return; + const agent = await getAgentFromClient(); setIsLiked((prev) => !prev); if (!likeUri && feedInfo) { try { diff --git a/src/components/contentDisplay/feedItem/FeedItem.tsx b/src/components/contentDisplay/feedItem/FeedItem.tsx index 4edbc36d..f5e1350b 100644 --- a/src/components/contentDisplay/feedItem/FeedItem.tsx +++ b/src/components/contentDisplay/feedItem/FeedItem.tsx @@ -7,11 +7,11 @@ import { BiPlus, BiSolidHeart, BiSolidTrash } from "react-icons/bi"; import Button from "@/components/actions/button/Button"; import { useState } from "react"; import { useRouter } from "next/navigation"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { toggleSaveFeed } from "@/lib/api/bsky/feed"; import Link from "next/link"; import { useQueryClient } from "@tanstack/react-query"; import { savedFeedsQueryKey } from "@/containers/settings/myFeedsContainer/MyFeedsContainer"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { feedItem: GeneratorView; @@ -23,13 +23,12 @@ export default function FeedItem(props: Props) { const { avatar, displayName, description, likeCount, creator } = feedItem; const [isSaved, setIsSaved] = useState(saved); const router = useRouter(); - const agent = useAgent(); const queryClient = useQueryClient(); const handleSave = async () => { - if (!agent) return; setIsSaved((prev) => !prev); try { + const agent = await getAgentFromClient(); const response = await toggleSaveFeed(agent, feedItem.uri); if (!response.success) { setIsSaved((prev) => !prev); diff --git a/src/components/contentDisplay/lists/Lists.tsx b/src/components/contentDisplay/lists/Lists.tsx index f74b4539..bc17b624 100644 --- a/src/components/contentDisplay/lists/Lists.tsx +++ b/src/components/contentDisplay/lists/Lists.tsx @@ -1,7 +1,6 @@ "use client"; import { getLists } from "@/lib/api/bsky/list"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; import { Fragment } from "react"; @@ -10,9 +9,9 @@ import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import ListsSkeleton from "./ListsSkeleton"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export default function Lists() { - const agent = useAgent(); const { data: session } = useSession(); const { @@ -26,8 +25,9 @@ export default function Lists() { fetchNextPage, } = useInfiniteQuery({ queryKey: ["lists"], - queryFn: ({ pageParam }) => { + queryFn: async ({ pageParam }) => { if (!session?.user.id) return; + const agent = await getAgentFromClient(); return getLists(session.user.id, pageParam, agent); }, initialPageParam: "", diff --git a/src/components/contentDisplay/notification/NotificationContent.tsx b/src/components/contentDisplay/notification/NotificationContent.tsx index a5679fe0..27394c5c 100644 --- a/src/components/contentDisplay/notification/NotificationContent.tsx +++ b/src/components/contentDisplay/notification/NotificationContent.tsx @@ -1,6 +1,5 @@ import { useQuery } from "@tanstack/react-query"; import { ContentFilterResult } from "../../../../types/feed"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { getPost } from "@/lib/api/bsky/feed"; import PostText from "@/components/dataDisplay/postText/postText"; import { AppBskyFeedDefs } from "@atproto/api"; @@ -12,6 +11,7 @@ import { useEffect, useState } from "react"; import PostHider from "@/components/dataDisplay/postHider/PostHider"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import NotificationContentSkeleton from "./NotificationContentSkeleton"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { uri: string; @@ -20,12 +20,14 @@ interface Props { export default function NotificationContnet(props: Props) { const { uri, filter } = props; - const agent = useAgent(); const router = useRouter(); const { status, data, error, isLoading, isFetching } = useQuery({ queryKey: ["notificationContent", uri], - queryFn: () => getPost(agent, uri), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getPost(agent, uri); + }, }); const post = @@ -35,8 +37,8 @@ export default function NotificationContnet(props: Props) { const label = post?.post.labels?.map((l) => l.val)[0] ?? ""; // ex. "nsfw", "suggestive" const embedLabel = post?.post.embed && post?.post.embed.record - ? (post.post.embed.record as ViewRecord)?.labels?.map((l) => l.val)[0] ?? - "" + ? ((post.post.embed.record as ViewRecord)?.labels?.map((l) => l.val)[0] ?? + "") : ""; const message = adultContentFilters.find((f) => f.values.includes(label || embedLabel)) diff --git a/src/components/contentDisplay/notification/NotificationPost.tsx b/src/components/contentDisplay/notification/NotificationPost.tsx index 17a01d67..f287f078 100644 --- a/src/components/contentDisplay/notification/NotificationPost.tsx +++ b/src/components/contentDisplay/notification/NotificationPost.tsx @@ -4,9 +4,9 @@ import FeedPost from "../feedPost/FeedPost"; import { getPost } from "@/lib/api/bsky/feed"; import { AppBskyFeedDefs } from "@atproto/api"; import { ContentFilterResult } from "../../../../types/feed"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useQuery } from "@tanstack/react-query"; import NotificationPostSkeleton from "./NotificationPostSkeleton"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { uri: string; @@ -15,7 +15,6 @@ interface Props { export default function NotificationPost(props: Props) { const { uri, filter } = props; - const agent = useAgent(); const { status, @@ -25,7 +24,10 @@ export default function NotificationPost(props: Props) { isFetching, } = useQuery({ queryKey: ["notificationPost", uri], - queryFn: () => getPost(agent, uri), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getPost(agent, uri); + }, }); return ( diff --git a/src/components/forms/loginForm/LoginForm.tsx b/src/components/forms/loginForm/LoginForm.tsx index 68722944..082b16dd 100644 --- a/src/components/forms/loginForm/LoginForm.tsx +++ b/src/components/forms/loginForm/LoginForm.tsx @@ -87,9 +87,7 @@ export default function LoginForm() { height={50} className="mx-auto mb-3" /> -

- Welcome Back -

+

Welcome

We recommend using an{" "} getUnreadNotificationsCount(agent), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getUnreadNotificationsCount(agent); + }, refetchInterval: 10000, }); diff --git a/src/components/navigational/feedTabs/FeedTabs.tsx b/src/components/navigational/feedTabs/FeedTabs.tsx index 585c9cd1..3a41786a 100644 --- a/src/components/navigational/feedTabs/FeedTabs.tsx +++ b/src/components/navigational/feedTabs/FeedTabs.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import TabItem from "../tabs/TabItem"; import Tabs from "../tabs/Tabs"; import { getSavedFeeds } from "@/lib/api/bsky/feed"; @@ -9,9 +8,9 @@ import FeedTabsSkeleton from "./FeedTabsSkeleton"; import React from "react"; import { useQuery } from "@tanstack/react-query"; import { useScrollContext } from "@/app/providers/scroll"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export default function FeedTabs() { - const agent = useAgent(); const pathname = usePathname(); const searchParams = useSearchParams(); const uri = searchParams.get("uri"); @@ -26,7 +25,10 @@ export default function FeedTabs() { isFetching, } = useQuery({ queryKey: ["savedFeeds"], - queryFn: () => getSavedFeeds(agent), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getSavedFeeds(agent); + }, }); return ( diff --git a/src/components/navigational/navbar/Navbar.tsx b/src/components/navigational/navbar/Navbar.tsx index 19c1bc58..263ac9d3 100644 --- a/src/components/navigational/navbar/Navbar.tsx +++ b/src/components/navigational/navbar/Navbar.tsx @@ -15,19 +15,21 @@ import { PiMagnifyingGlassBold, PiMagnifyingGlassFill } from "react-icons/pi"; import { HiClipboardList, HiOutlineClipboardList } from "react-icons/hi"; import { FaBell, FaRegBell } from "react-icons/fa6"; import { getUnreadNotificationsCount } from "@/lib/api/bsky/notification"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useQuery } from "@tanstack/react-query"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export default function Navbar() { const pathname = usePathname(); - const agent = useAgent(); const { data: notificationsCount, error, isFetching, } = useQuery({ queryKey: ["notificationsCount"], - queryFn: () => getUnreadNotificationsCount(agent), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getUnreadNotificationsCount(agent); + }, refetchInterval: 10000, }); diff --git a/src/containers/lists/ListMembersContainer.tsx b/src/containers/lists/ListMembersContainer.tsx index e54c4034..3c8e85d5 100644 --- a/src/containers/lists/ListMembersContainer.tsx +++ b/src/containers/lists/ListMembersContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import ProfileCardSkeleton from "@/components/contentDisplay/profileCard/ProfileCardSkeleton"; import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; @@ -9,6 +8,7 @@ import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import { getListMembers } from "@/lib/api/bsky/list"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { list: string; @@ -16,7 +16,6 @@ interface Props { export default function ListMembersContainer(props: Props) { const { list } = props; - const agent = useAgent(); const { status, @@ -29,7 +28,10 @@ export default function ListMembersContainer(props: Props) { hasNextPage, } = useInfiniteQuery({ queryKey: ["list members", list], - queryFn: ({ pageParam }) => getListMembers(agent, list, pageParam), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return getListMembers(agent, list, pageParam); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage.cursor, }); diff --git a/src/containers/lists/ListsContainer.tsx b/src/containers/lists/ListsContainer.tsx index 9f9fe167..0c6f54be 100644 --- a/src/containers/lists/ListsContainer.tsx +++ b/src/containers/lists/ListsContainer.tsx @@ -5,8 +5,8 @@ import ListsSkeleton from "@/components/contentDisplay/lists/ListsSkeleton"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import { getProfile } from "@/lib/api/bsky/actor"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; import { getLists } from "@/lib/api/bsky/list"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Fragment } from "react"; import InfiniteScroll from "react-infinite-scroll-component"; @@ -17,7 +17,6 @@ interface Props { export default function ListsContainer(props: Props) { const { handle } = props; - const agent = useAgent(); const { status, data: lists, @@ -30,6 +29,7 @@ export default function ListsContainer(props: Props) { } = useInfiniteQuery({ queryKey: ["user lists", handle], queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); const profile = await getProfile(handle, agent); if (!profile) throw new Error("Could not get user id to show lists"); return getLists(profile.did, pageParam, agent); diff --git a/src/containers/posts/UserPostsContainer.tsx b/src/containers/posts/UserPostsContainer.tsx index 1b9d6c53..71dfa16a 100644 --- a/src/containers/posts/UserPostsContainer.tsx +++ b/src/containers/posts/UserPostsContainer.tsx @@ -4,7 +4,6 @@ import FeedPostSkeleton from "@/components/contentDisplay/feedPost/FeedPostSkele import EndOfFeed from "@/components/feedback/endOfFeed/EndOfFeed"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import { getProfile } from "@/lib/api/bsky/actor"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import useProfilePosts from "@/lib/hooks/bsky/feed/useProfilePosts"; import { useQuery } from "@tanstack/react-query"; import PostContainer from "./PostContainer"; @@ -12,6 +11,7 @@ import usePreferences from "@/lib/hooks/bsky/actor/usePreferences"; import ComposeButton from "@/components/actions/composeButton/ComposeButton"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { mode: UserPostMode; @@ -20,7 +20,6 @@ interface Props { export default function UserPostsConatiner(props: Props) { const { mode, handle } = props; - const agent = useAgent(); const { data: profile, isLoading, @@ -28,7 +27,10 @@ export default function UserPostsConatiner(props: Props) { isRefetching, } = useQuery({ queryKey: ["profile", handle], - queryFn: () => getProfile(handle, agent), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getProfile(handle, agent); + }, }); const isBlocked = profile?.viewer?.blocking ? true : false; const hasBlockedYou = profile?.viewer?.blockedBy ? true : false; @@ -46,7 +48,7 @@ export default function UserPostsConatiner(props: Props) { const dataLength = userPostsData?.pages.reduce( (acc, page) => acc + (page?.data.feed.length ?? 0), - 0 + 0, ); const isEmpty = diff --git a/src/containers/search/PostSearchContainer.tsx b/src/containers/search/PostSearchContainer.tsx index 70d37a36..36494a22 100644 --- a/src/containers/search/PostSearchContainer.tsx +++ b/src/containers/search/PostSearchContainer.tsx @@ -1,7 +1,6 @@ "use client"; import { searchPosts } from "@/lib/api/bsky/actor"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Fragment } from "react"; import EndOfFeed from "@/components/feedback/endOfFeed/EndOfFeed"; @@ -10,6 +9,7 @@ import FeedPostSkeleton from "@/components/contentDisplay/feedPost/FeedPostSkele import SearchPost from "@/components/contentDisplay/searchPost/SearchPost"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { query: string; @@ -19,7 +19,6 @@ interface Props { export default function PostSearchContainer(props: Props) { const { query, sort } = props; const decoded = decodeURIComponent(query); - const agent = useAgent(); const { status, @@ -32,14 +31,17 @@ export default function PostSearchContainer(props: Props) { fetchNextPage, } = useInfiniteQuery({ queryKey: ["searchPosts", sort, query], - queryFn: ({ pageParam }) => searchPosts(decoded, pageParam, sort, agent), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return searchPosts(decoded, pageParam, sort, agent); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage?.cursor, }); const dataLength = posts?.pages.reduce( (acc, page) => acc + (page?.posts.length ?? 0), - 0 + 0, ); const isEmpty = !isFetching && !isFetchingNextPage && dataLength === 0; diff --git a/src/containers/search/UserSearchContainer.tsx b/src/containers/search/UserSearchContainer.tsx index b378abbd..7e009138 100644 --- a/src/containers/search/UserSearchContainer.tsx +++ b/src/containers/search/UserSearchContainer.tsx @@ -1,7 +1,6 @@ "use client"; import { searchProfiles } from "@/lib/api/bsky/actor"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Fragment } from "react"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; @@ -9,6 +8,7 @@ import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; import ProfileCardSkeleton from "@/components/contentDisplay/profileCard/ProfileCardSkeleton"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { query: string; @@ -16,7 +16,6 @@ interface Props { export default function UserSearchContainer(props: Props) { const { query } = props; - const agent = useAgent(); const { status, data: profiles, @@ -28,14 +27,17 @@ export default function UserSearchContainer(props: Props) { hasNextPage, } = useInfiniteQuery({ queryKey: ["searchProfiles", query], - queryFn: ({ pageParam }) => searchProfiles(agent, query, pageParam), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return searchProfiles(agent, query, pageParam); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage?.cursor, }); const dataLength = profiles?.pages.reduce( (acc, page) => acc + (page?.actors.length ?? 0), - 0 + 0, ); const isEmpty = !isFetching && !isFetchingNextPage && dataLength === 0; @@ -67,10 +69,7 @@ export default function UserSearchContainer(props: Props) { {isEmpty && (

- +
)} {isFetching && !isFetchingNextPage && ( diff --git a/src/containers/settings/blockedUsersContainer/BlockedUsersContainer.tsx b/src/containers/settings/blockedUsersContainer/BlockedUsersContainer.tsx index 9f3c52d5..dda68e54 100644 --- a/src/containers/settings/blockedUsersContainer/BlockedUsersContainer.tsx +++ b/src/containers/settings/blockedUsersContainer/BlockedUsersContainer.tsx @@ -2,16 +2,15 @@ import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; import { getBlockedUsers } from "@/lib/api/bsky/social"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Fragment } from "react"; import BlockedUsersContainerSkeleton from "./BlockedUsersContainerSkeleton"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export default function BlockedUsersContainer() { - const agent = useAgent(); const { status, data: profiles, @@ -23,7 +22,10 @@ export default function BlockedUsersContainer() { hasNextPage, } = useInfiniteQuery({ queryKey: ["getBlockedUsers"], - queryFn: ({ pageParam }) => getBlockedUsers(agent, pageParam), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return getBlockedUsers(agent, pageParam); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage.cursor, }); diff --git a/src/containers/settings/contentFilteringContainer/ContentFilteringContainer.tsx b/src/containers/settings/contentFilteringContainer/ContentFilteringContainer.tsx index 11463f1e..97c2c4b7 100644 --- a/src/containers/settings/contentFilteringContainer/ContentFilteringContainer.tsx +++ b/src/containers/settings/contentFilteringContainer/ContentFilteringContainer.tsx @@ -10,7 +10,6 @@ import { PreferencesResult, } from "../../../../types/feed"; import { LabelPreference } from "@atproto/api"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { updateContentFilterPreferences, updateIsAdultContentEnabled, @@ -22,6 +21,7 @@ import { import { ReactNode } from "react"; import ContentFilteringContainerSkeleton from "./ContentFilteringContainerSkeleton"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface OptionsProps { item: ContentFilter; @@ -69,7 +69,6 @@ export default function ContentFilteringContainer() { const adultContentFilters = preferences?.contentFilter?.adultContentFilters; const contentFilters = preferences?.contentFilter.contentFilters; const isAdultContentHidden = preferences?.contentFilter.isAdultContentHidden; - const agent = useAgent(); const queryClient = useQueryClient(); const updateIsAdultContentHidden = useMutation({ @@ -88,6 +87,7 @@ export default function ContentFilteringContainer() { }; }, ); + const agent = await getAgentFromClient(); await updateIsAdultContentEnabled(!value, agent); } catch (error) { console.log(error); @@ -128,6 +128,7 @@ export default function ContentFilteringContainer() { }; }, ); + const agent = await getAgentFromClient(); await updateContentFilterPreferences(pref, value, agent); } catch (error) { console.log(error); @@ -168,6 +169,7 @@ export default function ContentFilteringContainer() { }; }, ); + const agent = await getAgentFromClient(); await updateContentFilterPreferences(pref, value, agent); } catch (error) { console.log(error); diff --git a/src/containers/settings/homeFeedContainer/HomeFeedContainer.tsx b/src/containers/settings/homeFeedContainer/HomeFeedContainer.tsx index 1db666d2..c46bc881 100644 --- a/src/containers/settings/homeFeedContainer/HomeFeedContainer.tsx +++ b/src/containers/settings/homeFeedContainer/HomeFeedContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import usePreferences from "@/lib/hooks/bsky/actor/usePreferences"; import { Switch } from "@/components/inputs/switch/Switch"; @@ -10,9 +9,9 @@ import Label from "@/components/inputs/label/Label"; import { PreferencesResult } from "../../../../types/feed"; import HomeFeedContainerSkeleton from "./HomeFeedContainerSkeleton"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export default function HomeFeedContainer() { - const agent = useAgent(); const { isFetchingPreferences, preferences } = usePreferences(); const feedFilter = preferences?.feedFilter; const queryClient = useQueryClient(); @@ -33,6 +32,7 @@ export default function HomeFeedContainer() { }; }, ); + const agent = await getAgentFromClient(); await updateHomeFeedPreferences(prefs, agent); } catch (error) { console.log(error); diff --git a/src/containers/settings/mutedUsersContainer/MutedUsersContainer.tsx b/src/containers/settings/mutedUsersContainer/MutedUsersContainer.tsx index 6f27623f..7570ca7e 100644 --- a/src/containers/settings/mutedUsersContainer/MutedUsersContainer.tsx +++ b/src/containers/settings/mutedUsersContainer/MutedUsersContainer.tsx @@ -2,16 +2,15 @@ import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; import { getMutedUsers } from "@/lib/api/bsky/social"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Fragment } from "react"; import MutedUsersContainerSkeleton from "./MutedUsersContainerSkeleton"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export default function MutedUsersContainer() { - const agent = useAgent(); const { status, data: profiles, @@ -23,7 +22,10 @@ export default function MutedUsersContainer() { hasNextPage, } = useInfiniteQuery({ queryKey: ["getMutedUsers"], - queryFn: ({ pageParam }) => getMutedUsers(agent, pageParam), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return getMutedUsers(agent, pageParam); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage.cursor, }); diff --git a/src/containers/settings/myFeedsContainer/MyFeedsContainer.tsx b/src/containers/settings/myFeedsContainer/MyFeedsContainer.tsx index fcdf4a62..babbe611 100644 --- a/src/containers/settings/myFeedsContainer/MyFeedsContainer.tsx +++ b/src/containers/settings/myFeedsContainer/MyFeedsContainer.tsx @@ -1,7 +1,6 @@ "use client"; import { getSavedFeeds } from "@/lib/api/bsky/feed"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import FallbackFeed from "@/assets/images/fallbackFeed.png"; @@ -13,6 +12,7 @@ import Alert from "@/components/feedback/alert/Alert"; import MyFeedsContainerSkeleton from "./MyFeedsContainerSkeleton"; import { BiSolidTrash } from "react-icons/bi"; import { BiSolidBookmarkAlt } from "react-icons/bi"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface FeedItemProps { feedItem: SavedFeed; @@ -77,10 +77,12 @@ function FeedItem(props: FeedItemProps) { } export default function MyFeedsContainer() { - const agent = useAgent(); const { status, data, error, isLoading, isFetching } = useQuery({ queryKey: savedFeedsQueryKey, - queryFn: () => getSavedFeeds(agent), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getSavedFeeds(agent); + }, }); if (isLoading || isFetching) return ; diff --git a/src/containers/settings/threadPreferencesContainer/ThreadPreferencesContainer.tsx b/src/containers/settings/threadPreferencesContainer/ThreadPreferencesContainer.tsx index ce65d6b4..a2ad0815 100644 --- a/src/containers/settings/threadPreferencesContainer/ThreadPreferencesContainer.tsx +++ b/src/containers/settings/threadPreferencesContainer/ThreadPreferencesContainer.tsx @@ -6,12 +6,12 @@ import { Switch } from "@/components/inputs/switch/Switch"; import { updateThreadViewPreferences } from "@/lib/api/bsky/actor"; import { THREAD_VIEW_OPTIONS } from "@/lib/consts/settings"; import usePreferences from "@/lib/hooks/bsky/actor/usePreferences"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { BskyThreadViewPreference } from "@atproto/api"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { PreferencesResult } from "../../../../types/feed"; import ThreadPreferencesContainerSkeleton from "./ThreadPreferencesContainerSkeleton"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface ItemProps { value: string; @@ -30,7 +30,6 @@ function SortReplyItem(props: ItemProps) { export default function ThreadPreferencesContainer() { const { isFetchingPreferences, preferences } = usePreferences(); - const agent = useAgent(); const queryClient = useQueryClient(); const updateThreadPrefs = useMutation({ @@ -49,6 +48,7 @@ export default function ThreadPreferencesContainer() { }; }, ); + const agent = await getAgentFromClient(); await updateThreadViewPreferences(prefs, agent); } catch (error) { console.log(error); diff --git a/src/containers/thread/LikedByContainer.tsx b/src/containers/thread/LikedByContainer.tsx index 6a197dcf..2f066057 100644 --- a/src/containers/thread/LikedByContainer.tsx +++ b/src/containers/thread/LikedByContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import ProfileCardSkeleton from "@/components/contentDisplay/profileCard/ProfileCardSkeleton"; import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; @@ -9,6 +8,7 @@ import { getPostLikes } from "@/lib/api/bsky/feed"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { handle: string; @@ -17,7 +17,6 @@ interface Props { export default function LikedByContainer(props: Props) { const { handle, id } = props; - const agent = useAgent(); const { status, data: profiles, @@ -30,6 +29,7 @@ export default function LikedByContainer(props: Props) { } = useInfiniteQuery({ queryKey: ["getPostLikes", id], queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); const { data } = await agent.resolveHandle({ handle }); if (!data) return; const uri = `at://${data.did}/app.bsky.feed.post/${id}`; diff --git a/src/containers/thread/PostThreadContainer.tsx b/src/containers/thread/PostThreadContainer.tsx index 2a4cc12e..a4ac7d24 100644 --- a/src/containers/thread/PostThreadContainer.tsx +++ b/src/containers/thread/PostThreadContainer.tsx @@ -12,7 +12,6 @@ import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import RepliesContainer from "./RepliesContainer"; import ParentContainer from "./ParentContainer"; import WhoCanReply from "@/components/feedback/WhoCanReply/WhoCanReply"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import useOrganizeThread from "@/lib/hooks/bsky/feed/useOrganizeThread"; import usePreferences from "@/lib/hooks/bsky/actor/usePreferences"; import { getPostThread } from "@/lib/api/bsky/feed"; @@ -24,6 +23,7 @@ import ThreadActionsContainer from "./ThreadActionsContainer"; import { replyIncludes } from "@/lib/utils/text"; import useProfile from "@/lib/hooks/bsky/actor/useProfile"; import { useSession } from "next-auth/react"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { id: string; @@ -34,7 +34,6 @@ interface Props { export default function PostThreadContainer(props: Props) { const { id, handle, repliesTextFilter } = props; const [maxReplies, setMaxReplies] = useState(MAX_REPLY_CONTAINERS); - const agent = useAgent(); const router = useRouter(); const { data: session } = useSession(); const { data: profile } = useProfile(session?.user.bskySession.handle); @@ -48,6 +47,7 @@ export default function PostThreadContainer(props: Props) { } = useQuery({ queryKey: ["postThread", id], queryFn: async () => { + const agent = await getAgentFromClient(); const uri = `at://${handle}/app.bsky.feed.post/${id}`; return getPostThread(uri, agent); }, diff --git a/src/containers/thread/QuotesContainer.tsx b/src/containers/thread/QuotesContainer.tsx index 7151a643..369cf0a6 100644 --- a/src/containers/thread/QuotesContainer.tsx +++ b/src/containers/thread/QuotesContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Fragment } from "react"; import EndOfFeed from "@/components/feedback/endOfFeed/EndOfFeed"; @@ -10,6 +9,7 @@ import SearchPost from "@/components/contentDisplay/searchPost/SearchPost"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; import { getPostQuotes } from "@/lib/api/bsky/feed"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { id: string; @@ -18,7 +18,6 @@ interface Props { export default function QuotesContainer(props: Props) { const { id, handle } = props; - const agent = useAgent(); const { status, @@ -32,6 +31,7 @@ export default function QuotesContainer(props: Props) { } = useInfiniteQuery({ queryKey: ["postQuotes", id], queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); const { data } = await agent.resolveHandle({ handle }); if (!data) return; const uri = `at://${data.did}/app.bsky.feed.post/${id}`; @@ -43,7 +43,7 @@ export default function QuotesContainer(props: Props) { const dataLength = quotes?.pages.reduce( (acc, page) => acc + (page?.posts.length ?? 0), - 0 + 0, ); const isEmpty = !isFetching && !isFetchingNextPage && dataLength === 0; diff --git a/src/containers/thread/RepostedByContainer.tsx b/src/containers/thread/RepostedByContainer.tsx index 6a373534..b95ccdd6 100644 --- a/src/containers/thread/RepostedByContainer.tsx +++ b/src/containers/thread/RepostedByContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import ProfileCardSkeleton from "@/components/contentDisplay/profileCard/ProfileCardSkeleton"; import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; @@ -9,6 +8,7 @@ import { getPostReposts } from "@/lib/api/bsky/feed"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { handle: string; @@ -17,7 +17,7 @@ interface Props { export default function RepostedByContainer(props: Props) { const { handle, id } = props; - const agent = useAgent(); + const { status, data: profiles, @@ -30,6 +30,7 @@ export default function RepostedByContainer(props: Props) { } = useInfiniteQuery({ queryKey: ["getPostReposts", id], queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); const { data } = await agent.resolveHandle({ handle }); if (!data) return; const uri = `at://${data.did}/app.bsky.feed.post/${id}`; diff --git a/src/containers/users/FollowersContainer.tsx b/src/containers/users/FollowersContainer.tsx index 8cf4e0db..f3cbce61 100644 --- a/src/containers/users/FollowersContainer.tsx +++ b/src/containers/users/FollowersContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import ProfileCardSkeleton from "@/components/contentDisplay/profileCard/ProfileCardSkeleton"; import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; @@ -9,6 +8,7 @@ import { getFollowers } from "@/lib/api/bsky/social"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { handle: string; @@ -16,7 +16,6 @@ interface Props { export default function FollowersContainer(props: Props) { const { handle } = props; - const agent = useAgent(); const { status, data: profiles, @@ -28,7 +27,10 @@ export default function FollowersContainer(props: Props) { hasNextPage, } = useInfiniteQuery({ queryKey: ["getFollowers", handle], - queryFn: ({ pageParam }) => getFollowers(handle, agent, pageParam), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return getFollowers(handle, agent, pageParam); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage.data.cursor, }); diff --git a/src/containers/users/FollowingContainer.tsx b/src/containers/users/FollowingContainer.tsx index de391418..05863e30 100644 --- a/src/containers/users/FollowingContainer.tsx +++ b/src/containers/users/FollowingContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import ProfileCardSkeleton from "@/components/contentDisplay/profileCard/ProfileCardSkeleton"; import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; @@ -9,6 +8,7 @@ import { getFollows } from "@/lib/api/bsky/social"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { handle: string; @@ -16,7 +16,6 @@ interface Props { export default function FollowingContainer(props: Props) { const { handle } = props; - const agent = useAgent(); const { status, data: profiles, @@ -28,8 +27,10 @@ export default function FollowingContainer(props: Props) { hasNextPage, } = useInfiniteQuery({ queryKey: ["getFollowing", handle], - queryFn: ({ pageParam }) => - getFollows({ handle, agent, cursor: pageParam }), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return getFollows({ handle, agent, cursor: pageParam }); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage.data.cursor, }); diff --git a/src/containers/users/KnownFollowersContainer.tsx b/src/containers/users/KnownFollowersContainer.tsx index 2cb17470..87436d3d 100644 --- a/src/containers/users/KnownFollowersContainer.tsx +++ b/src/containers/users/KnownFollowersContainer.tsx @@ -1,11 +1,8 @@ "use client"; -import useAgent from "@/lib/hooks/bsky/useAgent"; -import { useInfiniteQuery } from "@tanstack/react-query"; import ProfileCardSkeleton from "@/components/contentDisplay/profileCard/ProfileCardSkeleton"; import ProfileCard from "@/components/contentDisplay/profileCard/ProfileCard"; import { Fragment } from "react"; -import { getFollowers } from "@/lib/api/bsky/social"; import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner"; import InfiniteScroll from "react-infinite-scroll-component"; import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; @@ -17,7 +14,6 @@ interface Props { export default function KnownFollowersContainer(props: Props) { const { handle } = props; - const agent = useAgent(); const { knownFollowers, isKnownFollowersEmpty, diff --git a/src/lib/api/auth/auth.ts b/src/lib/api/auth/auth.ts index 58067422..4b03cfb9 100644 --- a/src/lib/api/auth/auth.ts +++ b/src/lib/api/auth/auth.ts @@ -3,6 +3,7 @@ import NextAuth, { NextAuthOptions, Session, User } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { jwtDecode } from "jwt-decode"; import { createAgent } from "@/lib/api/bsky/agent"; +import { getService } from "@/app/api/auth/identity/actions"; export const authOptions: NextAuthOptions = { providers: [ @@ -23,27 +24,30 @@ export const authOptions: NextAuthOptions = { return null; } - const at = createAgent(); + const service = await getService(credentials.handle); + const agent = createAgent(service); - const result = await at.login({ + const result = await agent.login({ identifier: credentials.handle, password: credentials.password, }); - if (result.success && at.session) { + if (result.success && agent.session) { const user = { - id: at.session.did, - handle: at.session.handle, - email: at.session.email!, - emailConfirmed: at.session.emailConfirmed ?? false, - bskySession: at.session, + id: agent.session.did, + service: service, + handle: agent.session.handle, + email: agent.session.email!, + emailConfirmed: agent.session.emailConfirmed ?? false, + bskySession: agent.session, }; + return user; } else { - // an error will be displayed advising the user to check their details. + // an error will be displayed advising the user to check their details return null; - // (Optional) can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter + // (optional) can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter } }, }), @@ -52,6 +56,7 @@ export const authOptions: NextAuthOptions = { callbacks: { async jwt({ token, user }) { if (user && user.bskySession) { + token.service = user.service; token.id = user.id; token.handle = user.handle; token.email = user.email; @@ -63,10 +68,10 @@ export const authOptions: NextAuthOptions = { // add extra properties to session async session({ session, token }): Promise { - const at = createAgent(); - const receivedToken = token as JWT & User; + const agent = createAgent(receivedToken.service); + session.user.service = receivedToken.service; session.user.email = receivedToken.email; session.user.id = receivedToken.id; session.user.handle = receivedToken.handle; @@ -74,7 +79,7 @@ export const authOptions: NextAuthOptions = { session.user.bskySession = receivedToken.bskySession; const refreshToken: { iat: number; exp: number } = jwtDecode( - receivedToken.bskySession.refreshJwt + receivedToken.bskySession.refreshJwt, ); const now = Date.now(); @@ -84,18 +89,18 @@ export const authOptions: NextAuthOptions = { } const accessToken: { iat: number; exp: number } = jwtDecode( - receivedToken.bskySession.accessJwt + receivedToken.bskySession.accessJwt, ); if (now >= accessToken.exp * 1000) { console.log("Access token expired, refreshing"); - const { data } = await at.api.com.atproto.server.refreshSession( + const { data } = await agent.com.atproto.server.refreshSession( undefined, { headers: { authorization: "Bearer " + session.user.bskySession.refreshJwt, }, - } + }, ); session.user.bskySession.refreshJwt = data.refreshJwt; session.user.bskySession.accessJwt = data.accessJwt; diff --git a/src/lib/api/bsky/actor/index.ts b/src/lib/api/bsky/actor/index.ts index 1315b4da..33e0dec2 100644 --- a/src/lib/api/bsky/actor/index.ts +++ b/src/lib/api/bsky/actor/index.ts @@ -4,15 +4,12 @@ import { LabelPreference, BskyThreadViewPreference, } from "@atproto/api"; -import { getAgent } from "../agent"; +import { getAgentFromServer } from "../agent"; import { ContentFilterLabel } from "../../../../../types/feed"; -export const getProfile = async ( - handle: string | undefined, - agent?: Agent, -) => { +export const getProfile = async (handle: string | undefined, agent?: Agent) => { if (!handle) return; - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const profile = await agent.getProfile({ actor: handle }); if (!profile.success) throw new Error("Could not get profile"); @@ -20,7 +17,7 @@ export const getProfile = async ( }; export const getSuggestions = async () => { - const agent = await getAgent(); + const agent = await getAgentFromServer(); const suggestions = await agent.getSuggestions({ limit: 10 }); if (!suggestions.success) return null; return suggestions.data.actors; @@ -41,10 +38,7 @@ export const searchProfiles = async ( } }; -export const searchProfilesTypehead = async ( - agent: Agent, - term: string, -) => { +export const searchProfilesTypehead = async (agent: Agent, term: string) => { try { const results = await agent.searchActorsTypeahead({ term, limit: 5 }); if (!results.success) return null; @@ -61,7 +55,7 @@ export const searchPosts = async ( sort: "latest" | "top", agent?: Agent, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); try { const response = await agent.app.bsky.feed.searchPosts({ q: term, @@ -79,7 +73,7 @@ export const searchPosts = async ( }; export const getPreferences = async (agent?: Agent) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const prefs = await agent.app.bsky.actor.getPreferences(); if (!prefs.success) throw new Error("Could not get preferences"); return prefs.data.preferences; @@ -89,7 +83,7 @@ export const updateThreadViewPreferences = async ( pref: Partial, agent?: Agent, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const prefs = await agent.setThreadViewPrefs(pref); return prefs; }; @@ -97,7 +91,7 @@ export const updateHomeFeedPreferences = async ( pref: Partial, agent?: Agent, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const prefs = await agent.setFeedViewPrefs("home", pref); return prefs; }; @@ -106,7 +100,7 @@ export const updateIsAdultContentEnabled = async ( value: boolean, agent?: Agent, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const prefs = await agent.setAdultContentEnabled(value); return prefs; }; @@ -116,20 +110,20 @@ export const updateContentFilterPreferences = async ( value: LabelPreference, agent?: Agent, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const prefs = await agent.setContentLabelPref(pref, value); return prefs; }; export const muteUser = async (did: string, agent?: Agent) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const mute = await agent.mute(did); if (!mute.success) throw new Error("Could not mute user"); return mute.success; }; export const unMuteUser = async (did: string, agent?: Agent) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const mute = await agent.unmute(did); if (!mute.success) throw new Error("Could not unmute user"); return mute.success; @@ -140,7 +134,7 @@ export const blockUser = async ( did: string, agent?: Agent, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const res = await agent.app.bsky.graph.block.create( { repo: viewerDid }, { createdAt: new Date().toISOString(), subject: did }, @@ -154,6 +148,6 @@ export const unBlockUser = async ( rkey: string, agent?: Agent, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); await agent.app.bsky.graph.block.delete({ rkey: rkey, repo: viewerDid }); }; diff --git a/src/lib/api/bsky/agent.ts b/src/lib/api/bsky/agent.ts index 9d2657a3..99825270 100644 --- a/src/lib/api/bsky/agent.ts +++ b/src/lib/api/bsky/agent.ts @@ -1,34 +1,62 @@ import { AtpAgent } from "@atproto/api"; import { redirect } from "next/navigation"; import { getSessionFromServer } from "../auth/session"; +import { DEFAULT_SERVICE } from "@/lib/consts/general"; +import { getSession } from "next-auth/react"; +import { isServer } from "@/lib/utils/general"; +import { isSessionExpired } from "@/lib/utils/session"; -export const createAgent = () => { - const at = new AtpAgent({ - service: "https://bsky.social", +export const createAgent = (service: string) => { + return new AtpAgent({ + service, }); - - return at; }; export const getBskySession = async () => { - // TODO: allow PDS URL — using bsky.social for now - const at = new AtpAgent({ - service: "https://bsky.social", - }); - try { - const session = await getSessionFromServer(); - if (!session?.user.bskySession) redirect("/"); - const result = await at.resumeSession(session.user.bskySession); - if (!result.success) redirect("/"); + const session = isServer() + ? await getSessionFromServer() + : await getSession(); + + if (!session?.user.bskySession) { + throw new Error("No session found"); + } + + const agent = createAgent(session.user.service); + + if (isSessionExpired(session.user.bskySession)) { + const result = await agent.resumeSession(session.user.bskySession); + + if (!result.success) { + throw new Error("Could not resume session"); + } + } + + // session is not expired, use the current one + agent.sessionManager.session = session.user.bskySession; + + return agent; } catch (e) { - redirect("/"); + throw new Error("Could not get session"); } +}; - return at; +export const getAgentFromServer = async () => { + try { + const agent = await getBskySession(); + return agent; + } catch (error) { + console.error(error); + redirect("/"); + } }; -export const getAgent = async () => { - const agent = await getBskySession(); - return agent; +export const getAgentFromClient = async () => { + try { + const agent = await getBskySession(); + + return agent; + } catch (error) { + throw new Error("Could not get agent"); + } }; diff --git a/src/lib/api/bsky/feed/index.ts b/src/lib/api/bsky/feed/index.ts index c4af7374..33af1cb1 100644 --- a/src/lib/api/bsky/feed/index.ts +++ b/src/lib/api/bsky/feed/index.ts @@ -1,9 +1,9 @@ import { type Agent, AppBskyActorDefs } from "@atproto/api"; -import { getAgent } from "../agent"; +import { getAgentFromServer } from "../agent"; import { SavedFeed } from "../../../../../types/feed"; export const getPopularFeeds = async (search?: string) => { - const agent = await getAgent(); + const agent = await getAgentFromServer(); const popularFeeds = await agent.app.bsky.unspecced.getPopularFeedGenerators({ query: search, }); @@ -16,14 +16,14 @@ export const getPopularFeeds = async (search?: string) => { }; export const getSavedFeeds = async (agent?: Agent): Promise => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const prefs = await agent.app.bsky.actor.getPreferences(); if (!prefs.success) throw new Error("Could not fetch feeds"); const feedsPref = prefs.data.preferences.find( (pref) => AppBskyActorDefs.isSavedFeedsPref(pref) && - AppBskyActorDefs.validateSavedFeedsPref(pref).success + AppBskyActorDefs.validateSavedFeedsPref(pref).success, ) as AppBskyActorDefs.SavedFeedsPref | undefined; if (!feedsPref || feedsPref.saved.length === 0) return []; @@ -106,7 +106,7 @@ export const getFeed = async (agent: Agent, uri: string, cursor: string) => { }; export const getFeedInfo = async (uri: string, agent?: Agent) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const feed = await agent.app.bsky.feed.getFeedGenerator({ feed: uri }); if (!feed.success) throw new Error("Could not fetch feed info"); return feed.data; @@ -115,7 +115,7 @@ export const getFeedInfo = async (uri: string, agent?: Agent) => { export const getUserPosts = async ( agent: Agent, handle: string, - cursor: string + cursor: string, ) => { const posts = await agent.getAuthorFeed({ actor: handle, @@ -131,7 +131,7 @@ export const getUserPosts = async ( export const getUserReplyPosts = async ( agent: Agent, handle: string, - cursor: string + cursor: string, ) => { const posts = await agent.getAuthorFeed({ actor: handle, @@ -146,7 +146,7 @@ export const getUserReplyPosts = async ( export const getUserMediaPosts = async ( agent: Agent, handle: string, - cursor: string + cursor: string, ) => { const posts = await agent.getAuthorFeed({ actor: handle, @@ -161,9 +161,9 @@ export const getUserMediaPosts = async ( export const getUserLikes = async ( agent: Agent, handle: string, - cursor: string + cursor: string, ) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const likes = await agent.app.bsky.feed.getActorLikes({ actor: handle, cursor: cursor, @@ -229,7 +229,7 @@ export const getPost = async (agent: Agent, uri: string) => { }; export const getPostThread = async (uri: string, agent?: Agent) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); try { const posts = await agent.getPostThread({ uri: uri }); @@ -242,7 +242,7 @@ export const getPostThread = async (uri: string, agent?: Agent) => { export const getPostLikes = async ( agent: Agent, uri: string, - cursor: string + cursor: string, ) => { try { const likes = await agent.getLikes({ uri: uri, cursor: cursor, limit: 50 }); @@ -255,7 +255,7 @@ export const getPostLikes = async ( export const getPostReposts = async ( agent: Agent, uri: string, - cursor: string + cursor: string, ) => { try { const likes = await agent.getRepostedBy({ @@ -272,7 +272,7 @@ export const getPostReposts = async ( export const getPostQuotes = async ( agent: Agent, uri: string, - cursor: string + cursor: string, ) => { try { const quotes = await agent.app.bsky.feed.getQuotes({ diff --git a/src/lib/api/bsky/identity/index.ts b/src/lib/api/bsky/identity/index.ts new file mode 100644 index 00000000..fe10cbaa --- /dev/null +++ b/src/lib/api/bsky/identity/index.ts @@ -0,0 +1,27 @@ +import { HandleResolver } from "@atproto/identity"; + +export const getDidFromHandle = async (handle: string) => { + const handleResolver = new HandleResolver({}); + const did = await handleResolver.resolve(handle); + if (!did) { + throw new Error("Could not get DID"); + } + + return did; +}; + +export const getPDS = async (did: string) => { + const res = await fetch(`https://plc.directory/${did}`); + if (!res.ok) throw new Error("PDS not found"); + + const pds = (await res.json()) as { + service: { + id: string; + type: string; + serviceEndpoint: string; + }[]; + }; + const service = pds.service.find((x) => x.id === "#atproto_pds"); + if (!service) throw new Error("PDS not found"); + return service.serviceEndpoint; +}; diff --git a/src/lib/api/bsky/list/index.tsx b/src/lib/api/bsky/list/index.tsx index 942ffa15..6fc45a24 100644 --- a/src/lib/api/bsky/list/index.tsx +++ b/src/lib/api/bsky/list/index.tsx @@ -1,11 +1,7 @@ import { type Agent } from "@atproto/api"; -import { getAgent } from "../agent"; +import { getAgentFromServer } from "../agent"; -export const getLists = async ( - did: string, - cursor: string, - agent: Agent, -) => { +export const getLists = async (did: string, cursor: string, agent: Agent) => { const lists = await agent.app.bsky.graph.getLists({ actor: did, cursor: cursor, @@ -16,7 +12,7 @@ export const getLists = async ( }; export const getListInfo = async (uri: string, agent?: Agent) => { - if (!agent) agent = await getAgent(); + if (!agent) agent = await getAgentFromServer(); const feed = await agent.app.bsky.graph.getList({ list: uri }); if (!feed.success) throw new Error("Could not fetch feed info"); diff --git a/src/lib/consts/general.ts b/src/lib/consts/general.ts index b6074331..80c8cd1e 100644 --- a/src/lib/consts/general.ts +++ b/src/lib/consts/general.ts @@ -1 +1,2 @@ export const MAX_KNOWN_FOLLOWERS = 3; +export const DEFAULT_SERVICE = "https://bsky.social"; diff --git a/src/lib/hooks/bsky/actor/useBlockUser.tsx b/src/lib/hooks/bsky/actor/useBlockUser.tsx index a7a3c783..4ab875b8 100644 --- a/src/lib/hooks/bsky/actor/useBlockUser.tsx +++ b/src/lib/hooks/bsky/actor/useBlockUser.tsx @@ -1,10 +1,10 @@ import { AppBskyFeedDefs } from "@atproto/api"; -import useAgent from "../useAgent"; import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { blockUser, unBlockUser } from "@/lib/api/bsky/actor"; import { profileKey } from "../actor/useProfile"; import { ViewerState } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { author: AppBskyFeedDefs.PostView["author"]; @@ -16,7 +16,6 @@ export const useBlockKey = (did: string) => ["block", did]; export default function useBlockUser(props: Props) { const { author, viewer, viewerDID } = props; - const agent = useAgent(); const [blocked, setBlocked] = useState(!!author.viewer?.blocking); const queryClient = useQueryClient(); @@ -26,6 +25,7 @@ export default function useBlockUser(props: Props) { if (!blocked) { try { setBlocked(true); + const agent = await getAgentFromClient(); const res = await blockUser(viewerDID, author.did, agent); queryClient.setQueryData( profileKey(author.handle), @@ -37,7 +37,7 @@ export default function useBlockUser(props: Props) { blocking: res.uri, }, }; - } + }, ); } catch (err) { setBlocked(false); @@ -51,13 +51,14 @@ export default function useBlockUser(props: Props) { blocking: undefined, }, }; - } + }, ); } } else { try { setBlocked(false); - const rkey = viewer!.blocking!.split("/").pop()!; + const agent = await getAgentFromClient(); + const rkey = viewer!.blocking!.split("/").pop()!; await unBlockUser(viewerDID, rkey, agent); queryClient.setQueryData( profileKey(author.handle), @@ -69,7 +70,7 @@ export default function useBlockUser(props: Props) { blocking: undefined, }, }; - } + }, ); } catch (err) { setBlocked(true); @@ -83,7 +84,7 @@ export default function useBlockUser(props: Props) { blocking: author.did, }, }; - } + }, ); } } diff --git a/src/lib/hooks/bsky/actor/usePreferences.tsx b/src/lib/hooks/bsky/actor/usePreferences.tsx index ab57a562..76f1e07a 100644 --- a/src/lib/hooks/bsky/actor/usePreferences.tsx +++ b/src/lib/hooks/bsky/actor/usePreferences.tsx @@ -1,14 +1,12 @@ import { useQuery } from "@tanstack/react-query"; -import useAgent from "../useAgent"; import { getPreferences } from "@/lib/api/bsky/actor"; import getThreadPreferences, { getContentFilter, getFeedFilter, } from "@/lib/utils/feed"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export default function usePreferences() { - const agent = useAgent(); - const { status: statusPreferences, data: preferences, @@ -18,6 +16,7 @@ export default function usePreferences() { } = useQuery({ queryKey: ["preferences"], queryFn: async () => { + const agent = await getAgentFromClient(); const preferences = await getPreferences(agent); const feedFilter = getFeedFilter(preferences); const contentFilter = getContentFilter(preferences); diff --git a/src/lib/hooks/bsky/actor/useProfile.tsx b/src/lib/hooks/bsky/actor/useProfile.tsx index ba1218d3..297bbb45 100644 --- a/src/lib/hooks/bsky/actor/useProfile.tsx +++ b/src/lib/hooks/bsky/actor/useProfile.tsx @@ -1,19 +1,19 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import useAgent from "../useAgent"; import { getProfile } from "@/lib/api/bsky/actor"; import { follow, unfollow } from "@/lib/api/bsky/social"; import { AppBskyActorDefs } from "@atproto/api"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export const profileKey = (handle: string) => ["profile", handle]; export default function useProfile(handle: string) { - const agent = useAgent(); const queryClient = useQueryClient(); const { data, isLoading, isFetching, isRefetching, error } = useQuery({ queryKey: profileKey(handle), queryFn: async (): Promise< AppBskyActorDefs.ProfileViewDetailed | undefined > => { + const agent = await getAgentFromClient(); const profile = await getProfile(handle, agent); if (profile) { return profile; @@ -48,6 +48,7 @@ export default function useProfile(handle: string) { updateFollowCount(isCurrentlyFollowing ? "decrease" : "increase"); try { + const agent = await getAgentFromClient(); if (isCurrentlyFollowing && data.viewer?.following) { await unfollow(agent, data.viewer.following); } else { diff --git a/src/lib/hooks/bsky/actor/useSearchUsers.tsx b/src/lib/hooks/bsky/actor/useSearchUsers.tsx index ac1bf40f..ad42d3fe 100644 --- a/src/lib/hooks/bsky/actor/useSearchUsers.tsx +++ b/src/lib/hooks/bsky/actor/useSearchUsers.tsx @@ -1,7 +1,7 @@ import { useQueryClient } from "@tanstack/react-query"; -import useAgent from "../useAgent"; import { searchProfilesTypehead } from "@/lib/api/bsky/actor"; import { getFollows } from "@/lib/api/bsky/social"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { authorHandle?: string; @@ -9,7 +9,6 @@ interface Props { export default function useSearchUsers(props: Props) { const { authorHandle } = props; - const agent = useAgent(); const queryClient = useQueryClient(); return async (term: string) => { @@ -20,7 +19,10 @@ export default function useSearchUsers(props: Props) { const data = await queryClient.fetchQuery({ staleTime: 300 * 1000, // 5 minutes queryKey: ["followers"], - queryFn: () => getFollows({ handle: authorHandle, agent, limit: 5 }), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getFollows({ handle: authorHandle, agent, limit: 5 }); + }, }); return data.data.follows; } catch (error) { @@ -32,7 +34,10 @@ export default function useSearchUsers(props: Props) { const data = await queryClient.fetchQuery({ staleTime: 60 * 1000, // 1 minute queryKey: ["search", term], - queryFn: () => searchProfilesTypehead(agent, term), + queryFn: async () => { + const agent = await getAgentFromClient(); + return searchProfilesTypehead(agent, term); + }, }); return data?.actors; } catch (error) { diff --git a/src/lib/hooks/bsky/actor/useUpdateProfile.tsx b/src/lib/hooks/bsky/actor/useUpdateProfile.tsx index a097e6ad..f775f8ae 100644 --- a/src/lib/hooks/bsky/actor/useUpdateProfile.tsx +++ b/src/lib/hooks/bsky/actor/useUpdateProfile.tsx @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import toast from "react-hot-toast"; -import useAgent from "../useAgent"; import { compressImage } from "@/lib/utils/image"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { displayName: string | null; @@ -12,11 +12,11 @@ interface Props { export function useUpdateProfile(props: Props) { const { displayName, description, banner, avatar } = props; - const agent = useAgent(); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: async () => { + const agent = await getAgentFromClient(); await agent.upsertProfile(async (existing) => { const profile = existing || {}; if (displayName || displayName === "") { diff --git a/src/lib/hooks/bsky/feed/useDeletePost.tsx b/src/lib/hooks/bsky/feed/useDeletePost.tsx index 9914db18..3d08f674 100644 --- a/src/lib/hooks/bsky/feed/useDeletePost.tsx +++ b/src/lib/hooks/bsky/feed/useDeletePost.tsx @@ -1,8 +1,8 @@ import { AppBskyFeedDefs } from "@atproto/api"; -import useAgent from "../useAgent"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { removePost } from "../../../api/bsky/feed"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { post: AppBskyFeedDefs.PostView; @@ -12,13 +12,13 @@ export const useDeletePostKey = (postUri: string) => ["deletePost", postUri]; export default function useDeletePost(props: Props) { const { post } = props; - const agent = useAgent(); const queryClient = useQueryClient(); const deletePost = useMutation({ mutationKey: useDeletePostKey(post.uri), mutationFn: async () => { try { + const agent = await getAgentFromClient(); await removePost(agent, post.uri); } catch (err) { console.error(err); diff --git a/src/lib/hooks/bsky/feed/useFeed.tsx b/src/lib/hooks/bsky/feed/useFeed.tsx index 7d8f02da..9a671f03 100644 --- a/src/lib/hooks/bsky/feed/useFeed.tsx +++ b/src/lib/hooks/bsky/feed/useFeed.tsx @@ -1,7 +1,7 @@ -import useAgent from "../useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { getFeed, getTimeline } from "../../../api/bsky/feed"; import { getListFeed } from "@/lib/api/bsky/list"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export const useFeedKey = (feed: string) => [feed]; @@ -12,7 +12,7 @@ interface Props { export default function useFeed(props: Props) { const { feed, mode } = props; - const agent = useAgent(); + const { status, data: timeline, @@ -26,7 +26,9 @@ export default function useFeed(props: Props) { hasNextPage, } = useInfiniteQuery({ queryKey: useFeedKey(feed), - queryFn: ({ pageParam }) => { + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + if (mode === "feed") { if (feed === "timeline") { return getTimeline(agent, pageParam); diff --git a/src/lib/hooks/bsky/feed/useFeedInfo.tsx b/src/lib/hooks/bsky/feed/useFeedInfo.tsx index 21ad1105..5b344213 100644 --- a/src/lib/hooks/bsky/feed/useFeedInfo.tsx +++ b/src/lib/hooks/bsky/feed/useFeedInfo.tsx @@ -1,20 +1,19 @@ -import useAgent from "../useAgent"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; import { getFeedInfo, getSavedFeeds } from "../../../api/bsky/feed"; import { useQuery } from "@tanstack/react-query"; export const feedInfoKey = (feed: string) => ["feedInfo", feed]; export default function useFeedInfo(feed: string) { - const agent = useAgent(); - const { data, isLoading, isFetching, isRefetching, error } = useQuery({ queryKey: feedInfoKey(feed), queryFn: async () => { + const agent = await getAgentFromClient(); const feedInfo = await getFeedInfo(feed, agent); const savedFeeds = await getSavedFeeds(agent); const isSaved = savedFeeds.some((savedFeed) => savedFeed.uri === feed); const isPinned = savedFeeds.some( - (savedFeed) => savedFeed.uri === feed && savedFeed.pinned + (savedFeed) => savedFeed.uri === feed && savedFeed.pinned, ); const isLiked = feedInfo.view.viewer?.like !== null ? true : false; return { ...feedInfo, isSaved, isPinned, isLiked }; diff --git a/src/lib/hooks/bsky/feed/useLike.tsx b/src/lib/hooks/bsky/feed/useLike.tsx index acba5ae4..22779fec 100644 --- a/src/lib/hooks/bsky/feed/useLike.tsx +++ b/src/lib/hooks/bsky/feed/useLike.tsx @@ -1,9 +1,9 @@ import { AppBskyFeedDefs } from "@atproto/api"; -import useAgent from "../useAgent"; import { useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { likePost, unlikePost } from "../../../api/bsky/feed"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { post: AppBskyFeedDefs.PostView; @@ -13,7 +13,6 @@ export const useLikeKey = (postUri: string) => ["like", postUri]; export default function useLike(props: Props) { const { post } = props; - const agent = useAgent(); const [liked, setLiked] = useState(!!post.viewer?.like); const [likeUri, setLikeUri] = useState(post.viewer?.like); const likeCount = @@ -22,6 +21,7 @@ export default function useLike(props: Props) { const toggleLike = useMutation({ mutationKey: useLikeKey(post.uri), mutationFn: async () => { + const agent = await getAgentFromClient(); if (!likeUri) { try { setLiked(true); diff --git a/src/lib/hooks/bsky/feed/useMuteUser.tsx b/src/lib/hooks/bsky/feed/useMuteUser.tsx index 53cfae3d..52fcb9dd 100644 --- a/src/lib/hooks/bsky/feed/useMuteUser.tsx +++ b/src/lib/hooks/bsky/feed/useMuteUser.tsx @@ -1,10 +1,10 @@ import { AppBskyFeedDefs } from "@atproto/api"; -import useAgent from "../useAgent"; import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { muteUser, unMuteUser } from "@/lib/api/bsky/actor"; import { profileKey } from "../actor/useProfile"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { author: AppBskyFeedDefs.PostView["author"]; @@ -14,7 +14,6 @@ export const useMuteKey = (did: string) => ["mute", did]; export default function useLike(props: Props) { const { author } = props; - const agent = useAgent(); const [muted, setMuted] = useState(!!author.viewer?.muted); const queryClient = useQueryClient(); const queryKey = profileKey(author.handle); @@ -22,6 +21,8 @@ export default function useLike(props: Props) { const toggleMuteUser = useMutation({ mutationKey: useMuteKey(author.did), mutationFn: async () => { + const agent = await getAgentFromClient(); + if (!muted) { try { setMuted(true); diff --git a/src/lib/hooks/bsky/feed/useProfilePosts.tsx b/src/lib/hooks/bsky/feed/useProfilePosts.tsx index 3d85d269..6b5420be 100644 --- a/src/lib/hooks/bsky/feed/useProfilePosts.tsx +++ b/src/lib/hooks/bsky/feed/useProfilePosts.tsx @@ -1,10 +1,10 @@ +import { getAgentFromClient } from "@/lib/api/bsky/agent"; import { getUserPosts, getUserReplyPosts, getUserMediaPosts, getUserLikes, } from "../../../api/bsky/feed"; -import useAgent from "../useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; interface Props { @@ -19,7 +19,6 @@ export const useProfilePostsKey = (handle?: string, mode?: UserPostMode) => [ export default function useProfilePosts(props: Props) { const { mode, handle } = props; - const agent = useAgent(); const actor = handle; const chooseFetchFunction = (mode: string) => { @@ -48,8 +47,10 @@ export default function useProfilePosts(props: Props) { hasNextPage, } = useInfiniteQuery({ queryKey: useProfilePostsKey(handle, mode), - queryFn: ({ pageParam }) => - chooseFetchFunction(mode)(agent, actor, pageParam), + queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); + return chooseFetchFunction(mode)(agent, actor, pageParam); + }, initialPageParam: "", getNextPageParam: (lastPage) => lastPage.data.cursor, refetchOnWindowFocus: true, diff --git a/src/lib/hooks/bsky/feed/usePublishPost.tsx b/src/lib/hooks/bsky/feed/usePublishPost.tsx index d4dbcee1..e362a477 100644 --- a/src/lib/hooks/bsky/feed/usePublishPost.tsx +++ b/src/lib/hooks/bsky/feed/usePublishPost.tsx @@ -12,7 +12,6 @@ import { ComAtprotoLabelDefs, RichText, } from "@atproto/api"; -import useAgent from "../useAgent"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { detectLanguage, jsonToText } from "@/lib/utils/text"; import { compressImage } from "@/lib/utils/image"; @@ -20,6 +19,7 @@ import { JSONContent } from "@tiptap/react"; import toast from "react-hot-toast"; import { ThreadgateSetting } from "../../../../../types/feed"; import { getLinkFacets } from "@/lib/utils/link"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { text: JSONContent; @@ -45,13 +45,13 @@ export default function usePublishPost(props: Props) { label, threadGate, } = props; - const agent = useAgent(); const queryClient = useQueryClient(); const MAX_POST_LENGTH = 300; return useMutation({ mutationKey: ["publishPost"], mutationFn: async () => { + const agent = await getAgentFromClient(); const richText = new RichText({ text: jsonToText(text) }); const linkFacets = getLinkFacets(text); await richText.detectFacets(agent); diff --git a/src/lib/hooks/bsky/feed/useRepost.tsx b/src/lib/hooks/bsky/feed/useRepost.tsx index c5f84f40..022c99b8 100644 --- a/src/lib/hooks/bsky/feed/useRepost.tsx +++ b/src/lib/hooks/bsky/feed/useRepost.tsx @@ -1,9 +1,9 @@ import { AppBskyFeedDefs } from "@atproto/api"; -import useAgent from "../useAgent"; import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { repost, unRepost } from "../../../api/bsky/feed"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { post: AppBskyFeedDefs.PostView; @@ -13,7 +13,6 @@ export const useRepostKey = (postUri: string) => ["repost", postUri]; export default function useLike(props: Props) { const { post } = props; - const agent = useAgent(); const queryClient = useQueryClient(); const [reposted, setReposted] = useState(!!post.viewer?.repost); const [repostUri, setRepostUri] = useState(post.viewer?.repost); @@ -25,6 +24,7 @@ export default function useLike(props: Props) { const toggleRepost = useMutation({ mutationKey: useRepostKey(post.uri), mutationFn: async () => { + const agent = await getAgentFromClient(); if (!repostUri) { try { setReposted(true); diff --git a/src/lib/hooks/bsky/feed/useSaveFeed.tsx b/src/lib/hooks/bsky/feed/useSaveFeed.tsx index 2ad43e84..765065d9 100644 --- a/src/lib/hooks/bsky/feed/useSaveFeed.tsx +++ b/src/lib/hooks/bsky/feed/useSaveFeed.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { SavedFeed } from "../../../../../types/feed"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { togglePinFeed, toggleSaveFeed } from "@/lib/api/bsky/feed"; -import useAgent from "../useAgent"; import toast from "react-hot-toast"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { feedItem: SavedFeed; @@ -13,7 +13,6 @@ export default function useSaveFeed(props: Props) { const { feedItem } = props; const [isPinned, setIsPinned] = useState(feedItem.pinned); const queryClient = useQueryClient(); - const agent = useAgent(); const updatePinnedFeeds = (mode: "pin" | "unpin") => { queryClient.setQueryData(["savedFeeds"], (oldData: any) => { @@ -50,6 +49,7 @@ export default function useSaveFeed(props: Props) { updatePinnedFeeds(isCurrentlyPinned ? "unpin" : "pin"); try { + const agent = await getAgentFromClient(); await togglePinFeed(agent, feedItem.uri); } catch (error) { // revert to the old state in case of an error @@ -69,6 +69,7 @@ export default function useSaveFeed(props: Props) { mutationFn: async () => { updateSavedFeeds("remove"); try { + const agent = await getAgentFromClient(); await toggleSaveFeed(agent, feedItem.uri); } catch (error) { updateSavedFeeds("add"); @@ -84,6 +85,7 @@ export default function useSaveFeed(props: Props) { mutationFn: async () => { updateSavedFeeds("add"); try { + const agent = await getAgentFromClient(); await toggleSaveFeed(agent, feedItem.uri); } catch (error) { updateSavedFeeds("remove"); diff --git a/src/lib/hooks/bsky/list/useListInfo.tsx b/src/lib/hooks/bsky/list/useListInfo.tsx index fda2a03b..0247d078 100644 --- a/src/lib/hooks/bsky/list/useListInfo.tsx +++ b/src/lib/hooks/bsky/list/useListInfo.tsx @@ -1,15 +1,16 @@ -import useAgent from "../useAgent"; import { useQuery } from "@tanstack/react-query"; import { getListInfo } from "@/lib/api/bsky/list"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; export const listInfoKey = (list: string) => ["listInfo", list]; export default function useFeedInfo(list: string) { - const agent = useAgent(); - const { data, isLoading, isFetching, isRefetching, error } = useQuery({ queryKey: listInfoKey(list), - queryFn: () => getListInfo(list, agent), + queryFn: async () => { + const agent = await getAgentFromClient(); + return getListInfo(list, agent); + }, }); return { diff --git a/src/lib/hooks/bsky/notification/useNotification.tsx b/src/lib/hooks/bsky/notification/useNotification.tsx index f4df383e..edb73cbf 100644 --- a/src/lib/hooks/bsky/notification/useNotification.tsx +++ b/src/lib/hooks/bsky/notification/useNotification.tsx @@ -1,4 +1,3 @@ -import useAgent from "../useAgent"; import { useInfiniteQuery } from "@tanstack/react-query"; import { getNotifications, @@ -6,6 +5,7 @@ import { } from "@/lib/api/bsky/notification"; import { GroupedNotification } from "../../../../../types/feed"; import { Notification } from "@atproto/api/dist/client/types/app/bsky/notification/listNotifications"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { notificationType: NotificationReason | "all"; @@ -13,7 +13,6 @@ interface Props { export default function useNotification(props: Props) { const { notificationType } = props; - const agent = useAgent(); const groupNotifications = ( notifications: Notification[], ): GroupedNotification[] => { @@ -50,6 +49,7 @@ export default function useNotification(props: Props) { } = useInfiniteQuery({ queryKey: ["notifications", notificationType], queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); const res = await getNotifications(agent, pageParam); await updateSeenNotifications(agent); res.data.notifications = groupNotifications(res.data.notifications); diff --git a/src/lib/hooks/bsky/social/useKnownFollowers.tsx b/src/lib/hooks/bsky/social/useKnownFollowers.tsx index 07cbbd55..7a5e1740 100644 --- a/src/lib/hooks/bsky/social/useKnownFollowers.tsx +++ b/src/lib/hooks/bsky/social/useKnownFollowers.tsx @@ -1,7 +1,7 @@ import { useInfiniteQuery } from "@tanstack/react-query"; -import useAgent from "../useAgent"; import { getProfile } from "@/lib/api/bsky/actor"; import { getKnownFollowers } from "@/lib/api/bsky/social"; +import { getAgentFromClient } from "@/lib/api/bsky/agent"; interface Props { handle: string; @@ -9,7 +9,6 @@ interface Props { export default function useKnownFollowers(props: Props) { const { handle } = props; - const agent = useAgent(); const { status, @@ -23,6 +22,7 @@ export default function useKnownFollowers(props: Props) { } = useInfiniteQuery({ queryKey: ["known followers", handle], queryFn: async ({ pageParam }) => { + const agent = await getAgentFromClient(); const profile = await getProfile(handle, agent); if (!profile) throw new Error("Could not get user id to show lists"); return getKnownFollowers(agent, profile.did, pageParam); diff --git a/src/lib/hooks/bsky/useAgent.tsx b/src/lib/hooks/bsky/useAgent.tsx deleted file mode 100644 index e8e9a657..00000000 --- a/src/lib/hooks/bsky/useAgent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from "react"; -import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import { createAgent } from "@/lib/api/bsky/agent"; - -export default function useAgent() { - const [agent, setAgent] = useState(createAgent()); - const { data: session, status } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (status === "loading") return; - if (!session?.user.bskySession) { - router.push("/"); - return; - } - - const getAgent = async () => { - const bskySession = session.user.bskySession; - agent.sessionManager.session = bskySession; - }; - - getAgent(); - }, [agent, router, session?.user.bskySession, status]); - - return agent; -} diff --git a/src/lib/utils/general.ts b/src/lib/utils/general.ts new file mode 100644 index 00000000..b75413b9 --- /dev/null +++ b/src/lib/utils/general.ts @@ -0,0 +1 @@ +export const isServer = () => typeof window === "undefined"; diff --git a/src/lib/utils/session.ts b/src/lib/utils/session.ts new file mode 100644 index 00000000..20733205 --- /dev/null +++ b/src/lib/utils/session.ts @@ -0,0 +1,17 @@ +import { AtpSessionData } from "@atproto/api"; +import { jwtDecode } from "jwt-decode"; + +export const isSessionExpired = (session: AtpSessionData) => { + try { + if (session.accessJwt) { + const decoded = jwtDecode(session.accessJwt); + if (decoded.exp) { + const didExpire = Date.now() >= decoded.exp * 1000; + return didExpire; + } + } + } catch (e) { + throw new Error("Could not decode JWT"); + } + return true; +}; diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index 0046da88..35410d5e 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -9,6 +9,7 @@ declare module "next-auth" { handle: string; email: string; emailConfirmed: boolean; + service: string; bskySession: AtpSessionData; }