Skip to content

Commit

Permalink
✨ Allow adding all members of a list to workspace (#248)
Browse files Browse the repository at this point in the history
* ✨ Allow adding all members of a list to workspace

* ✨ Fetch list and show item count
  • Loading branch information
foysalit authored Nov 24, 2024
1 parent 52c78d4 commit bd289bc
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 19 deletions.
24 changes: 11 additions & 13 deletions app/repositories/[id]/[...record]/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default function RecordViewPageContent({
const uri = createAtUri({ did, collection, rkey })
const getRecord = async () => {
const { data: record } =
await labelerAgent.api.tools.ozone.moderation.getRecord({ uri })
await labelerAgent.tools.ozone.moderation.getRecord({ uri })
return record
}
const getThread = async () => {
Expand All @@ -92,7 +92,7 @@ export default function RecordViewPageContent({
}
try {
const { data: thread } =
await labelerAgent.api.app.bsky.feed.getPostThread({ uri })
await labelerAgent.app.bsky.feed.getPostThread({ uri })
return thread
} catch (err) {
if (err instanceof GetPostThread.NotFoundError) {
Expand All @@ -101,26 +101,24 @@ export default function RecordViewPageContent({
throw err
}
}
const getListProfiles = async () => {
const getList = async () => {
if (collection !== CollectionId.List) {
return undefined
}
// TODO: We need pagination here, right? how come getPostThread doesn't need it?
const { data: listData } =
await labelerAgent.api.app.bsky.graph.getList({
list: uri,
})
return listData.items.map(({ subject }) => subject)
const { data } = await labelerAgent.app.bsky.graph.getList({
list: uri,
})
return data
}
const [record, profiles, thread] = await Promise.allSettled([
const [record, listData, thread] = await Promise.allSettled([
getRecord(),
getListProfiles(),
getList(),
getThread(),
])
return {
record: record.status === 'fulfilled' ? record.value : undefined,
thread: thread.status === 'fulfilled' ? thread.value : undefined,
profiles: profiles.status === 'fulfilled' ? profiles.value : undefined,
listData: listData.status === 'fulfilled' ? listData.value : undefined,
}
},
})
Expand Down Expand Up @@ -200,9 +198,9 @@ export default function RecordViewPageContent({
/>
{data?.record && (
<RecordView
list={data.listData?.list}
record={data.record}
thread={data.thread}
profiles={data.profiles}
onReport={setReportUri}
onShowActionPanel={(subject) => setQuickActionPanelSubject(subject)}
/>
Expand Down
172 changes: 172 additions & 0 deletions components/list/Accounts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { ActionButton } from '@/common/buttons'
import { LoadMoreButton } from '@/common/LoadMoreButton'
import { ConfirmationModal } from '@/common/modals/confirmation'
import { AccountsGrid } from '@/repositories/AccountView'
import { useLabelerAgent } from '@/shell/ConfigurationContext'
import { useWorkspaceAddItemsMutation } from '@/workspace/hooks'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-toastify'

type ListAccountsProps = {
uri: string
}

const useListAccounts = ({ uri }: ListAccountsProps) => {
const labelerAgent = useLabelerAgent()

const abortController = useRef<AbortController | null>(null)
const [isConfirmationOpen, setIsConfirmationOpen] = useState(false)
const [isAdding, setIsAdding] = useState(false)
const { mutateAsync: addItemsToWorkspace } = useWorkspaceAddItemsMutation()

const { data, error, isLoading, fetchNextPage, hasNextPage } =
useInfiniteQuery({
queryKey: ['list-accounts', { uri }],
queryFn: async ({ pageParam }) => {
const { data } = await labelerAgent.app.bsky.graph.getList({
list: uri,
limit: 50,
cursor: pageParam,
})
return {
cursor: data.cursor,
profiles: data.items.map(({ subject }) => subject),
}
},
getNextPageParam: (lastPage) => lastPage.cursor,
})
const profiles = data?.pages.flatMap((page) => page.profiles)

const confirmAddToWorkspace = async () => {
// add items that are already loaded
if (profiles?.length) {
await addItemsToWorkspace(profiles.map((f) => f.did))
}
if (!data?.pageParams) {
setIsConfirmationOpen(false)
return
}
setIsAdding(true)
const newAbortController = new AbortController()
abortController.current = newAbortController

try {
let cursor = data.pageParams[0] as string | undefined
do {
const netItems = await labelerAgent.app.bsky.graph.getList(
{
list: uri,
cursor,
},
{ signal: abortController.current?.signal },
)
await addItemsToWorkspace(netItems.data.items.map((f) => f.subject.did))
cursor = netItems.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) {
if (abortController.current?.signal.reason === 'user-cancelled') {
toast.info('Stopped adding list members to workspace')
} else {
toast.error(`Something went wrong: ${(e as Error).message}`)
}
}
setIsAdding(false)
setIsConfirmationOpen(false)
}

useEffect(() => {
if (!isConfirmationOpen) {
abortController.current?.abort('user-cancelled')
}
}, [isConfirmationOpen])

useEffect(() => {
// User cancelled by closing this view (navigation, other?)
return () => abortController.current?.abort('user-cancelled')
}, [])

return {
profiles,
isLoading,
error,
fetchNextPage,
hasNextPage,
isAdding,
setIsAdding,
isConfirmationOpen,
confirmAddToWorkspace,
setIsConfirmationOpen,
}
}

export default function ListAccounts({ uri }: ListAccountsProps) {
const {
isConfirmationOpen,
setIsConfirmationOpen,
profiles,
isLoading,
error,
isAdding,
setIsAdding,
fetchNextPage,
hasNextPage,
confirmAddToWorkspace,
} = useListAccounts({ uri })

return (
<div>
<div className="flex flex-row mx-auto mt-4 max-w-5xl px-4 sm:px-6 lg:px-8 justify-end">
<ActionButton
size="sm"
disabled={!profiles?.length}
title={
!profiles?.length
? 'No users to be added to workspace'
: 'All users will be added to workspace'
}
appearance={!!profiles?.length ? 'primary' : 'outlined'}
onClick={() => setIsConfirmationOpen(true)}
>
Add all 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 to workspace?`}
description={
<>
Once confirmed, all users in this list will be added to the
workspace. If there are a lot of users in the list, this may take
quite some time but you can always stop the process and already
added users will remain in the workspace.
</>
}
/>
</div>

<AccountsGrid
isLoading={isLoading}
error={String(error ?? '')}
accounts={profiles}
/>

{hasNextPage && (
<div className="flex justify-center mb-4">
<LoadMoreButton onClick={() => fetchNextPage()} />
</div>
)}
</div>
)
}
18 changes: 12 additions & 6 deletions components/repositories/RecordView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
AppBskyFeedDefs,
AppBskyActorDefs,
ToolsOzoneModerationDefs,
AtUri,
AppBskyGraphDefs,
} from '@atproto/api'
import {
ChevronLeftIcon,
Expand All @@ -31,6 +33,8 @@ import { Likes } from '@/common/feeds/Likes'
import { Reposts } from '@/common/feeds/Reposts'
import { Thread } from '@/common/feeds/PostThread'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { CollectionId } from '@/reports/helpers/subject'
import ListAccounts from 'components/list/Accounts'

enum Views {
Details,
Expand All @@ -54,20 +58,22 @@ const TabKeys = {

export function RecordView({
record,
list,
thread,
profiles,
onReport,
onShowActionPanel,
}: {
list?: AppBskyGraphDefs.ListView
record: GetRecord.OutputSchema
thread?: GetPostThread.OutputSchema
profiles?: AppBskyActorDefs.ProfileView[]
onReport: (uri: string) => void
onShowActionPanel: (subject: string) => void
}) {
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const atUri = new AtUri(record.uri)
const isListRecord = atUri.collection === CollectionId.List

const currentView =
TabKeys[searchParams.get('tab') || 'details'] || TabKeys.details
Expand All @@ -80,11 +86,11 @@ export function RecordView({

const getTabViews = () => {
const views: TabView<Views>[] = [{ view: Views.Details, label: 'Details' }]
if (!!profiles?.length) {
if (isListRecord) {
views.push({
view: Views.Profiles,
label: 'Profiles',
sublabel: String(profiles.length),
sublabel: String(list?.listItemCount || ''),
})
}
if (!!thread) {
Expand Down Expand Up @@ -155,8 +161,8 @@ export function RecordView({
onSetCurrentView={setCurrentView}
/>
{currentView === Views.Details && <Details record={record} />}
{currentView === Views.Profiles && !!profiles?.length && (
<AccountsGrid error="" accounts={profiles} />
{currentView === Views.Profiles && (
<ListAccounts uri={record.uri} />
)}
{currentView === Views.Likes &&
!!thread &&
Expand Down

0 comments on commit bd289bc

Please sign in to comment.