From d4f66796967b763780d11bd4618a9c093f74178e Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Wed, 7 Feb 2024 00:24:36 +0100 Subject: [PATCH 01/13] :sparkles: Use dids in all links instead of handles --- .../[id]/[...record]/page-content.tsx | 196 ++++++++++++++++++ app/repositories/[id]/[...record]/page.tsx | 196 +----------------- app/repositories/[id]/page.tsx | 11 +- components/common/RecordCard.tsx | 4 +- components/common/feeds/RecordCard.tsx | 2 +- components/common/posts/PostsFeed.tsx | 4 +- components/common/posts/PostsTable.tsx | 4 +- components/list/RecordCard.tsx | 2 +- components/repositories/AccountView.tsx | 2 +- components/repositories/RecordView.tsx | 4 +- .../repositories/RedirectFromhandleToDid.tsx | 20 ++ components/repositories/RepositoriesTable.tsx | 2 +- .../repositories/useHandleToDidRedirect.tsx | 32 +++ .../shell/CommandPalette/useAsyncSearch.tsx | 14 +- lib/identity.ts | 16 ++ 15 files changed, 295 insertions(+), 214 deletions(-) create mode 100644 app/repositories/[id]/[...record]/page-content.tsx create mode 100644 components/repositories/RedirectFromhandleToDid.tsx create mode 100644 components/repositories/useHandleToDidRedirect.tsx create mode 100644 lib/identity.ts diff --git a/app/repositories/[id]/[...record]/page-content.tsx b/app/repositories/[id]/[...record]/page-content.tsx new file mode 100644 index 00000000..5a9d0c76 --- /dev/null +++ b/app/repositories/[id]/[...record]/page-content.tsx @@ -0,0 +1,196 @@ +'use client' +import { useQuery } from '@tanstack/react-query' +import { + AppBskyFeedGetPostThread as GetPostThread, + ComAtprotoAdminEmitModerationEvent, +} from '@atproto/api' +import { ReportPanel } from '@/reports/ReportPanel' +import { RecordView } from '@/repositories/RecordView' +import client from '@/lib/client' +import { createAtUri } from '@/lib/util' +import { createReport } from '@/repositories/createReport' +import { Loading, LoadingFailed } from '@/common/Loader' +import { CollectionId } from '@/reports/helpers/subject' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' +import { emitEvent } from '@/mod-event/helpers/emitEvent' +import { useEffect } from 'react' +import { useTitle } from 'react-use' + +const buildPageTitle = ({ + handle, + collection, + rkey, +}: { + handle?: string + collection?: string + rkey?: string +}) => { + let title = `Record Details` + + if (collection) { + const titleFromCollection = collection.split('.').pop() + if (titleFromCollection) { + title = + titleFromCollection[0].toUpperCase() + titleFromCollection.slice(1) + } + } + + if (handle) { + title += ` - ${handle}` + } + + if (rkey) { + title += ` - ${rkey}` + } + return title +} + +export default function RecordViewPageContent({ + params, +}: { + params: { id: string; record: string[] } +}) { + const id = decodeURIComponent(params.id) + const collection = params.record[0] && decodeURIComponent(params.record[0]) + const rkey = params.record[1] && decodeURIComponent(params.record[1]) + const { + data, + error, + refetch, + isLoading: isInitialLoading, + } = useQuery({ + queryKey: ['record', { id, collection, rkey }], + queryFn: async () => { + let did: string + if (id.startsWith('did:')) { + did = id + } else { + const { data } = await client.api.com.atproto.identity.resolveHandle({ + handle: id, + }) + did = data.did + } + const uri = createAtUri({ did, collection, rkey }) + const getRecord = async () => { + const { data: record } = await client.api.com.atproto.admin.getRecord( + { uri }, + { headers: client.adminHeaders() }, + ) + return record + } + const getThread = async () => { + if (collection !== CollectionId.Post) { + return undefined + } + try { + const { data: thread } = await client.api.app.bsky.feed.getPostThread( + { uri }, + { headers: client.adminHeaders() }, + ) + return thread + } catch (err) { + if (err instanceof GetPostThread.NotFoundError) { + return undefined + } + throw err + } + } + const getListProfiles = async () => { + if (collection !== CollectionId.List) { + return undefined + } + // TODO: We need pagination here, right? how come getPostThread doesn't need it? + const { data: listData } = await client.api.app.bsky.graph.getList({ + list: uri, + }) + return listData.items.map(({ subject }) => subject) + } + const [record, profiles, thread] = await Promise.all([ + getRecord(), + getListProfiles(), + getThread(), + ]) + return { record, thread, profiles } + }, + }) + + const searchParams = useSearchParams() + const router = useRouter() + const pathname = usePathname() + const quickOpenParam = searchParams.get('quickOpen') ?? '' + const reportUri = searchParams.get('reportUri') || undefined + const setQuickActionPanelSubject = (subject: string) => { + // This route should not have any search params but in case it does, let's make sure original params are maintained + const newParams = new URLSearchParams(searchParams) + if (!subject) { + newParams.delete('quickOpen') + } else { + newParams.set('quickOpen', subject) + } + router.push((pathname ?? '') + '?' + newParams.toString()) + } + const setReportUri = (uri?: string) => { + const newParams = new URLSearchParams(searchParams) + if (uri) { + newParams.set('reportUri', uri) + } else { + newParams.delete('reportUri') + } + router.push((pathname ?? '') + '?' + newParams.toString()) + } + + useEffect(() => { + if (reportUri === 'default' && data?.record) { + setReportUri(data?.record.uri) + } + }, [data, reportUri]) + + const pageTitle = buildPageTitle({ + handle: data?.record?.repo.handle, + rkey, + collection, + }) + useTitle(pageTitle) + + if (error) { + return + } + if (!data) { + return + } + return ( + <> + setQuickActionPanelSubject('')} + setSubject={setQuickActionPanelSubject} + subject={quickOpenParam} // select first subject if there are multiple + subjectOptions={[quickOpenParam]} + isInitialLoading={isInitialLoading} + onSubmit={async ( + vals: ComAtprotoAdminEmitModerationEvent.InputSchema, + ) => { + await emitEvent(vals) + refetch() + }} + /> + setReportUri(undefined)} + subject={reportUri} + onSubmit={async (vals) => { + await createReport(vals) + refetch() + }} + /> + setQuickActionPanelSubject(subject)} + /> + + ) +} diff --git a/app/repositories/[id]/[...record]/page.tsx b/app/repositories/[id]/[...record]/page.tsx index ad81ea5a..7c3d0efd 100644 --- a/app/repositories/[id]/[...record]/page.tsx +++ b/app/repositories/[id]/[...record]/page.tsx @@ -1,196 +1,18 @@ 'use client' -import { useQuery } from '@tanstack/react-query' -import { - AppBskyFeedGetPostThread as GetPostThread, - ComAtprotoAdminEmitModerationEvent, -} from '@atproto/api' -import { ReportPanel } from '@/reports/ReportPanel' -import { RecordView } from '@/repositories/RecordView' -import client from '@/lib/client' -import { createAtUri } from '@/lib/util' -import { createReport } from '@/repositories/createReport' -import { Loading, LoadingFailed } from '@/common/Loader' -import { CollectionId } from '@/reports/helpers/subject' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' -import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' -import { emitEvent } from '@/mod-event/helpers/emitEvent' -import { useEffect } from 'react' -import { useTitle } from 'react-use' +import { Suspense } from 'react' +import { RedirectFromHandleToDid } from '@/repositories/RedirectFromhandleToDid' +import RecordViewPageContent from './page-content' -const buildPageTitle = ({ - handle, - collection, - rkey, -}: { - handle?: string - collection?: string - rkey?: string -}) => { - let title = `Record Details` - - if (collection) { - const titleFromCollection = collection.split('.').pop() - if (titleFromCollection) { - title = - titleFromCollection[0].toUpperCase() + titleFromCollection.slice(1) - } - } - - if (handle) { - title += ` - ${handle}` - } - - if (rkey) { - title += ` - ${rkey}` - } - return title -} - -export default function Record({ +export default function RecordViewPage({ params, }: { params: { id: string; record: string[] } }) { - const id = decodeURIComponent(params.id) - const collection = params.record[0] && decodeURIComponent(params.record[0]) - const rkey = params.record[1] && decodeURIComponent(params.record[1]) - const { - data, - error, - refetch, - isLoading: isInitialLoading, - } = useQuery({ - queryKey: ['record', { id, collection, rkey }], - queryFn: async () => { - let did: string - if (id.startsWith('did:')) { - did = id - } else { - const { data } = await client.api.com.atproto.identity.resolveHandle({ - handle: id, - }) - did = data.did - } - const uri = createAtUri({ did, collection, rkey }) - const getRecord = async () => { - const { data: record } = await client.api.com.atproto.admin.getRecord( - { uri }, - { headers: client.adminHeaders() }, - ) - return record - } - const getThread = async () => { - if (collection !== CollectionId.Post) { - return undefined - } - try { - const { data: thread } = await client.api.app.bsky.feed.getPostThread( - { uri }, - { headers: client.adminHeaders() }, - ) - return thread - } catch (err) { - if (err instanceof GetPostThread.NotFoundError) { - return undefined - } - throw err - } - } - const getListProfiles = async () => { - if (collection !== CollectionId.List) { - return undefined - } - // TODO: We need pagination here, right? how come getPostThread doesn't need it? - const { data: listData } = await client.api.app.bsky.graph.getList({ - list: uri, - }) - return listData.items.map(({ subject }) => subject) - } - const [record, profiles, thread] = await Promise.all([ - getRecord(), - getListProfiles(), - getThread(), - ]) - return { record, thread, profiles } - }, - }) - - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - const quickOpenParam = searchParams.get('quickOpen') ?? '' - const reportUri = searchParams.get('reportUri') || undefined - const setQuickActionPanelSubject = (subject: string) => { - // This route should not have any search params but in case it does, let's make sure original params are maintained - const newParams = new URLSearchParams(searchParams) - if (!subject) { - newParams.delete('quickOpen') - } else { - newParams.set('quickOpen', subject) - } - router.push((pathname ?? '') + '?' + newParams.toString()) - } - const setReportUri = (uri?: string) => { - const newParams = new URLSearchParams(searchParams) - if (uri) { - newParams.set('reportUri', uri) - } else { - newParams.delete('reportUri') - } - router.push((pathname ?? '') + '?' + newParams.toString()) - } - - useEffect(() => { - if (reportUri === 'default' && data?.record) { - setReportUri(data?.record.uri) - } - }, [data, reportUri]) - - const pageTitle = buildPageTitle({ - handle: data?.record?.repo.handle, - rkey, - collection, - }) - useTitle(pageTitle) - - if (error) { - return - } - if (!data) { - return - } return ( - <> - setQuickActionPanelSubject('')} - setSubject={setQuickActionPanelSubject} - subject={quickOpenParam} // select first subject if there are multiple - subjectOptions={[quickOpenParam]} - isInitialLoading={isInitialLoading} - onSubmit={async ( - vals: ComAtprotoAdminEmitModerationEvent.InputSchema, - ) => { - await emitEvent(vals) - refetch() - }} - /> - setReportUri(undefined)} - subject={reportUri} - onSubmit={async (vals) => { - await createReport(vals) - refetch() - }} - /> - setQuickActionPanelSubject(subject)} - /> - + }> + + + + ) } diff --git a/app/repositories/[id]/page.tsx b/app/repositories/[id]/page.tsx index 5632eeaa..7f6afda6 100644 --- a/app/repositories/[id]/page.tsx +++ b/app/repositories/[id]/page.tsx @@ -1,13 +1,20 @@ 'use client' import { Suspense } from 'react' import { RepositoryViewPageContent } from './page-content' +import { RedirectFromHandleToDid } from '@/repositories/RedirectFromhandleToDid' -export default function RepositoryViewPage({ params }: { params: { id: string } }) { +export default function RepositoryViewPage({ + params, +}: { + params: { id: string } +}) { const { id: rawId } = params const id = decodeURIComponent(rawId) return ( }> - + + + ) } diff --git a/components/common/RecordCard.tsx b/components/common/RecordCard.tsx index fd4d56d9..35824937 100644 --- a/components/common/RecordCard.tsx +++ b/components/common/RecordCard.tsx @@ -191,7 +191,7 @@ export function InlineRepo(props: { did: string }) { /> {profile?.displayName ? ( @@ -237,7 +237,7 @@ export function RepoCard(props: { did: string }) {

{profile?.displayName ? ( diff --git a/components/common/feeds/RecordCard.tsx b/components/common/feeds/RecordCard.tsx index f1235d42..e7a939b6 100644 --- a/components/common/feeds/RecordCard.tsx +++ b/components/common/feeds/RecordCard.tsx @@ -69,7 +69,7 @@ export const FeedGeneratorRecordCard = ({ uri }: { uri: string }) => { {displayName} by - + @{creator.handle} {' '} diff --git a/components/common/posts/PostsFeed.tsx b/components/common/posts/PostsFeed.tsx index b87157ac..b0c69729 100644 --- a/components/common/posts/PostsFeed.tsx +++ b/components/common/posts/PostsFeed.tsx @@ -106,7 +106,7 @@ function PostHeader({ ) : undefined}

{item.post.author.displayName ? ( @@ -310,7 +310,7 @@ function PostEmbeds({ item }: { item: AppBskyFeedDefs.FeedViewPost }) { />

{embed.record.author.displayName ? ( diff --git a/components/common/posts/PostsTable.tsx b/components/common/posts/PostsTable.tsx index 3746c7ae..ce5613d0 100644 --- a/components/common/posts/PostsTable.tsx +++ b/components/common/posts/PostsTable.tsx @@ -73,14 +73,14 @@ export function PostAsRow({ {isRepost(item.reason) ? ( Reposted by{' '} - + @{item.reason.by.handle} ) : undefined} @{item.post.author.handle} diff --git a/components/list/RecordCard.tsx b/components/list/RecordCard.tsx index 8284b511..ef2d57b4 100644 --- a/components/list/RecordCard.tsx +++ b/components/list/RecordCard.tsx @@ -59,7 +59,7 @@ export const ListRecordCard = ({ uri }: { uri: string }) => { {name} by - + @{creator.handle} {' '} diff --git a/components/repositories/AccountView.tsx b/components/repositories/AccountView.tsx index b7d8bff5..2c021b57 100644 --- a/components/repositories/AccountView.tsx +++ b/components/repositories/AccountView.tsx @@ -651,7 +651,7 @@ export function AccountsGrid({

-
-
}> - - +
+ +
!!code2) + +export const LanguagePicker: React.FC = () => { + const searchParams = useSearchParams() + const router = useRouter() + const pathname = usePathname() + + const lang = searchParams.get('lang') + + const changeLanguage = (lang: string) => { + const nextParams = new URLSearchParams(searchParams) + + if (lang) { + nextParams.set('lang', lang) + } else { + nextParams.delete('lang') + } + + router.push((pathname ?? '') + '?' + nextParams.toString()) + } + + return ( + + ) +} From 7b229a9e7ddc6e0a9381f00d3f331000dd026442 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 16 Feb 2024 12:18:17 +0100 Subject: [PATCH 05/13] :sparkles: Move lang filter to use tags --- app/reports/page-content.tsx | 8 ++++---- components/common/LanguagePicker.tsx | 23 ++++++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/reports/page-content.tsx b/app/reports/page-content.tsx index 8b03d66a..b5917163 100644 --- a/app/reports/page-content.tsx +++ b/app/reports/page-content.tsx @@ -157,7 +157,7 @@ export const ReportsPageContent = () => { const includeMuted = !!params.get('includeMuted') const appealed = !!params.get('appealed') const reviewState = params.get('reviewState') - const lang = params.get('lang') + const tags = params.get('tags') const { sortField, sortDirection } = getSortParams(params) const { getReportSearchParams } = useFluentReportSearch() const { lastReviewedBy, subject, reporters } = getReportSearchParams() @@ -188,7 +188,7 @@ export const ReportsPageContent = () => { reporters, takendown, appealed, - lang, + tags, }, ], queryFn: async ({ pageParam }) => { @@ -212,8 +212,8 @@ export const ReportsPageContent = () => { queryParams.appealed = appealed } - if (lang) { - queryParams.langs = [lang] + if (tags) { + queryParams.tags = tags.split(',') } // For these fields, we only want to add them to the filter if the values are set, otherwise, defaults will kick in diff --git a/components/common/LanguagePicker.tsx b/components/common/LanguagePicker.tsx index b3592449..a1a87d42 100644 --- a/components/common/LanguagePicker.tsx +++ b/components/common/LanguagePicker.tsx @@ -9,15 +9,28 @@ export const LanguagePicker: React.FC = () => { const router = useRouter() const pathname = usePathname() - const lang = searchParams.get('lang') + const tagsParam = searchParams.get('tags') + const tags = tagsParam?.split(',') || [] + const lang = tags.find((tag) => tag.includes('lang:'))?.split(':')[1] - const changeLanguage = (lang: string) => { + const changeLanguage = (newLang: string) => { const nextParams = new URLSearchParams(searchParams) - if (lang) { - nextParams.set('lang', lang) + if (newLang) { + nextParams.set( + 'tags', + [ + ...tags.filter((tag) => !tag.includes('lang:')), + `lang:${newLang}`, + ].join(','), + ) } else { - nextParams.delete('lang') + const newTags = tags.filter((tag) => !tag.includes('lang:')) + if (newTags.length) { + nextParams.set('tags', newTags.join(',')) + } else { + nextParams.delete('tags') + } } router.push((pathname ?? '') + '?' + nextParams.toString()) From 461ed4aa95d68e220aadefc233838fa142b2829f Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 16 Feb 2024 15:10:09 +0100 Subject: [PATCH 06/13] :bug: Unique key for option --- components/common/LanguagePicker.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/common/LanguagePicker.tsx b/components/common/LanguagePicker.tsx index a1a87d42..baf16607 100644 --- a/components/common/LanguagePicker.tsx +++ b/components/common/LanguagePicker.tsx @@ -2,7 +2,7 @@ import { LANGUAGES } from '@/lib/locale/languages' import { useSearchParams, useRouter, usePathname } from 'next/navigation' import { Select } from './forms' -const languagesInPicker = LANGUAGES.filter(({ code2 }) => !!code2) +const languagesInPicker = LANGUAGES.filter(({ code2 }) => !!code2.trim()) export const LanguagePicker: React.FC = () => { const searchParams = useSearchParams() @@ -43,9 +43,9 @@ export const LanguagePicker: React.FC = () => { onChange={(e) => changeLanguage(e.target.value)} > - {languagesInPicker.map(({ code2, name }) => { + {languagesInPicker.map(({ code2, code3, name }) => { return ( - ) From 4256dcc8d947be50b0b087f01be31da1441b3b72 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Sat, 17 Feb 2024 16:19:51 +0100 Subject: [PATCH 07/13] :sparkles: Add tag display and emit form --- app/actions/ModActionPanel/QuickAction.tsx | 39 +++++++++++++++++++++- app/reports/page-content.tsx | 2 +- components/mod-event/EventItem.tsx | 30 +++++++++++++++++ components/mod-event/EventList.tsx | 34 ++++++++++++++++++- components/mod-event/ItemTitle.tsx | 4 +++ components/mod-event/SelectorButton.tsx | 1 + components/mod-event/constants.ts | 2 ++ components/mod-event/useModEventList.tsx | 19 +++++++++-- components/tags/utils.ts | 5 +++ 9 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 components/tags/utils.ts diff --git a/app/actions/ModActionPanel/QuickAction.tsx b/app/actions/ModActionPanel/QuickAction.tsx index bb520e23..606e2269 100644 --- a/app/actions/ModActionPanel/QuickAction.tsx +++ b/app/actions/ModActionPanel/QuickAction.tsx @@ -45,6 +45,7 @@ import { SubjectReviewStateBadge } from '@/subject/ReviewStateMarker' import { getProfileUriForDid } from '@/reports/helpers/subject' import { Dialog } from '@headlessui/react' import { SubjectSwitchButton } from '@/common/SubjectSwitchButton' +import { diffTags } from 'components/tags/utils' const FORM_ID = 'mod-action-panel' const useBreakpoint = createBreakpoint({ xs: 340, sm: 640 }) @@ -117,7 +118,9 @@ export function ModActionPanelQuick( title="No reports" className="h-10 w-10 text-green-300 align-text-bottom mx-auto mb-4" /> -

No reports found

+

+ No reports found +

)} @@ -174,6 +177,7 @@ function Form( const [modEventType, setModEventType] = useState( MOD_EVENTS.ACKNOWLEDGE, ) + const isTagEvent = modEventType === MOD_EVENTS.TAG const isLabelEvent = modEventType === MOD_EVENTS.LABEL const isMuteEvent = modEventType === MOD_EVENTS.MUTE const isCommentEvent = modEventType === MOD_EVENTS.COMMENT @@ -250,6 +254,15 @@ function Form( coreEvent.sticky = true } + if (formData.get('tags')) { + const tags = String(formData.get('tags')) + .split(',') + .map((tag) => tag.trim()) + const { add, remove } = diffTags(subjectStatus?.tags || [], tags) + coreEvent.add = add + coreEvent.remove = remove + } + const { subject: subjectInfo, record: recordInfo } = await createSubjectFromId(subject) @@ -526,6 +539,17 @@ function Form( + {subjectStatus?.tags?.length && ( +
+ + + {subjectStatus.tags.map((tag) => { + return {tag} + })} + + +
+ )} {/* This is only meant to be switched on in mobile/small screen view */} {/* The parent component ensures to toggle this based on the screen size */} @@ -566,6 +590,19 @@ function Form( )} + {isTagEvent && ( + + + + )} +