Merge branch 'develop' into hooks-migration
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
26adc7c29c
78 changed files with 475 additions and 569 deletions
|
@ -33,12 +33,14 @@ const preprocessAccount = v.transform((account: any) => {
|
|||
if (!account?.acct) return null;
|
||||
|
||||
const username = account.username || account.acct.split('@')[0];
|
||||
const fqn = guessFqn(account);
|
||||
|
||||
const fqn = account.fqn || guessFqn(account);
|
||||
const domain = fqn.split('@')[1] || '';
|
||||
|
||||
return {
|
||||
username,
|
||||
fqn,
|
||||
domain: fqn.split('@')[1] || '',
|
||||
domain,
|
||||
avatar_static: account.avatar_static || account.avatar,
|
||||
header_static: account.header_static || account.header,
|
||||
local: typeof account.pleroma?.is_local === 'boolean' ? account.pleroma.is_local : account.acct.split('@')[1] === undefined,
|
||||
|
@ -96,7 +98,7 @@ const baseAccountSchema = v.object({
|
|||
acct: v.fallback(v.string(), ''),
|
||||
url: v.pipe(v.string(), v.url()),
|
||||
display_name: v.fallback(v.string(), ''),
|
||||
content: v.fallback(v.pipe(v.string(), v.transform((note => note === '<p></p>' ? '' : note))), ''),
|
||||
note: v.fallback(v.pipe(v.string(), v.transform(note => note === '<p></p>' ? '' : note)), ''),
|
||||
avatar: v.fallback(v.string(), ''),
|
||||
avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
header: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
|
@ -110,7 +112,7 @@ const baseAccountSchema = v.object({
|
|||
noindex: v.fallback(v.nullable(v.boolean()), null),
|
||||
suspended: v.fallback(v.optional(v.boolean()), undefined),
|
||||
limited: v.fallback(v.optional(v.boolean()), undefined),
|
||||
created_at: v.fallback(datetimeSchema, new Date().toUTCString()),
|
||||
created_at: v.fallback(datetimeSchema, new Date().toISOString()),
|
||||
last_status_at: v.fallback(v.nullable(v.pipe(v.string(), v.isoDate())), null),
|
||||
statuses_count: v.fallback(v.number(), 0),
|
||||
followers_count: v.fallback(v.number(), 0),
|
||||
|
|
|
@ -7,7 +7,7 @@ import { datetimeSchema, filteredArray } from '../utils';
|
|||
import { adminIpSchema } from './ip';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Account/} */
|
||||
const adminAccountSchema = v.pipe(
|
||||
const adminAccountSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((account: any) => {
|
||||
if (!account.account) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as v from 'valibot';
|
|||
import { announcementSchema } from '../announcement';
|
||||
|
||||
/** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} */
|
||||
const adminAnnouncementSchema = v.pipe(
|
||||
const adminAnnouncementSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((announcement: any) => ({
|
||||
...announcement,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as v from 'valibot';
|
||||
|
||||
const adminRelaySchema = v.pipe(
|
||||
const adminRelaySchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((data: any) => ({ id: data.actor, ...data })),
|
||||
v.object({
|
||||
|
|
|
@ -8,7 +8,7 @@ import { datetimeSchema, filteredArray } from '../utils';
|
|||
import { adminAccountSchema } from './account';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Report/} */
|
||||
const adminReportSchema = v.pipe(
|
||||
const adminReportSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((report: any) => {
|
||||
if (report.actor) {
|
||||
|
|
|
@ -203,7 +203,7 @@ const pleromaSchema = coerceObject({
|
|||
)),
|
||||
enabled: v.fallback(v.boolean(), false),
|
||||
}),
|
||||
post_formats: v.fallback(v.optional(v.array(v.string())), undefined),
|
||||
post_formats: v.fallback(v.array(v.string()), ['text/plain']),
|
||||
restrict_unauthenticated: coerceObject({
|
||||
activities: coerceObject({
|
||||
local: v.fallback(v.boolean(), false),
|
||||
|
|
|
@ -7,7 +7,7 @@ const baseRuleSchema = v.object({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Rule/} */
|
||||
const ruleSchema = v.pipe(
|
||||
const ruleSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((data: any) => ({
|
||||
...data,
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as v from 'valibot';
|
|||
import { accountSchema } from './account';
|
||||
import { datetimeSchema } from './utils';
|
||||
|
||||
const scrobbleSchema = v.pipe(
|
||||
const scrobbleSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((scrobble: any) => scrobble ? {
|
||||
external_link: scrobble.externalLink,
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as v from 'valibot';
|
|||
import { accountSchema } from './account';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Suggestion} */
|
||||
const suggestionSchema = v.pipe(
|
||||
const suggestionSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((suggestion: any) => {
|
||||
/**
|
||||
|
|
|
@ -15,7 +15,7 @@ const translationMediaAttachment = v.object({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Translation/} */
|
||||
const translationSchema = v.pipe(
|
||||
const translationSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((translation: any) => {
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,7 @@ import { blurhashSchema } from './media-attachment';
|
|||
import { historySchema } from './tag';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/PreviewCard/#trends-link} */
|
||||
const trendsLinkSchema = v.pipe(
|
||||
const trendsLinkSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((link: any) => ({ ...link, id: link.url })),
|
||||
v.object({
|
||||
|
|
|
@ -1000,6 +1000,7 @@ const getFeatures = (instance: Instance) => {
|
|||
v.software === PLEROMA,
|
||||
v.software === MITRA,
|
||||
v.software === GOTOSOCIAL,
|
||||
instance.pleroma.metadata.post_formats.length > 1,
|
||||
]),
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pl-api",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"homepage": "https://github.com/mkljczk/pl-fe/tree/fork/packages/pl-api",
|
||||
"repository": {
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
"mini-css-extract-plugin": "^2.9.1",
|
||||
"multiselect-react-dropdown": "^2.0.25",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pl-api": "^0.1.2",
|
||||
"pl-api": "^0.1.3",
|
||||
"pl-hooks": "^0.0.1",
|
||||
"postcss": "^8.4.47",
|
||||
"process": "^0.11.10",
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
adminAnnouncementSchema,
|
||||
type AdminAnnouncement as BaseAdminAnnouncement,
|
||||
type AdminAnnouncement,
|
||||
type AdminCreateAnnouncementParams,
|
||||
type AdminUpdateAnnouncementParams,
|
||||
} from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/useClient';
|
||||
import { normalizeAnnouncement, AdminAnnouncement } from 'pl-fe/normalizers/announcement';
|
||||
import { queryClient } from 'pl-fe/queries/client';
|
||||
|
||||
import { useAnnouncements as useUserAnnouncements } from '../announcements/useAnnouncements';
|
||||
|
@ -20,7 +19,7 @@ const useAnnouncements = () => {
|
|||
const getAnnouncements = async () => {
|
||||
const data = await client.admin.announcements.getAnnouncements();
|
||||
|
||||
return data.items.map(normalizeAnnouncement<BaseAdminAnnouncement>);
|
||||
return data.items;
|
||||
};
|
||||
|
||||
const result = useQuery<ReadonlyArray<AdminAnnouncement>>({
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { announcementReactionSchema, type AnnouncementReaction } from 'pl-api';
|
||||
import { announcementReactionSchema, type AnnouncementReaction, type Announcement } from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/useClient';
|
||||
import { type Announcement, normalizeAnnouncement } from 'pl-fe/normalizers/announcement';
|
||||
import { queryClient } from 'pl-fe/queries/client';
|
||||
|
||||
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => v.parse(announcementReactionSchema, {
|
||||
|
@ -25,14 +24,9 @@ const updateReactions = (reactions: AnnouncementReaction[], name: string, count:
|
|||
const useAnnouncements = () => {
|
||||
const client = useClient();
|
||||
|
||||
const getAnnouncements = async () => {
|
||||
const data = await client.announcements.getAnnouncements();
|
||||
return data.map(normalizeAnnouncement);
|
||||
};
|
||||
|
||||
const { data, ...result } = useQuery<ReadonlyArray<Announcement>>({
|
||||
queryKey: ['announcements'],
|
||||
queryFn: getAnnouncements,
|
||||
queryFn: () => client.announcements.getAnnouncements(),
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }) => {
|
|||
size='sm'
|
||||
className='mr-2 rtl:ml-2 rtl:mr-0 [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
||||
>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -11,6 +11,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import ActionButton from 'pl-fe/features/ui/components/action-button';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { getAcct } from 'pl-fe/utils/accounts';
|
||||
|
@ -219,8 +220,9 @@ const Account = ({
|
|||
size='sm'
|
||||
weight='semibold'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
|
||||
|
@ -281,8 +283,9 @@ const Account = ({
|
|||
size='sm'
|
||||
weight='semibold'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
|
||||
|
@ -356,7 +359,7 @@ const Account = ({
|
|||
truncate
|
||||
size='sm'
|
||||
>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -3,8 +3,9 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import { getTextDirection } from 'pl-fe/utils/rtl';
|
||||
|
||||
import type { Mention as MentionEntity } from 'pl-api';
|
||||
import type { Announcement } from 'pl-fe/normalizers/announcement';
|
||||
import { ParsedContent } from '../parsed-content';
|
||||
|
||||
import type { Announcement, Mention as MentionEntity } from 'pl-api';
|
||||
|
||||
interface IAnnouncementContent {
|
||||
announcement: Announcement;
|
||||
|
@ -83,8 +84,9 @@ const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) =
|
|||
dir={direction}
|
||||
className='text-sm ltr:ml-0 rtl:mr-0'
|
||||
ref={node}
|
||||
dangerouslySetInnerHTML={{ __html: announcement.contentHtml }}
|
||||
/>
|
||||
>
|
||||
<ParsedContent html={announcement.content} emojis={announcement.emojis} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,8 +10,7 @@ import AnnouncementContent from './announcement-content';
|
|||
import ReactionsBar from './reactions-bar';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
import type { Announcement as AnnouncementEntity } from 'pl-fe/normalizers/announcement';
|
||||
import type { Announcement as AnnouncementEntity, CustomEmoji } from 'pl-api';
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity;
|
||||
|
|
|
@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import EventActionButton from 'pl-fe/features/event/components/event-action-button';
|
||||
import EventDate from 'pl-fe/features/event/components/event-date';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
@ -71,7 +72,9 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
|||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/outline/user.svg')} />
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<span>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</span>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import GroupHeaderImage from 'pl-fe/features/group/components/group-header-image';
|
||||
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
||||
|
@ -37,7 +38,9 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => (
|
|||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<HStack alignItems='center' space={1.5}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<Text size='lg' weight='bold'>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
|
|
|
@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Popover from 'pl-fe/components/ui/popover';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
||||
|
||||
|
@ -71,7 +72,9 @@ const GroupPopover = (props: IGroupPopoverContainer) => {
|
|||
|
||||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<Text size='lg' weight='bold'>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
<GroupPrivacy group={group} />
|
||||
|
|
|
@ -3,11 +3,14 @@ import DOMPurify from 'isomorphic-dompurify';
|
|||
import React, { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import HashtagLink from './hashtag-link';
|
||||
import HoverAccountWrapper from './hover-account-wrapper';
|
||||
import StatusMention from './status-mention';
|
||||
|
||||
import type { Mention } from 'pl-api';
|
||||
import type { CustomEmoji, Mention } from 'pl-api';
|
||||
|
||||
const nodesToText = (nodes: Array<DOMNode>): string =>
|
||||
nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array<DOMNode>) : '').join('');
|
||||
|
@ -19,14 +22,18 @@ interface IParsedContent {
|
|||
mentions?: Array<Mention>;
|
||||
/** Whether it's a status which has a quote. */
|
||||
hasQuote?: boolean;
|
||||
/** Related custom emojis. */
|
||||
emojis?: Array<CustomEmoji>;
|
||||
}
|
||||
|
||||
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote }) => {
|
||||
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote, emojis }) => {
|
||||
return useMemo(() => {
|
||||
if (html.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojiMap = emojis ? makeEmojiMap(emojis) : undefined;
|
||||
|
||||
const selectors: Array<string> = [];
|
||||
|
||||
// Explicit mentions
|
||||
|
@ -99,6 +106,14 @@ const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote })
|
|||
return fallback;
|
||||
}
|
||||
},
|
||||
|
||||
transform(reactNode) {
|
||||
if (typeof reactNode === 'string') {
|
||||
return <Emojify text={reactNode} emojis={emojiMap} />;
|
||||
}
|
||||
|
||||
return reactNode as JSX.Element;
|
||||
},
|
||||
};
|
||||
|
||||
return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
|||
import RelativeTimestamp from '../relative-timestamp';
|
||||
|
||||
import type { Selected } from './poll';
|
||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
|
|
|
@ -7,7 +7,9 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
|
||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
||||
import { ParsedContent } from '../parsed-content';
|
||||
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' },
|
||||
|
@ -65,8 +67,9 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
theme='inherit'
|
||||
weight='medium'
|
||||
align='center'
|
||||
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
|
||||
/>
|
||||
>
|
||||
<ParsedContent html={option.title} emojis={poll.emojis} />
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -133,9 +136,10 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
<Text
|
||||
theme='inherit'
|
||||
weight='medium'
|
||||
dangerouslySetInnerHTML={{ __html: (language && option.title_map_emojified) && option.title_map_emojified[language] || option.title_emojified }}
|
||||
className='relative'
|
||||
/>
|
||||
>
|
||||
<ParsedContent html={(language && option.title_map) && option.title_map[language] || option.title} emojis={poll.emojis} />
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<HStack space={2} alignItems='center' className='relative'>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useHistory, useRouteMatch } from 'react-router-dom';
|
|||
|
||||
import { blockAccount } from 'pl-fe/actions/accounts';
|
||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose';
|
||||
import { emojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import { editEvent } from 'pl-fe/actions/events';
|
||||
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'pl-fe/actions/interactions';
|
||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation';
|
||||
|
@ -104,6 +104,7 @@ const messages = defineMessages({
|
|||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
viewReactions: { id: 'status.view_reactions', defaultMessage: 'View reactions' },
|
||||
wrench: { id: 'status.wrench', defaultMessage: 'Wrench reaction' },
|
||||
addKnownLanguage: { id: 'status.add_known_language', defaultMessage: 'Do not auto-translate posts in {language}.' },
|
||||
translate: { id: 'status.translate', defaultMessage: 'Translate' },
|
||||
hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' },
|
||||
|
@ -143,7 +144,9 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
|
||||
const features = useFeatures();
|
||||
const instance = useInstance();
|
||||
const { autoTranslate, boostModal, deleteModal, knownLanguages } = useSettings();
|
||||
const { autoTranslate, boostModal, deleteModal, knownLanguages, showWrenchButton } = useSettings();
|
||||
|
||||
const wrenches = showWrenchButton && status.emoji_reactions.find(emoji => emoji.name === '🔧') || undefined;
|
||||
|
||||
const { translationLanguages } = useTranslationLanguages();
|
||||
|
||||
|
@ -153,7 +156,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
allow_unauthenticated: allowUnauthenticated,
|
||||
} = instance.pleroma.metadata.translation;
|
||||
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && !knownLanguages.includes(status.language);
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && !knownLanguages.includes(status.language);
|
||||
const supportsLanguages = (translationLanguages[status.language!]?.includes(intl.locale));
|
||||
|
||||
return autoTranslate && features.translations && renderTranslate && supportsLanguages;
|
||||
|
@ -211,10 +214,24 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleWrenchClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
if (!me) {
|
||||
onOpenUnauthorizedModal('DISLIKE');
|
||||
} else if (wrenches?.me) {
|
||||
dispatch(unEmojiReact(status, '🔧'));
|
||||
} else {
|
||||
dispatch(emojiReact(status, '🔧'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDislikeLongPress = status.dislikes_count ? () => {
|
||||
openModal('DISLIKES', { statusId: status.id });
|
||||
} : undefined;
|
||||
|
||||
const handleWrenchLongPress = wrenches?.count ? () => {
|
||||
openModal('REACTIONS', { statusId: status.id, reaction: wrenches.name });
|
||||
} : undefined;
|
||||
|
||||
const handlePickEmoji = (emoji: EmojiType) => {
|
||||
dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
|
||||
};
|
||||
|
@ -784,6 +801,20 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{me && !withLabels && features.emojiReacts && showWrenchButton && (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.wrench)}
|
||||
icon={require('@tabler/icons/outline/tool.svg')}
|
||||
color='accent'
|
||||
filled
|
||||
onClick={handleWrenchClick}
|
||||
onLongPress={handleWrenchLongPress}
|
||||
active={wrenches?.me}
|
||||
count={wrenches?.count || undefined}
|
||||
theme={statusActionButtonTheme}
|
||||
/>
|
||||
)}
|
||||
|
||||
{me && !withLabels && features.emojiReacts && (
|
||||
<EmojiPickerDropdown
|
||||
onPickEmoji={handlePickEmoji}
|
||||
|
|
|
@ -7,6 +7,7 @@ import Icon from 'pl-fe/components/icon';
|
|||
import Button from 'pl-fe/components/ui/button';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||
import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content';
|
||||
|
@ -106,13 +107,13 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
maybeSetOnlyEmoji();
|
||||
});
|
||||
|
||||
const parsedHtml = useMemo(
|
||||
const content = useMemo(
|
||||
(): string => translatable && status.translation
|
||||
? status.translation.content!
|
||||
: (status.contentMapHtml && status.currentLanguage)
|
||||
? (status.contentMapHtml[status.currentLanguage] || status.contentHtml)
|
||||
: status.contentHtml,
|
||||
[status.contentHtml, status.translation, status.currentLanguage],
|
||||
: (status.content_map && status.currentLanguage)
|
||||
? (status.content_map[status.currentLanguage] || status.content)
|
||||
: status.content,
|
||||
[status.content, status.translation, status.currentLanguage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -121,9 +122,9 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
|
||||
const withSpoiler = status.spoiler_text.length > 0;
|
||||
|
||||
const spoilerText = status.spoilerMapHtml && status.currentLanguage
|
||||
? status.spoilerMapHtml[status.currentLanguage] || status.spoilerHtml
|
||||
: status.spoilerHtml;
|
||||
const spoilerText = status.spoiler_text_map && status.currentLanguage
|
||||
? status.spoiler_text_map[status.currentLanguage] || status.spoiler_text
|
||||
: status.spoiler_text;
|
||||
|
||||
const direction = getTextDirection(status.search_index);
|
||||
const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', {
|
||||
|
@ -142,11 +143,9 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
if (spoilerText) {
|
||||
output.push(
|
||||
<Text key='spoiler' size='2xl' weight='medium'>
|
||||
<span
|
||||
className={clsx({ 'line-clamp-3': !expanded && lineClamp })}
|
||||
dangerouslySetInnerHTML={{ __html: spoilerText }}
|
||||
ref={spoilerNode}
|
||||
/>
|
||||
<span className={clsx({ 'line-clamp-3': !expanded && lineClamp })} ref={spoilerNode}>
|
||||
<Emojify text={spoilerText} emojis={status.emojis} />
|
||||
</span>
|
||||
{status.content && expandable && (
|
||||
<Button
|
||||
className='ml-2 align-middle'
|
||||
|
@ -179,7 +178,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
lang={status.language || undefined}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>,
|
||||
);
|
||||
}
|
||||
|
@ -207,7 +206,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
lang={status.language || undefined}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IStatusLanguagePicker {
|
||||
status: Pick<Status, 'id' | 'contentMapHtml' | 'currentLanguage'>;
|
||||
status: Pick<Status, 'id' | 'content_map' | 'currentLanguage'>;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ const StatusLanguagePicker: React.FC<IStatusLanguagePicker> = ({ status, showLab
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
if (!status.contentMapHtml || Object.keys(status.contentMapHtml).length < 2) return null;
|
||||
if (!status.content_map || Object.keys(status.content_map).length < 2) return null;
|
||||
|
||||
const icon = <Icon className='size-4 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/language.svg')} />;
|
||||
|
||||
|
@ -34,7 +34,7 @@ const StatusLanguagePicker: React.FC<IStatusLanguagePicker> = ({ status, showLab
|
|||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
||||
<DropdownMenu
|
||||
items={Object.keys(status.contentMapHtml).map((language) => ({
|
||||
items={Object.keys(status.content_map).map((language) => ({
|
||||
text: languages[language as Language] || language,
|
||||
action: () => dispatch(changeStatusLanguage(status.id, language)),
|
||||
active: language === status.currentLanguage,
|
||||
|
|
|
@ -13,6 +13,7 @@ import Icon from 'pl-fe/components/ui/icon';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import StatusTypeIcon from 'pl-fe/features/status/components/status-type-icon';
|
||||
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
||||
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
||||
|
@ -204,23 +205,17 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
className='hover:underline'
|
||||
>
|
||||
<bdi className='truncate'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: status.account.display_name_html,
|
||||
}}
|
||||
/>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
),
|
||||
group: (
|
||||
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: group.display_name_html,
|
||||
}}
|
||||
/>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</strong>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
|
@ -234,12 +229,9 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
const renderedAccounts = accounts.slice(0, 2).map(account => !!account && (
|
||||
<Link key={account.acct} to={`/@${account.acct}`} className='hover:underline'>
|
||||
<bdi className='truncate'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: account.display_name_html,
|
||||
}}
|
||||
/>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
));
|
||||
|
@ -294,7 +286,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
||||
<bdi className='truncate'>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
|
|
|
@ -16,7 +16,7 @@ import { useSettings } from 'pl-fe/hooks/useSettings';
|
|||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
|
||||
interface ITranslateButton {
|
||||
status: Pick<Status, 'id' | 'account' | 'contentHtml' | 'contentMapHtml' | 'language' | 'translating' | 'translation' | 'visibility'>;
|
||||
status: Pick<Status, 'id' | 'account' | 'content' | 'content_map' | 'language' | 'translating' | 'translation' | 'visibility'>;
|
||||
}
|
||||
|
||||
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||
|
@ -36,7 +36,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
allow_unauthenticated: allowUnauthenticated,
|
||||
} = instance.pleroma.metadata.translation;
|
||||
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language && !status.contentMapHtml?.[intl.locale];
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && intl.locale !== status.language && !status.content_map?.[intl.locale];
|
||||
|
||||
const supportsLanguages = (translationLanguages[status.language!]?.includes(intl.locale));
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import Account from 'pl-fe/components/account';
|
|||
import Icon from 'pl-fe/components/icon';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
|
||||
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
|
||||
|
||||
|
@ -27,7 +28,7 @@ const MovedNote: React.FC<IMovedNote> = ({ from, to }) => (
|
|||
id='notification.move'
|
||||
defaultMessage='{name} moved to {targetName}'
|
||||
values={{
|
||||
name: <span dangerouslySetInnerHTML={{ __html: from.display_name_html }} />,
|
||||
name: <span><Emojify text={from.display_name} emojis={from.emojis} /></span>,
|
||||
targetName: to.acct,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -2,16 +2,18 @@ import React from 'react';
|
|||
import { FormattedDate, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useAnnouncements } from 'pl-fe/api/hooks/admin/useAnnouncements';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { AdminAnnouncement } from 'pl-fe/normalizers/announcement';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import type { AdminAnnouncement } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.announcements', defaultMessage: 'Announcements' },
|
||||
deleteConfirm: { id: 'confirmations.admin.delete_announcement.confirm', defaultMessage: 'Delete' },
|
||||
|
@ -47,7 +49,9 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement }) => {
|
|||
return (
|
||||
<div key={announcement.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
|
||||
<Stack space={2}>
|
||||
<Text dangerouslySetInnerHTML={{ __html: announcement.contentHtml }} />
|
||||
<Text>
|
||||
<ParsedContent html={announcement.content} emojis={announcement.emojis} />
|
||||
</Text>
|
||||
{(announcement.starts_at || announcement.ends_at || announcement.all_day) && (
|
||||
<HStack space={2} wrap>
|
||||
{announcement.starts_at && (
|
||||
|
|
|
@ -9,7 +9,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { MediaGallery } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { ChatKeys, useChatActions } from 'pl-fe/queries/chats';
|
||||
|
@ -18,7 +18,7 @@ import { useModalsStore } from 'pl-fe/stores/modals';
|
|||
import { stripHTML } from 'pl-fe/utils/html';
|
||||
import { onlyEmoji } from 'pl-fe/utils/rich-content';
|
||||
|
||||
import type { Chat, CustomEmoji } from 'pl-api';
|
||||
import type { Chat } from 'pl-api';
|
||||
import type { Menu as IMenu } from 'pl-fe/components/dropdown-menu';
|
||||
import type { ChatMessage as ChatMessageEntity } from 'pl-fe/normalizers/chat-message';
|
||||
|
||||
|
@ -31,10 +31,6 @@ const messages = defineMessages({
|
|||
|
||||
const BIG_EMOJI_LIMIT = 3;
|
||||
|
||||
const makeEmojiMap = (record: ChatMessageEntity) =>
|
||||
record.emojis.reduce((map: Record<string, CustomEmoji>, emoji: CustomEmoji) =>
|
||||
(map[`:${emoji.shortcode}:`] = emoji, map), {});
|
||||
|
||||
const parsePendingContent = (content: string) => escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||
|
||||
const parseContent = (chatMessage: ChatMessageEntity) => {
|
||||
|
@ -42,8 +38,7 @@ const parseContent = (chatMessage: ChatMessageEntity) => {
|
|||
const pending = chatMessage.pending;
|
||||
const deleting = chatMessage.deleting;
|
||||
const formatted = (pending && !deleting) ? parsePendingContent(content) : content;
|
||||
const emojiMap = makeEmojiMap(chatMessage);
|
||||
return emojify(formatted, emojiMap);
|
||||
return formatted;
|
||||
};
|
||||
|
||||
interface IChatMessage {
|
||||
|
@ -244,12 +239,9 @@ const ChatMessage = (props: IChatMessage) => {
|
|||
ref={setBubbleRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Text
|
||||
size='sm'
|
||||
theme='inherit'
|
||||
className='break-word-nested'
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
<Text size='sm' theme='inherit' className='break-word-nested'>
|
||||
<Emojify text={content} emojis={chatMessage.emojis} />
|
||||
</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
)}
|
||||
|
|
|
@ -29,19 +29,27 @@ const ContentTypeButton: React.FC<IContentTypeButton> = ({ composeId }) => {
|
|||
|
||||
const handleChange = (contentType: string) => () => dispatch(changeComposeContentType(composeId, contentType));
|
||||
|
||||
const options = [
|
||||
{
|
||||
const postFormats = instance.pleroma.metadata.post_formats;
|
||||
|
||||
const options = [];
|
||||
|
||||
if (postFormats.includes('text/plain')) {
|
||||
options.push({
|
||||
icon: require('@tabler/icons/outline/pilcrow.svg'),
|
||||
text: intl.formatMessage(messages.content_type_plaintext),
|
||||
value: 'text/plain',
|
||||
},
|
||||
{ icon: require('@tabler/icons/outline/markdown.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (postFormats.includes('text/markdown')) {
|
||||
options.push({
|
||||
icon: require('@tabler/icons/outline/markdown.svg'),
|
||||
text: intl.formatMessage(messages.content_type_markdown),
|
||||
value: 'text/markdown',
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.pleroma.metadata.post_formats?.includes('text/html')) {
|
||||
if (postFormats.includes('text/html')) {
|
||||
options.push({
|
||||
icon: require('@tabler/icons/outline/html.svg'),
|
||||
text: intl.formatMessage(messages.content_type_html),
|
||||
|
@ -49,11 +57,13 @@ const ContentTypeButton: React.FC<IContentTypeButton> = ({ composeId }) => {
|
|||
});
|
||||
}
|
||||
|
||||
options.push({
|
||||
icon: require('@tabler/icons/outline/text-caption.svg'),
|
||||
text: intl.formatMessage(messages.content_type_wysiwyg),
|
||||
value: 'wysiwyg',
|
||||
});
|
||||
if (postFormats.includes('text/markdown')) {
|
||||
options.push({
|
||||
icon: require('@tabler/icons/outline/text-caption.svg'),
|
||||
text: intl.formatMessage(messages.content_type_wysiwyg),
|
||||
value: 'wysiwyg',
|
||||
});
|
||||
}
|
||||
|
||||
const option = options.find(({ value }) => value === contentType);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import Link from 'pl-fe/components/link';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useCompose } from 'pl-fe/hooks/useCompose';
|
||||
|
||||
interface IReplyGroupIndicator {
|
||||
|
@ -28,10 +29,11 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => {
|
|||
id='compose.reply_group_indicator.message'
|
||||
defaultMessage='Posting to {groupLink}'
|
||||
values={{
|
||||
groupLink: <Link
|
||||
to={`/groups/${group.id}`}
|
||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||
/>,
|
||||
groupLink: (
|
||||
<Link to={`/groups/${group.id}`}>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { Status } from 'pl-fe/normalizers/status';
|
|||
|
||||
interface IReplyIndicator {
|
||||
className?: string;
|
||||
status?: Pick<Status, 'account_id' | 'contentHtml' | 'created_at' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
|
||||
status?: Pick<Status, 'account_id' | 'content' | 'created_at' | 'emojis' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
|
||||
onCancel?: () => void;
|
||||
hideActions: boolean;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActi
|
|||
size='sm'
|
||||
direction={getTextDirection(status.search_index)}
|
||||
>
|
||||
<ParsedContent html={status.contentHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
<ParsedContent html={status.content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>
|
||||
|
||||
{status.media_attachments.length > 0 && (
|
||||
|
|
|
@ -70,13 +70,13 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
|
|||
withRelationship={false}
|
||||
/>
|
||||
|
||||
{!!account.note_emojified && (
|
||||
{!!account.note && (
|
||||
<Text
|
||||
truncate
|
||||
align='left'
|
||||
className='line-clamp-2 inline text-ellipsis [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
||||
>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
103
packages/pl-fe/src/features/emoji/emojify.tsx
Normal file
103
packages/pl-fe/src/features/emoji/emojify.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import split from 'graphemesplit';
|
||||
import React from 'react';
|
||||
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import unicodeMapping from './mapping';
|
||||
|
||||
import { validEmojiChar } from '.';
|
||||
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
|
||||
interface IMaybeEmoji {
|
||||
text: string;
|
||||
emojis: Record<string, CustomEmoji>;
|
||||
}
|
||||
|
||||
const MaybeEmoji: React.FC<IMaybeEmoji> = ({ text, emojis }) => {
|
||||
if (text.length < 3) return text;
|
||||
if (text in emojis) {
|
||||
const emoji = emojis[text];
|
||||
const filename = emoji.static_url;
|
||||
|
||||
if (filename?.length > 0) {
|
||||
return <img draggable={false} className='emojione' alt={text} title={text} src={filename} />;
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
interface IEmojify {
|
||||
text: string;
|
||||
emojis?: Array<CustomEmoji> | Record<string, CustomEmoji>;
|
||||
}
|
||||
|
||||
const Emojify: React.FC<IEmojify> = ({ text, emojis = {} }) => React.useMemo(() => {
|
||||
if (Array.isArray(emojis)) emojis = makeEmojiMap(emojis);
|
||||
|
||||
const nodes = [];
|
||||
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
const clearStack = () => {
|
||||
if (stack.length) nodes.push(stack);
|
||||
open = false;
|
||||
stack = '';
|
||||
};
|
||||
|
||||
for (let c of split(text)) {
|
||||
// convert FE0E selector to FE0F so it can be found in unimap
|
||||
if (c.codePointAt(c.length - 1) === 65038) {
|
||||
c = c.slice(0, -1) + String.fromCodePoint(65039);
|
||||
}
|
||||
|
||||
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
|
||||
const unqualified = c + String.fromCodePoint(65039);
|
||||
|
||||
if (c in unicodeMapping) {
|
||||
clearStack();
|
||||
|
||||
const { unified, shortcode } = unicodeMapping[c];
|
||||
|
||||
nodes.push(
|
||||
<img draggable={false} className='emojione' alt={c} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
|
||||
);
|
||||
} else if (unqualified in unicodeMapping) {
|
||||
clearStack();
|
||||
|
||||
const { unified, shortcode } = unicodeMapping[unqualified];
|
||||
|
||||
nodes.push(
|
||||
<img draggable={false} className='emojione' alt={unqualified} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
|
||||
);
|
||||
} else if (c === ':') {
|
||||
if (!open) {
|
||||
clearStack();
|
||||
}
|
||||
|
||||
stack += ':';
|
||||
|
||||
// we see another : we convert it and clear the stack buffer
|
||||
if (open) {
|
||||
nodes.push(<MaybeEmoji text={stack} emojis={emojis} />);
|
||||
stack = '';
|
||||
}
|
||||
|
||||
open = !open;
|
||||
} else {
|
||||
stack += c;
|
||||
|
||||
if (open && !validEmojiChar(c)) {
|
||||
clearStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length) nodes.push(stack);
|
||||
|
||||
return nodes;
|
||||
}, [text, emojis]);
|
||||
|
||||
export { Emojify as default };
|
|
@ -1,7 +1,3 @@
|
|||
import split from 'graphemesplit';
|
||||
|
||||
import unicodeMapping from './mapping';
|
||||
|
||||
import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from './data';
|
||||
import type { CustomEmoji as BaseCustomEmoji } from 'pl-api';
|
||||
|
||||
|
@ -57,148 +53,6 @@ const isAlphaNumeric = (c: string) => {
|
|||
const validEmojiChar = (c: string) =>
|
||||
isAlphaNumeric(c) || ['_', '-', '.'].includes(c);
|
||||
|
||||
const convertCustom = (shortname: string, filename: string) =>
|
||||
`<img draggable="false" class="emojione transition-transform ease-linear no-reduce-motion:hover:scale-125" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||
|
||||
const convertUnicode = (c: string) => {
|
||||
const { unified, shortcode } = unicodeMapping[c];
|
||||
|
||||
return `<img draggable="false" class="emojione transition-transform ease-linear no-reduce-motion:hover:scale-125" alt="${c}" title=":${shortcode}:" src="/packs/emoji/${unified}.svg" />`;
|
||||
};
|
||||
|
||||
const convertEmoji = (str: string, customEmojis: any) => {
|
||||
if (str.length < 3) return str;
|
||||
if (str in customEmojis) {
|
||||
const emoji = customEmojis[str];
|
||||
const filename = emoji.static_url;
|
||||
|
||||
if (filename?.length > 0) {
|
||||
return convertCustom(str, filename);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
const emojifyText = (str: string, customEmojis = {}) => {
|
||||
let buf = '';
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
const clearStack = () => {
|
||||
buf += stack;
|
||||
open = false;
|
||||
stack = '';
|
||||
};
|
||||
|
||||
for (let c of split(str)) {
|
||||
// convert FE0E selector to FE0F so it can be found in unimap
|
||||
if (c.codePointAt(c.length - 1) === 65038) {
|
||||
c = c.slice(0, -1) + String.fromCodePoint(65039);
|
||||
}
|
||||
|
||||
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
|
||||
const unqualified = c + String.fromCodePoint(65039);
|
||||
|
||||
if (c in unicodeMapping) {
|
||||
if (open) { // unicode emoji inside colon
|
||||
clearStack();
|
||||
}
|
||||
|
||||
buf += convertUnicode(c);
|
||||
} else if (unqualified in unicodeMapping) {
|
||||
if (open) { // unicode emoji inside colon
|
||||
clearStack();
|
||||
}
|
||||
|
||||
buf += convertUnicode(unqualified);
|
||||
} else if (c === ':') {
|
||||
stack += ':';
|
||||
|
||||
// we see another : we convert it and clear the stack buffer
|
||||
if (open) {
|
||||
buf += convertEmoji(stack, customEmojis);
|
||||
stack = '';
|
||||
}
|
||||
|
||||
open = !open;
|
||||
} else {
|
||||
if (open) {
|
||||
stack += c;
|
||||
|
||||
// if the stack is non-null and we see invalid chars it's a string not emoji
|
||||
// so we push it to the return result and clear it
|
||||
if (!validEmojiChar(c)) {
|
||||
clearStack();
|
||||
}
|
||||
} else {
|
||||
buf += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// never found a closing colon so it's just a raw string
|
||||
if (open) {
|
||||
buf += stack;
|
||||
}
|
||||
|
||||
return buf;
|
||||
};
|
||||
|
||||
const parseHTML = (str: string): { text: boolean; data: string }[] => {
|
||||
const tokens = [];
|
||||
let buf = '';
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
for (const c of str) {
|
||||
if (c === '<') {
|
||||
if (open) {
|
||||
tokens.push({ text: true, data: stack });
|
||||
stack = '<';
|
||||
} else {
|
||||
tokens.push({ text: true, data: buf });
|
||||
stack = '<';
|
||||
open = true;
|
||||
}
|
||||
} else if (c === '>') {
|
||||
if (open) {
|
||||
open = false;
|
||||
tokens.push({ text: false, data: stack + '>' });
|
||||
stack = '';
|
||||
buf = '';
|
||||
} else {
|
||||
buf += '>';
|
||||
}
|
||||
|
||||
} else {
|
||||
if (open) {
|
||||
stack += c;
|
||||
} else {
|
||||
buf += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
tokens.push({ text: true, data: buf + stack });
|
||||
} else if (buf !== '') {
|
||||
tokens.push({ text: true, data: buf });
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
const emojify = (str: string, customEmojis: Record<string, BaseCustomEmoji> = {}) =>
|
||||
parseHTML(str)
|
||||
.map(({ text, data }) => {
|
||||
if (!text) return data;
|
||||
if (data.length === 0 || data === ' ') return data;
|
||||
|
||||
return emojifyText(data, customEmojis);
|
||||
})
|
||||
.join('');
|
||||
|
||||
const buildCustomEmojis = (customEmojis: Array<BaseCustomEmoji>) => {
|
||||
const emojis: EmojiMart<EmojiMartCustom>[] = [];
|
||||
|
||||
|
@ -225,5 +79,5 @@ export {
|
|||
isCustomEmoji,
|
||||
isNativeEmoji,
|
||||
buildCustomEmojis,
|
||||
emojify as default,
|
||||
validEmojiChar,
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
|
||||
|
@ -414,7 +415,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
name: (
|
||||
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<span><Emojify text={account.display_name} emojis={account.emojis} /></span>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
</Link>
|
||||
|
|
|
@ -162,7 +162,7 @@ const EventInformation: React.FC<IEventInformation> = ({ params: { statusId: sta
|
|||
|
||||
return (
|
||||
<Stack className='mt-4 sm:p-2' space={2}>
|
||||
{!!status.contentHtml.trim() && (
|
||||
{!!status.content.trim() && (
|
||||
<Stack space={1}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='event.description' defaultMessage='Description' />
|
||||
|
|
|
@ -10,6 +10,7 @@ import Text from 'pl-fe/components/ui/text';
|
|||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
||||
import Emojify from '../emoji/emojify';
|
||||
import ActionButton from '../ui/components/action-button';
|
||||
import { HotKeys } from '../ui/components/hotkeys';
|
||||
|
||||
|
@ -41,14 +42,9 @@ const SuggestionItem: React.FC<ISuggestionItem> = ({ accountId }) => {
|
|||
|
||||
<Stack>
|
||||
<HStack alignItems='center' justifyContent='center' space={1}>
|
||||
<Text
|
||||
weight='semibold'
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
truncate
|
||||
align='center'
|
||||
size='sm'
|
||||
className='max-w-[95%]'
|
||||
/>
|
||||
<Text weight='semibold' truncate align='center' size='sm' className='max-w-[95%]'>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
|
|
|
@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import { isDefaultHeader } from 'pl-fe/utils/accounts';
|
||||
|
||||
|
@ -141,9 +142,10 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
<Text
|
||||
size='xl'
|
||||
weight='bold'
|
||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||
data-testid='group-name'
|
||||
/>
|
||||
>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
|
||||
<Stack data-testid='group-meta' space={1} alignItems='center'>
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
|
@ -157,7 +159,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
align='center'
|
||||
className='[&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
||||
>
|
||||
<ParsedContent html={group.note_emojified} />
|
||||
<ParsedContent html={group.note} emojis={group.emojis} />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
|||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
import toast from 'pl-fe/toast';
|
||||
import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
|
||||
import AvatarPicker from '../edit-profile/components/avatar-picker';
|
||||
import HeaderPicker from '../edit-profile/components/header-picker';
|
||||
|
@ -51,7 +52,7 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
|
|||
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
|
||||
|
||||
const displayName = useTextField(group?.display_name);
|
||||
const note = useTextField(group?.note_plain);
|
||||
const note = useTextField(unescapeHTML(group?.note));
|
||||
|
||||
const maxName = Number(instance.configuration.groups.max_characters_name);
|
||||
const maxNote = Number(instance.configuration.groups.max_characters_description);
|
||||
|
|
|
@ -13,6 +13,7 @@ import Text from 'pl-fe/components/ui/text';
|
|||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import Emojify from '../emoji/emojify';
|
||||
import ColumnForbidden from '../ui/components/column-forbidden';
|
||||
|
||||
type RouteParams = { groupId: string };
|
||||
|
@ -86,7 +87,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
|||
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/groups/${group.id}/manage/edit`}>
|
||||
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<span><Emojify text={group.display_name} emojis={group.emojis} /></span>
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
|
|
|
@ -7,13 +7,14 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import GroupActionButton from 'pl-fe/features/group/components/group-action-button';
|
||||
import { shortNumberFormat } from 'pl-fe/utils/numbers';
|
||||
|
||||
import type { Group } from 'pl-fe/normalizers/group';
|
||||
|
||||
interface IGroupListItem {
|
||||
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name_html' | 'locked' | 'members_count' | 'relationship'>;
|
||||
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name' | 'emojis' | 'locked' | 'members_count' | 'relationship'>;
|
||||
withJoinAction?: boolean;
|
||||
}
|
||||
|
||||
|
@ -34,11 +35,9 @@ const GroupListItem = (props: IGroupListItem) => {
|
|||
/>
|
||||
|
||||
<Stack className='overflow-hidden'>
|
||||
<Text
|
||||
weight='bold'
|
||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||
truncate
|
||||
/>
|
||||
<Text weight='bold' truncate>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={1} alignItems='center'>
|
||||
<Icon
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import Markup from 'pl-fe/components/markup';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
import { getTextDirection } from 'pl-fe/utils/rtl';
|
||||
|
@ -10,7 +10,6 @@ import { LogoText } from './logo-text';
|
|||
|
||||
const SiteBanner: React.FC = () => {
|
||||
const instance = useInstance();
|
||||
const description = useMemo(() => DOMPurify.sanitize(instance.description), [instance.description]);
|
||||
|
||||
return (
|
||||
<Stack space={6}>
|
||||
|
@ -21,9 +20,10 @@ const SiteBanner: React.FC = () => {
|
|||
{instance.description.trim().length > 0 && (
|
||||
<Markup
|
||||
size='lg'
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
direction={getTextDirection(description)}
|
||||
/>
|
||||
direction={getTextDirection(instance.description)}
|
||||
>
|
||||
<ParsedContent html={instance.description} />
|
||||
</Markup>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import StatusContainer from 'pl-fe/containers/status-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
|
@ -35,14 +36,15 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
|
|||
return output.join(', ');
|
||||
};
|
||||
|
||||
const buildLink = (account: Pick<Account, 'acct' | 'display_name_html' | 'id'>): JSX.Element => (
|
||||
const buildLink = (account: Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>): JSX.Element => (
|
||||
<HoverAccountWrapper key={account.acct} element='bdi' accountId={account.id}>
|
||||
<Link
|
||||
className='font-bold text-gray-800 hover:underline dark:text-gray-200'
|
||||
title={account.acct}
|
||||
to={`/@${account.acct}`}
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Link>
|
||||
</HoverAccountWrapper>
|
||||
);
|
||||
|
||||
|
@ -151,7 +153,7 @@ const messages: Record<NotificationType | 'reply', MessageDescriptor> = defineMe
|
|||
const buildMessage = (
|
||||
intl: IntlShape,
|
||||
type: NotificationType | 'reply',
|
||||
accounts: Array<Pick<Account, 'acct' | 'display_name_html' | 'id'>>,
|
||||
accounts: Array<Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>>,
|
||||
targetName: string,
|
||||
instanceTitle: string,
|
||||
): React.ReactNode => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Mutliselect, SelectDropdown } from 'pl-fe/features/forms';
|
|||
import SettingToggle from 'pl-fe/features/notifications/components/setting-toggle';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||
|
||||
import ThemeToggle from '../ui/components/theme-toggle';
|
||||
|
@ -98,6 +99,7 @@ const Preferences = () => {
|
|||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const settings = useSettings();
|
||||
const instance = useInstance();
|
||||
|
||||
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>, path: string[]) => {
|
||||
dispatch(changeSetting(path, event.target.value, { showAlert: true }));
|
||||
|
@ -123,11 +125,17 @@ const Preferences = () => {
|
|||
private: intl.formatMessage(messages.privacy_followers_only),
|
||||
}), [settings.locale]);
|
||||
|
||||
const defaultContentTypeOptions = React.useMemo(() => ({
|
||||
'text/plain': intl.formatMessage(messages.content_type_plaintext),
|
||||
'text/markdown': intl.formatMessage(messages.content_type_markdown),
|
||||
'text/html': intl.formatMessage(messages.content_type_html),
|
||||
}), [settings.locale]);
|
||||
const defaultContentTypeOptions = React.useMemo(() => {
|
||||
const postFormats = instance.pleroma.metadata.post_formats;
|
||||
|
||||
const options = Object.entries({
|
||||
'text/plain': intl.formatMessage(messages.content_type_plaintext),
|
||||
'text/markdown': intl.formatMessage(messages.content_type_markdown),
|
||||
'text/html': intl.formatMessage(messages.content_type_html),
|
||||
}).filter(([key]) => postFormats.includes(key));
|
||||
|
||||
if (options.length > 1) return Object.fromEntries(options);
|
||||
}, [settings.locale]);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
|
@ -179,7 +187,7 @@ const Preferences = () => {
|
|||
</ListItem>
|
||||
)}
|
||||
|
||||
{features.richText && (
|
||||
{features.richText && !!defaultContentTypeOptions && (
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.content_type_label' defaultMessage='Default post format' />}>
|
||||
<SelectDropdown
|
||||
className='max-w-[200px]'
|
||||
|
@ -206,6 +214,12 @@ const Preferences = () => {
|
|||
>
|
||||
<SettingToggle settings={settings} settingPath={['demetricator']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
{features.emojiReacts && (
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.wrench_label' defaultMessage='Display wrench reaction button' />} >
|
||||
<SettingToggle settings={settings} settingPath={['showWrenchButton']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<List>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useGroup } from 'pl-fe/api/hooks';
|
||||
import React, { useRef } from 'react';
|
||||
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useGroup } from 'pl-fe/api/hooks';
|
||||
import Account from 'pl-fe/components/account';
|
||||
import StatusContent from 'pl-fe/components/status-content';
|
||||
import StatusLanguagePicker from 'pl-fe/components/status-language-picker';
|
||||
|
@ -16,6 +16,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
||||
|
||||
import StatusInteractionBar from './status-interaction-bar';
|
||||
|
@ -23,6 +24,10 @@ import StatusTypeIcon from './status-type-icon';
|
|||
|
||||
import type { SelectedStatus } from 'pl-fe/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
applicationName: { id: 'status.application_name', defaultMessage: 'Sent form {name}' },
|
||||
});
|
||||
|
||||
interface IDetailedStatus {
|
||||
status: SelectedStatus;
|
||||
withMedia?: boolean;
|
||||
|
@ -65,7 +70,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
<Link to={`/groups/${status.group_id}`} className='hover:underline'>
|
||||
<bdi className='truncate'>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<span dangerouslySetInnerHTML={{ __html: groupQuery.group?.display_name_html }} />
|
||||
<Emojify text={groupQuery.group?.display_name} emojis={groupQuery.group?.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
|
@ -147,27 +152,40 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
|
||||
<HStack space={1} alignItems='center'>
|
||||
<span>
|
||||
<a href={actualStatus.url} target='_blank' rel='noopener' className='hover:underline'>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
<a href={actualStatus.url} target='_blank' rel='noopener' className='hover:underline'>
|
||||
<FormattedDate value={new Date(actualStatus.created_at)} hour12 year='numeric' month='short' day='2-digit' hour='numeric' minute='2-digit' />
|
||||
</Text>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
{actualStatus.edited_at && (
|
||||
<>
|
||||
{' · '}
|
||||
<div
|
||||
className='inline hover:underline'
|
||||
onClick={handleOpenCompareHistoryModal}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
{actualStatus.application && (
|
||||
<>
|
||||
{' · '}
|
||||
<a
|
||||
href={(actualStatus.application.website) ? actualStatus.application.website : '#'}
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className='hover:underline'
|
||||
title={intl.formatMessage(messages.applicationName, { name: actualStatus.application.name })}
|
||||
>
|
||||
{actualStatus.application.name}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{actualStatus.edited_at && (
|
||||
<>
|
||||
{' · '}
|
||||
<div
|
||||
className='inline hover:underline'
|
||||
onClick={handleOpenCompareHistoryModal}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(new Date(actualStatus.edited_at), { hour12: true, month: 'short', day: '2-digit', hour: 'numeric', minute: '2-digit' }) }} />
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</span>
|
||||
|
||||
<StatusTypeIcon status={actualStatus} />
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
|
||||
import sourceCode from 'pl-fe/utils/code';
|
||||
|
||||
|
@ -12,10 +12,9 @@ const LinkFooter: React.FC = (): JSX.Element => {
|
|||
return (
|
||||
<Text theme='muted' size='sm'>
|
||||
{plFeConfig.linkFooterMessage ? (
|
||||
<span
|
||||
className='inline-block align-middle'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(plFeConfig.linkFooterMessage) }}
|
||||
/>
|
||||
<span className='inline-block align-middle'>
|
||||
<Emojify text={plFeConfig.linkFooterMessage} />
|
||||
</span>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
|
|
|
@ -9,6 +9,7 @@ import Modal from 'pl-fe/components/ui/modal';
|
|||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
||||
|
@ -42,8 +43,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
body = (
|
||||
<div className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'>
|
||||
{versions?.map((version) => {
|
||||
const content = <ParsedContent html={version.contentHtml} mentions={status?.mentions} hasQuote={!!status?.quote_id} />;
|
||||
const spoilerContent = { __html: version.spoilerHtml };
|
||||
const content = <ParsedContent html={version.content} mentions={status?.mentions} hasQuote={!!status?.quote_id} emojis={version.emojis} />;
|
||||
|
||||
const poll = typeof version.poll !== 'string' && version.poll;
|
||||
|
||||
|
@ -51,7 +51,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
|
||||
{version.spoiler_text?.length > 0 && (
|
||||
<>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||
<span>
|
||||
<Emojify text={version.spoiler_text} emojis={version.emojis} />
|
||||
</span>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
|
@ -71,7 +73,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
role='radio'
|
||||
/>
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
|
||||
<span>
|
||||
<ParsedContent html={option.title} emojis={version.emojis} />
|
||||
</span>
|
||||
</HStack>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
@ -5,6 +5,7 @@ import ScrollableList from 'pl-fe/components/scrollable-list';
|
|||
import Modal from 'pl-fe/components/ui/modal';
|
||||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { makeGetAccount } from 'pl-fe/selectors';
|
||||
|
||||
|
@ -30,7 +31,13 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
|
|||
if (!account || !familiarFollowerIds) {
|
||||
body = <Spinner />;
|
||||
} else {
|
||||
const emptyMessage = <FormattedMessage id='account.familiar_followers.empty' defaultMessage='No one you know follows {name}.' values={{ name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} /> }} />;
|
||||
const emptyMessage = (
|
||||
<FormattedMessage
|
||||
id='account.familiar_followers.empty'
|
||||
defaultMessage='No one you know follows {name}.'
|
||||
values={{ name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
|
||||
/>
|
||||
);
|
||||
|
||||
body = (
|
||||
<ScrollableList
|
||||
|
@ -53,7 +60,7 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
|
|||
<FormattedMessage
|
||||
id='column.familiar_followers'
|
||||
defaultMessage='People you know following {name}'
|
||||
values={{ name: <span dangerouslySetInnerHTML={{ __html: account?.display_name_html || '' }} /> }}
|
||||
values={{ name: !!account && <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
|
||||
/>
|
||||
}
|
||||
onClose={onClickClose}
|
||||
|
|
|
@ -64,7 +64,7 @@ const ConfirmationStep: React.FC<IConfirmationStep> = ({ group }) => {
|
|||
size='md'
|
||||
className='mx-auto max-w-sm [&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
||||
>
|
||||
<ParsedContent html={group.note_emojified} />
|
||||
<ParsedContent html={group.note} emojis={group.emojis} />
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { fetchPinnedAccounts } from 'pl-fe/actions/accounts';
|
||||
import Widget from 'pl-fe/components/ui/widget';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { WhoToFollowPanel } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
@ -12,7 +13,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
|||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
|
||||
interface IPinnedAccountsPanel {
|
||||
account: Pick<Account, 'id' | 'display_name_html'>;
|
||||
account: Pick<Account, 'id' | 'display_name' | 'emojis'>;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
|
@ -36,7 +37,7 @@ const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit })
|
|||
id='pinned_accounts.title'
|
||||
defaultMessage='{name}’s choices'
|
||||
values={{
|
||||
name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />,
|
||||
name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span>,
|
||||
}}
|
||||
/>}
|
||||
>
|
||||
|
|
|
@ -8,7 +8,7 @@ import ProfileField from '../profile-field';
|
|||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
|
||||
interface IProfileFieldsPanel {
|
||||
account: Pick<Account, 'fields'>;
|
||||
account: Pick<Account, 'emojis' | 'fields'>;
|
||||
}
|
||||
|
||||
/** Custom profile fields for sidebar. */
|
||||
|
@ -16,7 +16,7 @@ const ProfileFieldsPanel: React.FC<IProfileFieldsPanel> = ({ account }) => (
|
|||
<Widget>
|
||||
<Stack space={4}>
|
||||
{account.fields.map((field, i) => (
|
||||
<ProfileField field={field} key={i} />
|
||||
<ProfileField field={field} key={i} emojis={account.emojis} />
|
||||
))}
|
||||
</Stack>
|
||||
</Widget>
|
||||
|
|
|
@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
|
||||
import { capitalize } from 'pl-fe/utils/strings';
|
||||
|
@ -122,8 +123,6 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
);
|
||||
}
|
||||
|
||||
const deactivated = account.deactivated ?? false;
|
||||
const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.display_name_html };
|
||||
const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
|
||||
const badges = getBadges();
|
||||
|
||||
|
@ -132,7 +131,11 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
<Stack space={2}>
|
||||
<Stack>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
|
||||
<Text size='lg' weight='bold' truncate>
|
||||
{account.deactivated
|
||||
? <FormattedMessage id='account.deactivated' defaultMessage='Deactivated' />
|
||||
: <Emojify text={account.display_name} emojis={account.emojis} />}
|
||||
</Text>
|
||||
|
||||
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
||||
|
||||
|
@ -160,9 +163,9 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
|
||||
<ProfileStats account={account} />
|
||||
|
||||
{!!account.note_emojified && (
|
||||
{!!account.note && (
|
||||
<Markup size='sm'>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Markup>
|
||||
)}
|
||||
|
||||
|
@ -208,7 +211,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
{account.fields.length > 0 && (
|
||||
<Stack space={2} className='mt-4 xl:hidden'>
|
||||
{account.fields.map((field, i) => (
|
||||
<ProfileField field={field} key={i} />
|
||||
<ProfileField field={field} key={i} emojis={account.emojis} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
|
|
@ -9,6 +9,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||
import { getAcct } from 'pl-fe/utils/accounts';
|
||||
|
@ -29,7 +30,6 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
|||
const fqn = useAppSelector((state) => displayFqn(state));
|
||||
|
||||
if (!account) return null;
|
||||
const displayNameHtml = { __html: account.display_name_html };
|
||||
const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct;
|
||||
const header = account.header;
|
||||
const verified = account.verified;
|
||||
|
@ -67,7 +67,9 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
|||
<Stack>
|
||||
<Link to={`/@${account.acct}`}>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
|
||||
<Text size='lg' weight='bold' truncate>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{verified && <VerificationBadge />}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||
import PollOption from 'pl-fe/components/polls/poll-option';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
|
||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
interface IPollPreview {
|
||||
poll: Poll;
|
||||
|
|
|
@ -9,6 +9,7 @@ import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper';
|
|||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
|
@ -51,12 +52,9 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
|
|||
<HoverAccountWrapper accountId={account.id} key={account.id} element='span'>
|
||||
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<Text
|
||||
size='sm'
|
||||
theme='primary'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
<Text size='sm' theme='primary' truncate>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
|
|
|
@ -3,9 +3,12 @@ import React from 'react';
|
|||
import { defineMessages, useIntl, FormatDateOptions } from 'react-intl';
|
||||
|
||||
import Markup from 'pl-fe/components/markup';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { CryptoAddress, LightningAddress } from 'pl-fe/features/ui/util/async-components';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
|
||||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
|
||||
|
@ -28,32 +31,35 @@ const dateFormatOptions: FormatDateOptions = {
|
|||
|
||||
interface IProfileField {
|
||||
field: Account['fields'][number];
|
||||
emojis?: Account['emojis'];
|
||||
}
|
||||
|
||||
/** Renders a single profile field. */
|
||||
const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
||||
const ProfileField: React.FC<IProfileField> = ({ field, emojis }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (isTicker(field.name)) {
|
||||
return (
|
||||
<CryptoAddress
|
||||
ticker={getTicker(field.name).toLowerCase()}
|
||||
address={field.value_plain}
|
||||
address={unescapeHTML(field.value)}
|
||||
/>
|
||||
);
|
||||
} else if (isZapEmoji(field.name)) {
|
||||
return <LightningAddress address={field.value_plain} />;
|
||||
return <LightningAddress address={unescapeHTML(field.value)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<dl>
|
||||
<dt title={field.name}>
|
||||
<Markup weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
|
||||
<Markup weight='bold' tag='span'>
|
||||
<Emojify text={field.name} emojis={emojis} />
|
||||
</Markup>
|
||||
</dt>
|
||||
|
||||
<dd
|
||||
className={clsx({ 'text-success-500': field.verified_at })}
|
||||
title={field.value_plain}
|
||||
title={unescapeHTML(field.value)}
|
||||
>
|
||||
<HStack space={2} alignItems='center'>
|
||||
{field.verified_at && (
|
||||
|
@ -62,7 +68,9 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
</span>
|
||||
)}
|
||||
|
||||
<Markup className='overflow-hidden break-words' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
|
||||
<Markup className='overflow-hidden break-words' tag='span'>
|
||||
<ParsedContent html={field.value} emojis={emojis} />
|
||||
</Markup>
|
||||
</HStack>
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
@ -1256,6 +1256,7 @@
|
|||
"preferences.fields.theme": "Theme",
|
||||
"preferences.fields.underline_links_label": "Always underline links in posts",
|
||||
"preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone",
|
||||
"preferences.fields.wrench_label": "Display wrench reaction button",
|
||||
"preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.",
|
||||
"preferences.notifications.advanced": "Show all notification categories",
|
||||
"preferences.options.content_type_html": "HTML",
|
||||
|
@ -1433,6 +1434,7 @@
|
|||
"status.add_known_language": "Do not auto-translate posts in {language}.",
|
||||
"status.admin_account": "Moderate @{name}",
|
||||
"status.admin_status": "Open this post in the moderation interface",
|
||||
"status.application_name": "Sent form {name}",
|
||||
"status.approval.pending": "Pending approval",
|
||||
"status.approval.rejected": "Rejected",
|
||||
"status.bookmark": "Bookmark",
|
||||
|
@ -1512,6 +1514,7 @@
|
|||
"status.visibility.local": "The post is only visible to users on your instance",
|
||||
"status.visibility.mutuals_only": "The post is only visible to people who mutually follow the author",
|
||||
"status.visibility.private": "The post is only visible to followers of the author",
|
||||
"status.wrench": "Wrench reaction",
|
||||
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
|
||||
"statuses.quote_tombstone": "Post is unavailable.",
|
||||
"statuses.tombstone": "One or more posts are unavailable.",
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { Account as BaseAccount } from 'pl-api';
|
||||
|
||||
const getDomainFromURL = (account: Pick<BaseAccount, 'url'>): string => {
|
||||
|
@ -34,8 +28,6 @@ const normalizeAccount = (account: BaseAccount) => {
|
|||
const domain = fqn.split('@')[1] || '';
|
||||
const note = account.note === '<p></p>' ? '' : account.note;
|
||||
|
||||
const emojiMap = makeEmojiMap(account.emojis);
|
||||
|
||||
return {
|
||||
...account,
|
||||
avatar: account.avatar || account.avatar_static || missingAvatar,
|
||||
|
@ -45,15 +37,6 @@ const normalizeAccount = (account: BaseAccount) => {
|
|||
fqn,
|
||||
domain,
|
||||
note,
|
||||
display_name_html: emojify(escapeTextContentForBrowser(account.display_name), emojiMap),
|
||||
note_emojified: emojify(account.note, emojiMap),
|
||||
note_plain: unescapeHTML(account.note),
|
||||
fields: account.fields.map(field => ({
|
||||
...field,
|
||||
name_emojified: emojify(escapeTextContentForBrowser(field.name), emojiMap),
|
||||
value_emojified: emojify(field.value, emojiMap),
|
||||
value_plain: unescapeHTML(field.value),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeCustomEmojiMap } from 'pl-fe/schemas/utils';
|
||||
|
||||
import type { AdminAnnouncement as BaseAdminAnnouncement, Announcement as BaseAnnouncement } from 'pl-api';
|
||||
|
||||
const normalizeAnnouncement = <T extends BaseAnnouncement = BaseAnnouncement>(announcement: T) => {
|
||||
const emojiMap = makeCustomEmojiMap(announcement.emojis);
|
||||
|
||||
const contentHtml = emojify(announcement.content, emojiMap);
|
||||
|
||||
return {
|
||||
...announcement,
|
||||
contentHtml,
|
||||
};
|
||||
};
|
||||
|
||||
type Announcement = ReturnType<typeof normalizeAnnouncement>;
|
||||
type AdminAnnouncement = ReturnType<typeof normalizeAnnouncement<BaseAdminAnnouncement>>;
|
||||
|
||||
export { normalizeAnnouncement, type AdminAnnouncement, type Announcement };
|
|
@ -1,9 +1,3 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { Group as BaseGroup } from 'pl-api';
|
||||
|
||||
const getDomainFromURL = (group: Pick<BaseGroup, 'url'>): string => {
|
||||
|
@ -20,9 +14,6 @@ const normalizeGroup = (group: BaseGroup) => {
|
|||
const missingHeader = require('pl-fe/assets/images/header-missing.png');
|
||||
|
||||
const domain = getDomainFromURL(group);
|
||||
const note = group.note === '<p></p>' ? '' : group.note;
|
||||
|
||||
const emojiMap = makeEmojiMap(group.emojis);
|
||||
|
||||
return {
|
||||
...group,
|
||||
|
@ -31,10 +22,6 @@ const normalizeGroup = (group: BaseGroup) => {
|
|||
header: group.header || group.header_static || missingHeader,
|
||||
header_static: group.header_static || group.header || missingHeader,
|
||||
domain,
|
||||
note,
|
||||
display_name_html: emojify(escapeTextContentForBrowser(group.display_name), emojiMap),
|
||||
note_emojified: emojify(group.note, emojiMap),
|
||||
note_plain: unescapeHTML(group.note),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { Status as BaseStatus, StatusEdit as BaseStatusEdit, CustomEmoji } from 'pl-api';
|
||||
|
||||
const sanitizeTitle = (text: string, emojiMap: Record<string, CustomEmoji>) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { ALLOWED_TAGS: ['img'] });
|
||||
|
||||
const normalizePoll = (poll: Exclude<BaseStatus['poll'], null>) => {
|
||||
const emojiMap = makeEmojiMap(poll.emojis);
|
||||
return {
|
||||
...poll,
|
||||
options: poll.options.map(option => ({
|
||||
...option,
|
||||
title_emojified: sanitizeTitle(option.title, emojiMap),
|
||||
title_map_emojified: option.title_map
|
||||
? Object.fromEntries(Object.entries(option.title_map).map(([key, title]) => [key, sanitizeTitle(title, emojiMap)]))
|
||||
: null,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePollEdit = (poll: Exclude<BaseStatusEdit['poll'], null>, emojis: Array<CustomEmoji>) => {
|
||||
const emojiMap = makeEmojiMap(emojis);
|
||||
return {
|
||||
...poll,
|
||||
options: poll.options.map(option => ({
|
||||
...option,
|
||||
title_emojified: sanitizeTitle(option.title, emojiMap),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
type Poll = ReturnType<typeof normalizePoll>;
|
||||
type PollEdit = ReturnType<typeof normalizePollEdit>;
|
||||
|
||||
export {
|
||||
normalizePoll,
|
||||
normalizePollEdit,
|
||||
type Poll,
|
||||
type PollEdit,
|
||||
};
|
|
@ -1,28 +0,0 @@
|
|||
/**
|
||||
* Status edit normalizer
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import { normalizePollEdit } from './poll';
|
||||
|
||||
import type { StatusEdit as BaseStatusEdit } from 'pl-api';
|
||||
|
||||
const normalizeStatusEdit = (statusEdit: BaseStatusEdit) => {
|
||||
const emojiMap = makeEmojiMap(statusEdit.emojis);
|
||||
|
||||
const poll = statusEdit.poll ? normalizePollEdit(statusEdit.poll, statusEdit.emojis) : null;
|
||||
|
||||
return {
|
||||
...statusEdit,
|
||||
poll,
|
||||
contentHtml: emojify(statusEdit.content, emojiMap),
|
||||
spoilerHtml: emojify(escapeTextContentForBrowser(statusEdit.spoiler_text), emojiMap),
|
||||
};
|
||||
};
|
||||
|
||||
type StatusEdit = ReturnType<typeof normalizeStatusEdit>
|
||||
|
||||
export { type StatusEdit, normalizeStatusEdit };
|
|
@ -3,18 +3,13 @@
|
|||
* Converts API statuses into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/status/}
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import { type Account as BaseAccount, type Status as BaseStatus, type CustomEmoji, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
|
||||
import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import { normalizeAccount } from './account';
|
||||
import { normalizeGroup } from './group';
|
||||
import { normalizePoll } from './poll';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
|
@ -23,10 +18,6 @@ type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group' |
|
|||
|
||||
type CalculatedValues = {
|
||||
search_index: string;
|
||||
contentHtml: string;
|
||||
spoilerHtml: string;
|
||||
contentMapHtml?: Record<string, string>;
|
||||
spoilerMapHtml?: Record<string, string>;
|
||||
expanded?: boolean | null;
|
||||
hidden?: boolean | null;
|
||||
translation?: Translation | null | false;
|
||||
|
@ -63,32 +54,20 @@ const buildSearchContent = (status: Pick<BaseStatus, 'poll' | 'mentions' | 'spoi
|
|||
return unescapeHTML(fields.join('\n\n')) || '';
|
||||
};
|
||||
|
||||
const calculateContent = (text: string, emojiMap: Record<string, CustomEmoji>) => emojify(text, emojiMap);
|
||||
const calculateSpoiler = (text: string, emojiMap: Record<string, CustomEmoji>) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { USE_PROFILES: { html: true } });
|
||||
|
||||
const calculateStatus = (status: BaseStatus, oldStatus?: OldStatus): CalculatedValues => {
|
||||
if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) {
|
||||
const {
|
||||
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, expanded, translation, currentLanguage,
|
||||
search_index, hidden, expanded, translation, currentLanguage,
|
||||
} = oldStatus;
|
||||
|
||||
return {
|
||||
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, expanded, translation, currentLanguage,
|
||||
search_index, hidden, expanded, translation, currentLanguage,
|
||||
};
|
||||
} else {
|
||||
const searchContent = buildSearchContent(status);
|
||||
const emojiMap = makeEmojiMap(status.emojis);
|
||||
|
||||
return {
|
||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
||||
contentHtml: calculateContent(status.content, emojiMap),
|
||||
spoilerHtml: calculateSpoiler(status.spoiler_text, emojiMap),
|
||||
contentMapHtml: status.content_map
|
||||
? Object.fromEntries(Object.entries(status.content_map)?.map(([key, value]) => [key, calculateContent(value, emojiMap)]))
|
||||
: undefined,
|
||||
spoilerMapHtml: status.spoiler_text_map
|
||||
? Object.fromEntries(Object.entries(status.spoiler_text_map).map(([key, value]) => [key, calculateSpoiler(value, emojiMap)]))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -123,9 +102,6 @@ const normalizeStatus = (status: BaseStatus & {
|
|||
} | null) = null;
|
||||
let media_attachments = status.media_attachments;
|
||||
|
||||
// Normalize poll
|
||||
const poll = status.poll ? normalizePoll(status.poll) : null;
|
||||
|
||||
if (status.event) {
|
||||
const firstAttachment = status.media_attachments[0];
|
||||
let banner: MediaAttachment | null = null;
|
||||
|
@ -167,7 +143,6 @@ const normalizeStatus = (status: BaseStatus & {
|
|||
content: status.content === '<p></p>' ? '' : status.content,
|
||||
filtered: status.filtered?.map(result => result.filter.title),
|
||||
event,
|
||||
poll,
|
||||
group,
|
||||
media_attachments,
|
||||
...calculated,
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { Status, Translation as BaseTranslation } from 'pl-api';
|
||||
|
||||
const normalizeTranslation = (translation: BaseTranslation, status: Pick<Status, 'emojis'>) => {
|
||||
const emojiMap = makeEmojiMap(status.emojis);
|
||||
const content = emojify(translation.content, emojiMap);
|
||||
|
||||
return {
|
||||
...translation,
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
type Translation = ReturnType<typeof normalizeTranslation>;
|
||||
|
||||
export { normalizeTranslation, type Translation };
|
|
@ -178,7 +178,7 @@ const useChatActions = (chatId: string) => {
|
|||
chat_id: variables.chatId,
|
||||
content: variables.content,
|
||||
id: pendingId,
|
||||
created_at: new Date(),
|
||||
created_at: new Date().toISOString(),
|
||||
account_id: account?.id,
|
||||
unread: true,
|
||||
}),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
import { PLEROMA, type CredentialAccount, type MediaAttachment, type Tag } from 'pl-api';
|
||||
import { Instance, PLEROMA, type CredentialAccount, type MediaAttachment, type Tag } from 'pl-api';
|
||||
|
||||
import { INSTANCE_FETCH_SUCCESS, InstanceAction } from 'pl-fe/actions/instance';
|
||||
import { isNativeEmoji } from 'pl-fe/features/emoji';
|
||||
import { tagHistory } from 'pl-fe/settings';
|
||||
import { hasIntegerMediaIds } from 'pl-fe/utils/status';
|
||||
|
@ -275,6 +276,12 @@ const importAccount = (compose: Compose, account: CredentialAccount) => {
|
|||
// }
|
||||
// };
|
||||
|
||||
const updateDefaultContentType = (compose: Compose, instance: Instance) => {
|
||||
const postFormats = instance.pleroma.metadata.post_formats;
|
||||
|
||||
return compose.update('content_type', type => postFormats.includes(type) ? type : postFormats.includes('text/markdown') ? 'text/markdown' : postFormats[0]);
|
||||
};
|
||||
|
||||
const updateCompose = (state: State, key: string, updater: (compose: Compose) => Compose) =>
|
||||
state.update(key, state.get('default')!, updater);
|
||||
|
||||
|
@ -282,7 +289,7 @@ const initialState: State = ImmutableMap({
|
|||
default: ReducerCompose({ idempotencyKey: crypto.randomUUID(), resetFileKey: getResetFileKey() }),
|
||||
});
|
||||
|
||||
const compose = (state = initialState, action: ComposeAction | EventsAction | MeAction | TimelineAction) => {
|
||||
const compose = (state = initialState, action: ComposeAction | EventsAction | InstanceAction | MeAction | TimelineAction) => {
|
||||
switch (action.type) {
|
||||
case COMPOSE_TYPE_CHANGE:
|
||||
return updateCompose(state, action.composeId, compose => compose.withMutations(map => {
|
||||
|
@ -588,6 +595,8 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
|||
.set('quote', null));
|
||||
case COMPOSE_FEDERATED_CHANGE:
|
||||
return updateCompose(state, action.composeId, compose => compose.update('federated', value => !value));
|
||||
case INSTANCE_FETCH_SUCCESS:
|
||||
return updateCompose(state, 'default', (compose) => updateDefaultContentType(compose, action.instance));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL, type HistoryAction } from 'pl-fe/actions/history';
|
||||
import { normalizeStatusEdit } from 'pl-fe/normalizers/status-edit';
|
||||
|
||||
import type { StatusEdit as BaseStatusEdit } from 'pl-api';
|
||||
import type { StatusEdit } from 'pl-api';
|
||||
|
||||
const HistoryRecord = ImmutableRecord({
|
||||
loading: false,
|
||||
|
@ -14,8 +13,8 @@ type State = ImmutableMap<string, ReturnType<typeof HistoryRecord>>;
|
|||
|
||||
const initialState: State = ImmutableMap();
|
||||
|
||||
const minifyStatusEdit = (statusEdit: BaseStatusEdit, i: number) => ({
|
||||
...normalizeStatusEdit(statusEdit), account: statusEdit.account.id, original: i === 0,
|
||||
const minifyStatusEdit = ({ account, ...statusEdit }: StatusEdit, i: number) => ({
|
||||
...statusEdit, account_id: account.id, original: i === 0,
|
||||
});
|
||||
|
||||
const history = (state: State = initialState, action: HistoryAction) => {
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { POLLS_IMPORT } from 'pl-fe/actions/importer';
|
||||
import { normalizePoll } from 'pl-fe/normalizers/poll';
|
||||
|
||||
import type { Status } from 'pl-api';
|
||||
import type { Poll, Status } from 'pl-api';
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
type State = ImmutableMap<string, ReturnType<typeof normalizePoll>>;
|
||||
type State = ImmutableMap<string, Poll>;
|
||||
|
||||
const importPolls = (state: State, polls: Array<Exclude<Status['poll'], null>>) =>
|
||||
state.withMutations(map =>
|
||||
polls.forEach(poll => map.set(poll.id, normalizePoll(poll))),
|
||||
polls.forEach(poll => map.set(poll.id, poll)),
|
||||
);
|
||||
|
||||
const initialState: State = ImmutableMap();
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Map as ImmutableMap } from 'immutable';
|
|||
import omit from 'lodash/omit';
|
||||
|
||||
import { normalizeStatus, Status as StatusRecord } from 'pl-fe/normalizers/status';
|
||||
import { normalizeTranslation } from 'pl-fe/normalizers/translation';
|
||||
import { simulateEmojiReact, simulateUnEmojiReact } from 'pl-fe/utils/emoji-reacts';
|
||||
|
||||
import {
|
||||
|
@ -173,11 +172,9 @@ const simulateDislike = (
|
|||
|
||||
/** Import translation from translation service into the store. */
|
||||
const importTranslation = (state: State, statusId: string, translation: Translation) => {
|
||||
const result = normalizeTranslation(translation, state.get(statusId)!);
|
||||
|
||||
return state.update(statusId, undefined as any, (status) => ({
|
||||
...status,
|
||||
translation: result,
|
||||
translation: translation,
|
||||
translating: false,
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ const settingsSchema = v.object({
|
|||
deleteModal: v.fallback(v.boolean(), true),
|
||||
missingDescriptionModal: v.fallback(v.boolean(), true),
|
||||
defaultPrivacy: v.fallback(v.picklist(['public', 'unlisted', 'private', 'direct']), 'public'),
|
||||
defaultContentType: v.fallback(v.picklist(['text/plain', 'text/markdown']), 'text/plain'),
|
||||
defaultContentType: v.fallback(v.picklist(['text/plain', 'text/markdown', 'text/html']), 'text/plain'),
|
||||
themeMode: v.fallback(v.picklist(['system', 'light', 'dark', 'black']), 'system'),
|
||||
locale: v.fallback(
|
||||
v.pipe(
|
||||
|
@ -35,6 +35,7 @@ const settingsSchema = v.object({
|
|||
preserveSpoilers: v.fallback(v.boolean(), false),
|
||||
autoTranslate: v.fallback(v.boolean(), false),
|
||||
knownLanguages: v.fallback(v.array(v.string()), []),
|
||||
showWrenchButton: v.fallback(v.boolean(), true),
|
||||
|
||||
systemFont: v.fallback(v.boolean(), false),
|
||||
demetricator: v.fallback(v.boolean(), false),
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import * as v from 'valibot';
|
||||
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
|
||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||
const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
|
||||
v.pipe(
|
||||
|
@ -14,13 +12,6 @@ const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
|
|||
)),
|
||||
);
|
||||
|
||||
/** Map a list of CustomEmoji to their shortcodes. */
|
||||
const makeCustomEmojiMap = (customEmojis: CustomEmoji[]) =>
|
||||
customEmojis.reduce<Record<string, CustomEmoji>>((result, emoji) => {
|
||||
result[`:${emoji.shortcode}:`] = emoji;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
/** valibot schema to force the value into an object, if it isn't already. */
|
||||
const coerceObject = <T extends v.ObjectEntries>(shape: T) =>
|
||||
v.pipe(
|
||||
|
@ -29,4 +20,4 @@ const coerceObject = <T extends v.ObjectEntries>(shape: T) =>
|
|||
v.object(shape),
|
||||
);
|
||||
|
||||
export { filteredArray, makeCustomEmojiMap, coerceObject };
|
||||
export { filteredArray, coerceObject };
|
||||
|
|
|
@ -7570,10 +7570,10 @@ pkg-dir@^4.1.0:
|
|||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
pl-api@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.1.2.tgz#08794b017f64c58ce128074afdd2144f715359e2"
|
||||
integrity sha512-HZNrEDvnL1+8yax7lZwe/vCELme8ieNQB6LzUKTQU8qqhMSk7EdLBeEUQok/csyAu0X+IaSsoWKMA91xYzpuGA==
|
||||
pl-api@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.1.3.tgz#72d434a0ec8d713e5b227b35497da33cf26975a6"
|
||||
integrity sha512-vcl3aGOy3AocQek3+S97QB0jIcF+iV66FvMqrFB/qnfo3Ryte9dcG6XcDxQ5LpogFQAILKk/Mm5uY2W9RZtwHA==
|
||||
dependencies:
|
||||
blurhash "^2.0.5"
|
||||
http-link-header "^1.1.3"
|
||||
|
|
Loading…
Reference in a new issue