Show collection preview cards and open collections links locally (#38643)
This commit is contained in:
parent
a8741495c4
commit
478dae0ab3
@ -11,7 +11,8 @@ export interface ApiCollectionJSON {
|
|||||||
account_id: string;
|
account_id: string;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
uri: string | null;
|
uri: string;
|
||||||
|
url: string;
|
||||||
local: boolean;
|
local: boolean;
|
||||||
item_count: number;
|
item_count: number;
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export interface CollectionAccountItem {
|
|||||||
id: string;
|
id: string;
|
||||||
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
||||||
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
||||||
position: number;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WrappedCollectionAccountItem {
|
export interface WrappedCollectionAccountItem {
|
||||||
|
|||||||
@ -547,13 +547,23 @@ class Status extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && !status.get('quote')) {
|
} else if (status.get('card') && !status.get('quote')) {
|
||||||
media = (
|
const cardUrl = status.getIn(['card', 'url']);
|
||||||
<Card
|
|
||||||
key={`${status.get('id')}-${status.get('edited_at')}`}
|
const taggedCollection = (
|
||||||
card={status.get('card')}
|
status.get('tagged_collections')
|
||||||
sensitive={status.get('sensitive')}
|
).find((item) => compareUrls(item.get('url'), cardUrl));
|
||||||
/>
|
|
||||||
);
|
if (taggedCollection) {
|
||||||
|
media = <CollectionPreviewCard collection={taggedCollection} />;
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
|
card={status.get('card')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import type { ComponentProps, FC } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||||
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
||||||
|
import { getCollectionPath } from '@/mastodon/features/collections/utils';
|
||||||
import type { OnElementHandler } from '@/mastodon/utils/html';
|
import type { OnElementHandler } from '@/mastodon/utils/html';
|
||||||
|
|
||||||
export interface HandledLinkProps {
|
export interface HandledLinkProps {
|
||||||
@ -13,6 +15,7 @@ export interface HandledLinkProps {
|
|||||||
prevText?: string;
|
prevText?: string;
|
||||||
hashtagAccountId?: string;
|
hashtagAccountId?: string;
|
||||||
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
|
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
|
||||||
|
collection?: Pick<ApiCollectionJSON, 'id'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||||
@ -21,6 +24,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
prevText,
|
prevText,
|
||||||
hashtagAccountId,
|
hashtagAccountId,
|
||||||
mention,
|
mention,
|
||||||
|
collection,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
@ -57,6 +61,15 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
} else if (collection) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={classNames(className)}
|
||||||
|
to={getCollectionPath(collection.id)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
|
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
|||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { injectIntl } from './intl';
|
import { injectIntl } from './intl';
|
||||||
import { HandledLink } from './status/handled_link';
|
import { HandledLink } from './status/handled_link';
|
||||||
|
import { compareUrls } from '../utils/compare_urls';
|
||||||
|
|
||||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
@ -71,17 +72,6 @@ const mapStateToProps = state => ({
|
|||||||
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
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 {
|
class StatusContent extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
identity: identityContextPropShape,
|
identity: identityContextPropShape,
|
||||||
@ -166,7 +156,13 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
handleElement = (element, { key, ...props }, children) => {
|
handleElement = (element, { key, ...props }, children) => {
|
||||||
if (element instanceof HTMLAnchorElement) {
|
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 (
|
return (
|
||||||
<HandledLink
|
<HandledLink
|
||||||
{...props}
|
{...props}
|
||||||
@ -174,6 +170,7 @@ class StatusContent extends PureComponent {
|
|||||||
text={element.innerText}
|
text={element.innerText}
|
||||||
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
||||||
mention={mention?.toJSON()}
|
mention={mention?.toJSON()}
|
||||||
|
collection={taggedCollection?.toJSON()}
|
||||||
key={key}
|
key={key}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
|||||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
import { domain } from 'mastodon/initial_state';
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
import classes from './collection_lockup.module.scss';
|
import classes from './collection_lockup.module.scss';
|
||||||
|
|
||||||
export const AvatarGrid: React.FC<{
|
export const AvatarGrid: React.FC<{
|
||||||
@ -67,7 +69,7 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h2 id={linkId}>
|
<h2 id={linkId}>
|
||||||
<Link to={`/collections/${id}`} className={classes.link}>
|
<Link to={getCollectionPath(id)} className={classes.link}>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type { MenuItem } from 'mastodon/models/dropdown_menu';
|
|||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { messages as editorMessages } from '../editor';
|
import { messages as editorMessages } from '../editor';
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
view: {
|
view: {
|
||||||
@ -120,7 +121,7 @@ export const CollectionMenu: React.FC<{
|
|||||||
const menu = useMemo(() => {
|
const menu = useMemo(() => {
|
||||||
const viewCollectionItem: MenuItem = {
|
const viewCollectionItem: MenuItem = {
|
||||||
text: intl.formatMessage(messages.view),
|
text: intl.formatMessage(messages.view),
|
||||||
to: `/collections/${id}`,
|
to: getCollectionPath(id),
|
||||||
};
|
};
|
||||||
const shareItems: MenuItem[] = [
|
const shareItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
@ -130,7 +131,7 @@ export const CollectionMenu: React.FC<{
|
|||||||
{
|
{
|
||||||
text: intl.formatMessage(messages.copyLink),
|
text: intl.formatMessage(messages.copyLink),
|
||||||
action: () => {
|
action: () => {
|
||||||
void navigator.clipboard.writeText(`/collections/${id}`);
|
void navigator.clipboard.writeText(getCollectionPath(id));
|
||||||
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
|
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -38,6 +38,8 @@ import {
|
|||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
import { WizardStepTitle } from './wizard_step_title';
|
import { WizardStepTitle } from './wizard_step_title';
|
||||||
|
|
||||||
@ -134,7 +136,7 @@ export const CollectionDetails: React.FC = () => {
|
|||||||
).then((result) => {
|
).then((result) => {
|
||||||
if (isFulfilled(result)) {
|
if (isFulfilled(result)) {
|
||||||
history.replace(`/@${currentUserName}/collections`);
|
history.replace(`/@${currentUserName}/collections`);
|
||||||
history.push(`/collections/${result.payload.collection.id}`, {
|
history.push(getCollectionPath(result.payload.collection.id), {
|
||||||
newCollection: true,
|
newCollection: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,3 +3,5 @@ import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
|||||||
export function areCollectionsEnabled() {
|
export function areCollectionsEnabled() {
|
||||||
return isServerFeatureEnabled('collections');
|
return isServerFeatureEnabled('collections');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
||||||
|
|||||||
@ -29,9 +29,12 @@ import StatusContent from 'mastodon/components/status_content';
|
|||||||
import { QuotedStatus } from 'mastodon/components/status_quoted';
|
import { QuotedStatus } from 'mastodon/components/status_quoted';
|
||||||
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
||||||
import { Audio } from 'mastodon/features/audio';
|
import { Audio } from 'mastodon/features/audio';
|
||||||
|
import { CollectionPreviewCard } from 'mastodon/features/collections/components/collection_preview_card';
|
||||||
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
|
||||||
import { Video } from 'mastodon/features/video';
|
import { Video } from 'mastodon/features/video';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
import type { CollectionAttachment } from 'mastodon/models/status';
|
||||||
|
import { compareUrls } from 'mastodon/utils/compare_urls';
|
||||||
|
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
|
|
||||||
@ -260,13 +263,25 @@ export const DetailedStatus: React.FC<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && !status.get('quote')) {
|
} else if (status.get('card') && !status.get('quote')) {
|
||||||
media = (
|
const cardUrl: string = status.getIn(['card', 'url']);
|
||||||
<Card
|
|
||||||
key={`${status.get('id')}-${status.get('edited_at')}`}
|
const taggedCollection = status
|
||||||
sensitive={status.get('sensitive')}
|
.get('tagged_collections')
|
||||||
card={status.get('card')}
|
.find((item: CollectionAttachment) =>
|
||||||
/>
|
compareUrls(item.get('url'), cardUrl),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (taggedCollection) {
|
||||||
|
media = <CollectionPreviewCard collection={taggedCollection} />;
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
card={status.get('card')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { RecordOf } from 'immutable';
|
import type { RecordOf } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||||
import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses';
|
import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses';
|
||||||
|
|
||||||
export type { StatusVisibility } from 'mastodon/api_types/statuses';
|
export type { StatusVisibility } from 'mastodon/api_types/statuses';
|
||||||
@ -10,3 +11,5 @@ export type Status = Immutable.Map<string, unknown>;
|
|||||||
export type Card = RecordOf<ApiPreviewCardJSON>;
|
export type Card = RecordOf<ApiPreviewCardJSON>;
|
||||||
|
|
||||||
export type MediaAttachment = Immutable.Map<string, unknown>;
|
export type MediaAttachment = Immutable.Map<string, unknown>;
|
||||||
|
|
||||||
|
export type CollectionAttachment = RecordOf<ApiCollectionJSON>;
|
||||||
|
|||||||
14
app/javascript/mastodon/utils/compare_urls.ts
Normal file
14
app/javascript/mastodon/utils/compare_urls.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user