This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

UI for the semble page counts

+173 -21
+5 -2
src/webapp/app/(dashboard)/url/page.tsx
··· 22 22 redirect('/'); 23 23 } 24 24 25 - const { metadata } = await getUrlMetadata(url); 25 + const { metadata } = await getUrlMetadata({ url, includeStats: true }); 26 26 const domain = getDomain(url); 27 27 const title = metadata.title ? `${metadata.title} (${domain})` : url; 28 28 ··· 51 51 redirect('/'); 52 52 } 53 53 54 + // Fetch metadata with stats for the tabs 55 + const { stats } = await getUrlMetadata({ url, includeStats: true }); 56 + 54 57 return ( 55 58 <SemblePageClient viaCardId={viaCardId}> 56 59 <Suspense fallback={<SembleContainerSkeleton />} key={url + 'container'}> 57 - <SembleContainer url={url} viaCardId={viaCardId} /> 60 + <SembleContainer url={url} viaCardId={viaCardId} stats={stats} /> 58 61 </Suspense> 59 62 </SemblePageClient> 60 63 );
+1 -1
src/webapp/app/api/opengraph/semble/route.tsx
··· 22 22 let metadata: Metadata = {}; 23 23 if (url) { 24 24 try { 25 - const result = await getUrlMetadata(getUrlFromSlug([url])); 25 + const result = await getUrlMetadata({ url: getUrlFromSlug([url]) }); 26 26 const libraries = await getLibrariesForUrl(getUrlFromSlug([url])); 27 27 28 28 metadata = {
+2
src/webapp/features/cards/lib/cardKeys.ts
··· 23 23 sortOrder, 24 24 urlType, 25 25 ], 26 + urlMetadata: (url: string, options?: { includeStats?: boolean }) => 27 + [...cardKeys.all(), 'metadata', url, options] as const, 26 28 };
+8 -3
src/webapp/features/cards/lib/dal.ts
··· 1 1 import { verifySessionOnClient, logoutUser } from '@/lib/auth/dal'; 2 2 import { createSembleClient } from '@/services/client.apiClient'; 3 - import { CardSortField, SortOrder, UrlType } from '@semble/types'; 3 + import { 4 + CardSortField, 5 + GetUrlMetadataParams, 6 + SortOrder, 7 + UrlType, 8 + } from '@semble/types'; 4 9 import { cache } from 'react'; 5 10 6 11 interface PageParams { ··· 11 16 urlType?: UrlType; 12 17 } 13 18 14 - export const getUrlMetadata = cache(async (url: string) => { 19 + export const getUrlMetadata = cache(async (params: GetUrlMetadataParams) => { 15 20 const client = createSembleClient(); 16 - const response = await client.getUrlMetadata(url); 21 + const response = await client.getUrlMetadata(params); 17 22 18 23 return response; 19 24 });
+4
src/webapp/features/cards/lib/mutations/useAddCard.tsx
··· 45 45 queryClient.invalidateQueries({ 46 46 queryKey: collectionKeys.bySembleUrl(variables.url), 47 47 }); 48 + // Invalidate URL metadata with stats to update tab counts 49 + queryClient.invalidateQueries({ 50 + queryKey: cardKeys.urlMetadata(variables.url, { includeStats: true }), 51 + }); 48 52 49 53 // invalidate each collection query individually 50 54 variables.collectionIds?.forEach((id) => {
+16
src/webapp/features/cards/lib/mutations/useRemoveCardFromCollections.tsx
··· 1 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 2 import { removeCardFromCollection } from '../dal'; 3 3 import { collectionKeys } from '@/features/collections/lib/collectionKeys'; 4 + import { sembleKeys } from '@/features/semble/lib/sembleKeys'; 4 5 5 6 export default function useRemoveCardFromCollections() { 6 7 const queryClient = useQueryClient(); ··· 20 21 queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() }); 21 22 queryClient.invalidateQueries({ queryKey: collectionKeys.mine() }); 22 23 queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); 24 + queryClient.invalidateQueries({ queryKey: sembleKeys.all() }); 25 + // Invalidate all URL metadata queries with stats to update tab counts 26 + queryClient.invalidateQueries({ 27 + predicate: (query): boolean => { 28 + const key = query.queryKey as unknown[]; 29 + return !!( 30 + key[0] === 'cards' && 31 + key[1] === 'metadata' && 32 + key[3] && 33 + typeof key[3] === 'object' && 34 + 'includeStats' in key[3] && 35 + key[3].includeStats === true 36 + ); 37 + }, 38 + }); 23 39 24 40 variables.collectionIds.forEach((id) => { 25 41 queryClient.invalidateQueries({
+14
src/webapp/features/cards/lib/mutations/useRemoveCardFromLibrary.tsx
··· 20 20 queryClient.invalidateQueries({ queryKey: feedKeys.all() }); 21 21 queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); 22 22 queryClient.invalidateQueries({ queryKey: sembleKeys.all() }); 23 + // Invalidate all URL metadata queries with stats to update tab counts 24 + queryClient.invalidateQueries({ 25 + predicate: (query): boolean => { 26 + const key = query.queryKey as unknown[]; 27 + return !!( 28 + key[0] === 'cards' && 29 + key[1] === 'metadata' && 30 + key[3] && 31 + typeof key[3] === 'object' && 32 + 'includeStats' in key[3] && 33 + key[3].includeStats === true 34 + ); 35 + }, 36 + }); 23 37 }, 24 38 }); 25 39
+14
src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx
··· 43 43 queryClient.invalidateQueries({ queryKey: feedKeys.all() }); 44 44 queryClient.invalidateQueries({ queryKey: sembleKeys.all() }); 45 45 queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); 46 + // Invalidate all URL metadata queries with stats to update tab counts 47 + queryClient.invalidateQueries({ 48 + predicate: (query): boolean => { 49 + const key = query.queryKey as unknown[]; 50 + return !!( 51 + key[0] === 'cards' && 52 + key[1] === 'metadata' && 53 + key[3] && 54 + typeof key[3] === 'object' && 55 + 'includeStats' in key[3] && 56 + key[3].includeStats === true 57 + ); 58 + }, 59 + }); 46 60 47 61 // invalidate each collection query individually 48 62 variables.addToCollectionIds?.forEach((id) => {
+29 -6
src/webapp/features/cards/lib/queries/useUrlMetadata.tsx
··· 1 - import { useSuspenseQuery } from '@tanstack/react-query'; 1 + import { useSuspenseQuery, useQuery } from '@tanstack/react-query'; 2 2 import { getUrlMetadata } from '../dal'; 3 + import type { UrlAggregateStats } from '@semble/types'; 4 + import { cardKeys } from '../cardKeys'; 3 5 4 - interface Props { 6 + interface PropsWithStats { 5 7 url: string; 8 + includeStats: true; 9 + initialData?: { stats?: UrlAggregateStats }; 6 10 } 7 11 12 + interface PropsWithoutStats { 13 + url: string; 14 + includeStats?: false; 15 + } 16 + 17 + type Props = PropsWithStats | PropsWithoutStats; 18 + 8 19 export default function useUrlMetadata(props: Props) { 9 - const metadata = useSuspenseQuery({ 10 - queryKey: [props.url], 11 - queryFn: () => getUrlMetadata(props.url), 20 + // Use regular useQuery when we have initialData (with stats) 21 + // Use useSuspenseQuery for basic metadata queries without stats 22 + if (props.includeStats && 'initialData' in props) { 23 + return useQuery({ 24 + queryKey: cardKeys.urlMetadata(props.url, { includeStats: true }), 25 + queryFn: () => getUrlMetadata({ url: props.url, includeStats: true }), 26 + initialData: props.initialData, 27 + }); 28 + } 29 + 30 + return useSuspenseQuery({ 31 + queryKey: cardKeys.urlMetadata(props.url, { 32 + includeStats: props.includeStats, 33 + }), 34 + queryFn: () => 35 + getUrlMetadata({ url: props.url, includeStats: props.includeStats }), 12 36 }); 13 - return metadata; 14 37 }
+13
src/webapp/features/connections/lib/mutations/useCreateConnection.tsx
··· 2 2 import { createConnection } from '../dal'; 3 3 import { connectionKeys } from '../connectionKeys'; 4 4 import { ConnectionType } from '@semble/types'; 5 + import { cardKeys } from '@/features/cards/lib/cardKeys'; 5 6 6 7 export default function useCreateConnection() { 7 8 const queryClient = useQueryClient(); ··· 28 29 // Invalidate backward connections for target URL 29 30 queryClient.invalidateQueries({ 30 31 queryKey: connectionKeys.backwardForUrl(variables.targetUrl), 32 + }); 33 + 34 + // Invalidate URL metadata with stats for both source and target URLs to update tab counts 35 + queryClient.invalidateQueries({ 36 + queryKey: cardKeys.urlMetadata(variables.sourceUrl, { 37 + includeStats: true, 38 + }), 39 + }); 40 + queryClient.invalidateQueries({ 41 + queryKey: cardKeys.urlMetadata(variables.targetUrl, { 42 + includeStats: true, 43 + }), 31 44 }); 32 45 }, 33 46 });
+14
src/webapp/features/connections/lib/mutations/useDeleteConnection.tsx
··· 14 14 onSuccess: () => { 15 15 // Invalidate all connection queries to ensure deletions are reflected everywhere 16 16 queryClient.invalidateQueries({ queryKey: connectionKeys.all() }); 17 + // Invalidate all URL metadata queries with stats to update tab counts 18 + queryClient.invalidateQueries({ 19 + predicate: (query): boolean => { 20 + const key = query.queryKey as unknown[]; 21 + return !!( 22 + key[0] === 'cards' && 23 + key[1] === 'metadata' && 24 + key[3] && 25 + typeof key[3] === 'object' && 26 + 'includeStats' in key[3] && 27 + key[3].includeStats === true 28 + ); 29 + }, 30 + }); 17 31 }, 18 32 }); 19 33
+14
src/webapp/features/connections/lib/mutations/useUpdateConnection.tsx
··· 14 14 onSuccess: () => { 15 15 // Invalidate all connection queries to ensure updates appear everywhere 16 16 queryClient.invalidateQueries({ queryKey: connectionKeys.all() }); 17 + // Invalidate all URL metadata queries with stats to update tab counts 18 + queryClient.invalidateQueries({ 19 + predicate: (query): boolean => { 20 + const key = query.queryKey as unknown[]; 21 + return !!( 22 + key[0] === 'cards' && 23 + key[1] === 'metadata' && 24 + key[3] && 25 + typeof key[3] === 'object' && 26 + 'includeStats' in key[3] && 27 + key[3].includeStats === true 28 + ); 29 + }, 30 + }); 17 31 }, 18 32 }); 19 33
+1 -1
src/webapp/features/graph/lib/queries/useGraphNodeUrl.tsx
··· 19 19 queryKey: url ? [url] : ['graph', 'url', 'metadata', 'empty'], 20 20 queryFn: () => { 21 21 if (!url) throw new Error('URL is required'); 22 - return getUrlMetadata(url); 22 + return getUrlMetadata({ url }); 23 23 }, 24 24 enabled: enabled && !!url, 25 25 staleTime: 3 * 60 * 1000, // 3 minutes - URLs change more often
+1 -1
src/webapp/features/platforms/bluesky/components/blueskySemblePost/BlueskySemblePost.tsx
··· 39 39 export default async function BlueskySemblePost(props: Props) { 40 40 const postUri = getPostUriFromUrl(props.url); 41 41 const data = await getBlueskyPost(postUri); 42 - const { metadata } = await getUrlMetadata(props.url); 42 + const { metadata } = await getUrlMetadata({ url: props.url }); 43 43 const urlTypeIcon = getUrlTypeIcon(metadata.type as UrlType); 44 44 const IconComponent = urlTypeIcon as IconType; 45 45
+22 -4
src/webapp/features/semble/components/sembleTabs/SembleTabs.tsx
··· 11 11 } from '@mantine/core'; 12 12 import TabItem from './TabItem'; 13 13 import { useFeatureFlags } from '@/lib/clientFeatureFlags'; 14 + import type { UrlAggregateStats } from '@semble/types'; 15 + import useUrlMetadata from '@/features/cards/lib/queries/useUrlMetadata'; 14 16 15 17 import SembleNotesContainer from '../../containers/sembleNotesContainer/SembleNotesContainer'; 16 18 import SembleNotesContainerSkeleton from '../../containers/sembleNotesContainer/Skeleton.SembleNotesContainer'; ··· 31 33 32 34 interface Props { 33 35 url: string; 36 + stats?: UrlAggregateStats; 34 37 } 35 38 36 39 type TabValue = 'notes' | 'collections' | 'addedBy' | 'similar' | 'connections'; ··· 38 41 export default function SembleTabs(props: Props) { 39 42 const [activeTab, setActiveTab] = useState<TabValue>('similar'); 40 43 const { data: featureFlags } = useFeatureFlags(); 44 + const { data: urlMetadata } = useUrlMetadata({ 45 + url: props.url, 46 + includeStats: true, 47 + initialData: props.stats ? { stats: props.stats } : undefined, 48 + }); 49 + 50 + const stats = urlMetadata?.stats; 41 51 42 52 return ( 43 53 <Tabs ··· 50 60 <Group wrap="nowrap"> 51 61 <TabItem value="similar">Similar cards</TabItem> 52 62 {featureFlags?.connections && ( 53 - <TabItem value="connections">Connections</TabItem> 63 + <TabItem value="connections" count={stats?.connections.all.total}> 64 + Connections 65 + </TabItem> 54 66 )} 55 - <TabItem value="notes">Notes</TabItem> 56 - <TabItem value="collections">Collections</TabItem> 57 - <TabItem value="addedBy">Added by</TabItem> 67 + <TabItem value="notes" count={stats?.noteCount}> 68 + Notes 69 + </TabItem> 70 + <TabItem value="collections" count={stats?.collectionCount}> 71 + Collections 72 + </TabItem> 73 + <TabItem value="addedBy" count={stats?.libraryCount}> 74 + Added by 75 + </TabItem> 58 76 <TabItem value="mentions">Mentions</TabItem> 59 77 </Group> 60 78 </TabsList>
+8 -1
src/webapp/features/semble/components/sembleTabs/TabItem.tsx
··· 6 6 interface Props { 7 7 value: string; 8 8 children: string; 9 + count?: number; 9 10 } 10 11 11 12 export default function TabItem(props: Props) { 13 + // Display count if provided and greater than 0 14 + const displayText = 15 + props.count && props.count > 0 16 + ? `${props.children} (${props.count})` 17 + : props.children; 18 + 12 19 return ( 13 20 <TabsTab 14 21 value={props.value} ··· 19 26 posthog.capture(`Semble: ${props.value} tab`); 20 27 }} 21 28 > 22 - {props.children} 29 + {displayText} 23 30 </TabsTab> 24 31 ); 25 32 }
+4 -1
src/webapp/features/semble/components/urlMetadataHeader/UrlMetadataHeader.tsx
··· 26 26 } 27 27 28 28 export default async function UrlMetadataHeader(props: Props) { 29 - const { metadata } = await getUrlMetadata(props.url); 29 + const { metadata } = await getUrlMetadata({ 30 + url: props.url, 31 + includeStats: true, 32 + }); 30 33 const urlTypeIcon = getUrlTypeIcon(metadata.type as UrlType); 31 34 const IconComponent = urlTypeIcon as IconType; 32 35
+3 -1
src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx
··· 7 7 import BlueskySembleHeader from '@/features/platforms/bluesky/container/blueskySembleHeader/BlueskySembleHeader'; 8 8 import { detectUrlPlatform, SupportedPlatform } from '@/lib/utils/link'; 9 9 import BlueskySembleHeaderSkeleton from '@/features/platforms/bluesky/container/blueskySembleHeader/Skeleton.BlueskySembleHeader'; 10 + import type { UrlAggregateStats } from '@semble/types'; 10 11 11 12 interface Props { 12 13 url: string; 13 14 viaCardId?: string; 15 + stats?: UrlAggregateStats; 14 16 } 15 17 16 18 export default function SembleContainer(props: Props) { ··· 42 44 <SembleHeader url={props.url} viaCardId={props.viaCardId} /> 43 45 </Suspense> 44 46 )} 45 - <SembleTabs url={props.url} /> 47 + <SembleTabs url={props.url} stats={props.stats} /> 46 48 </Stack> 47 49 </Container> 48 50 </Container>