Fix types, at least
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
0fc8a2993f
commit
7fb07b66be
42 changed files with 204 additions and 269 deletions
|
@ -119,7 +119,7 @@
|
|||
"html-react-parser": "^5.0.0",
|
||||
"http-link-header": "^1.0.2",
|
||||
"immer": "^10.0.0",
|
||||
"immutable": "^4.2.1",
|
||||
"immutable": "^4.3.7",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"intl-messageformat": "10.5.11",
|
||||
"intl-pluralrules": "^2.0.0",
|
||||
|
|
|
@ -20,7 +20,7 @@ import { getSettings } from './settings';
|
|||
import { createStatus } from './statuses';
|
||||
|
||||
import type { EditorState } from 'lexical';
|
||||
import type { Tag } from 'pl-api';
|
||||
import type { CreateStatusParams, Tag } from 'pl-api';
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { Account, Group } from 'soapbox/schemas';
|
||||
|
@ -373,22 +373,31 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
const idempotencyKey = compose.idempotencyKey;
|
||||
const contentType = compose.content_type === 'wysiwyg' ? 'text/markdown' : compose.content_type;
|
||||
|
||||
const params: Record<string, any> = {
|
||||
const params: CreateStatusParams = {
|
||||
status,
|
||||
in_reply_to_id: compose.in_reply_to,
|
||||
quote_id: compose.quote,
|
||||
media_ids: media.map(item => item.id),
|
||||
in_reply_to_id: compose.in_reply_to || undefined,
|
||||
quote_id: compose.quote || undefined,
|
||||
media_ids: media.map(item => item.id).toArray(),
|
||||
sensitive: compose.sensitive,
|
||||
spoiler_text: compose.spoiler_text,
|
||||
visibility: compose.privacy,
|
||||
content_type: contentType,
|
||||
poll: compose.poll,
|
||||
scheduled_at: compose.schedule,
|
||||
language: compose.language || compose.suggested_language,
|
||||
to,
|
||||
scheduled_at: compose.schedule?.toISOString(),
|
||||
language: compose.language || compose.suggested_language || undefined,
|
||||
to: to.size ? to.toArray() : undefined,
|
||||
federated: compose.federated,
|
||||
};
|
||||
|
||||
if (compose.poll) {
|
||||
params.poll = {
|
||||
options: compose.poll.options.toArray(),
|
||||
expires_in: compose.poll.expires_in,
|
||||
multiple: compose.poll.multiple,
|
||||
hide_totals: compose.poll.hide_totals,
|
||||
options_map: compose.poll.options_map.toJS(),
|
||||
};
|
||||
}
|
||||
|
||||
if (compose.language && compose.textMap.size) {
|
||||
params.status_map = compose.textMap.toJS();
|
||||
params.status_map[compose.language] = status;
|
||||
|
@ -398,14 +407,13 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
params.spoiler_text_map[compose.language] = compose.spoiler_text;
|
||||
}
|
||||
|
||||
if (params.poll) {
|
||||
const poll = params.poll.toJS();
|
||||
poll.options.forEach((option: any, index: number) => poll.options_map[index][compose.language!] = option);
|
||||
params.poll = poll;
|
||||
const poll = params.poll;
|
||||
if (poll?.options_map) {
|
||||
poll.options.forEach((option: any, index: number) => poll.options_map![index][compose.language!] = option);
|
||||
}
|
||||
}
|
||||
|
||||
if (compose.privacy === 'group') {
|
||||
if (compose.privacy === 'group' && compose.group_id) {
|
||||
params.group_id = compose.group_id;
|
||||
}
|
||||
|
||||
|
|
|
@ -283,7 +283,10 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
|||
acc.accounts[item.target.id] = item.target;
|
||||
}
|
||||
|
||||
// TODO actually check for type
|
||||
// @ts-ignore
|
||||
if (item.status?.id) {
|
||||
// @ts-ignore
|
||||
acc.statuses[item.status.id] = item.status;
|
||||
}
|
||||
|
||||
|
|
|
@ -64,16 +64,14 @@ const createStatus = (params: CreateStatusParams, idempotencyKey: string, status
|
|||
|
||||
return (statusId === null ? getClient(getState()).statuses.createStatus(params) : getClient(getState()).statuses.editStatus(statusId, params))
|
||||
.then((status) => {
|
||||
// The backend might still be processing the rich media attachment
|
||||
if (!status.card && shouldHaveCard(status)) {
|
||||
status.expectsCard = true;
|
||||
}
|
||||
// The backend might still be processing the rich media attachment
|
||||
const expectsCard = !status.card && shouldHaveCard(status);
|
||||
|
||||
dispatch(importFetchedStatus(status, idempotencyKey));
|
||||
dispatch(importFetchedStatus({ ...status, expectsCard }, idempotencyKey));
|
||||
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!statusId });
|
||||
|
||||
// Poll the backend for the updated card
|
||||
if (status.expectsCard) {
|
||||
if (expectsCard) {
|
||||
const delay = 1000;
|
||||
|
||||
const poll = (retries = 5) => {
|
||||
|
|
|
@ -22,7 +22,7 @@ const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts =
|
|||
const { entity: account, isUnauthorized, ...result } = useEntityLookup<Account>(
|
||||
Entities.ACCOUNTS,
|
||||
(account) => account.acct.toLowerCase() === acct?.toLowerCase(),
|
||||
() => client.accounts.lookupAccount(acct),
|
||||
() => client.accounts.lookupAccount(acct!),
|
||||
{ schema: accountSchema, enabled: !!acct },
|
||||
);
|
||||
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { announcementReactionSchema, announcementSchema, type Announcement, type AnnouncementReaction } from 'pl-api';
|
||||
import { announcementReactionSchema, type Announcement as BaseAnnouncement, type AnnouncementReaction } from 'pl-api';
|
||||
|
||||
import emojify from 'soapbox/features/emoji';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { makeCustomEmojiMap } from 'soapbox/schemas/utils';
|
||||
|
||||
interface Announcement extends BaseAnnouncement {
|
||||
contentHtml: string;
|
||||
}
|
||||
|
||||
const transformAnnouncement = (announcement: BaseAnnouncement) => {
|
||||
const emojiMap = makeCustomEmojiMap(announcement.emojis);
|
||||
|
||||
const contentHtml = emojify(announcement.content, emojiMap);
|
||||
|
||||
return {
|
||||
...announcement,
|
||||
contentHtml,
|
||||
};
|
||||
};
|
||||
|
||||
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => announcementReactionSchema.parse({
|
||||
...reaction,
|
||||
|
@ -25,7 +42,7 @@ const useAnnouncements = () => {
|
|||
|
||||
const getAnnouncements = async () => {
|
||||
const data = await client.announcements.getAnnouncements();
|
||||
return data;
|
||||
return data.map(transformAnnouncement);
|
||||
};
|
||||
|
||||
const { data, ...result } = useQuery<ReadonlyArray<Announcement>>({
|
||||
|
@ -42,18 +59,18 @@ const useAnnouncements = () => {
|
|||
retry: false,
|
||||
onMutate: ({ announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, 1, true),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
},
|
||||
onError: (_, { announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, false),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -66,18 +83,18 @@ const useAnnouncements = () => {
|
|||
retry: false,
|
||||
onMutate: ({ announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, false),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
},
|
||||
onError: (_, { announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, 1, true),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -93,4 +110,4 @@ const useAnnouncements = () => {
|
|||
const compareAnnouncements = (a: Announcement, b: Announcement): number =>
|
||||
new Date(a.starts_at || a.published_at).getDate() - new Date(b.starts_at || b.published_at).getDate();
|
||||
|
||||
export { updateReactions, useAnnouncements };
|
||||
export { updateReactions, useAnnouncements, type Announcement };
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useClient } from 'soapbox/hooks';
|
|||
import { groupSchema } from 'soapbox/schemas';
|
||||
|
||||
interface CreateGroupParams {
|
||||
display_name?: string;
|
||||
display_name: string;
|
||||
note?: string;
|
||||
avatar?: File;
|
||||
header?: File;
|
||||
|
|
|
@ -10,7 +10,7 @@ const useBookmarkFolders = () => {
|
|||
|
||||
const { entities, ...result } = useEntities<BookmarkFolder>(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
() => client.myAccount.getBookmarkFolders,
|
||||
() => client.myAccount.getBookmarkFolders(),
|
||||
{ enabled: features.bookmarkFolders, schema: bookmarkFolderSchema },
|
||||
);
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import { Entities } from 'soapbox/entity-store/entities';
|
|||
import { useDeleteEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
const useDeleteBookmarkFolder = (folderId: string) => {
|
||||
const useDeleteBookmarkFolder = () => {
|
||||
const client = useClient();
|
||||
|
||||
const { deleteEntity, isSubmitting } = useDeleteEntity(
|
||||
Entities.BOOKMARK_FOLDERS,
|
||||
() => client.myAccount.deleteBookmarkFolder(folderId),
|
||||
(folderId: string) => client.myAccount.deleteBookmarkFolder(folderId),
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -3,10 +3,11 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import { getTextDirection } from 'soapbox/utils/rtl';
|
||||
|
||||
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/schemas';
|
||||
import type { Announcement } from 'soapbox/api/hooks/announcements/useAnnouncements';
|
||||
import type { Mention as MentionEntity } from 'soapbox/schemas';
|
||||
|
||||
interface IAnnouncementContent {
|
||||
announcement: AnnouncementEntity;
|
||||
announcement: Announcement;
|
||||
}
|
||||
|
||||
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {
|
||||
|
|
|
@ -9,7 +9,7 @@ import AnnouncementContent from './announcement-content';
|
|||
import ReactionsBar from './reactions-bar';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { Announcement as AnnouncementEntity } from 'soapbox/schemas';
|
||||
import type { Announcement as AnnouncementEntity } from 'soapbox/api/hooks/announcements/useAnnouncements';
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity;
|
||||
|
|
|
@ -31,7 +31,7 @@ const AnnouncementsPanel = () => {
|
|||
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
|
||||
<Card className='relative black:rounded-xl black:border black:border-gray-800' size='md' variant='rounded'>
|
||||
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
|
||||
{announcements!.map((announcement) => (
|
||||
{announcements.map((announcement) => (
|
||||
<Announcement
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
|
|
|
@ -3,7 +3,9 @@ import { useHistory, useParams } from 'react-router-dom';
|
|||
|
||||
import { toggleMainWindow } from 'soapbox/actions/chats';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
import { IChat, useChat } from 'soapbox/queries/chats';
|
||||
import { useChat } from 'soapbox/queries/chats';
|
||||
|
||||
import type { Chat } from 'pl-api';
|
||||
|
||||
const ChatContext = createContext<any>({
|
||||
isOpen: false,
|
||||
|
@ -69,7 +71,7 @@ const ChatProvider: React.FC<IChatProvider> = ({ children }) => {
|
|||
};
|
||||
|
||||
interface IChatContext {
|
||||
chat: IChat | null;
|
||||
chat: Chat | null;
|
||||
isOpen: boolean;
|
||||
isUsingMainChatPage?: boolean;
|
||||
toggleChatPane(): void;
|
||||
|
|
|
@ -23,7 +23,7 @@ interface UseBatchedEntitiesOpts<TEntity extends Entity> {
|
|||
const useBatchedEntities = <TEntity extends Entity>(
|
||||
expandedPath: ExpandedEntitiesPath,
|
||||
ids: string[],
|
||||
entityFn: EntityFn<TEntity[]>,
|
||||
entityFn: EntityFn<string[]>,
|
||||
opts: UseBatchedEntitiesOpts<TEntity> = {},
|
||||
) => {
|
||||
const getState = useGetState();
|
||||
|
|
|
@ -24,7 +24,7 @@ interface UseEntityOpts<TEntity extends Entity> {
|
|||
|
||||
const useEntity = <TEntity extends Entity>(
|
||||
path: EntityPath,
|
||||
entityFn: EntityFn<TEntity>,
|
||||
entityFn: EntityFn<void>,
|
||||
opts: UseEntityOpts<TEntity> = {},
|
||||
) => {
|
||||
const [isFetching, setPromise] = useLoading(true);
|
||||
|
|
|
@ -37,8 +37,8 @@ const createList = (): EntityList => ({
|
|||
|
||||
/** Create an empty entity list state. */
|
||||
const createListState = (): EntityListState => ({
|
||||
next: undefined,
|
||||
prev: undefined,
|
||||
next: null,
|
||||
prev: null,
|
||||
totalCount: 0,
|
||||
error: null,
|
||||
fetched: false,
|
||||
|
|
|
@ -13,6 +13,8 @@ import { Checkbox, Form, FormGroup, FormActions, Button, Input, Textarea, Select
|
|||
import CaptchaField from 'soapbox/features/auth-login/components/captcha';
|
||||
import { useAppDispatch, useSettings, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
|
||||
import type { CreateAccountParams } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
username: { id: 'registration.fields.username_placeholder', defaultMessage: 'Username' },
|
||||
username_hint: { id: 'registration.fields.username_hint', defaultMessage: 'Only letters, numbers, and underscores are allowed.' },
|
||||
|
@ -53,7 +55,13 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
|
||||
const [captchaLoading, setCaptchaLoading] = useState(true);
|
||||
const [submissionLoading, setSubmissionLoading] = useState(false);
|
||||
const [params, setParams] = useState(ImmutableMap<string, any>());
|
||||
const [params, setParams] = useState<CreateAccountParams>({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
agreement: false,
|
||||
locale: '',
|
||||
});
|
||||
const [captchaIdempotencyKey, setCaptchaIdempotencyKey] = useState(uuidv4());
|
||||
const [usernameUnavailable, setUsernameUnavailable] = useState(false);
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('');
|
||||
|
@ -61,39 +69,35 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
|
||||
const controller = useRef(new AbortController());
|
||||
|
||||
const updateParams = (map: any) => {
|
||||
setParams(params.merge(ImmutableMap(map)));
|
||||
};
|
||||
|
||||
const onInputChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = e => {
|
||||
updateParams({ [e.target.name]: e.target.value });
|
||||
setParams(params => ({ ...params, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
const onUsernameChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
updateParams({ username: e.target.value });
|
||||
setParams(params => ({ ...params, username: e.target.value }));
|
||||
setUsernameUnavailable(false);
|
||||
controller.current.abort();
|
||||
controller.current = new AbortController();
|
||||
|
||||
const domain = params.get('domain');
|
||||
const domain = params.domain;
|
||||
usernameAvailable(e.target.value, domain ? domains!.find(({ id }) => id === domain)?.domain : undefined);
|
||||
};
|
||||
|
||||
const onDomainChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
|
||||
updateParams({ domain: e.target.value || null });
|
||||
setParams(params => ({ ...params, domain: e.target.value || undefined }));
|
||||
setUsernameUnavailable(false);
|
||||
|
||||
controller.current.abort();
|
||||
controller.current = new AbortController();
|
||||
|
||||
const username = params.get('username');
|
||||
const username = params.username;
|
||||
if (username) {
|
||||
usernameAvailable(username, domains!.find(({ id }) => id === e.target.value)?.domain);
|
||||
}
|
||||
};
|
||||
|
||||
const onCheckboxChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
updateParams({ [e.target.name]: e.target.checked });
|
||||
setParams(params => ({ ...params, [e.target.name]: e.target.checked }));
|
||||
};
|
||||
|
||||
const onPasswordChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
|
@ -106,7 +110,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
};
|
||||
|
||||
const onPasswordConfirmChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
const password = params.get('password', '');
|
||||
const password = params.password || '';
|
||||
const passwordConfirmation = e.target.value;
|
||||
setPasswordConfirmation(passwordConfirmation);
|
||||
|
||||
|
@ -120,7 +124,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
};
|
||||
|
||||
const onBirthdayChange = (birthday: string) => {
|
||||
updateParams({ birthday });
|
||||
setParams(params => ({ ...params, birthday }));
|
||||
};
|
||||
|
||||
const launchModal = () => {
|
||||
|
@ -129,7 +133,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
<FormattedMessage
|
||||
id='confirmations.register.needs_confirmation'
|
||||
defaultMessage='Please check your inbox at {email} for confirmation instructions. You will need to verify your email address to continue.'
|
||||
values={{ email: <strong>{params.get('email')}</strong> }}
|
||||
values={{ email: <strong>{params.email}</strong> }}
|
||||
/></p>}
|
||||
{needsApproval && <p>
|
||||
<FormattedMessage
|
||||
|
@ -160,7 +164,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const passwordsMatch = () => params.get('password', '') === passwordConfirmation;
|
||||
const passwordsMatch = () => params.password === passwordConfirmation;
|
||||
|
||||
const usernameAvailable = useCallback(debounce((username, domain?: string) => {
|
||||
if (!supportsAccountLookup) return;
|
||||
|
@ -186,19 +190,18 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const normalParams = params.withMutations(params => {
|
||||
// Locale for confirmation email
|
||||
params.set('locale', locale);
|
||||
const normalParams = {
|
||||
...params,
|
||||
locale,
|
||||
};
|
||||
|
||||
// Pleroma invites
|
||||
if (inviteToken) {
|
||||
params.set('token', inviteToken);
|
||||
}
|
||||
});
|
||||
if (inviteToken) {
|
||||
params.token = inviteToken;
|
||||
}
|
||||
|
||||
setSubmissionLoading(true);
|
||||
|
||||
dispatch(register(normalParams.toJS()))
|
||||
dispatch(register(normalParams))
|
||||
.then(postRegisterAction)
|
||||
.catch(() => {
|
||||
setSubmissionLoading(false);
|
||||
|
@ -212,10 +215,11 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
|
||||
const onFetchCaptcha = (captcha: ImmutableMap<string, any>) => {
|
||||
setCaptchaLoading(false);
|
||||
updateParams({
|
||||
setParams(params => ({
|
||||
...params,
|
||||
captcha_token: captcha.get('token'),
|
||||
captcha_answer_data: captcha.get('answer_data'),
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
const onFetchCaptchaFail = () => {
|
||||
|
@ -224,7 +228,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
|
||||
const refreshCaptcha = () => {
|
||||
setCaptchaIdempotencyKey(uuidv4());
|
||||
updateParams({ captcha_solution: '' });
|
||||
setParams(params => ({ ...params, captcha_solution: '' }));
|
||||
};
|
||||
|
||||
const isLoading = captchaLoading || submissionLoading;
|
||||
|
@ -247,7 +251,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
pattern='^[a-zA-Z\d_-]+'
|
||||
icon={require('@tabler/icons/outline/at.svg')}
|
||||
onChange={onUsernameChange}
|
||||
value={params.get('username', '')}
|
||||
value={params.username}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -256,7 +260,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
<FormGroup>
|
||||
<Select
|
||||
onChange={onDomainChange}
|
||||
value={params.get('domain')}
|
||||
value={params.domain}
|
||||
>
|
||||
{domains.map(({ id, domain }) => (
|
||||
<option key={id} value={id}>{domain}</option>
|
||||
|
@ -273,7 +277,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onInputChange}
|
||||
value={params.get('email', '')}
|
||||
value={params.email}
|
||||
required
|
||||
/>
|
||||
|
||||
|
@ -285,7 +289,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onPasswordChange}
|
||||
value={params.get('password', '')}
|
||||
value={params.password}
|
||||
required
|
||||
/>
|
||||
|
||||
|
@ -308,7 +312,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
|
||||
{birthdayRequired && (
|
||||
<BirthdayInput
|
||||
value={params.get('birthday')}
|
||||
value={params.birthday || ''}
|
||||
onChange={onBirthdayChange}
|
||||
required
|
||||
/>
|
||||
|
@ -323,7 +327,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
placeholder={intl.formatMessage(messages.reasonHint)}
|
||||
maxLength={500}
|
||||
onChange={onInputChange}
|
||||
value={params.get('reason', '')}
|
||||
value={params.reason || ''}
|
||||
autoGrow
|
||||
required
|
||||
/>
|
||||
|
@ -337,7 +341,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
onClick={onCaptchaClick}
|
||||
idempotencyKey={captchaIdempotencyKey}
|
||||
name='captcha_solution'
|
||||
value={params.get('captcha_solution', '')}
|
||||
value={params.captcha_solution || ''}
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
|
@ -346,7 +350,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
<Checkbox
|
||||
name='agreement'
|
||||
onChange={onCheckboxChange}
|
||||
checked={params.get('agreement', false)}
|
||||
checked={params.agreement}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -356,7 +360,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
<Checkbox
|
||||
name='accepts_email_list'
|
||||
onChange={onCheckboxChange}
|
||||
checked={params.get('accepts_email_list', false)}
|
||||
checked={params.accepts_email_list}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
|
|
@ -9,8 +9,9 @@ import { Avatar, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
|||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { IChat, useChatActions } from 'soapbox/queries/chats';
|
||||
import { useChatActions } from 'soapbox/queries/chats';
|
||||
|
||||
import type { Chat } from 'pl-api';
|
||||
import type { Menu } from 'soapbox/components/dropdown-menu';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -23,7 +24,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IChatListItemInterface {
|
||||
chat: IChat;
|
||||
chat: Chat;
|
||||
onClick: (chat: any) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ import { Components, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
|||
import { Avatar, Button, Divider, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import PlaceholderChatMessage from 'soapbox/features/placeholder/components/placeholder-chat-message';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { IChat, useChatActions, useChatMessages } from 'soapbox/queries/chats';
|
||||
import { useChatActions, useChatMessages } from 'soapbox/queries/chats';
|
||||
|
||||
import ChatMessage from './chat-message';
|
||||
|
||||
import type { Chat } from 'pl-api';
|
||||
import type { ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -21,7 +22,7 @@ const messages = defineMessages({
|
|||
|
||||
type TimeFormat = 'today' | 'date';
|
||||
|
||||
const timeChange = (prev: ChatMessageEntity, curr: ChatMessageEntity): TimeFormat | null => {
|
||||
const timeChange = (prev: Pick<ChatMessageEntity, 'created_at'>, curr: Pick<ChatMessageEntity, 'created_at'>): TimeFormat | null => {
|
||||
const prevDate = new Date(prev.created_at).getDate();
|
||||
const currDate = new Date(curr.created_at).getDate();
|
||||
const nowDate = new Date().getDate();
|
||||
|
@ -57,7 +58,7 @@ const Scroller: Components['Scroller'] = React.forwardRef((props, ref) => {
|
|||
|
||||
interface IChatMessageList {
|
||||
/** Chat the messages are being rendered from. */
|
||||
chat: IChat;
|
||||
chat: Chat;
|
||||
}
|
||||
|
||||
/** Scrollable list of chat messages. */
|
||||
|
|
|
@ -11,11 +11,12 @@ import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
|||
import emojify from 'soapbox/features/emoji';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats';
|
||||
import { ChatKeys, useChatActions } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { stripHTML } from 'soapbox/utils/html';
|
||||
import { onlyEmoji } from 'soapbox/utils/rich-content';
|
||||
|
||||
import type { Chat } from 'pl-api';
|
||||
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
|
||||
import type { ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -43,7 +44,7 @@ const parseContent = (chatMessage: ChatMessageEntity) => {
|
|||
};
|
||||
|
||||
interface IChatMessage {
|
||||
chat: IChat;
|
||||
chat: Chat;
|
||||
chatMessage: ChatMessageEntity;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
|
||||
import { IChat } from 'soapbox/queries/chats';
|
||||
|
||||
import ChatList from '../../chat-list';
|
||||
|
||||
import type { Chat } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.chats', defaultMessage: 'Chats' },
|
||||
});
|
||||
|
@ -15,7 +16,7 @@ const ChatPageSidebar = () => {
|
|||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const handleClickChat = (chat: IChat) => {
|
||||
const handleClickChat = (chat: Chat) => {
|
||||
history.push(`/chats/${chat.id}`);
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { Stack } from 'soapbox/components/ui';
|
||||
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import { IChat, useChats } from 'soapbox/queries/chats';
|
||||
import { useChats } from 'soapbox/queries/chats';
|
||||
|
||||
import ChatList from '../chat-list';
|
||||
import ChatSearch from '../chat-search/chat-search';
|
||||
|
@ -16,13 +16,15 @@ import { Pane } from '../ui';
|
|||
|
||||
import Blankslate from './blankslate';
|
||||
|
||||
import type { Chat } from 'pl-api';
|
||||
|
||||
const ChatPane = () => {
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
|
||||
const { screen, changeScreen, isOpen, toggleChatPane } = useChatContext();
|
||||
const { chatsQuery: { data: chats, isLoading } } = useChats();
|
||||
|
||||
const handleClickChat = (nextChat: IChat) => {
|
||||
const handleClickChat = (nextChat: Chat) => {
|
||||
changeScreen(ChatWidgetScreens.CHAT, nextChat.id);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
|
|||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import useAccountSearch from 'soapbox/queries/search';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
import type { Account } from 'pl-api';
|
||||
|
||||
interface IResults {
|
||||
accountSearchResult: ReturnType<typeof useAccountSearch>;
|
||||
|
|
|
@ -6,12 +6,13 @@ import { uploadMedia } from 'soapbox/actions/media';
|
|||
import { Stack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { normalizeAttachment } from 'soapbox/normalizers';
|
||||
import { IChat, useChatActions } from 'soapbox/queries/chats';
|
||||
import { useChatActions } from 'soapbox/queries/chats';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import ChatComposer from './chat-composer';
|
||||
import ChatMessageList from './chat-message-list';
|
||||
|
||||
import type { Chat as ChatEntity } from 'pl-api';
|
||||
import type { PlfeResponse } from 'soapbox/api';
|
||||
import type { Attachment } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -23,7 +24,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ChatInterface {
|
||||
chat: IChat;
|
||||
chat: ChatEntity;
|
||||
inputRef?: MutableRefObject<HTMLTextAreaElement | null>;
|
||||
className?: string;
|
||||
}
|
||||
|
|
|
@ -196,7 +196,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
const renderButtons = useCallback(() => (
|
||||
<HStack alignItems='center' space={2}>
|
||||
{features.media && <UploadButtonContainer composeId={id} />}
|
||||
<UploadButtonContainer composeId={id} />
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} condensed={shouldCondense} />
|
||||
{features.polls && <PollButton composeId={id} />}
|
||||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||
|
|
|
@ -20,7 +20,7 @@ const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
|
|||
const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! }));
|
||||
const to = compose.to;
|
||||
|
||||
if (!features.explicitAddressing || !status || !to) {
|
||||
if (!features.createStatusExplicitAddressing || !status || !to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ const Settings = () => {
|
|||
<Preferences />
|
||||
</CardBody>
|
||||
|
||||
{(features.security || features.accountAliases) && (
|
||||
{(features.security || features.manageAccountAliases) && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle title={intl.formatMessage(messages.other)} />
|
||||
|
|
|
@ -2,15 +2,15 @@ import React, { useMemo, useState } from 'react';
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCreateGroup, type CreateGroupParams } from 'soapbox/api/hooks';
|
||||
import { useCreateGroup } from 'soapbox/api/hooks';
|
||||
import { Modal, Stack } from 'soapbox/components/ui';
|
||||
import { type Group } from 'soapbox/schemas';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import ConfirmationStep from './steps/confirmation-step';
|
||||
import DetailsStep from './steps/details-step';
|
||||
import PrivacyStep from './steps/privacy-step';
|
||||
|
||||
import type { CreateGroupParams } from 'pl-api';
|
||||
import type { PlfeResponse } from 'soapbox/api';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -22,7 +22,6 @@ const messages = defineMessages({
|
|||
enum Steps {
|
||||
ONE = 'ONE',
|
||||
TWO = 'TWO',
|
||||
THREE = 'THREE',
|
||||
}
|
||||
|
||||
interface ICreateGroupModal {
|
||||
|
@ -34,7 +33,7 @@ const CreateGroupModal: React.FC<ICreateGroupModal> = ({ onClose }) => {
|
|||
|
||||
const [group, setGroup] = useState<Group | null>(null);
|
||||
const [params, setParams] = useState<CreateGroupParams>({
|
||||
group_visibility: 'everyone',
|
||||
display_name: '',
|
||||
});
|
||||
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
|
||||
|
||||
|
@ -46,24 +45,19 @@ const CreateGroupModal: React.FC<ICreateGroupModal> = ({ onClose }) => {
|
|||
|
||||
const confirmationText = useMemo(() => {
|
||||
switch (currentStep) {
|
||||
case Steps.THREE:
|
||||
return intl.formatMessage(messages.done);
|
||||
case Steps.TWO:
|
||||
return intl.formatMessage(messages.create);
|
||||
return intl.formatMessage(messages.done);
|
||||
default:
|
||||
return intl.formatMessage(messages.next);
|
||||
return intl.formatMessage(messages.create);
|
||||
}
|
||||
}, [currentStep]);
|
||||
|
||||
const handleNextStep = () => {
|
||||
switch (currentStep) {
|
||||
case Steps.ONE:
|
||||
setCurrentStep(Steps.TWO);
|
||||
break;
|
||||
case Steps.TWO:
|
||||
createGroup(params, {
|
||||
onSuccess(group) {
|
||||
setCurrentStep(Steps.THREE);
|
||||
setCurrentStep(Steps.TWO);
|
||||
setGroup(group);
|
||||
},
|
||||
onError(error: { response?: PlfeResponse }) {
|
||||
|
@ -74,7 +68,7 @@ const CreateGroupModal: React.FC<ICreateGroupModal> = ({ onClose }) => {
|
|||
},
|
||||
});
|
||||
break;
|
||||
case Steps.THREE:
|
||||
case Steps.TWO:
|
||||
handleClose();
|
||||
break;
|
||||
default:
|
||||
|
@ -85,26 +79,13 @@ const CreateGroupModal: React.FC<ICreateGroupModal> = ({ onClose }) => {
|
|||
const renderStep = () => {
|
||||
switch (currentStep) {
|
||||
case Steps.ONE:
|
||||
return <PrivacyStep params={params} onChange={setParams} />;
|
||||
case Steps.TWO:
|
||||
return <DetailsStep params={params} onChange={setParams} />;
|
||||
case Steps.THREE:
|
||||
case Steps.TWO:
|
||||
return <ConfirmationStep group={group} />;
|
||||
}
|
||||
};
|
||||
|
||||
const renderModalTitle = () => {
|
||||
switch (currentStep) {
|
||||
case Steps.ONE:
|
||||
return <FormattedMessage id='navigation_bar.create_group' defaultMessage='Create group' />;
|
||||
default:
|
||||
if (params.group_visibility === 'everyone') {
|
||||
return <FormattedMessage id='navigation_bar.create_group.public' defaultMessage='Create public group' />;
|
||||
} else {
|
||||
return <FormattedMessage id='navigation_bar.create_group.private' defaultMessage='Create private group' />;
|
||||
}
|
||||
}
|
||||
};
|
||||
const renderModalTitle = () => <FormattedMessage id='navigation_bar.create_group' defaultMessage='Create group' />;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { CreateGroupParams } from 'soapbox/api/hooks';
|
||||
import { Form, FormGroup, Input, Textarea } from 'soapbox/components/ui';
|
||||
import AvatarPicker from 'soapbox/features/edit-profile/components/avatar-picker';
|
||||
import HeaderPicker from 'soapbox/features/edit-profile/components/header-picker';
|
||||
|
@ -9,6 +8,8 @@ import { useAppSelector, useInstance } from 'soapbox/hooks';
|
|||
import { usePreview } from 'soapbox/hooks/forms';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import type { CreateGroupParams } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
|
||||
groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
|
||||
|
@ -17,7 +18,7 @@ const messages = defineMessages({
|
|||
|
||||
interface IDetailsStep {
|
||||
params: CreateGroupParams;
|
||||
onChange(params: CreateGroupParams): void;
|
||||
onChange: (params: CreateGroupParams) => void;
|
||||
}
|
||||
|
||||
const DetailsStep: React.FC<IDetailsStep> = ({ params, onChange }) => {
|
||||
|
@ -52,7 +53,10 @@ const DetailsStep: React.FC<IDetailsStep> = ({ params, onChange }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleImageClear = (property: keyof CreateGroupParams) => () => onChange({ [property]: undefined });
|
||||
const handleImageClear = (property: keyof CreateGroupParams) => () => onChange({
|
||||
...params,
|
||||
[property]: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form>
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { type CreateGroupParams } from 'soapbox/api/hooks';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { Form, FormGroup, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IPrivacyStep {
|
||||
params: CreateGroupParams;
|
||||
onChange(params: CreateGroupParams): void;
|
||||
}
|
||||
|
||||
const PrivacyStep: React.FC<IPrivacyStep> = ({ params, onChange }) => {
|
||||
const visibility = params.group_visibility || 'everyone';
|
||||
|
||||
const onChangePrivacy = (group_visibility: CreateGroupParams['group_visibility']) => {
|
||||
onChange({ ...params, group_visibility });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack className='mx-auto max-w-xs py-10' space={2}>
|
||||
<Text size='3xl' weight='bold' align='center'>
|
||||
<FormattedMessage id='manage_group.get_started' defaultMessage='Let’s get started!' />
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage id='manage_group.tagline' defaultMessage='Groups connect you with others based on shared interests.' />
|
||||
</Text>
|
||||
</Stack>
|
||||
<Form>
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='manage_group.privacy.label' defaultMessage='Privacy settings' />}
|
||||
>
|
||||
<List>
|
||||
<ListItem
|
||||
label={<Text weight='medium'><FormattedMessage id='manage_group.privacy.public.label' defaultMessage='Public' /></Text>}
|
||||
hint={<FormattedMessage id='manage_group.privacy.public.hint' defaultMessage='Discoverable. Anyone can join.' />}
|
||||
onSelect={() => onChangePrivacy('everyone')}
|
||||
isSelected={visibility === 'everyone'}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
label={<Text weight='medium'><FormattedMessage id='manage_group.privacy.private.label' defaultMessage='Private (Owner approval required)' /></Text>}
|
||||
hint={<FormattedMessage id='manage_group.privacy.private.hint' defaultMessage='Discoverable. Users can join after their request is approved.' />}
|
||||
onSelect={() => onChangePrivacy('members_only')}
|
||||
isSelected={visibility === 'members_only'}
|
||||
/>
|
||||
</List>
|
||||
</FormGroup>
|
||||
<Text size='sm' theme='muted' align='center'>
|
||||
<FormattedMessage id='manage_group.privacy.hint' defaultMessage='These settings cannot be changed later.' />
|
||||
</Text>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { PrivacyStep as default };
|
|
@ -1,17 +0,0 @@
|
|||
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
import type { Account, EmbeddedEntity } from 'soapbox/types/entities';
|
||||
|
||||
const ChatRecord = ImmutableRecord({
|
||||
account: null as EmbeddedEntity<Account>,
|
||||
id: '',
|
||||
unread: 0,
|
||||
last_message: '' as string || null,
|
||||
updated_at: '',
|
||||
});
|
||||
|
||||
const normalizeChat = (chat: Record<string, any>) => ChatRecord(
|
||||
ImmutableMap(fromJS(chat)),
|
||||
);
|
||||
|
||||
export { ChatRecord, normalizeChat };
|
|
@ -2,7 +2,6 @@ export { AccountRecord, FieldRecord, normalizeAccount } from './account';
|
|||
export { AdminAccountRecord, normalizeAdminAccount } from './admin-account';
|
||||
export { AdminReportRecord, normalizeAdminReport } from './admin-report';
|
||||
export { AttachmentRecord, normalizeAttachment } from './attachment';
|
||||
export { ChatRecord, normalizeChat } from './chat';
|
||||
export { ChatMessageRecord, normalizeChatMessage } from './chat-message';
|
||||
export { EmojiRecord, normalizeEmoji } from './emoji';
|
||||
export { FilterRecord, normalizeFilter } from './filter';
|
||||
|
|
|
@ -7,42 +7,36 @@ import { useStatContext } from 'soapbox/contexts/stat-context';
|
|||
import { useAppDispatch, useAppSelector, useClient, useFeatures, useLoggedIn, useOwnAccount } from 'soapbox/hooks';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
import { reOrderChatListItems } from 'soapbox/utils/chats';
|
||||
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
|
||||
import { flattenPages, updatePageItem } from 'soapbox/utils/queries';
|
||||
|
||||
import { queryClient } from './client';
|
||||
import { useFetchRelationships } from './relationships';
|
||||
|
||||
import type { Chat, ChatMessage, PaginatedResponse } from 'pl-api';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { Chat, ChatMessage as BaseChatMessage, PaginatedResponse } from 'pl-api';
|
||||
|
||||
interface IChat {
|
||||
account: Account;
|
||||
created_at: string;
|
||||
id: string;
|
||||
last_message: null | {
|
||||
account_id: string;
|
||||
chat_id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
id: string;
|
||||
unread: boolean;
|
||||
};
|
||||
unread: number;
|
||||
}
|
||||
const transformChatMessage = (chatMessage: BaseChatMessage) => ({
|
||||
...chatMessage,
|
||||
pending: false as boolean,
|
||||
});
|
||||
|
||||
type ChatMessage = ReturnType<typeof transformChatMessage>;
|
||||
|
||||
const ChatKeys = {
|
||||
chat: (chatId?: string) => ['chats', 'chat', chatId] as const,
|
||||
chatMessages: (chatId: string) => ['chats', 'messages', chatId] as const,
|
||||
};
|
||||
|
||||
const useChatMessages = (chat: IChat) => {
|
||||
const useChatMessages = (chat: Chat) => {
|
||||
const client = useClient();
|
||||
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat.account.id, 'blocked_by']));
|
||||
|
||||
const getChatMessages = async (chatId: string, pageParam?: Pick<PaginatedResponse<ChatMessage>, 'next'>): Promise<PaginatedResponse<ChatMessage>> => {
|
||||
const getChatMessages = async (chatId: string, pageParam?: Pick<PaginatedResponse<BaseChatMessage>, 'next'>) => {
|
||||
const response = await (pageParam?.next ? pageParam.next() : client.chats.getChatMessages(chatId));
|
||||
|
||||
return response;
|
||||
return {
|
||||
...response,
|
||||
items: response.items.map(transformChatMessage),
|
||||
};
|
||||
};
|
||||
|
||||
const queryInfo = useInfiniteQuery({
|
||||
|
@ -51,11 +45,11 @@ const useChatMessages = (chat: IChat) => {
|
|||
enabled: !isBlocked,
|
||||
gcTime: 0,
|
||||
staleTime: 0,
|
||||
initialPageParam: { next: null as (() => Promise<PaginatedResponse<ChatMessage>>) | null },
|
||||
initialPageParam: { next: null as (() => Promise<PaginatedResponse<BaseChatMessage>>) | null },
|
||||
getNextPageParam: (config) => config,
|
||||
});
|
||||
|
||||
const data = flattenPages(queryInfo.data)?.reverse();
|
||||
const data = flattenPages<ChatMessage>(queryInfo.data as any)?.reverse();
|
||||
|
||||
return {
|
||||
...queryInfo,
|
||||
|
@ -142,7 +136,7 @@ const useChatActions = (chatId: string) => {
|
|||
client.chats.markChatAsRead(chatId, lastReadId)
|
||||
.then((data) => {
|
||||
updatePageItem(['chats', 'search'], data, (o, n) => o.id === n.id);
|
||||
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResult<unknown>>>(['chats', 'search']);
|
||||
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResponse<unknown>>>(['chats', 'search']);
|
||||
|
||||
if (queryData) {
|
||||
const flattenedQueryData: any = flattenPages(queryData)?.map((chat: any) => {
|
||||
|
@ -152,7 +146,7 @@ const useChatActions = (chatId: string) => {
|
|||
return chat;
|
||||
}
|
||||
});
|
||||
setUnreadChatsCount(sumBy(flattenedQueryData, (chat: IChat) => chat.unread));
|
||||
setUnreadChatsCount(sumBy(flattenedQueryData, (chat: Chat) => chat.unread));
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -238,4 +232,4 @@ const useChatActions = (chatId: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
export { type IChat, ChatKeys, useChat, useChatActions, useChats, useChatMessages };
|
||||
export { ChatKeys, useChat, useChatActions, useChats, useChatMessages, type ChatMessage };
|
||||
|
|
|
@ -23,7 +23,7 @@ const useAccountSearch = (q: string) => {
|
|||
placeholderData: keepPreviousData,
|
||||
initialPageParam: {},
|
||||
getNextPageParam: () => {
|
||||
if (queryInfo.data[queryInfo.data.length - 1].length !== 10) {
|
||||
if (queryInfo.data?.pages[queryInfo.data.pages.length - 1].length !== 10) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ const PollRecord = ImmutableRecord({
|
|||
options_map: ImmutableList<ImmutableMap<Language, string>>([ImmutableMap(), ImmutableMap()]),
|
||||
expires_in: 24 * 3600,
|
||||
multiple: false,
|
||||
hide_totals: false,
|
||||
});
|
||||
|
||||
const ReducerCompose = ImmutableRecord({
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
SUGGESTIONS_DISMISS,
|
||||
} from 'soapbox/actions/suggestions';
|
||||
|
||||
import type { Suggestion as SuggestionEntity } from 'pl-api';
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const SuggestionRecord = ImmutableRecord({
|
||||
|
@ -24,7 +25,7 @@ const ReducerRecord = ImmutableRecord({
|
|||
type State = ReturnType<typeof ReducerRecord>;
|
||||
type Suggestion = ReturnType<typeof SuggestionRecord>;
|
||||
|
||||
const importSuggestions = (state: State, suggestions: Suggestion[]) =>
|
||||
const importSuggestions = (state: State, suggestions: SuggestionEntity[]) =>
|
||||
state.withMutations(state => {
|
||||
state.update('items', items => items.concat(suggestions.map(x => ({ ...x, account: x.account.id })).map(suggestion => SuggestionRecord(suggestion))));
|
||||
state.set('isLoading', false);
|
||||
|
|
|
@ -5,7 +5,7 @@ import z from 'zod';
|
|||
* https://docs.joinmastodon.org/entities/CustomEmoji/
|
||||
*/
|
||||
const customEmojiSchema = z.object({
|
||||
category: z.string().catch(''),
|
||||
category: z.string().nullable().catch(null),
|
||||
shortcode: z.string(),
|
||||
static_url: z.string().catch(''),
|
||||
url: z.string(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import z from 'zod';
|
||||
|
||||
import type { CustomEmoji } from './custom-emoji';
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
|
||||
/** Ensure HTML content is a string, and drop empty `<p>` tags. */
|
||||
const contentSchema = z.string().catch('').transform((value) => value === '<p></p>' ? '' : value);
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
AdminAccountRecord,
|
||||
AdminReportRecord,
|
||||
AttachmentRecord,
|
||||
ChatRecord,
|
||||
ChatMessageRecord,
|
||||
EmojiRecord,
|
||||
FieldRecord,
|
||||
|
@ -26,7 +25,6 @@ import type { LegacyMap } from 'soapbox/utils/legacy';
|
|||
type AdminAccount = ReturnType<typeof AdminAccountRecord>;
|
||||
type AdminReport = ReturnType<typeof AdminReportRecord>;
|
||||
type Attachment = ReturnType<typeof AttachmentRecord>;
|
||||
type Chat = ReturnType<typeof ChatRecord>;
|
||||
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
||||
type Emoji = ReturnType<typeof EmojiRecord>;
|
||||
type Field = ReturnType<typeof FieldRecord>;
|
||||
|
@ -58,7 +56,6 @@ export {
|
|||
AdminAccount,
|
||||
AdminReport,
|
||||
Attachment,
|
||||
Chat,
|
||||
ChatMessage,
|
||||
Emoji,
|
||||
Field,
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { InfiniteData } from '@tanstack/react-query';
|
||||
import sumBy from 'lodash/sumBy';
|
||||
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
import { ChatKeys } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { Chat, ChatMessage } from 'soapbox/types/entities';
|
||||
|
||||
import { compareDate } from './comparators';
|
||||
import { appendPageItem, flattenPages, PaginatedResult, sortQueryData, updatePageItem } from './queries';
|
||||
import { appendPageItem, flattenPages, sortQueryData, updatePageItem } from './queries';
|
||||
|
||||
import type { InfiniteData } from '@tanstack/react-query';
|
||||
import type { Chat, PaginatedResponse } from 'pl-api';
|
||||
import type { ChatMessage } from 'soapbox/types/entities';
|
||||
|
||||
interface ChatPayload extends Omit<Chat, 'last_message'> {
|
||||
last_message: ChatMessage | null;
|
||||
|
@ -38,7 +40,7 @@ const reOrderChatListItems = () => {
|
|||
*/
|
||||
const checkIfChatExists = (chatId: string) => {
|
||||
const currentChats = flattenPages(
|
||||
queryClient.getQueryData<InfiniteData<PaginatedResult<Chat>>>(['chats', 'search']),
|
||||
queryClient.getQueryData<InfiniteData<PaginatedResponse<Chat>>>(['chats', 'search']),
|
||||
);
|
||||
|
||||
return currentChats?.find((chat: Chat) => chat.id === chatId);
|
||||
|
@ -78,17 +80,10 @@ const updateChatListItem = (newChat: ChatPayload) => {
|
|||
/** Get unread chats count. */
|
||||
const getUnreadChatsCount = (): number => {
|
||||
const chats = flattenPages(
|
||||
queryClient.getQueryData<InfiniteData<PaginatedResult<Chat>>>(['chats', 'search']),
|
||||
queryClient.getQueryData<InfiniteData<PaginatedResponse<Chat>>>(['chats', 'search']),
|
||||
);
|
||||
|
||||
return sumBy(chats, chat => chat.unread);
|
||||
};
|
||||
|
||||
/** Update the query cache for an individual Chat Message */
|
||||
const updateChatMessage = (chatMessage: ChatMessage) => updatePageItem(
|
||||
ChatKeys.chatMessages(chatMessage.chat_id),
|
||||
normalizeChatMessage(chatMessage),
|
||||
(o, n) => o.id === n.id,
|
||||
);
|
||||
|
||||
export { updateChatListItem, updateChatMessage, getUnreadChatsCount, reOrderChatListItems };
|
||||
export { updateChatListItem, getUnreadChatsCount, reOrderChatListItems };
|
|
@ -3,12 +3,6 @@ import { queryClient } from 'soapbox/queries/client';
|
|||
import type { InfiniteData, QueryKey } from '@tanstack/react-query';
|
||||
import type { PaginatedResponse } from 'pl-api';
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
result: T[];
|
||||
hasMore: boolean;
|
||||
next?: () => Promise<PaginatedResponse<any>> | null;
|
||||
}
|
||||
|
||||
interface Entity {
|
||||
id: string;
|
||||
}
|
||||
|
@ -24,7 +18,7 @@ const deduplicateById = <T extends Entity>(entities: T[]): T[] => {
|
|||
};
|
||||
|
||||
/** Flatten paginated results into a single array. */
|
||||
const flattenPages = <T>(queryData: InfiniteData<PaginatedResult<T> | PaginatedResponse<T>> | undefined) => {
|
||||
const flattenPages = <T>(queryData: InfiniteData<PaginatedResponse<T>> | undefined) => {
|
||||
const data = queryData?.pages.reduce<T[]>(
|
||||
(prev: T[], curr) => [...prev, ...((curr as any).result || (curr as any).items)],
|
||||
[],
|
||||
|
@ -39,10 +33,10 @@ const flattenPages = <T>(queryData: InfiniteData<PaginatedResult<T> | PaginatedR
|
|||
|
||||
/** Traverse pages and update the item inside if found. */
|
||||
const updatePageItem = <T>(queryKey: QueryKey, newItem: T, isItem: (item: T, newItem: T) => boolean) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResult<T>>>({ queryKey }, (data) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResponse<T>>>({ queryKey }, (data) => {
|
||||
if (data) {
|
||||
const pages = data.pages.map(page => {
|
||||
const result = page.result.map(item => isItem(item, newItem) ? newItem : item);
|
||||
const result = page.items.map(item => isItem(item, newItem) ? newItem : item);
|
||||
return { ...page, result };
|
||||
});
|
||||
return { ...data, pages };
|
||||
|
@ -52,10 +46,10 @@ const updatePageItem = <T>(queryKey: QueryKey, newItem: T, isItem: (item: T, new
|
|||
|
||||
/** Insert the new item at the beginning of the first page. */
|
||||
const appendPageItem = <T>(queryKey: QueryKey, newItem: T) => {
|
||||
queryClient.setQueryData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||
queryClient.setQueryData<InfiniteData<PaginatedResponse<T>>>(queryKey, (data) => {
|
||||
if (data) {
|
||||
const pages = [...data.pages];
|
||||
pages[0] = { ...pages[0], result: [newItem, ...pages[0].result] };
|
||||
pages[0] = { ...pages[0], items: [newItem, ...pages[0].items] };
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
|
@ -63,11 +57,11 @@ const appendPageItem = <T>(queryKey: QueryKey, newItem: T) => {
|
|||
|
||||
/** Remove an item inside if found. */
|
||||
const removePageItem = <T>(queryKey: QueryKey, itemToRemove: T, isItem: (item: T, newItem: T) => boolean) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResult<T>>>({ queryKey }, (data) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResponse<T>>>({ queryKey }, (data) => {
|
||||
if (data) {
|
||||
const pages = data.pages.map(page => {
|
||||
const result = page.result.filter(item => !isItem(item, itemToRemove));
|
||||
return { ...page, result };
|
||||
const items = page.items.filter(item => !isItem(item, itemToRemove));
|
||||
return { ...page, items };
|
||||
});
|
||||
return { ...data, pages };
|
||||
}
|
||||
|
@ -88,7 +82,7 @@ const paginateQueryData = <T>(array: T[] | undefined) =>
|
|||
}, []);
|
||||
|
||||
const sortQueryData = <T>(queryKey: QueryKey, comparator: (a: T, b: T) => number) => {
|
||||
queryClient.setQueryData<InfiniteData<PaginatedResult<T>>>(queryKey, (prevResult) => {
|
||||
queryClient.setQueryData<InfiniteData<PaginatedResponse<T>>>(queryKey, (prevResult) => {
|
||||
if (prevResult) {
|
||||
const nextResult = { ...prevResult };
|
||||
const flattenedQueryData = flattenPages(nextResult);
|
||||
|
@ -106,7 +100,6 @@ const sortQueryData = <T>(queryKey: QueryKey, comparator: (a: T, b: T) => number
|
|||
};
|
||||
|
||||
export {
|
||||
type PaginatedResult,
|
||||
flattenPages,
|
||||
updatePageItem,
|
||||
appendPageItem,
|
||||
|
|
|
@ -6475,11 +6475,16 @@ immer@^10.0.3:
|
|||
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9"
|
||||
integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==
|
||||
|
||||
immutable@^4.0.0, immutable@^4.2.1:
|
||||
immutable@^4.0.0:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
|
||||
integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
|
||||
|
||||
immutable@^4.3.7:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||
|
||||
import-fresh@^3.2.1, import-fresh@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
|
|
Loading…
Reference in a new issue