diff --git a/src/lib/api/coves/types.ts b/src/lib/api/coves/types.ts index 8eb9afe3..6559d656 100644 --- a/src/lib/api/coves/types.ts +++ b/src/lib/api/coves/types.ts @@ -423,6 +423,7 @@ export interface VideoEmbed { export interface RecordEmbed { $type: | 'social.coves.embed.post' + | 'social.coves.embed.post#view' | 'social.coves.embed.record' | 'social.coves.embed.record#view' post: StrongRef diff --git a/src/lib/app/util.svelte.test.ts b/src/lib/app/util.svelte.test.ts index 405d7b93..32ddd33b 100644 --- a/src/lib/app/util.svelte.test.ts +++ b/src/lib/app/util.svelte.test.ts @@ -3,7 +3,6 @@ import type { AuthorView, CommunityRef } from '$lib/api/coves/types' import type { DID, Handle } from '$lib/types/atproto' import { canParseUrl, - communityHandleFromSlug, communityLink, communitySlug, escapeHtml, @@ -93,73 +92,32 @@ describe('communitySlug', () => { }) // --------------------------------------------------------------------------- -// communityHandleFromSlug() +// communitySlug() — slug stability +// +// A slug goes to the API verbatim, so slugifying an already-slugified handle +// must be a no-op. There is no inverse function: reconstructing the handle by +// re-adding "c-" is what used to 404 every bridged community, whose stored +// handle never had the prefix to begin with. // --------------------------------------------------------------------------- -describe('communityHandleFromSlug', () => { - it('prepends c- to a plain slug', () => { - expect(communityHandleFromSlug('gaming.coves.social')).toBe( - 'c-gaming.coves.social', - ) - }) - - it('does not double-prefix a slug that already starts with c-', () => { - expect(communityHandleFromSlug('c-gaming.coves.social')).toBe( - 'c-gaming.coves.social', - ) - }) - - it('prepends c- to a slug without dots', () => { - expect(communityHandleFromSlug('gaming')).toBe('c-gaming') +describe('communitySlug stability', () => { + it('is idempotent for a prefixed handle', () => { + const handle = 'c-gaming.coves.social' + expect(communitySlug(communitySlug(handle))).toBe(communitySlug(handle)) }) - it('handles an empty string by prepending c-', () => { - expect(communityHandleFromSlug('')).toBe('c-') + it('leaves a bridged handle untouched through repeated slugging', () => { + const bridged = 'selfhosted.lemmy-world.tdpl.io' + expect(communitySlug(bridged)).toBe(bridged) + expect(communitySlug(communitySlug(bridged))).toBe(bridged) }) it('passes a did:plc DID through unchanged', () => { - expect(communityHandleFromSlug('did:plc:abc123xyz')).toBe( - 'did:plc:abc123xyz', - ) + expect(communitySlug('did:plc:abc123xyz')).toBe('did:plc:abc123xyz') }) it('passes a did:web DID through unchanged', () => { - expect(communityHandleFromSlug('did:web:coves.social')).toBe( - 'did:web:coves.social', - ) - }) - - it('still prefixes a handle-shaped slug that merely contains "did"', () => { - expect(communityHandleFromSlug('did.coves.social')).toBe( - 'c-did.coves.social', - ) - }) -}) - -// --------------------------------------------------------------------------- -// communitySlug / communityHandleFromSlug round-trip -// --------------------------------------------------------------------------- - -describe('communitySlug <-> communityHandleFromSlug round-trip', () => { - it('round-trip: communityHandleFromSlug(communitySlug(handle)) === handle for c- prefixed handle', () => { - const handle = 'c-gaming.coves.social' - expect(communityHandleFromSlug(communitySlug(handle))).toBe(handle) - }) - - it('round-trip: communitySlug(communityHandleFromSlug(slug)) === slug for plain slug', () => { - const slug = 'gaming.coves.social' - expect(communitySlug(communityHandleFromSlug(slug))).toBe(slug) - }) - - it('round-trip preserves identity for handle without c- prefix', () => { - const handle = 'tech.coves.social' - // communitySlug('tech.coves.social') -> 'tech.coves.social' (no c- to strip) - // communityHandleFromSlug('tech.coves.social') -> 'c-tech.coves.social' - // This is NOT a round-trip identity for non-c- handles, which is expected - // since the canonical handle form uses c- prefix - expect(communityHandleFromSlug(communitySlug(handle))).toBe( - 'c-tech.coves.social', - ) + expect(communitySlug('did:web:coves.social')).toBe('did:web:coves.social') }) }) diff --git a/src/lib/app/util.svelte.ts b/src/lib/app/util.svelte.ts index c939060f..67c8f2f7 100644 --- a/src/lib/app/util.svelte.ts +++ b/src/lib/app/util.svelte.ts @@ -42,32 +42,25 @@ export const fullCommunityName = (name: string, actorId: string): string => { } /** - * Strips the "c-" prefix from a community handle to produce a URL-friendly slug. + * Strips the "c-" prefix from a community handle to produce its canonical form. * - * Coves community handles use a "c-" prefix convention (e.g. "c-mycommunity") - * to distinguish community actors from user actors in the ATProto namespace. - * Route params and URLs use the bare slug without the prefix. + * Communities provisioned on a Coves instance use a "c-" prefix convention + * (e.g. "c-mycommunity.coves.social") to distinguish community actors from + * user actors in the ATProto namespace. Communities bridged in from other + * platforms keep their source handle and carry no prefix, so this strips the + * prefix only when it is actually there. + * + * The prefix is internal plumbing: route params, URLs, and any handle shown to + * a user all use the bare form. There is deliberately no inverse — a slug is + * sent to the API as-is, because the bare form is ambiguous (both + * "gardening.coves.social" and a bridged "linux.lemmy-ml.tdpl.io" are + * prefix-free) and only the AppView knows which stored handle it maps to. It + * resolves either form. */ export function communitySlug(handle: string): string { return handle.startsWith('c-') ? handle.slice(2) : handle } -/** - * Prepends the "c-" prefix to a URL slug to reconstruct the community handle. - * - * Coves community handles use a "c-" prefix convention (e.g. "c-mycommunity") - * to distinguish community actors from user actors in the ATProto namespace. - * This reverses {@link communitySlug} for API calls that expect the full handle. - * - * DIDs (e.g. "did:plc:abc123") are passed through unchanged: the route param - * matcher accepts URL-encoded DIDs as well as handles, and a DID already - * identifies the community actor without any handle prefix. - */ -export function communityHandleFromSlug(slug: string): string { - if (slug.startsWith('did:')) return slug - return slug.startsWith('c-') ? slug : `c-${slug}` -} - export const placeholders = { get: (type: 'url' | 'post' | 'body' | 'comment') => { switch (type) { diff --git a/src/lib/feature/community/CommunityLink.svelte b/src/lib/feature/community/CommunityLink.svelte index 9464c234..5dae6844 100644 --- a/src/lib/feature/community/CommunityLink.svelte +++ b/src/lib/feature/community/CommunityLink.svelte @@ -1,6 +1,7 @@