diff --git a/src/webapp/api-client/clients/FeedClient.ts b/src/webapp/api-client/clients/FeedClient.ts index 865d2c358..ac02b01e0 100644 --- a/src/webapp/api-client/clients/FeedClient.ts +++ b/src/webapp/api-client/clients/FeedClient.ts @@ -15,6 +15,7 @@ export class FeedClient extends BaseClient { if (params?.beforeActivityId) searchParams.set('beforeActivityId', params.beforeActivityId); if (params?.urlType) searchParams.set('urlType', params.urlType); + if (params?.source) searchParams.set('source', params.source); const queryString = searchParams.toString(); const endpoint = queryString @@ -31,6 +32,7 @@ export class FeedClient extends BaseClient { if (params?.page) searchParams.set('page', params.page.toString()); if (params?.limit) searchParams.set('limit', params.limit.toString()); if (params?.urlType) searchParams.set('urlType', params.urlType); + if (params?.source) searchParams.set('source', params.source); const queryString = searchParams.toString(); const endpoint = queryString diff --git a/src/webapp/components/CollectionSelector.tsx b/src/webapp/components/CollectionSelector.tsx index 387c23696..611897ce3 100644 --- a/src/webapp/components/CollectionSelector.tsx +++ b/src/webapp/components/CollectionSelector.tsx @@ -13,6 +13,8 @@ import { import { ApiClient, Collection } from '@/api-client'; import { useCollectionSearch } from '@/hooks/useCollectionSearch'; import { CreateCollectionModal } from './CreateCollectionModal'; +import { isMarginUri, getMarginUrl } from '@/lib/utils/margin'; +import MarginLogo from '@/components/MarginLogo'; interface LocalCollection extends Collection { authorId: string; // Extended for local component use @@ -105,11 +107,22 @@ export function CollectionSelector({ {existingCollections.length !== 1 && 's'}: - {existingCollections.map((collection) => ( - - {collection.name} - - ))} + {existingCollections.map((collection) => { + const marginUrl = getMarginUrl( + collection.uri, + collection.author?.handle, + ); + return ( + + + {collection.name} + {isMarginUri(collection.uri) && ( + + )} + + + ); + })} )} @@ -163,52 +176,61 @@ export function CollectionSelector({ )} - {availableCollections.map((collection, index) => ( - handleCollectionToggle(collection.id)} - > - - - - - {collection.name} - - - {collection.cardCount} cards - - - {collection.description && ( - - {collection.description} - - )} - - handleCollectionToggle(collection.id)} - disabled={disabled} - onClick={(e) => e.stopPropagation()} - size="sm" - /> - - - ))} + {availableCollections.map((collection, index) => { + const marginUrl = getMarginUrl( + collection.uri, + collection.author?.handle, + ); + return ( + handleCollectionToggle(collection.id)} + > + + + + + {collection.name} + + {isMarginUri(collection.uri) && ( + + )} + + {collection.cardCount} cards + + + {collection.description && ( + + {collection.description} + + )} + + handleCollectionToggle(collection.id)} + disabled={disabled} + onClick={(e) => e.stopPropagation()} + size="sm" + /> + + + ); + })} ) : searchText.trim() ? ( diff --git a/src/webapp/components/MarginLogo.tsx b/src/webapp/components/MarginLogo.tsx new file mode 100644 index 000000000..510d7df27 --- /dev/null +++ b/src/webapp/components/MarginLogo.tsx @@ -0,0 +1,46 @@ +import { Anchor, Box, Tooltip } from '@mantine/core'; +import { MouseEvent } from 'react'; + +interface Props { + size?: number; + marginUrl?: string | null; +} + +export default function MarginLogo({ size = 16, marginUrl }: Props) { + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + }; + + const logo = ( + + + + + ); + + if (!marginUrl) { + return logo; + } + + return ( + + + {logo} + + + ); +} diff --git a/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx b/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx index 3c112c68e..bc29a0538 100644 --- a/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx +++ b/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx @@ -21,6 +21,8 @@ import { IoMdLink } from 'react-icons/io'; import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; import { track } from '@vercel/analytics'; import useMyCollections from '@/features/collections/lib/queries/useMyCollections'; +import { isMarginUri, getMarginUrl } from '@/lib/utils/margin'; +import MarginLogo from '@/components/MarginLogo'; interface Props { isOpen: boolean; @@ -152,29 +154,40 @@ export default function AddCardDrawer(props: Props) { : 'Manage/View all'} - {myCollections.map((col) => ( - - ))} + {myCollections.map((col) => { + const marginUrl = getMarginUrl( + col.uri, + col.author?.handle, + ); + return ( + + ); + })} diff --git a/src/webapp/features/cards/components/urlCard/UrlCard.tsx b/src/webapp/features/cards/components/urlCard/UrlCard.tsx index 24dd49287..912b84d74 100644 --- a/src/webapp/features/cards/components/urlCard/UrlCard.tsx +++ b/src/webapp/features/cards/components/urlCard/UrlCard.tsx @@ -14,6 +14,7 @@ import UrlCardDebugView from '../UrlCardDebugView/UrlCardDebugView'; interface Props { id: string; url: string; + uri?: string; cardContent: UrlCard['cardContent']; note?: UrlCard['note']; currentCollection?: Collection; @@ -65,7 +66,12 @@ export default function UrlCard(props: Props) { onAuxClick={handleAuxClick} > - + {settings.tinkerMode && ( - - e.stopPropagation()} - component={Link} - href={props.cardContent.url} - target="_blank" - c={'gray'} - lineClamp={1} - w={'fit-content'} - fz={'sm'} - > - {domain} - - + + + e.stopPropagation()} + component={Link} + href={props.cardContent.url} + target="_blank" + c={'gray'} + lineClamp={1} + w={'fit-content'} + fz={'sm'} + > + {domain} + + + {isMarginUri(props.uri) && ( + + )} + {props.cardContent.title && ( {props.cardContent.title} diff --git a/src/webapp/features/cards/components/urlCardContent/UrlCardContent.tsx b/src/webapp/features/cards/components/urlCardContent/UrlCardContent.tsx index 545eacad1..2eedee41b 100644 --- a/src/webapp/features/cards/components/urlCardContent/UrlCardContent.tsx +++ b/src/webapp/features/cards/components/urlCardContent/UrlCardContent.tsx @@ -14,7 +14,9 @@ import { useUserSettings } from '@/features/settings/lib/queries/useUserSettings interface Props { url: string; + uri?: string; cardContent: UrlCard['cardContent']; + authorHandle?: string; } export default function UrlCardContent(props: Props) { @@ -31,13 +33,23 @@ export default function UrlCardContent(props: Props) { ) { return ( } + fallback={ + + } > }> + } /> @@ -59,5 +71,11 @@ export default function UrlCardContent(props: Props) { return ; } - return ; + return ( + + ); } diff --git a/src/webapp/features/cards/containers/cardsContainerContent/CardsContainerContent.tsx b/src/webapp/features/cards/containers/cardsContainerContent/CardsContainerContent.tsx index 1ac9ddcf1..725135521 100644 --- a/src/webapp/features/cards/containers/cardsContainerContent/CardsContainerContent.tsx +++ b/src/webapp/features/cards/containers/cardsContainerContent/CardsContainerContent.tsx @@ -89,6 +89,7 @@ export default function CardsContainerContent(props: Props) { ) => { e.stopPropagation(); @@ -64,9 +67,14 @@ export default function CollectionCard(props: Props) { - - {collection.name} - + + + {collection.name} + + {isMarginUri(collection.uri) && ( + + )} + {props.showAuthor && ( void; disabled?: boolean; + uri?: string; + authorHandle?: string; } export default function CollectionSelectorItem(props: Props) { + const marginUrl = getMarginUrl(props.uri, props.authorHandle); + return ( - - {props.name} {'·'} {props.cardCount}{' '} - {props.cardCount === 1 ? 'card' : 'cards'} - + + + {props.name} {'·'} {props.cardCount}{' '} + {props.cardCount === 1 ? 'card' : 'cards'} + + {isMarginUri(props.uri) && ( + + )} + props.onChange(checked, c)} disabled={isDisabled} + uri={c.uri} + authorHandle={c.author?.handle} /> ); })} @@ -51,6 +57,8 @@ export default function CollectionSelectorItemList(props: Props) { value={c.id} checked={!!props.selectedCollections.find((col) => col.id === c.id)} onChange={(checked) => props.onChange(checked, c)} + uri={c.uri} + authorHandle={c.author?.handle} /> ))} diff --git a/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx b/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx index e1418e3ee..9c0a7cbaa 100644 --- a/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx +++ b/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx @@ -25,6 +25,8 @@ import { useAuth } from '@/hooks/useAuth'; import { useOs } from '@mantine/hooks'; import { CardFilters } from '@/features/cards/components/cardFilters/CardFilters'; import useGemCollectionSearch from '../../lib/queries/useGemCollectionSearch'; +import { isMarginUri, getMarginUrl } from '@/lib/utils/margin'; +import MarginLogo from '@/components/MarginLogo'; interface Props { rkey: string; @@ -52,6 +54,7 @@ export default function CollectionContainer(props: Props) { searchResults && searchResults.collections.length > 0; const isAuthor = user?.handle === firstPage?.author.handle; + const marginUrl = getMarginUrl(firstPage?.uri, firstPage?.author.handle); // Create share URL for Bluesky intent const currentUrl = typeof window !== 'undefined' ? window.location.href : ''; @@ -75,7 +78,12 @@ export default function CollectionContainer(props: Props) { Collection - {firstPage.name} + + {firstPage.name} + {isMarginUri(firstPage.uri) && ( + + )} + {firstPage.description && ( {firstPage.description} diff --git a/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx b/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx index 21b372493..341656941 100644 --- a/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx +++ b/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx @@ -74,6 +74,7 @@ export default function CollectionContainerContent(props: Props) { (sourceFromUrl); + + const [, startTransition] = useTransition(); + const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption(), }); + const sourceCombobox = useCombobox({ + onDropdownClose: () => sourceCombobox.resetSelectedOption(), + }); + const selected = options.find((o) => o.value === currentValue); + const selectedSource = + sourceOptions.find((o) => o.value === optimisticSource) || sourceOptions[0]; + + const handleSourceClick = (source: ActivitySource | null) => { + startTransition(() => { + setOptimisticSource(source); + + const params = new URLSearchParams(searchParams.toString()); + if (source) { + params.set('source', source); + } else { + params.delete('source'); + } + + router.push(`?${params.toString()}`, { scroll: false }); + }); + + sourceCombobox.closeDropdown(); + }; return ( @@ -75,6 +114,45 @@ export default function FeedControls() { + + { + const option = sourceOptions.find( + (o) => String(o.value) === value, + ); + if (option) { + handleSourceClick(option.value); + } + }} + width={150} + > + + + + + + + {sourceOptions.map((option) => ( + + {option.label} + + ))} + + + + {isGemsFeed && (