Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import { ComputerDesktopIcon, GlobeAltIcon } from "@heroicons/react/24/outline";
2import {
3 type AuthenticatedSessionsRequest,
4 PageSize,
5 useAuthenticatedSessionsQuery,
6 useRevokeAuthenticationMutation
7} from "@hey/indexer";
8import type { ApolloClientError } from "@hey/types/errors";
9import dayjs from "dayjs";
10import { memo, useCallback, useState } from "react";
11import { toast } from "sonner";
12import { WindowVirtualizer } from "virtua";
13import Loader from "@/components/Shared/Loader";
14import { Button, EmptyState, ErrorMessage } from "@/components/Shared/UI";
15import errorToast from "@/helpers/errorToast";
16import useLoadMoreOnIntersect from "@/hooks/useLoadMoreOnIntersect";
17import { useAccountStore } from "@/store/persisted/useAccountStore";
18
19const List = () => {
20 const { currentAccount } = useAccountStore();
21 const [revoking, setRevoking] = useState(false);
22 const [revokeingSessionId, setRevokeingSessionId] = useState<null | string>(
23 null
24 );
25
26 const onError = useCallback((error: ApolloClientError) => {
27 setRevoking(false);
28 setRevokeingSessionId(null);
29 errorToast(error);
30 }, []);
31
32 const onCompleted = () => {
33 setRevoking(false);
34 setRevokeingSessionId(null);
35 toast.success("Session revoked");
36 };
37
38 const [revokeAuthentication] = useRevokeAuthenticationMutation({
39 onCompleted,
40 onError,
41 update: (cache) => {
42 cache.evict({ id: "ROOT_QUERY" });
43 }
44 });
45
46 const handleRevoke = async (authenticationId: string) => {
47 setRevoking(true);
48 setRevokeingSessionId(authenticationId);
49
50 return await revokeAuthentication({
51 variables: { request: { authenticationId } }
52 });
53 };
54
55 const request: AuthenticatedSessionsRequest = { pageSize: PageSize.Fifty };
56 const { data, error, fetchMore, loading } = useAuthenticatedSessionsQuery({
57 skip: !currentAccount?.address,
58 variables: { request }
59 });
60
61 const authenticatedSessions = data?.authenticatedSessions?.items;
62 const pageInfo = data?.authenticatedSessions?.pageInfo;
63 const hasMore = pageInfo?.next;
64
65 const handleEndReached = useCallback(async () => {
66 if (hasMore) {
67 await fetchMore({
68 variables: { request: { ...request, cursor: pageInfo?.next } }
69 });
70 }
71 }, [fetchMore, hasMore, pageInfo?.next, request]);
72
73 const loadMoreRef = useLoadMoreOnIntersect(handleEndReached);
74
75 if (loading) {
76 return <Loader className="my-10" />;
77 }
78
79 if (error) {
80 return (
81 <ErrorMessage
82 className="m-5"
83 error={error}
84 title="Failed to load sessions"
85 />
86 );
87 }
88
89 if (!authenticatedSessions?.length) {
90 return (
91 <EmptyState
92 hideCard
93 icon={<GlobeAltIcon className="size-8" />}
94 message="You are not logged in on any other devices!"
95 />
96 );
97 }
98
99 return (
100 <div className="virtual-divider-list-window">
101 <WindowVirtualizer>
102 {authenticatedSessions.map((session) => (
103 <div
104 className="flex flex-wrap items-start justify-between p-5"
105 key={session.authenticationId}
106 >
107 <div>
108 <div className="mb-3 flex items-center space-x-2">
109 <ComputerDesktopIcon className="size-8" />
110 <div>
111 {session.browser ? <span>{session.browser}</span> : null}
112 {session.os ? <span> - {session.os}</span> : null}
113 </div>
114 </div>
115 <div className="space-y-1 text-gray-500 text-sm dark:text-gray-200">
116 {session.origin ? (
117 <div>
118 <b>Origin -</b> {session.origin}
119 </div>
120 ) : null}
121 <div>
122 <b>Registered -</b>{" "}
123 {dayjs(session.createdAt).format("MMM D, YYYY - h:mm:ss A")}
124 </div>
125 <div>
126 <b>Last accessed -</b>{" "}
127 {dayjs(session.updatedAt).format("MMM D, YYYY - h:mm:ss A")}
128 </div>
129 <div>
130 <b>Expires at -</b>{" "}
131 {dayjs(session.expiresAt).format("MMM D, YYYY - h:mm:ss A")}
132 </div>
133 </div>
134 </div>
135 <Button
136 disabled={
137 revoking && revokeingSessionId === session.authenticationId
138 }
139 loading={
140 revoking && revokeingSessionId === session.authenticationId
141 }
142 onClick={() => handleRevoke(session.authenticationId)}
143 >
144 Revoke
145 </Button>
146 </div>
147 ))}
148 {hasMore && <span ref={loadMoreRef} />}
149 </WindowVirtualizer>
150 </div>
151 );
152};
153
154export default memo(List);