Skip to content

Commit

Permalink
✨ Allow users to bulk add all users who liked a post to workspace (#196)
Browse files Browse the repository at this point in the history
* ✨ Allow users to bulk add all users who liked a post to workspace

* ✨ Show follower, follows and post count on profile card

* ✨ Show post controls selectively

* 🐛 Check for the right control value
  • Loading branch information
foysalit authored Sep 25, 2024
1 parent 042f051 commit 7e3534f
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 42 deletions.
1 change: 1 addition & 0 deletions app/actions/ModActionPanel/QuickAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ function Form(
setModEventType(MOD_EVENTS.TAKEDOWN)
}),
)

return (
<>
{/* The inline styling is not ideal but there's no easy way to set calc() values in tailwind */}
Expand Down
36 changes: 32 additions & 4 deletions components/common/RecordCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
AppBskyActorDefs,
ComAtprotoLabelDefs,
} from '@atproto/api'
import { buildBlueSkyAppUrl, parseAtUri } from '@/lib/util'
import { buildBlueSkyAppUrl, parseAtUri, pluralize } from '@/lib/util'
import { PostAsCard } from './posts/PostsFeed'
import Link from 'next/link'
import { LoadingDense, displayError, LoadingFailedDense } from './Loader'
Expand Down Expand Up @@ -86,7 +86,7 @@ function PostCard({ uri, showLabels }: { uri: string; showLabels?: boolean }) {
renderRecord={(record) => (
<PostAsCard
dense
controls={false}
controls={[]}
item={{
post: {
uri: record.uri,
Expand Down Expand Up @@ -116,9 +116,9 @@ function PostCard({ uri, showLabels }: { uri: string; showLabels?: boolean }) {
return (
<PostAsCard
dense
controls={false}
item={{ post: data.thread.post }}
showLabels={showLabels}
item={{ post: data.thread.post }}
controls={['like', 'repost', 'workspace']}
/>
)
}
Expand Down Expand Up @@ -340,6 +340,34 @@ export function RepoCard(props: { did: string }) {
{profile.description}
</p>
)}
{!!profile && (
<div className="flex flex-row items-center gap-2">
<Link
href={`/repositories/${repo.did}?tab=followers`}
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-400 underline hover:underline cursor-pointer"
>
<span className="text-sm">
{pluralize(profile?.followersCount || 0, 'follower')}
</span>
</Link>
<Link
href={`/repositories/${repo.did}?tab=follows`}
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-400 underline hover:underline cursor-pointer"
>
<span className="text-sm">
{pluralize(profile?.followsCount || 0, 'follow')}
</span>
</Link>
<Link
href={`/repositories/${repo.did}?tab=posts`}
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-400 underline hover:underline cursor-pointer"
>
<span className="text-sm">
{pluralize(profile?.postsCount || 0, 'post')}
</span>
</Link>
</div>
)}
{takendown && (
<p className="pt-1 pb-1">
<LoadingFailedDense
Expand Down
96 changes: 90 additions & 6 deletions components/common/feeds/Likes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { AccountsGrid } from '@/repositories/AccountView'
import { useInfiniteQuery } from '@tanstack/react-query'
import { LoadMoreButton } from '../LoadMoreButton'
import { usePdsAgent } from '@/shell/AuthContext'
import { ActionButton } from '../buttons'
import { ConfirmationModal } from '../modals/confirmation'
import { useState } from 'react'
import { useWorkspaceAddItemsMutation } from '@/workspace/hooks'
import { toast } from 'react-toastify'
import { Agent } from '@atproto/api'
import { pluralize } from '@/lib/util'

const useLikes = (uri: string, cid?: string) => {
const pdsAgent = usePdsAgent()

const useLikes = (pdsAgent: Agent, uri: string, cid?: string) => {
return useInfiniteQuery({
queryKey: ['likes', { uri, cid }],
queryFn: async ({ pageParam }) => {
const { data } = await pdsAgent.api.app.bsky.feed.getLikes({
const { data } = await pdsAgent.app.bsky.feed.getLikes({
uri,
cid,
limit: 50,
Expand All @@ -22,11 +27,90 @@ const useLikes = (uri: string, cid?: string) => {
}

export const Likes = ({ uri, cid }: { uri: string; cid?: string }) => {
const { data, fetchNextPage, hasNextPage } = useLikes(uri, cid)
const pdsAgent = usePdsAgent()
const { data, fetchNextPage, hasNextPage, error } = useLikes(
pdsAgent,
uri,
cid,
)
const likes = data?.pages.flatMap((page) => page.likes) || []

const [isConfirmationOpen, setIsConfirmationOpen] = useState(false)
const [isAdding, setIsAdding] = useState(false)
const { mutateAsync: addItemsToWorkspace } = useWorkspaceAddItemsMutation()

const confirmAddToWorkspace = async () => {
// add items that are already loaded
await addItemsToWorkspace(likes.map((l) => l.actor.did))
if (!data?.pageParams) {
setIsConfirmationOpen(false)
return
}
setIsAdding(true)

try {
let cursor = data.pageParams[0] as string | undefined
do {
const nextLikes = await pdsAgent.app.bsky.feed.getLikes({
uri,
cid,
cursor,
limit: 50,
})
const dids = nextLikes.data?.likes.map((l) => l.actor.did) || []
if (dids.length) await addItemsToWorkspace(dids)
cursor = nextLikes.data.cursor
// if the modal is closed, that means the user decided not to add any more user to workspace
} while (cursor && isConfirmationOpen)
} catch (e) {
toast.error(`Something went wrong: ${(e as Error).message}`)
}
setIsAdding(false)
setIsConfirmationOpen(false)
}

return (
<>
<AccountsGrid error="" accounts={likes.map((l) => l.actor)} />
{!!likes?.length && (
<div className="flex flex-row justify-end pt-2 mx-auto mt-2 max-w-5xl px-4 sm:px-6 lg:px-8">
<ActionButton
appearance="primary"
size="sm"
onClick={() => setIsConfirmationOpen(true)}
>
Add {pluralize(likes.length, 'user')} to workspace
</ActionButton>

<ConfirmationModal
onConfirm={() => {
if (isAdding) {
setIsAdding(false)
setIsConfirmationOpen(false)
return
}

confirmAddToWorkspace()
}}
isOpen={isConfirmationOpen}
setIsOpen={setIsConfirmationOpen}
confirmButtonText={isAdding ? 'Stop adding' : 'Yes, add all'}
title={`Add users to workspace?`}
description={
<>
Once confirmed, all the users who liked the post will be added
to the workspace. For posts with a lot of likes, this may take
quite some time but you can always stop the process and already
added users will remain in the workspace.
</>
}
/>
</div>
)}

<AccountsGrid
error={error?.['message']}
accounts={likes.map((l) => l.actor)}
/>
{hasNextPage && (
<div className="flex justify-center">
<LoadMoreButton onClick={() => fetchNextPage()} />
Expand Down
1 change: 0 additions & 1 deletion components/common/feeds/PostThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export function ThreadPost({
<PostAsCard
className="bg-transparent px-3 py-2"
item={thread}
controls={false}
dense
/>
</ThreadPostWrapper>
Expand Down
108 changes: 78 additions & 30 deletions components/common/posts/PostsFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import {
} from '@heroicons/react/24/outline'
import { LoadMore } from '../LoadMore'
import { isRepost } from '@/lib/types'
import { buildBlueSkyAppUrl, classNames, parseAtUri } from '@/lib/util'
import {
buildBlueSkyAppUrl,
classNames,
parseAtUri,
pluralize,
} from '@/lib/util'
import { getActionClassNames } from '@/reports/ModerationView/ActionHelpers'
import { RichText } from '../RichText'
import { LabelList, ModerationLabel } from '../labels'
Expand All @@ -38,6 +43,7 @@ import {
} from '@/workspace/hooks'
import { ImageList } from './ImageList'
import { useGraphicMediaPreferences } from '@/config/useLocalPreferences'
import { HandThumbUpIcon } from '@heroicons/react/24/solid'
const VideoPlayer = dynamic(() => import('@/common/video/player'), {
ssr: false,
})
Expand Down Expand Up @@ -66,17 +72,27 @@ export function PostsFeed({
)
}

export type PostControl = 'like' | 'repost' | 'view' | 'report' | 'workspace'

export const PostControlOptions = [
'like',
'repost',
'view',
'report',
'workspace',
] as const

export function PostAsCard({
item,
dense,
controls = true,
onReport,
className = '',
showLabels = true,
controls = [...PostControlOptions],
}: {
item: AppBskyFeedDefs.FeedViewPost
dense?: boolean
controls?: boolean
controls?: PostControl[]
onReport?: (uri: string) => void
className?: string
showLabels?: boolean
Expand All @@ -87,7 +103,9 @@ export function PostAsCard({
<PostContent item={item} dense={dense} />
<PostEmbeds item={item} />
{showLabels && <PostLabels item={item} dense={dense} />}
{controls && <PostControls item={item} onReport={onReport} />}
{!!controls?.length && (
<PostControls item={item} onReport={onReport} controls={controls} />
)}
</div>
)
}
Expand Down Expand Up @@ -489,50 +507,80 @@ export function RecordEmbedView({
function PostControls({
item,
onReport,
controls,
}: {
item: AppBskyFeedDefs.FeedViewPost
onReport?: (uri: string) => void
controls: PostControl[]
}) {
const { data: workspaceList } = useWorkspaceList()
const { mutate: addToWorkspace } = useWorkspaceAddItemsMutation()
const { mutate: removeFromWorkspace } = useWorkspaceRemoveItemsMutation()
const isInWorkspace = workspaceList?.includes(item.post.uri)
const recordPath = `/repositories/${item.post.uri.replace('at://', '')}`

return (
<div className="flex gap-3 pl-10">
<Link
href={`/repositories/${item.post.uri.replace('at://', '')}`}
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
>
<DocumentMagnifyingGlassIcon className="w-4 h-4" />
<span className="text-sm">View</span>
</Link>
<button
type="button"
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
onClick={() => onReport?.(item.post.uri)}
>
<ExclamationCircleIcon className="w-4 h-4" />
<span className="text-sm">Report</span>
</button>
{isInWorkspace ? (
<button
type="button"
{controls.includes('like') && (
<Link
href={`${recordPath}?tab=likes`}
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
onClick={() => removeFromWorkspace([item.post.uri])}
>
<FolderMinusIcon className="w-4 h-4" />
<span className="text-sm">Remove from workspace</span>
</button>
) : (
<span className="text-sm">
{pluralize(item.post.likeCount || 0, 'like')}
</span>
</Link>
)}
{controls.includes('repost') && (
<Link
href={`${recordPath}?tab=reposts`}
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
>
<span className="text-sm">
{pluralize(item.post.repostCount || 0, 'repost')}
</span>
</Link>
)}
{controls.includes('view') && (
<Link
href={`${recordPath}`}
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
>
<DocumentMagnifyingGlassIcon className="w-4 h-4" />
<span className="text-sm">View</span>
</Link>
)}
{controls.includes('report') && (
<button
type="button"
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
onClick={() => addToWorkspace([item.post.uri])}
onClick={() => onReport?.(item.post.uri)}
>
<FolderPlusIcon className="w-4 h-4" />
<span className="text-sm">Add to workspace</span>
<ExclamationCircleIcon className="w-4 h-4" />
<span className="text-sm">Report</span>
</button>
)}

{controls.includes('workspace') &&
(isInWorkspace ? (
<button
type="button"
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
onClick={() => removeFromWorkspace([item.post.uri])}
>
<FolderMinusIcon className="w-4 h-4" />
<span className="text-sm">Remove from workspace</span>
</button>
) : (
<button
type="button"
className="flex gap-1 items-center rounded-md pt-2 pb-1 text-gray-500 dark:text-gray-50 hover:underline cursor-pointer"
onClick={() => addToWorkspace([item.post.uri])}
>
<FolderPlusIcon className="w-4 h-4" />
<span className="text-sm">Add to workspace</span>
</button>
))}
</div>
)
}
Expand Down
Loading

0 comments on commit 7e3534f

Please sign in to comment.