diff --git a/src/api/index.ts b/src/api/index.ts index 00342fbb4..52aefd291 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -21,9 +21,6 @@ const getLinks = (response: Pick): LinkHeader => const getNextLink = (response: Pick): string | undefined => getLinks(response).refs.find(link => link.rel === 'next')?.uri; -const getPrevLink = (response: Pick): string | undefined => - getLinks(response).refs.find(link => link.rel === 'prev')?.uri; - /** * Dumb client for grabbing static files. * It uses FE_SUBDIRECTORY and parses JSON if possible. @@ -58,7 +55,6 @@ export { type PlfeResponse, getLinks, getNextLink, - getPrevLink, staticFetch, getClient, }; diff --git a/src/jest/factory.ts b/src/jest/factory.ts index fa8cebf49..7cdabb88c 100644 --- a/src/jest/factory.ts +++ b/src/jest/factory.ts @@ -4,6 +4,7 @@ import { groupRelationshipSchema, groupSchema, instanceSchema, + previewCardSchema, relationshipSchema, statusSchema, GroupRoles, @@ -12,16 +13,12 @@ import { type GroupMember, type GroupRelationship, type Instance, + type PreviewCard, type Relationship, type Status, } from 'pl-api'; import { v4 as uuidv4 } from 'uuid'; -import { - cardSchema, - type Card, -} from 'soapbox/schemas'; - import type { PartialDeep } from 'type-fest'; // TODO: there's probably a better way to create these factory functions. @@ -33,8 +30,8 @@ const buildAccount = (props: PartialDeep = {}): Account => url: `https://soapbox.test/users/${uuidv4()}`, }, props)); -const buildCard = (props: PartialDeep = {}): Card => - cardSchema.parse(Object.assign({ +const buildCard = (props: PartialDeep = {}): PreviewCard => + previewCardSchema.parse(Object.assign({ url: 'https://soapbox.test', }, props)); diff --git a/src/reducers/backups.tsx b/src/reducers/backups.ts similarity index 100% rename from src/reducers/backups.tsx rename to src/reducers/backups.ts diff --git a/src/schemas/card.test.ts b/src/schemas/card.test.ts deleted file mode 100644 index f9b672c28..000000000 --- a/src/schemas/card.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { cardSchema } from './card'; - -describe('cardSchema', () => { - it('adds base fields', () => { - const card = { url: 'https://soapbox.test' }; - const result = cardSchema.parse(card); - - expect(result.type).toEqual('link'); - expect(result.url).toEqual(card.url); - }); -}); diff --git a/src/schemas/card.ts b/src/schemas/card.ts deleted file mode 100644 index 7cb0fd717..000000000 --- a/src/schemas/card.ts +++ /dev/null @@ -1,92 +0,0 @@ -import punycode from 'punycode'; - -import DOMPurify from 'isomorphic-dompurify'; -import { z } from 'zod'; - -const IDNA_PREFIX = 'xn--'; - -/** - * Card (aka link preview). - * https://docs.joinmastodon.org/entities/card/ - */ -const cardSchema = z.object({ - author_name: z.string().catch(''), - author_url: z.string().url().catch(''), - blurhash: z.string().nullable().catch(null), - description: z.string().catch(''), - embed_url: z.string().url().catch(''), - height: z.number().catch(0), - html: z.string().catch(''), - image: z.string().nullable().catch(null), - pleroma: z.object({ - opengraph: z.object({ - width: z.number(), - height: z.number(), - html: z.string(), - thumbnail_url: z.string().url(), - }).optional().catch(undefined), - }).optional().catch(undefined), - provider_name: z.string().catch(''), - provider_url: z.string().url().catch(''), - title: z.string().catch(''), - type: z.enum(['link', 'photo', 'video', 'rich']).catch('link'), - url: z.string().url(), - width: z.number().catch(0), -}).transform(({ pleroma, ...card }) => { - if (!card.provider_name) { - card.provider_name = decodeIDNA(new URL(card.url).hostname); - } - - if (pleroma?.opengraph) { - if (!card.width && !card.height) { - card.width = pleroma.opengraph.width; - card.height = pleroma.opengraph.height; - } - - if (!card.html) { - card.html = pleroma.opengraph.html; - } - - if (!card.image) { - card.image = pleroma.opengraph.thumbnail_url; - } - } - - const html = DOMPurify.sanitize(card.html, { - ALLOWED_TAGS: ['iframe'], - ALLOWED_ATTR: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'], - RETURN_DOM: true, - }); - - html.querySelectorAll('iframe').forEach((frame) => { - try { - const src = new URL(frame.src); - if (src.protocol !== 'https:') { - throw new Error('iframe must be https'); - } - if (src.origin === location.origin) { - throw new Error('iframe must not be same origin'); - } - frame.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-presentation'); - } catch (e) { - frame.remove(); - } - }); - - card.html = html.innerHTML; - - if (!card.html) { - card.type = 'link'; - } - - return card; -}); - -const decodeIDNA = (domain: string): string => domain - .split('.') - .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) - .join('.'); - -type Card = z.infer; - -export { cardSchema, type Card }; \ No newline at end of file diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 1202f2271..a15d63506 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,6 +1,5 @@ export { adminAnnouncementSchema, type AdminAnnouncement } from './admin-announcement'; -export { cardSchema, type Card } from './card'; export { domainSchema, type Domain } from './domain'; export { moderationLogEntrySchema, type ModerationLogEntry } from './moderation-log-entry'; export { relaySchema, type Relay } from './relay'; -export { ruleSchema, adminRuleSchema, type Rule, type AdminRule } from './rule'; +export { adminRuleSchema, type AdminRule } from './rule'; diff --git a/src/schemas/poll.test.ts b/src/schemas/poll.test.ts deleted file mode 100644 index 3cf37f5ef..000000000 --- a/src/schemas/poll.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -// import { pollSchema } from './poll'; - -describe('normalizePoll()', () => { - it('adds base fields', () => { - const poll = { id: '1', options: [{ title: 'Apples' }, { title: 'Oranges' }] }; - const result = pollSchema.parse(poll); - - const expected = { - options: [ - { title: 'Apples', votes_count: 0 }, - { title: 'Oranges', votes_count: 0 }, - ], - emojis: [], - expired: false, - multiple: false, - voters_count: 0, - votes_count: 0, - own_votes: null, - voted: false, - }; - - expect(result).toMatchObject(expected); - }); - - it('normalizes a Pleroma logged-out poll', async () => { - const { poll } = await import('soapbox/__fixtures__/pleroma-status-with-poll.json'); - const result = pollSchema.parse(poll); - - // Adds logged-in fields - expect(result.voted).toBe(false); - expect(result.own_votes).toBe(null); - }); - - it('normalizes poll with emojis', async () => { - const { poll } = await import('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json'); - const result = pollSchema.parse(poll); - - // Emojifies poll options - expect(result.options[1]?.title_emojified) - .toContain('emojione'); - - expect(result.emojis[1]?.shortcode).toEqual('soapbox'); - }); -}); diff --git a/src/schemas/rule.ts b/src/schemas/rule.ts index 2bab0b7ed..78a0924a3 100644 --- a/src/schemas/rule.ts +++ b/src/schemas/rule.ts @@ -1,22 +1,12 @@ import { z } from 'zod'; -const baseRuleSchema = z.object({ +const adminRuleSchema = z.object({ id: z.string(), text: z.string().catch(''), hint: z.string().catch(''), -}); - -const ruleSchema = z.preprocess((data: any) => ({ - ...data, - hint: data.hint || data.subtext, -}), baseRuleSchema); - -type Rule = z.infer; - -const adminRuleSchema = baseRuleSchema.extend({ priority: z.number().nullable().catch(null), }); type AdminRule = z.infer; -export { ruleSchema, adminRuleSchema, type Rule, type AdminRule }; \ No newline at end of file +export { adminRuleSchema, type AdminRule }; \ No newline at end of file diff --git a/src/utils/input.ts b/src/utils/input.ts index 2c878d697..52cf37e6a 100644 --- a/src/utils/input.ts +++ b/src/utils/input.ts @@ -8,13 +8,4 @@ const normalizeUsername = (username: string): string => { } }; -const slugify = (text: string): string => text - .trim() - .toLowerCase() - .replace(/[^\w]/g, '-') // replace non-word characters with a hyphen - .replace(/-+/g, '-'); // replace multiple hyphens with a single hyphen - -export { - normalizeUsername, - slugify, -}; \ No newline at end of file +export { normalizeUsername }; diff --git a/src/utils/legacy.ts b/src/utils/legacy.ts deleted file mode 100644 index 98a99ff3d..000000000 --- a/src/utils/legacy.ts +++ /dev/null @@ -1,9 +0,0 @@ -interface LegacyMap { - get(key: any): unknown; - getIn(keyPath: any[]): unknown; - toJS(): any; -} - -export { - type LegacyMap, -}; \ No newline at end of file diff --git a/src/utils/normalizers.ts b/src/utils/normalizers.ts index 64caf9dd1..310676ea1 100644 --- a/src/utils/normalizers.ts +++ b/src/utils/normalizers.ts @@ -1,8 +1,5 @@ import z from 'zod'; -/** Use new value only if old value is undefined */ -const mergeDefined = (oldVal: any, newVal: any) => oldVal === undefined ? newVal : oldVal; - const makeEmojiMap = (emojis: any) => emojis.reduce((obj: any, emoji: any) => { obj[`:${emoji.shortcode}:`] = emoji; return obj; @@ -11,33 +8,7 @@ const makeEmojiMap = (emojis: any) => emojis.reduce((obj: any, emoji: any) => { /** Normalize entity ID */ const normalizeId = (id: any): string | null => z.string().nullable().catch(null).parse(id); -type Normalizer = (value: V) => R; - -/** - * Allows using any legacy normalizer function as a zod schema. - * - * @example - * ```ts - * const statusSchema = toSchema(normalizeStatus); - * statusSchema.parse(status); - * ``` - */ -const toSchema = (normalizer: Normalizer) => z.custom().transform(normalizer); - -/** Legacy normalizer transition helper function. */ -const maybeFromJS = (value: any): unknown => { - if ('toJS' in value) { - return value.toJS(); - } else { - return value; - } -}; - export { - type Normalizer, - mergeDefined, makeEmojiMap, normalizeId, - toSchema, - maybeFromJS, }; diff --git a/src/utils/numbers.test.tsx b/src/utils/numbers.test.tsx index 1d69e960d..ebdb272eb 100644 --- a/src/utils/numbers.test.tsx +++ b/src/utils/numbers.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen } from 'soapbox/jest/test-helpers'; -import { isIntegerId, secondsToDays, shortNumberFormat } from './numbers'; +import { isIntegerId, shortNumberFormat } from './numbers'; test('isIntegerId()', () => { expect(isIntegerId('0')).toBe(true); @@ -14,14 +14,6 @@ test('isIntegerId()', () => { expect(isIntegerId(null as any)).toBe(false); expect(isIntegerId(undefined as any)).toBe(false); }); - -test('secondsToDays', () => { - expect(secondsToDays(604800)).toEqual(7); - expect(secondsToDays(1209600)).toEqual(14); - expect(secondsToDays(2592000)).toEqual(30); - expect(secondsToDays(7776000)).toEqual(90); -}); - describe('shortNumberFormat', () => { test('handles non-numbers', () => { render(
{shortNumberFormat('not-number')}
, undefined, null); diff --git a/src/utils/numbers.tsx b/src/utils/numbers.tsx index cfe242aca..5f19c826a 100644 --- a/src/utils/numbers.tsx +++ b/src/utils/numbers.tsx @@ -8,8 +8,6 @@ const isNumber = (value: unknown): value is number => typeof value === 'number' /** The input is a number and is not NaN. */ const realNumberSchema = z.coerce.number().refine(n => !isNaN(n)); -const secondsToDays = (seconds: number) => Math.floor(seconds / (3600 * 24)); - const roundDown = (num: number) => { if (num >= 100 && num < 1000) { num = Math.floor(num); @@ -58,7 +56,6 @@ const isIntegerId = (id: string): boolean => new RegExp(/^-?[0-9]+$/g).test(id); export { isNumber, realNumberSchema, - secondsToDays, roundDown, shortNumberFormat, isIntegerId, diff --git a/src/utils/redirect.ts b/src/utils/redirect.ts index 861ea82dc..aacf706ac 100644 --- a/src/utils/redirect.ts +++ b/src/utils/redirect.ts @@ -1,15 +1,7 @@ import { useEffect } from 'react'; -import type { Location } from 'soapbox/types/history'; - const LOCAL_STORAGE_REDIRECT_KEY = 'plfe:redirect-uri'; -const cacheCurrentUrl = (location: Location) => { - const actualUrl = encodeURIComponent(`${location.pathname}${location.search}`); - localStorage.setItem(LOCAL_STORAGE_REDIRECT_KEY, actualUrl); - return actualUrl; -}; - const getRedirectUrl = () => { let redirectUri = localStorage.getItem(LOCAL_STORAGE_REDIRECT_KEY); if (redirectUri) { @@ -34,4 +26,4 @@ const useCachedLocationHandler = () => { return null; }; -export { cacheCurrentUrl, getRedirectUrl, useCachedLocationHandler }; +export { getRedirectUrl, useCachedLocationHandler }; diff --git a/src/utils/status.ts b/src/utils/status.ts index 99fdc80bf..b9e09ec86 100644 --- a/src/utils/status.ts +++ b/src/utils/status.ts @@ -56,15 +56,6 @@ const textForScreenReader = ( return values.join(', '); }; -/** Get reblogged status if any, otherwise return the original status. */ -const getActualStatus = (status: T): Omit => { - if (status?.reblog && typeof status?.reblog === 'object') { - return status.reblog; - } else { - return status; - } -}; - const getStatusIdsFromLinksInContent = (content: string): string[] => { const urls = content.match(RegExp(`${window.location.origin}/@([a-z\\d_-]+(?:@[^@\\s]+)?)/posts/[a-z0-9]+(?!\\S)`, 'gi')); @@ -81,6 +72,5 @@ export { shouldHaveCard, hasIntegerMediaIds, textForScreenReader, - getActualStatus, getStatusIdsFromLinksInContent, };