From 0e4878c733a71e9f0e91777134bab773dc09a608 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 23 Apr 2026 11:16:54 +0200 Subject: [PATCH] [Glitch] Show collection preview cards and open collections links locally Port 478dae0ab3420853d1d9ff2f6b3efcb85b322fd3 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/api_types/collections.ts | 5 ++-- .../flavours/glitch/components/status.jsx | 23 +++++++++----- .../glitch/components/status/handled_link.tsx | 13 ++++++++ .../glitch/components/status_content.jsx | 21 ++++++------- .../components/collection_lockup.tsx | 4 ++- .../components/collection_menu.tsx | 5 ++-- .../features/collections/editor/details.tsx | 4 ++- .../glitch/features/collections/utils.ts | 2 ++ .../status/components/detailed_status.tsx | 30 ++++++++++++++----- .../flavours/glitch/models/status.ts | 3 ++ .../flavours/glitch/utils/compare_urls.ts | 14 +++++++++ 11 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 app/javascript/flavours/glitch/utils/compare_urls.ts diff --git a/app/javascript/flavours/glitch/api_types/collections.ts b/app/javascript/flavours/glitch/api_types/collections.ts index 3edaa64c95..2ba20eb514 100644 --- a/app/javascript/flavours/glitch/api_types/collections.ts +++ b/app/javascript/flavours/glitch/api_types/collections.ts @@ -11,7 +11,8 @@ export interface ApiCollectionJSON { account_id: string; id: string; - uri: string | null; + uri: string; + url: string; local: boolean; item_count: number; @@ -56,7 +57,7 @@ export interface CollectionAccountItem { id: string; account_id?: string; // Only present when state is 'accepted' (or the collection is your own) state: 'pending' | 'accepted' | 'rejected' | 'revoked'; - position: number; + created_at: string; } export interface WrappedCollectionAccountItem { diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 8005965459..b692fea962 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -642,13 +642,22 @@ class Status extends ImmutablePureComponent { mediaIcons.push('video-camera'); } } else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted && !status.get('quote')) { - media.push( - , - ); + const cardUrl = status.getIn(['card', 'url']); + + const taggedCollection = ( + status.get('tagged_collections') + ).find((item) => compareUrls(item.get('url'), cardUrl)); + if (taggedCollection) { + media.push(); + } else { + media.push( + , + ); + } mediaIcons.push('link'); } diff --git a/app/javascript/flavours/glitch/components/status/handled_link.tsx b/app/javascript/flavours/glitch/components/status/handled_link.tsx index 2b1a1bb763..55a2a47948 100644 --- a/app/javascript/flavours/glitch/components/status/handled_link.tsx +++ b/app/javascript/flavours/glitch/components/status/handled_link.tsx @@ -4,7 +4,9 @@ import type { ComponentProps, FC } from 'react'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; +import type { ApiCollectionJSON } from '@/flavours/glitch/api_types/collections'; import type { ApiMentionJSON } from '@/flavours/glitch/api_types/statuses'; +import { getCollectionPath } from '@/flavours/glitch/features/collections/utils'; import { useAppSelector } from '@/flavours/glitch/store'; import type { OnElementHandler } from '@/flavours/glitch/utils/html'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; @@ -15,6 +17,7 @@ export interface HandledLinkProps { prevText?: string; hashtagAccountId?: string; mention?: Pick; + collection?: Pick; } const textMatchesTarget = (text: string, origin: string, host: string) => { @@ -111,6 +114,7 @@ export const HandledLink: FC> = ({ prevText, hashtagAccountId, mention, + collection, className, children, ...props @@ -180,6 +184,15 @@ export const HandledLink: FC> = ({ {children} ); + } else if (collection) { + return ( + + {children} + + ); } // Non-absolute paths treated as internal links. This shouldn't happen, but just in case. diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index 40a1d26ccd..eac90b1156 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -18,6 +18,7 @@ import { languages as preloadedLanguages } from 'flavours/glitch/initial_state'; import { EmojiHTML } from './emoji/html'; import { injectIntl } from './intl'; import { HandledLink } from './status/handled_link'; +import { compareUrls } from '../utils/compare_urls'; const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) @@ -71,17 +72,6 @@ const mapStateToProps = state => ({ languages: state.getIn(['server', 'translationLanguages', 'items']), }); -const compareUrls = (href1, href2) => { - try { - const url1 = new URL(href1); - const url2 = new URL(href2); - - return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search; - } catch { - return false; - } -}; - class StatusContent extends PureComponent { static propTypes = { identity: identityContextPropShape, @@ -166,7 +156,13 @@ class StatusContent extends PureComponent { handleElement = (element, { key, ...props }, children) => { if (element instanceof HTMLAnchorElement) { - const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url'))); + const mention = this.props.status.get('mentions').find( + item => compareUrls(element.href, item.get('url')) + ); + const taggedCollection = this.props.status.get('tagged_collections').find( + item => compareUrls(element.href, item.get('url')) + ) + return ( {children} diff --git a/app/javascript/flavours/glitch/features/collections/components/collection_lockup.tsx b/app/javascript/flavours/glitch/features/collections/components/collection_lockup.tsx index 03afe7c5cd..4833af1de4 100644 --- a/app/javascript/flavours/glitch/features/collections/components/collection_lockup.tsx +++ b/app/javascript/flavours/glitch/features/collections/components/collection_lockup.tsx @@ -13,6 +13,8 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp import { useAccount } from 'flavours/glitch/hooks/useAccount'; import { domain } from 'flavours/glitch/initial_state'; +import { getCollectionPath } from '../utils'; + import classes from './collection_lockup.module.scss'; export const AvatarGrid: React.FC<{ @@ -67,7 +69,7 @@ export const CollectionLockup: React.FC = ({ />

- + {name}

diff --git a/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx b/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx index b4ab4770af..da3c5b2481 100644 --- a/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx +++ b/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx @@ -15,6 +15,7 @@ import type { MenuItem } from 'flavours/glitch/models/dropdown_menu'; import { useAppDispatch } from 'flavours/glitch/store'; import { messages as editorMessages } from '../editor'; +import { getCollectionPath } from '../utils'; const messages = defineMessages({ view: { @@ -120,7 +121,7 @@ export const CollectionMenu: React.FC<{ const menu = useMemo(() => { const viewCollectionItem: MenuItem = { text: intl.formatMessage(messages.view), - to: `/collections/${id}`, + to: getCollectionPath(id), }; const shareItems: MenuItem[] = [ { @@ -130,7 +131,7 @@ export const CollectionMenu: React.FC<{ { text: intl.formatMessage(messages.copyLink), action: () => { - void navigator.clipboard.writeText(`/collections/${id}`); + void navigator.clipboard.writeText(getCollectionPath(id)); dispatch(showAlert({ message: messages.copyLinkConfirmation })); }, }, diff --git a/app/javascript/flavours/glitch/features/collections/editor/details.tsx b/app/javascript/flavours/glitch/features/collections/editor/details.tsx index 63b6acc90e..8025ad617f 100644 --- a/app/javascript/flavours/glitch/features/collections/editor/details.tsx +++ b/app/javascript/flavours/glitch/features/collections/editor/details.tsx @@ -38,6 +38,8 @@ import { } from 'flavours/glitch/reducers/slices/collections'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; +import { getCollectionPath } from '../utils'; + import classes from './styles.module.scss'; import { WizardStepTitle } from './wizard_step_title'; @@ -134,7 +136,7 @@ export const CollectionDetails: React.FC = () => { ).then((result) => { if (isFulfilled(result)) { history.replace(`/@${currentUserName}/collections`); - history.push(`/collections/${result.payload.collection.id}`, { + history.push(getCollectionPath(result.payload.collection.id), { newCollection: true, }); } diff --git a/app/javascript/flavours/glitch/features/collections/utils.ts b/app/javascript/flavours/glitch/features/collections/utils.ts index cf7972f6f7..f1b712e155 100644 --- a/app/javascript/flavours/glitch/features/collections/utils.ts +++ b/app/javascript/flavours/glitch/features/collections/utils.ts @@ -3,3 +3,5 @@ import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment'; export function areCollectionsEnabled() { return isServerFeatureEnabled('collections'); } + +export const getCollectionPath = (id: string) => `/collections/${id}`; diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx index a72d9ad714..3d9a11154b 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx @@ -30,10 +30,13 @@ import StatusContent from 'flavours/glitch/components/status_content'; import { QuotedStatus } from 'flavours/glitch/components/status_quoted'; import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon'; import { Audio } from 'flavours/glitch/features/audio'; +import { CollectionPreviewCard } from 'flavours/glitch/features/collections/components/collection_preview_card'; import scheduleIdleTask from 'flavours/glitch/features/ui/util/schedule_idle_task'; import { Video } from 'flavours/glitch/features/video'; import { useIdentity } from 'flavours/glitch/identity_context'; +import type { CollectionAttachment } from 'flavours/glitch/models/status'; import { useAppSelector } from 'flavours/glitch/store'; +import { compareUrls } from 'flavours/glitch/utils/compare_urls'; import Card from './card'; @@ -292,13 +295,26 @@ export const DetailedStatus: React.FC<{ mediaIcons.push('video-camera'); } } else if (status.get('card') && !status.get('quote')) { - media = ( - - ); + const cardUrl: string = status.getIn(['card', 'url']); + + const taggedCollection = status + .get('tagged_collections') + .find((item: CollectionAttachment) => + compareUrls(item.get('url'), cardUrl), + ); + + if (taggedCollection) { + media = ; + } else { + media = ( + + ); + } + mediaIcons.push('link'); } diff --git a/app/javascript/flavours/glitch/models/status.ts b/app/javascript/flavours/glitch/models/status.ts index e18c822946..5ca02a3935 100644 --- a/app/javascript/flavours/glitch/models/status.ts +++ b/app/javascript/flavours/glitch/models/status.ts @@ -1,5 +1,6 @@ import type { RecordOf } from 'immutable'; +import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections'; import type { ApiPreviewCardJSON } from 'flavours/glitch/api_types/statuses'; export type { StatusVisibility } from 'flavours/glitch/api_types/statuses'; @@ -10,3 +11,5 @@ export type Status = Immutable.Map; export type Card = RecordOf; export type MediaAttachment = Immutable.Map; + +export type CollectionAttachment = RecordOf; diff --git a/app/javascript/flavours/glitch/utils/compare_urls.ts b/app/javascript/flavours/glitch/utils/compare_urls.ts new file mode 100644 index 0000000000..de4f6220e8 --- /dev/null +++ b/app/javascript/flavours/glitch/utils/compare_urls.ts @@ -0,0 +1,14 @@ +export function compareUrls(href1: string, href2: string) { + try { + const url1 = new URL(href1); + const url2 = new URL(href2); + + return ( + url1.origin === url2.origin && + url1.pathname === url2.pathname && + url1.search === url2.search + ); + } catch { + return false; + } +}