Merge branch 'block-group-members' into 'develop'
Use Entity Hooks for Blocking Group members See merge request soapbox-pub/soapbox!2353
This commit is contained in:
commit
709edaefad
21 changed files with 282 additions and 83 deletions
|
@ -14,10 +14,11 @@ import RelativeTimestamp from './relative-timestamp';
|
|||
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
||||
|
||||
import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
|
||||
import type { Account as AccountSchema } from 'soapbox/schemas';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IInstanceFavicon {
|
||||
account: AccountEntity
|
||||
account: AccountEntity | AccountSchema
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
|
@ -67,7 +68,7 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
|
|||
};
|
||||
|
||||
export interface IAccount {
|
||||
account: AccountEntity
|
||||
account: AccountEntity | AccountSchema
|
||||
action?: React.ReactElement
|
||||
actionAlignment?: 'center' | 'top'
|
||||
actionIcon?: string
|
||||
|
|
|
@ -73,7 +73,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<li className='truncate focus-within:ring-2 focus-within:ring-primary-500'>
|
||||
<li className='truncate focus-visible:ring-2 focus-visible:ring-primary-500'>
|
||||
<a
|
||||
href={item.href || item.to || '#'}
|
||||
role='button'
|
||||
|
|
|
@ -271,6 +271,10 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
|||
};
|
||||
}, [refs.floating.current]);
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children ? (
|
||||
|
|
|
@ -8,7 +8,7 @@ const themes = {
|
|||
tertiary:
|
||||
'bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500',
|
||||
accent: 'border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-gray-100 focus:ring-secondary-300',
|
||||
danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:bg-danger-800 focus:text-gray-200 dark:focus:bg-danger-600 dark:focus:text-gray-100',
|
||||
danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:ring-danger-500',
|
||||
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
|
||||
outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10',
|
||||
muted: 'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500',
|
||||
|
|
|
@ -131,7 +131,12 @@ function useEntities<TEntity extends Entity>(
|
|||
const selectCache = (state: RootState, path: EntityPath) => state.entities[path[0]];
|
||||
|
||||
/** Get list at path from Redux. */
|
||||
const selectList = (state: RootState, path: EntityPath) => selectCache(state, path)?.lists[path[1]];
|
||||
const selectList = (state: RootState, path: EntityPath) => {
|
||||
const [, ...listKeys] = path;
|
||||
const listKey = listKeys.join(':');
|
||||
|
||||
return selectCache(state, path)?.lists[listKey];
|
||||
};
|
||||
|
||||
/** Select a particular item from a list state. */
|
||||
function selectListState<K extends keyof EntityListState>(state: RootState, path: EntityPath, key: K) {
|
||||
|
|
|
@ -67,7 +67,7 @@ function useEntityActions<TEntity extends Entity = Entity, P = any>(
|
|||
}
|
||||
|
||||
return {
|
||||
createEntity: endpoints.post ? createEntity : undefined,
|
||||
createEntity: createEntity,
|
||||
deleteEntity: endpoints.delete ? deleteEntity : undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
|||
const isRequested = group.relationship?.requested;
|
||||
const isNonMember = !group.relationship?.member && !isRequested;
|
||||
const isAdmin = group.relationship?.role === 'admin';
|
||||
const isBlocked = group.relationship?.blocked_by;
|
||||
|
||||
const onJoinGroup = () => joinGroup.mutate(group);
|
||||
|
||||
|
@ -41,6 +42,10 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
|||
|
||||
const onCancelRequest = () => cancelRequest.mutate(group);
|
||||
|
||||
if (isBlocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNonMember) {
|
||||
return (
|
||||
<Button
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { groupBlock, groupDemoteAccount, groupKick, groupPromoteAccount } from 'soapbox/actions/groups';
|
||||
import { groupDemoteAccount, groupKick, groupPromoteAccount } from 'soapbox/actions/groups';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import Account from 'soapbox/components/account';
|
||||
import { HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import { useAccount, useAppDispatch } from 'soapbox/hooks';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu/dropdown-menu';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { deleteEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useAccount, useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
import { useBlockGroupMember } from 'soapbox/hooks/api/groups/useBlockGroupMember';
|
||||
import { BaseGroupRoles, useGroupRoles } from 'soapbox/hooks/useGroupRoles';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
|
@ -17,10 +19,11 @@ import type { Account as AccountEntity, Group, GroupMember } from 'soapbox/types
|
|||
|
||||
const messages = defineMessages({
|
||||
blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' },
|
||||
blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to block @{name} from interacting with this group?' },
|
||||
blocked: { id: 'group.group_mod_block.success', defaultMessage: 'Blocked @{name} from group' },
|
||||
blockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Ban From Group' },
|
||||
blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to ban @{name} from the group?' },
|
||||
blocked: { id: 'group.group_mod_block.success', defaultMessage: 'You have successfully blocked @{name} from the group' },
|
||||
demotedToUser: { id: 'group.group_mod_demote.success', defaultMessage: 'Demoted @{name} to group user' },
|
||||
groupModBlock: { id: 'group.group_mod_block', defaultMessage: 'Block @{name} from group' },
|
||||
groupModBlock: { id: 'group.group_mod_block', defaultMessage: 'Ban from group' },
|
||||
groupModDemote: { id: 'group.group_mod_demote', defaultMessage: 'Demote @{name}' },
|
||||
groupModKick: { id: 'group.group_mod_kick', defaultMessage: 'Kick @{name} from group' },
|
||||
groupModPromoteAdmin: { id: 'group.group_mod_promote_admin', defaultMessage: 'Promote @{name} to group administrator' },
|
||||
|
@ -43,9 +46,11 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
|||
const { member, group } = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const intl = useIntl();
|
||||
|
||||
const { normalizeRole } = useGroupRoles();
|
||||
const blockGroupMember = useBlockGroupMember(group, member);
|
||||
|
||||
const account = useAccount(member.account.id) as AccountEntity;
|
||||
|
||||
|
@ -70,11 +75,13 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
|||
|
||||
const handleBlockFromGroup = () => {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.blockFromGroupHeading),
|
||||
message: intl.formatMessage(messages.blockFromGroupMessage, { name: account.username }),
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(groupBlock(group.id, account.id)).then(() =>
|
||||
toast.success(intl.formatMessage(messages.blocked, { name: account.acct })),
|
||||
),
|
||||
onConfirm: () => blockGroupMember({ account_ids: [member.account.id] }).then(() => {
|
||||
dispatch(deleteEntities([member.id], Entities.GROUP_MEMBERSHIPS));
|
||||
toast.success(intl.formatMessage(messages.blocked, { name: account.acct }));
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
|
@ -118,15 +125,19 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
|||
(isMemberModerator || isMemberUser) &&
|
||||
member.role !== group.relationship.role
|
||||
) {
|
||||
if (features.groupsKick) {
|
||||
items.push({
|
||||
text: intl.formatMessage(messages.groupModKick, { name: account.username }),
|
||||
icon: require('@tabler/icons/user-minus.svg'),
|
||||
action: handleKickFromGroup,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
text: intl.formatMessage(messages.groupModBlock, { name: account.username }),
|
||||
icon: require('@tabler/icons/ban.svg'),
|
||||
action: handleBlockFromGroup,
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -176,40 +187,7 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
|||
</span>
|
||||
) : null}
|
||||
|
||||
{menu.length > 0 && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
className='px-2'
|
||||
iconClassName='h-4 w-4'
|
||||
children={null}
|
||||
/>
|
||||
|
||||
<MenuList className='w-56'>
|
||||
{menu.map((menuItem, idx) => {
|
||||
if (typeof menuItem?.text === 'undefined') {
|
||||
return <MenuDivider key={idx} />;
|
||||
} else {
|
||||
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
||||
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.target || '_self' };
|
||||
|
||||
return (
|
||||
<Comp key={idx} {...itemProps} className='group'>
|
||||
<HStack space={2} alignItems='center'>
|
||||
{menuItem.icon && (
|
||||
<SvgIcon src={menuItem.icon} className='h-4 w-4 flex-none text-gray-700 group-hover:text-gray-800' />
|
||||
)}
|
||||
|
||||
<div className='truncate'>{menuItem.text}</div>
|
||||
</HStack>
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
<DropdownMenu items={menu} />
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
|
|||
<GroupMemberListItem
|
||||
group={group as Group}
|
||||
member={member}
|
||||
key={member?.account}
|
||||
key={member.account.id}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { openModal } from 'soapbox/actions/modals';
|
|||
import { Button, HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -35,7 +36,7 @@ const messages = defineMessages({
|
|||
|
||||
interface IActionButton {
|
||||
/** Target account for the action. */
|
||||
account: AccountEntity
|
||||
account: AccountEntity | Account
|
||||
/** Type of action to prioritize, eg on Blocks and Mutes pages. */
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request'
|
||||
/** Displays shorter text on the "Awaiting approval" button. */
|
||||
|
|
15
app/soapbox/hooks/api/groups/useBlockGroupMember.ts
Normal file
15
app/soapbox/hooks/api/groups/useBlockGroupMember.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||
|
||||
import type { Group, GroupMember } from 'soapbox/schemas';
|
||||
|
||||
function useBlockGroupMember(group: Group, groupMember: GroupMember) {
|
||||
const { createEntity } = useEntityActions(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||
{ post: `/api/v1/groups/${group.id}/blocks` },
|
||||
);
|
||||
|
||||
return createEntity;
|
||||
}
|
||||
|
||||
export { useBlockGroupMember };
|
|
@ -1,3 +1,5 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities, useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { groupSchema, Group } from 'soapbox/schemas/group';
|
||||
|
@ -40,7 +42,7 @@ function useGroupRelationship(groupId: string) {
|
|||
return useEntity<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, groupId],
|
||||
`/api/v1/groups/relationships?id[]=${groupId}`,
|
||||
{ schema: groupRelationshipSchema },
|
||||
{ schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -48,7 +50,7 @@ function useGroupRelationships(groupIds: string[]) {
|
|||
const q = groupIds.map(id => `id[]=${id}`).join('&');
|
||||
const endpoint = groupIds.length ? `/api/v1/groups/relationships?${q}` : undefined;
|
||||
const { entities, ...result } = useEntities<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, q],
|
||||
[Entities.GROUP_RELATIONSHIPS, ...groupIds],
|
||||
endpoint,
|
||||
{ schema: groupRelationshipSchema },
|
||||
);
|
||||
|
|
|
@ -472,7 +472,7 @@
|
|||
"confirmations.block.message": "Are you sure you want to block {name}?",
|
||||
"confirmations.block_from_group.confirm": "Block",
|
||||
"confirmations.block_from_group.heading": "Block group member",
|
||||
"confirmations.block_from_group.message": "Are you sure you want to block @{name} from interacting with this group?",
|
||||
"confirmations.block_from_group.message": "Are you sure you want to ban @{name} from the group?",
|
||||
"confirmations.cancel.confirm": "Discard",
|
||||
"confirmations.cancel.heading": "Discard post",
|
||||
"confirmations.cancel.message": "Are you sure you want to cancel creating this post?",
|
||||
|
@ -770,8 +770,8 @@
|
|||
"group.cancel_request": "Cancel Request",
|
||||
"group.group_mod_authorize": "Accept",
|
||||
"group.group_mod_authorize.success": "Accepted @{name} to group",
|
||||
"group.group_mod_block": "Block @{name} from group",
|
||||
"group.group_mod_block.success": "Blocked @{name} from group",
|
||||
"group.group_mod_block": "Ban from group",
|
||||
"group.group_mod_block.success": "You have successfully blocked @{name} from the group",
|
||||
"group.group_mod_demote": "Demote @{name}",
|
||||
"group.group_mod_demote.success": "Demoted @{name} to group user",
|
||||
"group.group_mod_kick": "Kick @{name} from group",
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
SignUpPanel,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
import { useGroup, useOwnAccount } from 'soapbox/hooks';
|
||||
import { Group } from 'soapbox/schemas';
|
||||
|
||||
import { Tabs } from '../components/ui';
|
||||
|
||||
|
@ -27,6 +28,32 @@ interface IGroupPage {
|
|||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const PrivacyBlankslate = () => (
|
||||
<Stack space={4} className='py-10' alignItems='center'>
|
||||
<div className='rounded-full bg-gray-200 p-3'>
|
||||
<Icon src={require('@tabler/icons/eye-off.svg')} className='h-6 w-6 text-gray-600' />
|
||||
</div>
|
||||
|
||||
<Text theme='muted'>
|
||||
Content is only visible to group members
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const BlockedBlankslate = ({ group }: { group: Group }) => (
|
||||
<Stack space={4} className='py-10' alignItems='center'>
|
||||
<div className='rounded-full bg-danger-200 p-3'>
|
||||
<Icon src={require('@tabler/icons/eye-off.svg')} className='h-6 w-6 text-danger-600' />
|
||||
</div>
|
||||
|
||||
<Text theme='muted'>
|
||||
You are banned from
|
||||
{' '}
|
||||
<Text theme='inherit' tag='span' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
/** Page to display a group. */
|
||||
const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
|
||||
const intl = useIntl();
|
||||
|
@ -37,7 +64,8 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
|
|||
|
||||
const { group } = useGroup(id);
|
||||
|
||||
const isNonMember = !group?.relationship?.member;
|
||||
const isMember = !!group?.relationship?.member;
|
||||
const isBlocked = group?.relationship?.blocked_by;
|
||||
const isPrivate = group?.locked;
|
||||
|
||||
// if ((group as any) === false) {
|
||||
|
@ -59,6 +87,16 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
|
|||
},
|
||||
];
|
||||
|
||||
const renderChildren = () => {
|
||||
if (!isMember && isPrivate) {
|
||||
return <PrivacyBlankslate />;
|
||||
} else if (isBlocked) {
|
||||
return <BlockedBlankslate group={group} />;
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout.Main>
|
||||
|
@ -70,17 +108,7 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
|
|||
activeItem={match.path}
|
||||
/>
|
||||
|
||||
{(isNonMember && isPrivate) ? (
|
||||
<Stack space={4} className='py-10' alignItems='center'>
|
||||
<div className='rounded-full bg-gray-200 p-3'>
|
||||
<Icon src={require('@tabler/icons/eye-off.svg')} className='h-6 w-6 text-gray-600' />
|
||||
</div>
|
||||
|
||||
<Text theme='muted'>
|
||||
Content is only visible to group members
|
||||
</Text>
|
||||
</Stack>
|
||||
) : children}
|
||||
{renderChildren()}
|
||||
</Column>
|
||||
|
||||
{!me && (
|
||||
|
|
124
app/soapbox/schemas/account.ts
Normal file
124
app/soapbox/schemas/account.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import z from 'zod';
|
||||
|
||||
import emojify from 'soapbox/features/emoji';
|
||||
|
||||
import { customEmojiSchema } from './custom-emoji';
|
||||
import { relationshipSchema } from './relationship';
|
||||
import { filteredArray, makeCustomEmojiMap } from './utils';
|
||||
|
||||
const avatarMissing = require('assets/images/avatar-missing.png');
|
||||
const headerMissing = require('assets/images/header-missing.png');
|
||||
|
||||
const accountSchema = z.object({
|
||||
accepting_messages: z.boolean().catch(false),
|
||||
accepts_chat_messages: z.boolean().catch(false),
|
||||
acct: z.string().catch(''),
|
||||
avatar: z.string().catch(avatarMissing),
|
||||
avatar_static: z.string().catch(''),
|
||||
birthday: z.string().catch(''),
|
||||
bot: z.boolean().catch(false),
|
||||
chats_onboarded: z.boolean().catch(true),
|
||||
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||
discoverable: z.boolean().catch(false),
|
||||
display_name: z.string().catch(''),
|
||||
emojis: filteredArray(customEmojiSchema).catch([]),
|
||||
favicon: z.string().catch(''),
|
||||
fields: z.any(), // TODO
|
||||
followers_count: z.number().catch(0),
|
||||
following_count: z.number().catch(0),
|
||||
fqn: z.string().catch(''),
|
||||
header: z.string().catch(headerMissing),
|
||||
header_static: z.string().catch(''),
|
||||
id: z.string(),
|
||||
last_status_at: z.string().catch(''),
|
||||
location: z.string().catch(''),
|
||||
locked: z.boolean().catch(false),
|
||||
moved: z.any(), // TODO
|
||||
mute_expires_at: z.union([
|
||||
z.string(),
|
||||
z.null(),
|
||||
]).catch(null),
|
||||
note: z.string().catch(''),
|
||||
pleroma: z.any(), // TODO
|
||||
source: z.any(), // TODO
|
||||
statuses_count: z.number().catch(0),
|
||||
uri: z.string().catch(''),
|
||||
url: z.string().catch(''),
|
||||
username: z.string().catch(''),
|
||||
verified: z.boolean().default(false),
|
||||
website: z.string().catch(''),
|
||||
|
||||
/**
|
||||
* Internal fields
|
||||
*/
|
||||
display_name_html: z.string().catch(''),
|
||||
domain: z.string().catch(''),
|
||||
note_emojified: z.string().catch(''),
|
||||
relationship: relationshipSchema.nullable().catch(null),
|
||||
|
||||
/**
|
||||
* Misc
|
||||
*/
|
||||
other_settings: z.any(),
|
||||
}).transform((account) => {
|
||||
const customEmojiMap = makeCustomEmojiMap(account.emojis);
|
||||
|
||||
// Birthday
|
||||
const birthday = account.pleroma?.birthday || account.other_settings?.birthday;
|
||||
account.birthday = birthday;
|
||||
|
||||
// Verified
|
||||
const verified = account.verified === true || account.pleroma?.tags?.includes('verified');
|
||||
account.verified = verified;
|
||||
|
||||
// Location
|
||||
const location = account.location
|
||||
|| account.pleroma?.location
|
||||
|| account.other_settings?.location;
|
||||
account.location = location;
|
||||
|
||||
// Username
|
||||
const acct = account.acct || '';
|
||||
const username = account.username || '';
|
||||
account.username = username || acct.split('@')[0];
|
||||
|
||||
// Display Name
|
||||
const displayName = account.display_name || '';
|
||||
account.display_name = displayName.trim().length === 0 ? account.username : displayName;
|
||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), customEmojiMap);
|
||||
|
||||
// Discoverable
|
||||
const discoverable = Boolean(account.discoverable || account.source?.pleroma?.discoverable);
|
||||
account.discoverable = discoverable;
|
||||
|
||||
// Message Acceptance
|
||||
const acceptsChatMessages = Boolean(account.pleroma?.accepts_chat_messages || account?.accepting_messages);
|
||||
account.accepts_chat_messages = acceptsChatMessages;
|
||||
|
||||
// Notes
|
||||
account.note_emojified = emojify(account.note, customEmojiMap);
|
||||
|
||||
/**
|
||||
* Todo
|
||||
* - internal fields
|
||||
* - donor
|
||||
* - tags
|
||||
* - fields
|
||||
* - pleroma legacy fields
|
||||
* - emojification
|
||||
* - domain
|
||||
* - guessFqn
|
||||
* - fqn
|
||||
* - favicon
|
||||
* - staff fields
|
||||
* - birthday
|
||||
* - note
|
||||
*/
|
||||
|
||||
return account;
|
||||
});
|
||||
|
||||
type Account = z.infer<typeof accountSchema>;
|
||||
|
||||
export { accountSchema, Account };
|
|
@ -1,5 +1,7 @@
|
|||
import z from 'zod';
|
||||
|
||||
import { accountSchema } from './account';
|
||||
|
||||
enum TruthSocialGroupRoles {
|
||||
ADMIN = 'owner',
|
||||
MODERATOR = 'admin',
|
||||
|
@ -14,7 +16,7 @@ enum BaseGroupRoles {
|
|||
|
||||
const groupMemberSchema = z.object({
|
||||
id: z.string(),
|
||||
account: z.any(),
|
||||
account: accountSchema,
|
||||
role: z.union([
|
||||
z.nativeEnum(TruthSocialGroupRoles),
|
||||
z.nativeEnum(BaseGroupRoles),
|
||||
|
|
|
@ -5,6 +5,8 @@ const groupRelationshipSchema = z.object({
|
|||
member: z.boolean().catch(false),
|
||||
requested: z.boolean().catch(false),
|
||||
role: z.string().nullish().catch(null),
|
||||
blocked_by: z.boolean().catch(false),
|
||||
notifying: z.boolean().nullable().catch(null),
|
||||
});
|
||||
|
||||
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
/**
|
||||
* Schemas
|
||||
*/
|
||||
export { accountSchema } from './account';
|
||||
export { customEmojiSchema } from './custom-emoji';
|
||||
export { groupSchema } from './group';
|
||||
export { groupMemberSchema } from './group-member';
|
||||
export { groupRelationshipSchema } from './group-relationship';
|
||||
export { relationshipSchema } from './relationship';
|
||||
|
||||
/**
|
||||
* Entity Types
|
||||
*/
|
||||
export type { Account } from './account';
|
||||
export type { CustomEmoji } from './custom-emoji';
|
||||
export type { Group } from './group';
|
||||
export type { GroupMember } from './group-member';
|
||||
export type { GroupRelationship } from './group-relationship';
|
||||
export type { Relationship } from './relationship';
|
||||
|
|
22
app/soapbox/schemas/relationship.ts
Normal file
22
app/soapbox/schemas/relationship.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import z from 'zod';
|
||||
|
||||
const relationshipSchema = z.object({
|
||||
blocked_by: z.boolean().catch(false),
|
||||
blocking: z.boolean().catch(false),
|
||||
domain_blocking: z.boolean().catch(false),
|
||||
endorsed: z.boolean().catch(false),
|
||||
followed_by: z.boolean().catch(false),
|
||||
following: z.boolean().catch(false),
|
||||
id: z.string(),
|
||||
muting: z.boolean().catch(false),
|
||||
muting_notifications: z.boolean().catch(false),
|
||||
note: z.string().catch(''),
|
||||
notifying: z.boolean().catch(false),
|
||||
requested: z.boolean().catch(false),
|
||||
showing_reblogs: z.boolean().catch(false),
|
||||
subscribing: z.boolean().catch(false),
|
||||
});
|
||||
|
||||
type Relationship = z.infer<typeof relationshipSchema>;
|
||||
|
||||
export { relationshipSchema, Relationship };
|
|
@ -1,6 +1,7 @@
|
|||
import type { Account } from 'soapbox/types/entities';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
const getDomainFromURL = (account: Account): string => {
|
||||
const getDomainFromURL = (account: AccountEntity): string => {
|
||||
try {
|
||||
const url = account.url;
|
||||
return new URL(url).host;
|
||||
|
@ -9,12 +10,12 @@ const getDomainFromURL = (account: Account): string => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getDomain = (account: Account): string => {
|
||||
export const getDomain = (account: AccountEntity): string => {
|
||||
const domain = account.acct.split('@')[1];
|
||||
return domain ? domain : getDomainFromURL(account);
|
||||
};
|
||||
|
||||
export const getBaseURL = (account: Account): string => {
|
||||
export const getBaseURL = (account: AccountEntity): string => {
|
||||
try {
|
||||
return new URL(account.url).origin;
|
||||
} catch {
|
||||
|
@ -22,16 +23,16 @@ export const getBaseURL = (account: Account): string => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getAcct = (account: Account, displayFqn: boolean): string => (
|
||||
export const getAcct = (account: AccountEntity | Account, displayFqn: boolean): string => (
|
||||
displayFqn === true ? account.fqn : account.acct
|
||||
);
|
||||
|
||||
export const isLocal = (account: Account): boolean => {
|
||||
export const isLocal = (account: AccountEntity | Account): boolean => {
|
||||
const domain: string = account.acct.split('@')[1];
|
||||
return domain === undefined ? true : false;
|
||||
};
|
||||
|
||||
export const isRemote = (account: Account): boolean => !isLocal(account);
|
||||
export const isRemote = (account: AccountEntity): boolean => !isLocal(account);
|
||||
|
||||
/** Default header filenames from various backends */
|
||||
const DEFAULT_HEADERS = [
|
||||
|
|
|
@ -516,6 +516,11 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
*/
|
||||
groupsDiscovery: v.software === TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Can kick user from Group.
|
||||
*/
|
||||
groupsKick: v.software !== TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Can query pending Group requests.
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue