Merge branch 'groups-zod' into 'develop'
Groups zod See merge request soapbox-pub/soapbox!2338
This commit is contained in:
commit
a40222c2de
|
@ -18,7 +18,7 @@ describe('<GroupActionButton />', () => {
|
||||||
|
|
||||||
describe('with a private group', () => {
|
describe('with a private group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
group = group.set('locked', true);
|
group = { ...group, locked: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the Request Access button', () => {
|
it('should render the Request Access button', () => {
|
||||||
|
@ -30,7 +30,7 @@ describe('<GroupActionButton />', () => {
|
||||||
|
|
||||||
describe('with a public group', () => {
|
describe('with a public group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
group = group.set('locked', false);
|
group = { ...group, locked: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the Join Group button', () => {
|
it('should render the Join Group button', () => {
|
||||||
|
@ -52,7 +52,7 @@ describe('<GroupActionButton />', () => {
|
||||||
|
|
||||||
describe('with a private group', () => {
|
describe('with a private group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
group = group.set('locked', true);
|
group = { ...group, locked: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the Request Access button', () => {
|
it('should render the Request Access button', () => {
|
||||||
|
@ -64,7 +64,7 @@ describe('<GroupActionButton />', () => {
|
||||||
|
|
||||||
describe('with a public group', () => {
|
describe('with a public group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
group = group.set('locked', false);
|
group = { ...group, locked: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the Join Group button', () => {
|
it('should render the Join Group button', () => {
|
||||||
|
|
|
@ -9,20 +9,6 @@ import GroupMemberCount from '../group-member-count';
|
||||||
let group: Group;
|
let group: Group;
|
||||||
|
|
||||||
describe('<GroupMemberCount />', () => {
|
describe('<GroupMemberCount />', () => {
|
||||||
describe('without support for "members_count"', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
group = normalizeGroup({
|
|
||||||
members_count: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null', () => {
|
|
||||||
render(<GroupMemberCount group={group} />);
|
|
||||||
|
|
||||||
expect(screen.queryAllByTestId('group-member-count')).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with support for "members_count"', () => {
|
describe('with support for "members_count"', () => {
|
||||||
describe('with 1 member', () => {
|
describe('with 1 member', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -10,10 +10,6 @@ interface IGroupMemberCount {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupMemberCount = ({ group }: IGroupMemberCount) => {
|
const GroupMemberCount = ({ group }: IGroupMemberCount) => {
|
||||||
if (typeof group.members_count === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text theme='inherit' tag='span' size='sm' weight='medium' data-testid='group-member-count'>
|
<Text theme='inherit' tag='span' size='sm' weight='medium' data-testid='group-member-count'>
|
||||||
{shortNumberFormat(group.members_count)}
|
{shortNumberFormat(group.members_count)}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { useEntities, useEntity } from 'soapbox/entity-store/hooks';
|
import { useEntities, useEntity } from 'soapbox/entity-store/hooks';
|
||||||
import { normalizeGroup, normalizeGroupRelationship } from 'soapbox/normalizers';
|
import { groupSchema, Group } from 'soapbox/schemas/group';
|
||||||
|
import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship';
|
||||||
import type { Group, GroupRelationship } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
function useGroups() {
|
function useGroups() {
|
||||||
const { entities, ...result } = useEntities<Group>(['Group', ''], '/api/v1/groups', { parser: parseGroup });
|
const { entities, ...result } = useEntities<Group>(['Group', ''], '/api/v1/groups', { parser: parseGroup });
|
||||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
const groups = entities.map((group) => group.set('relationship', relationships[group.id] || null));
|
const groups = entities.map((group) => ({ ...group, relationship: relationships[group.id] || null }));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
@ -21,7 +20,7 @@ function useGroup(groupId: string, refetch = true) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
group: group?.set('relationship', relationship || null),
|
group: group ? { ...group, relationship: relationship || null } : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +44,18 @@ function useGroupRelationships(groupIds: string[]) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: normalizers currently don't have the desired API.
|
const parseGroup = (entity: unknown) => {
|
||||||
// TODO: rewrite normalizers as Zod parsers.
|
const result = groupSchema.safeParse(entity);
|
||||||
const parseGroup = (entity: unknown) => entity ? normalizeGroup(entity as Record<string, any>) : undefined;
|
if (result.success) {
|
||||||
const parseGroupRelationship = (entity: unknown) => entity ? normalizeGroupRelationship(entity as Record<string, any>) : undefined;
|
return result.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseGroupRelationship = (entity: unknown) => {
|
||||||
|
const result = groupRelationshipSchema.safeParse(entity);
|
||||||
|
if (result.success) {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export { useGroup, useGroups };
|
export { useGroup, useGroups };
|
|
@ -23,13 +23,14 @@ export const GroupRecord = ImmutableRecord({
|
||||||
created_at: '',
|
created_at: '',
|
||||||
display_name: '',
|
display_name: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
emojis: ImmutableList<Emoji>(),
|
emojis: [] as Emoji[],
|
||||||
|
group_visibility: '',
|
||||||
header: '',
|
header: '',
|
||||||
header_static: '',
|
header_static: '',
|
||||||
id: '',
|
id: '',
|
||||||
locked: false,
|
locked: false,
|
||||||
membership_required: false,
|
membership_required: false,
|
||||||
members_count: undefined as number | undefined,
|
members_count: 0,
|
||||||
note: '',
|
note: '',
|
||||||
statuses_visibility: 'public',
|
statuses_visibility: 'public',
|
||||||
uri: '',
|
uri: '',
|
||||||
|
@ -69,7 +70,7 @@ const normalizeHeader = (group: ImmutableMap<string, any>) => {
|
||||||
/** Normalize emojis */
|
/** Normalize emojis */
|
||||||
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||||
const emojis = entity.get('emojis', ImmutableList()).map(normalizeEmoji);
|
const emojis = entity.get('emojis', ImmutableList()).map(normalizeEmoji);
|
||||||
return entity.set('emojis', emojis);
|
return entity.set('emojis', emojis.toArray());
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Set display name from username, if applicable */
|
/** Set display name from username, if applicable */
|
||||||
|
|
|
@ -12,7 +12,7 @@ export { EmojiReactionRecord } from './emoji-reaction';
|
||||||
export { FilterRecord, normalizeFilter } from './filter';
|
export { FilterRecord, normalizeFilter } from './filter';
|
||||||
export { FilterKeywordRecord, normalizeFilterKeyword } from './filter-keyword';
|
export { FilterKeywordRecord, normalizeFilterKeyword } from './filter-keyword';
|
||||||
export { FilterStatusRecord, normalizeFilterStatus } from './filter-status';
|
export { FilterStatusRecord, normalizeFilterStatus } from './filter-status';
|
||||||
export { GroupRecord, normalizeGroup } from './group';
|
export { normalizeGroup } from './group';
|
||||||
export { GroupRelationshipRecord, normalizeGroupRelationship } from './group-relationship';
|
export { GroupRelationshipRecord, normalizeGroupRelationship } from './group-relationship';
|
||||||
export { HistoryRecord, normalizeHistory } from './history';
|
export { HistoryRecord, normalizeHistory } from './history';
|
||||||
export { InstanceRecord, normalizeInstance } from './instance';
|
export { InstanceRecord, normalizeInstance } from './instance';
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a custom emoji.
|
||||||
|
* https://docs.joinmastodon.org/entities/CustomEmoji/
|
||||||
|
*/
|
||||||
|
const customEmojiSchema = z.object({
|
||||||
|
category: z.string().catch(''),
|
||||||
|
shortcode: z.string(),
|
||||||
|
static_url: z.string().catch(''),
|
||||||
|
url: z.string(),
|
||||||
|
visible_in_picker: z.boolean().catch(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
type CustomEmoji = z.infer<typeof customEmojiSchema>;
|
||||||
|
|
||||||
|
export { customEmojiSchema, CustomEmoji };
|
|
@ -0,0 +1,12 @@
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
const groupRelationshipSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
member: z.boolean().catch(false),
|
||||||
|
requested: z.boolean().catch(false),
|
||||||
|
role: z.string().nullish().catch(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;
|
||||||
|
|
||||||
|
export { groupRelationshipSchema, GroupRelationship };
|
|
@ -0,0 +1,49 @@
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import emojify from 'soapbox/features/emoji';
|
||||||
|
import { unescapeHTML } from 'soapbox/utils/html';
|
||||||
|
|
||||||
|
import { customEmojiSchema } from './custom-emoji';
|
||||||
|
import { groupRelationshipSchema } from './group-relationship';
|
||||||
|
import { filteredArray, makeCustomEmojiMap } from './utils';
|
||||||
|
|
||||||
|
const avatarMissing = require('assets/images/avatar-missing.png');
|
||||||
|
const headerMissing = require('assets/images/header-missing.png');
|
||||||
|
|
||||||
|
const groupSchema = z.object({
|
||||||
|
avatar: z.string().catch(avatarMissing),
|
||||||
|
avatar_static: z.string().catch(''),
|
||||||
|
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||||
|
display_name: z.string().catch(''),
|
||||||
|
domain: z.string().catch(''),
|
||||||
|
emojis: filteredArray(customEmojiSchema).catch([]),
|
||||||
|
group_visibility: z.string().catch(''), // TruthSocial
|
||||||
|
header: z.string().catch(headerMissing),
|
||||||
|
header_static: z.string().catch(''),
|
||||||
|
id: z.string().catch(''),
|
||||||
|
locked: z.boolean().catch(false),
|
||||||
|
membership_required: z.boolean().catch(false),
|
||||||
|
members_count: z.number().catch(0),
|
||||||
|
note: z.string().transform(note => note === '<p></p>' ? '' : note).catch(''),
|
||||||
|
relationship: groupRelationshipSchema.nullable().catch(null), // Dummy field to be overwritten later
|
||||||
|
statuses_visibility: z.string().catch('public'),
|
||||||
|
uri: z.string().catch(''),
|
||||||
|
url: z.string().catch(''),
|
||||||
|
}).transform(group => {
|
||||||
|
group.avatar_static = group.avatar_static || group.avatar;
|
||||||
|
group.header_static = group.header_static || group.header;
|
||||||
|
group.locked = group.locked || group.group_visibility === 'members_only'; // TruthSocial
|
||||||
|
|
||||||
|
const customEmojiMap = makeCustomEmojiMap(group.emojis);
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
display_name_html: emojify(escapeTextContentForBrowser(group.display_name), customEmojiMap),
|
||||||
|
note_emojified: emojify(group.note, customEmojiMap),
|
||||||
|
note_plain: unescapeHTML(group.note),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
type Group = z.infer<typeof groupSchema>;
|
||||||
|
|
||||||
|
export { groupSchema, Group };
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { customEmojiSchema, CustomEmoji } from './custom-emoji';
|
||||||
|
export { groupSchema, Group } from './group';
|
||||||
|
export { groupRelationshipSchema, GroupRelationship } from './group-relationship';
|
|
@ -0,0 +1,21 @@
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import type { CustomEmoji } from './custom-emoji';
|
||||||
|
|
||||||
|
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||||
|
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
|
||||||
|
return z.any().array().transform((arr) => (
|
||||||
|
arr.map((item) => schema.safeParse(item).success ? item as z.infer<T> : undefined)
|
||||||
|
.filter((item): item is z.infer<T> => Boolean(item))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map a list of CustomEmoji to their shortcodes. */
|
||||||
|
function makeCustomEmojiMap(customEmojis: CustomEmoji[]) {
|
||||||
|
return customEmojis.reduce<Record<string, CustomEmoji>>((result, emoji) => {
|
||||||
|
result[`:${emoji.shortcode}:`] = emoji;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { filteredArray, makeCustomEmojiMap };
|
|
@ -14,8 +14,6 @@ import {
|
||||||
FilterRecord,
|
FilterRecord,
|
||||||
FilterKeywordRecord,
|
FilterKeywordRecord,
|
||||||
FilterStatusRecord,
|
FilterStatusRecord,
|
||||||
GroupRecord,
|
|
||||||
GroupRelationshipRecord,
|
|
||||||
HistoryRecord,
|
HistoryRecord,
|
||||||
InstanceRecord,
|
InstanceRecord,
|
||||||
ListRecord,
|
ListRecord,
|
||||||
|
@ -48,8 +46,6 @@ type Field = ReturnType<typeof FieldRecord>;
|
||||||
type Filter = ReturnType<typeof FilterRecord>;
|
type Filter = ReturnType<typeof FilterRecord>;
|
||||||
type FilterKeyword = ReturnType<typeof FilterKeywordRecord>;
|
type FilterKeyword = ReturnType<typeof FilterKeywordRecord>;
|
||||||
type FilterStatus = ReturnType<typeof FilterStatusRecord>;
|
type FilterStatus = ReturnType<typeof FilterStatusRecord>;
|
||||||
type Group = ReturnType<typeof GroupRecord>;
|
|
||||||
type GroupRelationship = ReturnType<typeof GroupRelationshipRecord>;
|
|
||||||
type History = ReturnType<typeof HistoryRecord>;
|
type History = ReturnType<typeof HistoryRecord>;
|
||||||
type Instance = ReturnType<typeof InstanceRecord>;
|
type Instance = ReturnType<typeof InstanceRecord>;
|
||||||
type List = ReturnType<typeof ListRecord>;
|
type List = ReturnType<typeof ListRecord>;
|
||||||
|
@ -95,8 +91,6 @@ export {
|
||||||
Filter,
|
Filter,
|
||||||
FilterKeyword,
|
FilterKeyword,
|
||||||
FilterStatus,
|
FilterStatus,
|
||||||
Group,
|
|
||||||
GroupRelationship,
|
|
||||||
History,
|
History,
|
||||||
Instance,
|
Instance,
|
||||||
List,
|
List,
|
||||||
|
@ -114,3 +108,8 @@ export {
|
||||||
APIEntity,
|
APIEntity,
|
||||||
EmbeddedEntity,
|
EmbeddedEntity,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Group,
|
||||||
|
GroupRelationship,
|
||||||
|
} from 'soapbox/schemas';
|
|
@ -194,7 +194,8 @@
|
||||||
"webpack-cli": "^5.0.0",
|
"webpack-cli": "^5.0.0",
|
||||||
"webpack-deadcode-plugin": "^0.1.16",
|
"webpack-deadcode-plugin": "^0.1.16",
|
||||||
"webpack-merge": "^5.8.0",
|
"webpack-merge": "^5.8.0",
|
||||||
"wicg-inert": "^3.1.1"
|
"wicg-inert": "^3.1.1",
|
||||||
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
|
|
|
@ -18294,6 +18294,11 @@ yocto-queue@^0.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zod@^3.21.4:
|
||||||
|
version "3.21.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
|
||||||
|
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
|
||||||
|
|
||||||
zwitch@^1.0.0:
|
zwitch@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||||
|
|
Loading…
Reference in New Issue