Skip to content

Commit

Permalink
feat: post labels
Browse files Browse the repository at this point in the history
  • Loading branch information
ImLunaHey committed Jan 12, 2025
1 parent 661bdd7 commit 099cef1
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 8 deletions.
60 changes: 60 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@ariakit/react": "^0.4.15",
"@atproto/api": "^0.13.25",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.4",
Expand Down
57 changes: 49 additions & 8 deletions src/components/post-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ import { BSkyPost } from '../lib/bluesky/types/bsky-post';
import { cn } from '../lib/utils';
import { useRepost } from '../lib/bluesky/hooks/use-repost';
import { FacetedText } from './faceted-text';
import { PostEmbed } from './post-embed';
import { Link } from './ui/link';
import { ErrorBoundary } from './error-boundary';
import { useSettings } from '../hooks/use-setting';
import { FormattedNumber } from './ui/formatted-number';
import TimeAgo from 'react-timeago-i18n';
Expand All @@ -41,6 +39,10 @@ import { memo, useState } from 'react';
import { toast } from 'sonner';
import { usePlausible } from '@/hooks/use-plausible';
import { useBlueskyStore } from '@/lib/bluesky/store';
import { usePostLabels } from '@/lib/bluesky/hooks/use-post-labels';
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@radix-ui/react-accordion';
import { ErrorBoundary } from './error-boundary';
import { PostEmbed } from './post-embed';

const contextToText = (context: string) => {
if (context === 'following') return 'following';
Expand Down Expand Up @@ -204,22 +206,24 @@ const PostDropdownMenu = ({ post, setTranslatedText }: { post: BSkyPost; setTran
);
};

type PostCardProps = {
post: BSkyPost | undefined | null;
type PostCardInnerProps = {
post: BSkyPost;
context?: string;
className?: string;
parent?: boolean;
};

function PostCardInner({ post, context, className, parent = false }: PostCardProps) {
function PostCardInner({ post, context, className, parent = false }: PostCardInnerProps) {
const { t } = useTranslation(['app', 'post']);
const agent = useBlueskyStore((state) => state.agent);
const like = useLike();
const unlike = useUnlike();
const repost = useRepost();
const { isAuthenticated } = useAuth();
const { experiments } = useSettings();
const navigate = useNavigate();
const [translatedText, setTranslatedText] = useState<string | null>(null);
const { moderation } = usePostLabels({ agent, post });

const handleLike = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
Expand All @@ -241,7 +245,11 @@ function PostCardInner({ post, context, className, parent = false }: PostCardPro
repost.mutate({ uri, cid });
};

if (!post) return null;
// Hide post if it's filtered
if (moderation?.ui('contentList').filter) return null;
const contentMedia = moderation?.ui('contentMedia');
const moderationLabel = contentMedia?.blurs[0]?.type === 'label' ? contentMedia.blurs[0]?.labelDef.locales[0] : null;
const moderationFilter = moderation?.ui('contentMedia').filter;

const onClick = () => {
const cellText = document.getSelection();
Expand Down Expand Up @@ -318,7 +326,29 @@ function PostCardInner({ post, context, className, parent = false }: PostCardPro
<FormattedText text={post?.record.text} />
)}
</p>
<ErrorBoundary>{post.embed && <PostEmbed embed={post.embed} />}</ErrorBoundary>
<ErrorBoundary>
{moderationFilter ? null : moderationLabel ? (
<Accordion type="single" collapsible onClick={(event) => event.stopPropagation()}>
<AccordionItem value="item-1">
<AccordionTrigger className="w-full group">
<div className="flex items-center space-x-2 rounded-sm hover:bg-neutral-500 hover:bg-opacity-10 gap-1 border justify-between p-2">
<div className="flex items-center gap-1">
<AlertTriangleIcon size={20} />
{moderationLabel?.name}
</div>
<div className="group-data-[state=open]:hidden">{'show'}</div>
<div className="hidden group-data-[state=open]:flex">{'hide'}</div>
</div>
</AccordionTrigger>
<AccordionContent className="pt-2">
<ErrorBoundary>{post.embed && <PostEmbed embed={post.embed} />}</ErrorBoundary>
</AccordionContent>
</AccordionItem>
</Accordion>
) : (
post.embed && <PostEmbed embed={post.embed} />
)}
</ErrorBoundary>
<div className="flex items-center text-gray-500 dark:text-gray-400 justify-between">
<Link
to="/profile/$handle/post/$postId"
Expand Down Expand Up @@ -399,4 +429,15 @@ function PostCardInner({ post, context, className, parent = false }: PostCardPro
);
}

export const PostCard = memo(PostCardInner);
type PostCardProps = {
post: BSkyPost | null | undefined;
context?: string;
className?: string;
parent?: boolean;
};

export const PostCard = memo(function PostCard(props: PostCardProps) {
if (!props.post) return null;

return <PostCardInner {...props} />;
});
46 changes: 46 additions & 0 deletions src/components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { forwardRef } from 'react';

export const Accordion = AccordionPrimitive.Root;

export const AccordionItem = forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />);
AccordionItem.displayName = 'AccordionItem';

export const AccordionTrigger = forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180',
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;

export const AccordionContent = forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
52 changes: 52 additions & 0 deletions src/lib/bluesky/hooks/use-post-labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { BskyAgent, moderatePost, ModerationOpts } from '@atproto/api';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { BSkyPost } from '../types/bsky-post';

interface PostLabelsConfig {
agent: BskyAgent;
post: BSkyPost | null | undefined;
}

export const usePostLabels = ({ agent, post }: PostLabelsConfig) => {
// Fetch user preferences including moderation settings
const { data: preferences, isLoading: prefsLoading } = useQuery({
queryKey: ['preferences', agent.session?.did],
queryFn: async () => agent.getPreferences(),
enabled: !!agent.session?.did,
});

// Fetch label definitions
const { data: labelDefs, isLoading: defsLoading } = useQuery({
queryKey: ['labelDefinitions', preferences?.moderationPrefs],
queryFn: async () => agent.getLabelDefinitions(preferences!),
enabled: !!preferences,
});

// Create moderation options
const moderationOpts: ModerationOpts | undefined = useMemo(() => {
if (!preferences || !labelDefs) return undefined;

return {
userDid: agent.session?.did,
prefs: preferences.moderationPrefs,
labelDefs,
};
}, [agent.session?.did, preferences, labelDefs]);

// Get moderation UI state
const moderation = useMemo(() => {
if (!moderationOpts || !post) return null;

return moderatePost(post, moderationOpts);
}, [moderationOpts, post]);

return {
moderation,
isLoading: prefsLoading || defsLoading,
error: null,
// Raw data for custom handling
preferences,
labelDefs,
};
};
22 changes: 22 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ export default {
5: 'hsl(var(--chart-5))',
},
},
keyframes: {
'accordion-down': {
from: {
height: '0',
},
to: {
height: 'var(--radix-accordion-content-height)',
},
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)',
},
to: {
height: '0',
},
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [
Expand Down

0 comments on commit 099cef1

Please sign in to comment.