Merge remote-tracking branch 'soapbox/develop' into lexical
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
import { Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { normalizeCard } from '../card';
|
||||
|
||||
describe('normalizeCard()', () => {
|
||||
it('adds base fields', () => {
|
||||
const card = {};
|
||||
const result = normalizeCard(card);
|
||||
|
||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
||||
expect(result.type).toEqual('link');
|
||||
expect(result.url).toEqual('');
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { normalizePoll } from '../poll';
|
||||
|
||||
describe('normalizePoll()', () => {
|
||||
it('adds base fields', () => {
|
||||
const poll = { options: [{ title: 'Apples' }] };
|
||||
const result = normalizePoll(poll);
|
||||
|
||||
const expected = {
|
||||
options: [{ title: 'Apples', votes_count: 0 }],
|
||||
emojis: [],
|
||||
expired: false,
|
||||
multiple: false,
|
||||
voters_count: 0,
|
||||
votes_count: 0,
|
||||
own_votes: null,
|
||||
voted: false,
|
||||
};
|
||||
|
||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
||||
expect(ImmutableRecord.isRecord(result.options.get(0))).toBe(true);
|
||||
expect(result.toJS()).toMatchObject(expected);
|
||||
});
|
||||
|
||||
it('normalizes a Pleroma logged-out poll', () => {
|
||||
const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll.json');
|
||||
const result = normalizePoll(poll);
|
||||
|
||||
// Adds logged-in fields
|
||||
expect(result.voted).toBe(false);
|
||||
expect(result.own_votes).toBe(null);
|
||||
});
|
||||
|
||||
it('normalizes poll with emojis', () => {
|
||||
const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json');
|
||||
const result = normalizePoll(poll);
|
||||
|
||||
// Emojifies poll options
|
||||
expect(result.options.get(1)?.title_emojified)
|
||||
.toContain('emojione');
|
||||
|
||||
// Parses emojis as Immutable.Record's
|
||||
expect(ImmutableRecord.isRecord(result.emojis.get(0))).toBe(true);
|
||||
expect(result.emojis.get(1)?.shortcode).toEqual('soapbox');
|
||||
});
|
||||
});
|
||||
@@ -146,12 +146,16 @@ describe('normalizeStatus()', () => {
|
||||
});
|
||||
|
||||
it('normalizes poll and poll options', () => {
|
||||
const status = { poll: { options: [{ title: 'Apples' }] } };
|
||||
const status = { poll: { id: '1', options: [{ title: 'Apples' }, { title: 'Oranges' }] } };
|
||||
const result = normalizeStatus(status);
|
||||
const poll = result.poll as Poll;
|
||||
|
||||
const expected = {
|
||||
options: [{ title: 'Apples', votes_count: 0 }],
|
||||
id: '1',
|
||||
options: [
|
||||
{ title: 'Apples', votes_count: 0 },
|
||||
{ title: 'Oranges', votes_count: 0 },
|
||||
],
|
||||
emojis: [],
|
||||
expired: false,
|
||||
multiple: false,
|
||||
@@ -161,9 +165,7 @@ describe('normalizeStatus()', () => {
|
||||
voted: false,
|
||||
};
|
||||
|
||||
expect(ImmutableRecord.isRecord(poll)).toBe(true);
|
||||
expect(ImmutableRecord.isRecord(poll.options.get(0))).toBe(true);
|
||||
expect(poll.toJS()).toMatchObject(expected);
|
||||
expect(poll).toMatchObject(expected);
|
||||
});
|
||||
|
||||
it('normalizes a Pleroma logged-out poll', () => {
|
||||
@@ -182,12 +184,10 @@ describe('normalizeStatus()', () => {
|
||||
const poll = result.poll as Poll;
|
||||
|
||||
// Emojifies poll options
|
||||
expect(poll.options.get(1)?.title_emojified)
|
||||
expect(poll.options[1].title_emojified)
|
||||
.toContain('emojione');
|
||||
|
||||
// Parses emojis as Immutable.Record's
|
||||
expect(ImmutableRecord.isRecord(poll.emojis.get(0))).toBe(true);
|
||||
expect(poll.emojis.get(1)?.shortcode).toEqual('soapbox');
|
||||
expect(poll.emojis[1].shortcode).toEqual('soapbox');
|
||||
});
|
||||
|
||||
it('normalizes a card', () => {
|
||||
@@ -195,7 +195,6 @@ describe('normalizeStatus()', () => {
|
||||
const result = normalizeStatus(status);
|
||||
const card = result.card as Card;
|
||||
|
||||
expect(ImmutableRecord.isRecord(card)).toBe(true);
|
||||
expect(card.type).toEqual('link');
|
||||
expect(card.provider_url).toEqual('https://soapbox.pub');
|
||||
});
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* Card normalizer:
|
||||
* Converts API cards into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/card/}
|
||||
*/
|
||||
import punycode from 'punycode';
|
||||
|
||||
import { Record as ImmutableRecord, Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import { groupSchema, type Group } from 'soapbox/schemas';
|
||||
import { mergeDefined } from 'soapbox/utils/normalizers';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/card/
|
||||
export const CardRecord = ImmutableRecord({
|
||||
author_name: '',
|
||||
author_url: '',
|
||||
blurhash: null as string | null,
|
||||
description: '',
|
||||
embed_url: '',
|
||||
group: null as null | Group,
|
||||
height: 0,
|
||||
html: '',
|
||||
image: null as string | null,
|
||||
provider_name: '',
|
||||
provider_url: '',
|
||||
title: '',
|
||||
type: 'link',
|
||||
url: '',
|
||||
width: 0,
|
||||
});
|
||||
|
||||
const IDNA_PREFIX = 'xn--';
|
||||
|
||||
const decodeIDNA = (domain: string): string => {
|
||||
return domain
|
||||
.split('.')
|
||||
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
|
||||
.join('.');
|
||||
};
|
||||
|
||||
const getHostname = (url: string): string => {
|
||||
const parser = document.createElement('a');
|
||||
parser.href = url;
|
||||
return parser.hostname;
|
||||
};
|
||||
|
||||
/** Fall back to Pleroma's OG data */
|
||||
const normalizePleromaOpengraph = (card: ImmutableMap<string, any>) => {
|
||||
const opengraph = ImmutableMap({
|
||||
width: card.getIn(['pleroma', 'opengraph', 'width']),
|
||||
height: card.getIn(['pleroma', 'opengraph', 'height']),
|
||||
html: card.getIn(['pleroma', 'opengraph', 'html']),
|
||||
image: card.getIn(['pleroma', 'opengraph', 'thumbnail_url']),
|
||||
});
|
||||
|
||||
return card.mergeWith(mergeDefined, opengraph);
|
||||
};
|
||||
|
||||
/** Set provider from URL if not found */
|
||||
const normalizeProviderName = (card: ImmutableMap<string, any>) => {
|
||||
const providerName = card.get('provider_name') || decodeIDNA(getHostname(card.get('url')));
|
||||
return card.set('provider_name', providerName);
|
||||
};
|
||||
|
||||
const normalizeGroup = (card: ImmutableMap<string, any>) => {
|
||||
try {
|
||||
const group = groupSchema.parse(card.get('group').toJS());
|
||||
return card.set('group', group);
|
||||
} catch (_e) {
|
||||
return card.set('group', null);
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeCard = (card: Record<string, any>) => {
|
||||
return CardRecord(
|
||||
ImmutableMap(fromJS(card)).withMutations(card => {
|
||||
normalizePleromaOpengraph(card);
|
||||
normalizeProviderName(card);
|
||||
normalizeGroup(card);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
} from 'immutable';
|
||||
|
||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||
|
||||
import { normalizeEmojiReaction } from './emoji-reaction';
|
||||
import { emojiReactionSchema } from 'soapbox/schemas';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
|
||||
import type { Attachment, Card, Emoji, EmojiReaction } from 'soapbox/types/entities';
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ChatMessageRecord = ImmutableRecord({
|
||||
created_at: '',
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
expiration: null as number | null,
|
||||
emoji_reactions: null as ImmutableList<EmojiReaction> | null,
|
||||
emoji_reactions: null as readonly EmojiReaction[] | null,
|
||||
id: '',
|
||||
unread: false,
|
||||
deleting: false,
|
||||
@@ -41,13 +41,8 @@ const normalizeMedia = (status: ImmutableMap<string, any>) => {
|
||||
};
|
||||
|
||||
const normalizeChatMessageEmojiReaction = (chatMessage: ImmutableMap<string, any>) => {
|
||||
const emojiReactions = chatMessage.get('emoji_reactions');
|
||||
|
||||
if (emojiReactions) {
|
||||
return chatMessage.set('emoji_reactions', ImmutableList(emojiReactions.map(normalizeEmojiReaction)));
|
||||
} else {
|
||||
return chatMessage;
|
||||
}
|
||||
const emojiReactions = ImmutableList(chatMessage.get('emoji_reactions') || []);
|
||||
return chatMessage.set('emoji_reactions', filteredArray(emojiReactionSchema).parse(emojiReactions.toJS()));
|
||||
};
|
||||
|
||||
/** Rewrite `<p></p>` to empty string. */
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/emoji/
|
||||
export const EmojiReactionRecord = ImmutableRecord({
|
||||
name: '',
|
||||
count: null as number | null,
|
||||
me: false,
|
||||
});
|
||||
|
||||
export const normalizeEmojiReaction = (emojiReaction: Record<string, any>) => {
|
||||
return EmojiReactionRecord(
|
||||
ImmutableMap(fromJS(emojiReaction)),
|
||||
);
|
||||
};
|
||||
@@ -4,11 +4,9 @@ export { AdminReportRecord, normalizeAdminReport } from './admin-report';
|
||||
export { AnnouncementRecord, normalizeAnnouncement } from './announcement';
|
||||
export { AnnouncementReactionRecord, normalizeAnnouncementReaction } from './announcement-reaction';
|
||||
export { AttachmentRecord, normalizeAttachment } from './attachment';
|
||||
export { CardRecord, normalizeCard } from './card';
|
||||
export { ChatRecord, normalizeChat } from './chat';
|
||||
export { ChatMessageRecord, normalizeChatMessage } from './chat-message';
|
||||
export { EmojiRecord, normalizeEmoji } from './emoji';
|
||||
export { EmojiReactionRecord } from './emoji-reaction';
|
||||
export { FilterRecord, normalizeFilter } from './filter';
|
||||
export { FilterKeywordRecord, normalizeFilterKeyword } from './filter-keyword';
|
||||
export { FilterStatusRecord, normalizeFilterStatus } from './filter-status';
|
||||
@@ -20,11 +18,8 @@ export { ListRecord, normalizeList } from './list';
|
||||
export { LocationRecord, normalizeLocation } from './location';
|
||||
export { MentionRecord, normalizeMention } from './mention';
|
||||
export { NotificationRecord, normalizeNotification } from './notification';
|
||||
export { PollRecord, PollOptionRecord, normalizePoll } from './poll';
|
||||
export { RelationshipRecord, normalizeRelationship } from './relationship';
|
||||
export { StatusRecord, normalizeStatus } from './status';
|
||||
export { StatusEditRecord, normalizeStatusEdit } from './status-edit';
|
||||
export { TagRecord, normalizeTag } from './tag';
|
||||
|
||||
export { AdRecord, normalizeAd } from './soapbox/ad';
|
||||
export { SoapboxConfigRecord, normalizeSoapboxConfig } from './soapbox/soapbox-config';
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Poll normalizer:
|
||||
* Converts API polls into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/poll/}
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import {
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
import emojify from 'soapbox/features/emoji';
|
||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
|
||||
import type { Emoji, PollOption } from 'soapbox/types/entities';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/poll/
|
||||
export const PollRecord = ImmutableRecord({
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
expired: false,
|
||||
expires_at: '',
|
||||
id: '',
|
||||
multiple: false,
|
||||
options: ImmutableList<PollOption>(),
|
||||
voters_count: 0,
|
||||
votes_count: 0,
|
||||
own_votes: null as ImmutableList<number> | null,
|
||||
voted: false,
|
||||
pleroma: ImmutableMap<string, any>(),
|
||||
});
|
||||
|
||||
// Sub-entity of Poll
|
||||
export const PollOptionRecord = ImmutableRecord({
|
||||
title: '',
|
||||
votes_count: 0,
|
||||
|
||||
// Internal fields
|
||||
title_emojified: '',
|
||||
});
|
||||
|
||||
// Normalize emojis
|
||||
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||
return entity.update('emojis', ImmutableList(), emojis => {
|
||||
return emojis.map(normalizeEmoji);
|
||||
});
|
||||
};
|
||||
|
||||
const normalizePollOption = (option: ImmutableMap<string, any> | string, emojis: ImmutableList<ImmutableMap<string, string>> = ImmutableList()) => {
|
||||
const emojiMap = makeEmojiMap(emojis);
|
||||
|
||||
if (typeof option === 'string') {
|
||||
const titleEmojified = emojify(escapeTextContentForBrowser(option), emojiMap);
|
||||
|
||||
return PollOptionRecord({
|
||||
title: option,
|
||||
title_emojified: titleEmojified,
|
||||
});
|
||||
}
|
||||
|
||||
const titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
||||
|
||||
return PollOptionRecord(
|
||||
option.set('title_emojified', titleEmojified),
|
||||
);
|
||||
};
|
||||
|
||||
// Normalize poll options
|
||||
const normalizePollOptions = (poll: ImmutableMap<string, any>) => {
|
||||
const emojis = poll.get('emojis');
|
||||
|
||||
return poll.update('options', (options: ImmutableList<ImmutableMap<string, any>>) => {
|
||||
return options.map(option => normalizePollOption(option, emojis));
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize own_votes to `null` if empty (like Mastodon)
|
||||
const normalizePollOwnVotes = (poll: ImmutableMap<string, any>) => {
|
||||
return poll.update('own_votes', ownVotes => {
|
||||
return ownVotes?.size > 0 ? ownVotes : null;
|
||||
});
|
||||
};
|
||||
|
||||
// Whether the user voted in the poll
|
||||
const normalizePollVoted = (poll: ImmutableMap<string, any>) => {
|
||||
return poll.update('voted', voted => {
|
||||
return typeof voted === 'boolean' ? voted : poll.get('own_votes')?.size > 0;
|
||||
});
|
||||
};
|
||||
|
||||
export const normalizePoll = (poll: Record<string, any>) => {
|
||||
return PollRecord(
|
||||
ImmutableMap(fromJS(poll)).withMutations((poll: ImmutableMap<string, any>) => {
|
||||
normalizeEmojis(poll);
|
||||
normalizePollOptions(poll);
|
||||
normalizePollOwnVotes(poll);
|
||||
normalizePollVoted(poll);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Relationship normalizer:
|
||||
* Converts API relationships into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/relationship/}
|
||||
*/
|
||||
import {
|
||||
Map as ImmutableMap,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/relationship/
|
||||
// https://api.pleroma.social/#operation/AccountController.relationships
|
||||
export const RelationshipRecord = ImmutableRecord({
|
||||
blocked_by: false,
|
||||
blocking: false,
|
||||
domain_blocking: false,
|
||||
endorsed: false,
|
||||
followed_by: false,
|
||||
following: false,
|
||||
id: '',
|
||||
muting: false,
|
||||
muting_notifications: false,
|
||||
note: '',
|
||||
notifying: false,
|
||||
requested: false,
|
||||
showing_reblogs: false,
|
||||
subscribing: false,
|
||||
});
|
||||
|
||||
export const normalizeRelationship = (relationship: Record<string, any>) => {
|
||||
return RelationshipRecord(
|
||||
ImmutableMap(fromJS(relationship)),
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
Map as ImmutableMap,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
import { CardRecord, normalizeCard } from '../card';
|
||||
|
||||
import type { Ad } from 'soapbox/features/ads/providers';
|
||||
|
||||
export const AdRecord = ImmutableRecord<Ad>({
|
||||
card: CardRecord(),
|
||||
impression: undefined as string | undefined,
|
||||
expires_at: undefined as string | undefined,
|
||||
reason: undefined as string | undefined,
|
||||
});
|
||||
|
||||
/** Normalizes an ad from Soapbox Config. */
|
||||
export const normalizeAd = (ad: Record<string, any>) => {
|
||||
const map = ImmutableMap<string, any>(fromJS(ad));
|
||||
const card = normalizeCard(map.get('card'));
|
||||
const expiresAt = map.get('expires_at') || map.get('expires');
|
||||
|
||||
return AdRecord(map.merge({
|
||||
card,
|
||||
expires_at: expiresAt,
|
||||
}));
|
||||
};
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
} from 'immutable';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
|
||||
import { adSchema } from 'soapbox/schemas';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
import { normalizeUsername } from 'soapbox/utils/input';
|
||||
import { toTailwind } from 'soapbox/utils/tailwind';
|
||||
import { generateAccent } from 'soapbox/utils/theme';
|
||||
|
||||
import { normalizeAd } from './ad';
|
||||
|
||||
import type {
|
||||
Ad,
|
||||
PromoPanelItem,
|
||||
@@ -125,8 +125,12 @@ export const SoapboxConfigRecord = ImmutableRecord({
|
||||
type SoapboxConfigMap = ImmutableMap<string, any>;
|
||||
|
||||
const normalizeAds = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
|
||||
const ads = ImmutableList<Record<string, any>>(soapboxConfig.get('ads'));
|
||||
return soapboxConfig.set('ads', ads.map(normalizeAd));
|
||||
if (soapboxConfig.has('ads')) {
|
||||
const ads = filteredArray(adSchema).parse(soapboxConfig.get('ads').toJS());
|
||||
return soapboxConfig.set('ads', ads);
|
||||
} else {
|
||||
return soapboxConfig;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeCryptoAddress = (address: unknown): CryptoAddress => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import emojify from 'soapbox/features/emoji';
|
||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||
import { normalizePoll } from 'soapbox/normalizers/poll';
|
||||
import { pollSchema } from 'soapbox/schemas';
|
||||
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
|
||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
|
||||
@@ -50,9 +50,10 @@ const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||
|
||||
// Normalize the poll in the status, if applicable
|
||||
const normalizeStatusPoll = (statusEdit: ImmutableMap<string, any>) => {
|
||||
if (statusEdit.hasIn(['poll', 'options'])) {
|
||||
return statusEdit.update('poll', ImmutableMap(), normalizePoll);
|
||||
} else {
|
||||
try {
|
||||
const poll = pollSchema.parse(statusEdit.get('poll').toJS());
|
||||
return statusEdit.set('poll', poll);
|
||||
} catch (_e) {
|
||||
return statusEdit.set('poll', null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,10 +11,9 @@ import {
|
||||
} from 'immutable';
|
||||
|
||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||
import { normalizeCard } from 'soapbox/normalizers/card';
|
||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||
import { normalizeMention } from 'soapbox/normalizers/mention';
|
||||
import { normalizePoll } from 'soapbox/normalizers/poll';
|
||||
import { cardSchema, pollSchema } from 'soapbox/schemas';
|
||||
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
|
||||
@@ -109,18 +108,20 @@ const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||
|
||||
// Normalize the poll in the status, if applicable
|
||||
const normalizeStatusPoll = (status: ImmutableMap<string, any>) => {
|
||||
if (status.hasIn(['poll', 'options'])) {
|
||||
return status.update('poll', ImmutableMap(), normalizePoll);
|
||||
} else {
|
||||
try {
|
||||
const poll = pollSchema.parse(status.get('poll').toJS());
|
||||
return status.set('poll', poll);
|
||||
} catch (_e) {
|
||||
return status.set('poll', null);
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize card
|
||||
const normalizeStatusCard = (status: ImmutableMap<string, any>) => {
|
||||
if (status.get('card')) {
|
||||
return status.update('card', ImmutableMap(), normalizeCard);
|
||||
} else {
|
||||
try {
|
||||
const card = cardSchema.parse(status.get('card').toJS());
|
||||
return status.set('card', card);
|
||||
} catch (e) {
|
||||
return status.set('card', null);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user