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}
+ >
+
+ }
+ onClick={() => sourceCombobox.toggleDropdown()}
+ >
+ {selectedSource?.label}
+
+
+
+
+
+ {sourceOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
{isGemsFeed && (