Merge remote-tracking branch 'soapbox/develop' into lexical
This commit is contained in:
commit
3d433c6a75
26 changed files with 189 additions and 55 deletions
|
@ -5,6 +5,7 @@ import { shouldHaveCard } from 'soapbox/utils/status';
|
||||||
import api, { getNextLink } from '../api';
|
import api, { getNextLink } from '../api';
|
||||||
|
|
||||||
import { setComposeToStatus } from './compose';
|
import { setComposeToStatus } from './compose';
|
||||||
|
import { fetchGroupRelationships } from './groups';
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import { openModal } from './modals';
|
import { openModal } from './modals';
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
|
@ -124,6 +125,9 @@ const fetchStatus = (id: string) => {
|
||||||
|
|
||||||
return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
|
return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
|
||||||
dispatch(importFetchedStatus(status));
|
dispatch(importFetchedStatus(status));
|
||||||
|
if (status.group) {
|
||||||
|
dispatch(fetchGroupRelationships([status.group.id]));
|
||||||
|
}
|
||||||
dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
|
dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
|
||||||
return status;
|
return status;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getSettings } from 'soapbox/actions/settings';
|
||||||
import { normalizeStatus } from 'soapbox/normalizers';
|
import { normalizeStatus } from 'soapbox/normalizers';
|
||||||
import { shouldFilter } from 'soapbox/utils/timelines';
|
import { shouldFilter } from 'soapbox/utils/timelines';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getNextLink, getPrevLink } from '../api';
|
||||||
|
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ const parseTags = (tags: Record<string, any[]> = {}, mode: 'any' | 'all' | 'none
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceHomeTimeline = (
|
const replaceHomeTimeline = (
|
||||||
accountId: string | null,
|
accountId: string | undefined,
|
||||||
{ maxId }: Record<string, any> = {},
|
{ maxId }: Record<string, any> = {},
|
||||||
done?: () => void,
|
done?: () => void,
|
||||||
) => (dispatch: AppDispatch, _getState: () => RootState) => {
|
) => (dispatch: AppDispatch, _getState: () => RootState) => {
|
||||||
|
@ -162,7 +162,12 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
||||||
return dispatch(noOpAsync());
|
return dispatch(noOpAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.max_id && !params.pinned && (timeline.items || ImmutableOrderedSet()).size > 0) {
|
if (
|
||||||
|
!params.max_id &&
|
||||||
|
!params.pinned &&
|
||||||
|
(timeline.items || ImmutableOrderedSet()).size > 0 &&
|
||||||
|
!path.includes('max_id=')
|
||||||
|
) {
|
||||||
params.since_id = timeline.getIn(['items', 0]);
|
params.since_id = timeline.getIn(['items', 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,9 +176,16 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
||||||
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
||||||
|
|
||||||
return api(getState).get(path, { params }).then(response => {
|
return api(getState).get(path, { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore));
|
dispatch(expandTimelineSuccess(
|
||||||
|
timelineId,
|
||||||
|
response.data,
|
||||||
|
getNextLink(response),
|
||||||
|
getPrevLink(response),
|
||||||
|
response.status === 206,
|
||||||
|
isLoadingRecent,
|
||||||
|
isLoadingMore,
|
||||||
|
));
|
||||||
done();
|
done();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||||
|
@ -181,9 +193,26 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const expandHomeTimeline = ({ accountId, maxId }: Record<string, any> = {}, done = noOp) => {
|
interface ExpandHomeTimelineOpts {
|
||||||
const endpoint = accountId ? `/api/v1/accounts/${accountId}/statuses` : '/api/v1/timelines/home';
|
accountId?: string
|
||||||
const params: any = { max_id: maxId };
|
maxId?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HomeTimelineParams {
|
||||||
|
max_id?: string
|
||||||
|
exclude_replies?: boolean
|
||||||
|
with_muted?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandHomeTimeline = ({ url, accountId, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => {
|
||||||
|
const endpoint = url || (accountId ? `/api/v1/accounts/${accountId}/statuses` : '/api/v1/timelines/home');
|
||||||
|
const params: HomeTimelineParams = {};
|
||||||
|
|
||||||
|
if (!url && maxId) {
|
||||||
|
params.max_id = maxId;
|
||||||
|
}
|
||||||
|
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
params.exclude_replies = true;
|
params.exclude_replies = true;
|
||||||
params.with_muted = true;
|
params.with_muted = true;
|
||||||
|
@ -237,11 +266,20 @@ const expandTimelineRequest = (timeline: string, isLoadingMore: boolean) => ({
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expandTimelineSuccess = (timeline: string, statuses: APIEntity[], next: string | null, partial: boolean, isLoadingRecent: boolean, isLoadingMore: boolean) => ({
|
const expandTimelineSuccess = (
|
||||||
|
timeline: string,
|
||||||
|
statuses: APIEntity[],
|
||||||
|
next: string | undefined,
|
||||||
|
prev: string | undefined,
|
||||||
|
partial: boolean,
|
||||||
|
isLoadingRecent: boolean,
|
||||||
|
isLoadingMore: boolean,
|
||||||
|
) => ({
|
||||||
type: TIMELINE_EXPAND_SUCCESS,
|
type: TIMELINE_EXPAND_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
statuses,
|
statuses,
|
||||||
next,
|
next,
|
||||||
|
prev,
|
||||||
partial,
|
partial,
|
||||||
isLoadingRecent,
|
isLoadingRecent,
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
|
|
|
@ -181,7 +181,9 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSiblings = () => {
|
const getSiblings = () => {
|
||||||
return Array(...(ref.current!.parentElement!.childNodes as any as ChildNode[])).filter(node => node !== ref.current);
|
return Array(...(ref.current!.parentElement!.childNodes as any as ChildNode[]))
|
||||||
|
.filter(node => (node as HTMLDivElement).id !== 'toaster')
|
||||||
|
.filter(node => node !== ref.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -536,7 +536,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
return menu;
|
return menu;
|
||||||
};
|
};
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.visibility);
|
const publicStatus = ['public', 'unlisted', 'group'].includes(status.visibility);
|
||||||
|
|
||||||
const replyCount = status.replies_count;
|
const replyCount = status.replies_count;
|
||||||
const reblogCount = status.reblogs_count;
|
const reblogCount = status.reblogs_count;
|
||||||
|
@ -609,7 +609,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
replyTitle = intl.formatMessage(messages.replyAll);
|
replyTitle = intl.formatMessage(messages.replyAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canShare = ('share' in navigator) && status.visibility === 'public';
|
const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack data-testid='status-action-bar'>
|
<HStack data-testid='status-action-bar'>
|
||||||
|
|
|
@ -53,7 +53,7 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
|
||||||
src={icon}
|
src={icon}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
{
|
{
|
||||||
'fill-accent-300 hover:fill-accent-300': active && filled && color === COLORS.accent,
|
'fill-accent-300 text-accent-300 hover:fill-accent-300': active && filled && color === COLORS.accent,
|
||||||
},
|
},
|
||||||
iconClassName,
|
iconClassName,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -16,6 +16,7 @@ const messages = defineMessages({
|
||||||
export type StreamfieldComponent<T> = React.ComponentType<{
|
export type StreamfieldComponent<T> = React.ComponentType<{
|
||||||
value: T
|
value: T
|
||||||
onChange: (value: T) => void
|
onChange: (value: T) => void
|
||||||
|
autoFocus: boolean
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
interface IStreamfield {
|
interface IStreamfield {
|
||||||
|
@ -72,7 +73,12 @@ const Streamfield: React.FC<IStreamfield> = ({
|
||||||
<Stack space={1}>
|
<Stack space={1}>
|
||||||
{values.map((value, i) => value?._destroy ? null : (
|
{values.map((value, i) => value?._destroy ? null : (
|
||||||
<HStack space={2} alignItems='center'>
|
<HStack space={2} alignItems='center'>
|
||||||
<Component key={i} onChange={handleChange(i)} value={value} />
|
<Component
|
||||||
|
key={i}
|
||||||
|
onChange={handleChange(i)}
|
||||||
|
value={value}
|
||||||
|
autoFocus={i > 0}
|
||||||
|
/>
|
||||||
{values.length > minItems && onRemoveItem && (
|
{values.length > minItems && onRemoveItem && (
|
||||||
<IconButton
|
<IconButton
|
||||||
iconClassName='h-4 w-4'
|
iconClassName='h-4 w-4'
|
||||||
|
|
|
@ -190,7 +190,14 @@ const SoapboxMount = () => {
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
|
|
||||||
<GdprBanner />
|
<GdprBanner />
|
||||||
<Toaster position='top-right' containerClassName='top-10' containerStyle={{ top: 75 }} />
|
|
||||||
|
<div id='toaster'>
|
||||||
|
<Toaster
|
||||||
|
position='top-right'
|
||||||
|
containerClassName='top-10'
|
||||||
|
containerStyle={{ top: 75 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ScrollContext>
|
</ScrollContext>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useAppDispatch, useLoading } from 'soapbox/hooks';
|
import { useAppDispatch, useLoading } from 'soapbox/hooks';
|
||||||
|
@ -23,7 +24,7 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
|
||||||
const [isSubmitting, setPromise] = useLoading();
|
const [isSubmitting, setPromise] = useLoading();
|
||||||
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
||||||
|
|
||||||
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity> = {}): Promise<void> {
|
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity, AxiosError> = {}): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await setPromise(entityFn(data));
|
const result = await setPromise(entityFn(data));
|
||||||
const schema = opts.schema || z.custom<TEntity>();
|
const schema = opts.schema || z.custom<TEntity>();
|
||||||
|
@ -36,8 +37,12 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
|
||||||
callbacks.onSuccess(entity);
|
callbacks.onSuccess(entity);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (callbacks.onError) {
|
if (error instanceof AxiosError) {
|
||||||
callbacks.onError(error);
|
if (callbacks.onError) {
|
||||||
|
callbacks.onError(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,18 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Form, FormActions, FormGroup, Input, Stack } from 'soapbox/components/ui';
|
import { Button, Form, FormActions, FormGroup, Input, Stack } from 'soapbox/components/ui';
|
||||||
|
import { useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
import ConsumersList from './consumers-list';
|
import ConsumersList from './consumers-list';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
username: {
|
username: {
|
||||||
id: 'login.fields.username_label',
|
id: 'login.fields.username_label',
|
||||||
defaultMessage: 'Email or username',
|
defaultMessage: 'E-mail or username',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
id: 'login.fields.email_label',
|
||||||
|
defaultMessage: 'E-mail address',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
id: 'login.fields.password_placeholder',
|
id: 'login.fields.password_placeholder',
|
||||||
|
@ -24,6 +29,10 @@ interface ILoginForm {
|
||||||
|
|
||||||
const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const usernameLabel = intl.formatMessage(features.logInWithUsername ? messages.username : messages.email);
|
||||||
|
const passwordLabel = intl.formatMessage(messages.password);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -33,10 +42,10 @@ const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
||||||
|
|
||||||
<Stack className='mx-auto sm:w-2/3 sm:pt-10 md:w-1/2' space={5}>
|
<Stack className='mx-auto sm:w-2/3 sm:pt-10 md:w-1/2' space={5}>
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormGroup labelText={intl.formatMessage(messages.username)}>
|
<FormGroup labelText={usernameLabel}>
|
||||||
<Input
|
<Input
|
||||||
aria-label={intl.formatMessage(messages.username)}
|
aria-label={usernameLabel}
|
||||||
placeholder={intl.formatMessage(messages.username)}
|
placeholder={usernameLabel}
|
||||||
type='text'
|
type='text'
|
||||||
name='username'
|
name='username'
|
||||||
autoCorrect='off'
|
autoCorrect='off'
|
||||||
|
@ -46,7 +55,7 @@ const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
labelText={intl.formatMessage(messages.password)}
|
labelText={passwordLabel}
|
||||||
hintText={
|
hintText={
|
||||||
<Link to='/reset-password' className='hover:underline'>
|
<Link to='/reset-password' className='hover:underline'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -57,8 +66,8 @@ const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
aria-label={intl.formatMessage(messages.password)}
|
aria-label={passwordLabel}
|
||||||
placeholder={intl.formatMessage(messages.password)}
|
placeholder={passwordLabel}
|
||||||
type='password'
|
type='password'
|
||||||
name='password'
|
name='password'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
|
|
|
@ -4,17 +4,19 @@ import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { resetPassword } from 'soapbox/actions/security';
|
import { resetPassword } from 'soapbox/actions/security';
|
||||||
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
|
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' },
|
nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'E-mail or username' },
|
||||||
|
email: { id: 'password_reset.fields.email_placeholder', defaultMessage: 'E-mail address' },
|
||||||
confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' },
|
confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const PasswordReset = () => {
|
const PasswordReset = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
@ -43,7 +45,7 @@ const PasswordReset = () => {
|
||||||
|
|
||||||
<div className='mx-auto sm:w-2/3 sm:pt-10 md:w-1/2'>
|
<div className='mx-auto sm:w-2/3 sm:pt-10 md:w-1/2'>
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormGroup labelText={intl.formatMessage(messages.nicknameOrEmail)}>
|
<FormGroup labelText={intl.formatMessage(features.logInWithUsername ? messages.nicknameOrEmail : messages.email)}>
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
name='nickname_or_email'
|
name='nickname_or_email'
|
||||||
|
|
|
@ -30,7 +30,7 @@ const CarouselItem = React.forwardRef((
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
dispatch(replaceHomeTimeline(null, { maxId: null }, () => setLoading(false)));
|
dispatch(replaceHomeTimeline(undefined, { maxId: null }, () => setLoading(false)));
|
||||||
|
|
||||||
if (onPinned) {
|
if (onPinned) {
|
||||||
onPinned(null);
|
onPinned(null);
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Input, Streamfield } from 'soapbox/components/ui';
|
import { Input, Streamfield } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
hashtagPlaceholder: { id: 'manage_group.fields.hashtag_placeholder', defaultMessage: 'Add a topic' },
|
hashtagPlaceholder: { id: 'manage_group.fields.hashtag_placeholder', defaultMessage: 'Add a topic' },
|
||||||
});
|
});
|
||||||
|
@ -30,12 +32,7 @@ const GroupTagsField: React.FC<IGroupTagsField> = ({ tags, onChange, onAddItem,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IHashtagField {
|
const HashtagField: StreamfieldComponent<string> = ({ value, onChange, autoFocus = false }) => {
|
||||||
value: string
|
|
||||||
onChange: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const HashtagField: React.FC<IHashtagField> = ({ value, onChange }) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
||||||
|
@ -49,6 +46,7 @@ const HashtagField: React.FC<IHashtagField> = ({ value, onChange }) => {
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={intl.formatMessage(messages.hashtagPlaceholder)}
|
placeholder={intl.formatMessage(messages.hashtagPlaceholder)}
|
||||||
|
autoFocus={autoFocus}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Button, Column, Form, FormActions, FormGroup, Icon, Input, Spinner, Tex
|
||||||
import { useAppSelector, useInstance } from 'soapbox/hooks';
|
import { useAppSelector, useInstance } from 'soapbox/hooks';
|
||||||
import { useGroup, useUpdateGroup } from 'soapbox/hooks/api';
|
import { useGroup, useUpdateGroup } from 'soapbox/hooks/api';
|
||||||
import { useImageField, useTextField } from 'soapbox/hooks/forms';
|
import { useImageField, useTextField } from 'soapbox/hooks/forms';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
|
import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
|
||||||
|
|
||||||
import AvatarPicker from './components/group-avatar-picker';
|
import AvatarPicker from './components/group-avatar-picker';
|
||||||
|
@ -20,7 +21,7 @@ const messages = defineMessages({
|
||||||
heading: { id: 'navigation_bar.edit_group', defaultMessage: 'Edit Group' },
|
heading: { id: 'navigation_bar.edit_group', defaultMessage: 'Edit Group' },
|
||||||
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
|
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
|
||||||
groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
|
groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
|
||||||
success: { id: 'manage_group.success', defaultMessage: 'Group saved!' },
|
groupSaved: { id: 'group.update.success', defaultMessage: 'Group successfully saved' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IEditGroup {
|
interface IEditGroup {
|
||||||
|
@ -61,6 +62,17 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { id: groupId } }) => {
|
||||||
avatar: avatar.file,
|
avatar: avatar.file,
|
||||||
header: header.file,
|
header: header.file,
|
||||||
tags,
|
tags,
|
||||||
|
}, {
|
||||||
|
onSuccess() {
|
||||||
|
toast.success(intl.formatMessage(messages.groupSaved));
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
const message = (error.response?.data as any)?.error;
|
||||||
|
|
||||||
|
if (error.response?.status === 422 && typeof message !== 'undefined') {
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
|
@ -27,9 +27,10 @@ const HomeTimeline: React.FC = () => {
|
||||||
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
|
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
|
||||||
const currentAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId as string | undefined);
|
const currentAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId as string | undefined);
|
||||||
const currentAccountRelationship = useAppSelector(state => currentAccountId ? state.relationships.get(currentAccountId) : null);
|
const currentAccountRelationship = useAppSelector(state => currentAccountId ? state.relationships.get(currentAccountId) : null);
|
||||||
|
const next = useAppSelector(state => state.timelines.get('home')?.next);
|
||||||
|
|
||||||
const handleLoadMore = (maxId: string) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
dispatch(expandHomeTimeline({ maxId, accountId: currentAccountId }));
|
dispatch(expandHomeTimeline({ url: next, maxId, accountId: currentAccountId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mastodon generates the feed in Redis, and can return a partial timeline
|
// Mastodon generates the feed in Redis, and can return a partial timeline
|
||||||
|
@ -52,7 +53,7 @@ const HomeTimeline: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
return dispatch(expandHomeTimeline({ maxId: null, accountId: currentAccountId }));
|
return dispatch(expandHomeTimeline({ accountId: currentAccountId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { fetchInstance } from 'soapbox/actions/instance';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import SiteLogo from 'soapbox/components/site-logo';
|
import SiteLogo from 'soapbox/components/site-logo';
|
||||||
import { Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
|
import { Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
|
||||||
import { useSoapboxConfig, useOwnAccount, useAppDispatch, useRegistrationStatus } from 'soapbox/hooks';
|
import { useSoapboxConfig, useOwnAccount, useAppDispatch, useRegistrationStatus, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
import Sonar from './sonar';
|
import Sonar from './sonar';
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ const messages = defineMessages({
|
||||||
home: { id: 'header.home.label', defaultMessage: 'Home' },
|
home: { id: 'header.home.label', defaultMessage: 'Home' },
|
||||||
login: { id: 'header.login.label', defaultMessage: 'Log in' },
|
login: { id: 'header.login.label', defaultMessage: 'Log in' },
|
||||||
register: { id: 'header.register.label', defaultMessage: 'Register' },
|
register: { id: 'header.register.label', defaultMessage: 'Register' },
|
||||||
username: { id: 'header.login.username.placeholder', defaultMessage: 'Email or username' },
|
username: { id: 'header.login.username.placeholder', defaultMessage: 'E-mail or username' },
|
||||||
|
email: { id: 'header.login.email.placeholder', defaultMessage: 'E-mail address' },
|
||||||
password: { id: 'header.login.password.label', defaultMessage: 'Password' },
|
password: { id: 'header.login.password.label', defaultMessage: 'Password' },
|
||||||
forgotPassword: { id: 'header.login.forgot_password', defaultMessage: 'Forgot password?' },
|
forgotPassword: { id: 'header.login.forgot_password', defaultMessage: 'Forgot password?' },
|
||||||
});
|
});
|
||||||
|
@ -26,6 +27,7 @@ const messages = defineMessages({
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
@ -123,7 +125,7 @@ const Header = () => {
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(event) => setUsername(event.target.value.trim())}
|
onChange={(event) => setUsername(event.target.value.trim())}
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.username)}
|
placeholder={intl.formatMessage(features.logInWithUsername ? messages.username : messages.email)}
|
||||||
className='max-w-[200px]'
|
className='max-w-[200px]'
|
||||||
autoCorrect='off'
|
autoCorrect='off'
|
||||||
autoCapitalize='off'
|
autoCapitalize='off'
|
||||||
|
|
|
@ -35,7 +35,7 @@ const TestTimeline: React.FC = () => {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch(importFetchedStatuses(MOCK_STATUSES));
|
dispatch(importFetchedStatuses(MOCK_STATUSES));
|
||||||
dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, null, false, false, false));
|
dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, undefined, undefined, false, false, false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -183,7 +183,7 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
||||||
<Icon src={ADDRESS_ICONS[location.type] || require('@tabler/icons/map-pin.svg')} />
|
<Icon src={ADDRESS_ICONS[location.type] || require('@tabler/icons/map-pin.svg')} />
|
||||||
<Stack className='grow'>
|
<Stack className='grow'>
|
||||||
<Text>{location.description}</Text>
|
<Text>{location.description}</Text>
|
||||||
<Text theme='muted' size='xs'>{[location.street, location.locality, location.country].filter(val => val.trim()).join(' · ')}</Text>
|
<Text theme='muted' size='xs'>{[location.street, location.locality, location.country].filter(val => val?.trim()).join(' · ')}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<IconButton title={intl.formatMessage(messages.resetLocation)} src={require('@tabler/icons/x.svg')} onClick={() => onChangeLocation(null)} />
|
<IconButton title={intl.formatMessage(messages.resetLocation)} src={require('@tabler/icons/x.svg')} onClick={() => onChangeLocation(null)} />
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { openSidebar } from 'soapbox/actions/sidebar';
|
||||||
import SiteLogo from 'soapbox/components/site-logo';
|
import SiteLogo from 'soapbox/components/site-logo';
|
||||||
import { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
|
import { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
|
||||||
import Search from 'soapbox/features/compose/components/search';
|
import Search from 'soapbox/features/compose/components/search';
|
||||||
import { useAppDispatch, useOwnAccount, useRegistrationStatus } from 'soapbox/hooks';
|
import { useAppDispatch, useFeatures, useOwnAccount, useRegistrationStatus } from 'soapbox/hooks';
|
||||||
|
|
||||||
import ProfileDropdown from './profile-dropdown';
|
import ProfileDropdown from './profile-dropdown';
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ import type { AxiosError } from 'axios';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
login: { id: 'navbar.login.action', defaultMessage: 'Log in' },
|
login: { id: 'navbar.login.action', defaultMessage: 'Log in' },
|
||||||
username: { id: 'navbar.login.username.placeholder', defaultMessage: 'Email or username' },
|
username: { id: 'navbar.login.username.placeholder', defaultMessage: 'E-mail or username' },
|
||||||
|
email: { id: 'navbar.login.email.placeholder', defaultMessage: 'E-mail address' },
|
||||||
password: { id: 'navbar.login.password.label', defaultMessage: 'Password' },
|
password: { id: 'navbar.login.password.label', defaultMessage: 'Password' },
|
||||||
forgotPassword: { id: 'navbar.login.forgot_password', defaultMessage: 'Forgot password?' },
|
forgotPassword: { id: 'navbar.login.forgot_password', defaultMessage: 'Forgot password?' },
|
||||||
});
|
});
|
||||||
|
@ -25,6 +26,7 @@ const messages = defineMessages({
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
const { isOpen } = useRegistrationStatus();
|
const { isOpen } = useRegistrationStatus();
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
const node = useRef(null);
|
const node = useRef(null);
|
||||||
|
@ -111,7 +113,7 @@ const Navbar = () => {
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(event) => setUsername(event.target.value)}
|
onChange={(event) => setUsername(event.target.value)}
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.username)}
|
placeholder={intl.formatMessage(features.logInWithUsername ? messages.username : messages.email)}
|
||||||
className='max-w-[200px]'
|
className='max-w-[200px]'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups';
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntities, useEntity } from 'soapbox/entity-store/hooks';
|
import { useEntities, useEntity } from 'soapbox/entity-store/hooks';
|
||||||
import { useApi } from 'soapbox/hooks';
|
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
||||||
import { groupSchema, Group } from 'soapbox/schemas/group';
|
import { groupSchema, Group } from 'soapbox/schemas/group';
|
||||||
import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship';
|
import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship';
|
||||||
|
|
||||||
|
@ -48,12 +50,24 @@ function useGroup(groupId: string, refetch = true) {
|
||||||
|
|
||||||
function useGroupRelationship(groupId: string) {
|
function useGroupRelationship(groupId: string) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return useEntity<GroupRelationship>(
|
const { entity: groupRelationship, ...result } = useEntity<GroupRelationship>(
|
||||||
[Entities.GROUP_RELATIONSHIPS, groupId],
|
[Entities.GROUP_RELATIONSHIPS, groupId],
|
||||||
() => api.get(`/api/v1/groups/relationships?id[]=${groupId}`),
|
() => api.get(`/api/v1/groups/relationships?id[]=${groupId}`),
|
||||||
{ schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) },
|
{ schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupRelationship?.id) {
|
||||||
|
dispatch(fetchGroupRelationshipsSuccess([groupRelationship]));
|
||||||
|
}
|
||||||
|
}, [groupRelationship?.id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity: groupRelationship,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGroupRelationships(groupIds: string[]) {
|
function useGroupRelationships(groupIds: string[]) {
|
||||||
|
|
|
@ -809,6 +809,7 @@
|
||||||
"group.tabs.members": "Members",
|
"group.tabs.members": "Members",
|
||||||
"group.tags.hint": "Add up to 3 keywords that will serve as core topics of discussion in the group.",
|
"group.tags.hint": "Add up to 3 keywords that will serve as core topics of discussion in the group.",
|
||||||
"group.tags.label": "Tags",
|
"group.tags.label": "Tags",
|
||||||
|
"group.update.success": "Group successfully saved",
|
||||||
"group.upload_banner": "Upload photo",
|
"group.upload_banner": "Upload photo",
|
||||||
"groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.",
|
"groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.",
|
||||||
"groups.discover.popular.show_more": "Show More",
|
"groups.discover.popular.show_more": "Show More",
|
||||||
|
@ -839,6 +840,7 @@
|
||||||
"hashtag.column_header.tag_mode.any": "or {additional}",
|
"hashtag.column_header.tag_mode.any": "or {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "without {additional}",
|
"hashtag.column_header.tag_mode.none": "without {additional}",
|
||||||
"header.home.label": "Home",
|
"header.home.label": "Home",
|
||||||
|
"header.login.email.placeholder": "E-mail address",
|
||||||
"header.login.forgot_password": "Forgot password?",
|
"header.login.forgot_password": "Forgot password?",
|
||||||
"header.login.label": "Log in",
|
"header.login.label": "Log in",
|
||||||
"header.login.password.label": "Password",
|
"header.login.password.label": "Password",
|
||||||
|
@ -923,6 +925,7 @@
|
||||||
"lists.subheading": "Your lists",
|
"lists.subheading": "Your lists",
|
||||||
"loading_indicator.label": "Loading…",
|
"loading_indicator.label": "Loading…",
|
||||||
"location_search.placeholder": "Find an address",
|
"location_search.placeholder": "Find an address",
|
||||||
|
"login.fields.email_label": "E-mail address",
|
||||||
"login.fields.instance_label": "Instance",
|
"login.fields.instance_label": "Instance",
|
||||||
"login.fields.instance_placeholder": "example.com",
|
"login.fields.instance_placeholder": "example.com",
|
||||||
"login.fields.otp_code_hint": "Enter the two-factor code generated by your phone app or use one of your recovery codes",
|
"login.fields.otp_code_hint": "Enter the two-factor code generated by your phone app or use one of your recovery codes",
|
||||||
|
@ -965,7 +968,6 @@
|
||||||
"manage_group.privacy.private.label": "Private (Owner approval required)",
|
"manage_group.privacy.private.label": "Private (Owner approval required)",
|
||||||
"manage_group.privacy.public.hint": "Discoverable. Anyone can join.",
|
"manage_group.privacy.public.hint": "Discoverable. Anyone can join.",
|
||||||
"manage_group.privacy.public.label": "Public",
|
"manage_group.privacy.public.label": "Public",
|
||||||
"manage_group.success": "Group saved!",
|
|
||||||
"manage_group.tagline": "Groups connect you with others based on shared interests.",
|
"manage_group.tagline": "Groups connect you with others based on shared interests.",
|
||||||
"media_panel.empty_message": "No media found.",
|
"media_panel.empty_message": "No media found.",
|
||||||
"media_panel.title": "Media",
|
"media_panel.title": "Media",
|
||||||
|
@ -1012,6 +1014,7 @@
|
||||||
"mute_modal.duration": "Duration",
|
"mute_modal.duration": "Duration",
|
||||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||||
"navbar.login.action": "Log in",
|
"navbar.login.action": "Log in",
|
||||||
|
"navbar.login.email.placeholder": "E-mail address",
|
||||||
"navbar.login.forgot_password": "Forgot password?",
|
"navbar.login.forgot_password": "Forgot password?",
|
||||||
"navbar.login.password.label": "Password",
|
"navbar.login.password.label": "Password",
|
||||||
"navbar.login.username.placeholder": "Email or username",
|
"navbar.login.username.placeholder": "Email or username",
|
||||||
|
@ -1114,6 +1117,7 @@
|
||||||
"onboarding.suggestions.title": "Suggested accounts",
|
"onboarding.suggestions.title": "Suggested accounts",
|
||||||
"onboarding.view_feed": "View Feed",
|
"onboarding.view_feed": "View Feed",
|
||||||
"password_reset.confirmation": "Check your email for confirmation.",
|
"password_reset.confirmation": "Check your email for confirmation.",
|
||||||
|
"password_reset.fields.email_placeholder": "E-mail address",
|
||||||
"password_reset.fields.username_placeholder": "Email or username",
|
"password_reset.fields.username_placeholder": "Email or username",
|
||||||
"password_reset.header": "Reset Password",
|
"password_reset.header": "Reset Password",
|
||||||
"password_reset.reset": "Reset password",
|
"password_reset.reset": "Reset password",
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('normalizeInstance()', () => {
|
||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
max_characters_name: 50,
|
max_characters_name: 50,
|
||||||
max_characters_description: 100,
|
max_characters_description: 160,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: '',
|
description: '',
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const InstanceRecord = ImmutableRecord({
|
||||||
}),
|
}),
|
||||||
groups: ImmutableMap<string, number>({
|
groups: ImmutableMap<string, number>({
|
||||||
max_characters_name: 50,
|
max_characters_name: 50,
|
||||||
max_characters_description: 100,
|
max_characters_description: 160,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
description: '',
|
description: '',
|
||||||
|
|
|
@ -20,7 +20,7 @@ import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||||
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
|
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
|
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
|
||||||
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self';
|
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self' | 'group';
|
||||||
|
|
||||||
export type EventJoinMode = 'free' | 'restricted' | 'invite';
|
export type EventJoinMode = 'free' | 'restricted' | 'invite';
|
||||||
export type EventJoinState = 'pending' | 'reject' | 'accept';
|
export type EventJoinState = 'pending' | 'reject' | 'accept';
|
||||||
|
|
|
@ -43,7 +43,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
<>
|
<>
|
||||||
<Layout.Main className='space-y-3 pt-3 dark:divide-gray-800 sm:pt-0'>
|
<Layout.Main className='space-y-3 pt-3 dark:divide-gray-800 sm:pt-0'>
|
||||||
{me && (
|
{me && (
|
||||||
<Card variant='rounded' ref={composeBlock}>
|
<Card className='relative z-[1]' variant='rounded' ref={composeBlock}>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<HStack alignItems='start' space={4}>
|
<HStack alignItems='start' space={4}>
|
||||||
<Link to={`/@${acct}`}>
|
<Link to={`/@${acct}`}>
|
||||||
|
|
|
@ -46,6 +46,8 @@ const TimelineRecord = ImmutableRecord({
|
||||||
top: true,
|
top: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
|
next: undefined as string | undefined,
|
||||||
|
prev: undefined as string | undefined,
|
||||||
items: ImmutableOrderedSet<string>(),
|
items: ImmutableOrderedSet<string>(),
|
||||||
queuedItems: ImmutableOrderedSet<string>(), //max= MAX_QUEUED_ITEMS
|
queuedItems: ImmutableOrderedSet<string>(), //max= MAX_QUEUED_ITEMS
|
||||||
feedAccountId: null,
|
feedAccountId: null,
|
||||||
|
@ -87,13 +89,23 @@ const setFailed = (state: State, timelineId: string, failed: boolean) => {
|
||||||
return state.update(timelineId, TimelineRecord(), timeline => timeline.set('loadingFailed', failed));
|
return state.update(timelineId, TimelineRecord(), timeline => timeline.set('loadingFailed', failed));
|
||||||
};
|
};
|
||||||
|
|
||||||
const expandNormalizedTimeline = (state: State, timelineId: string, statuses: ImmutableList<ImmutableMap<string, any>>, next: string | null, isPartial: boolean, isLoadingRecent: boolean) => {
|
const expandNormalizedTimeline = (
|
||||||
|
state: State,
|
||||||
|
timelineId: string,
|
||||||
|
statuses: ImmutableList<ImmutableMap<string, any>>,
|
||||||
|
next: string | undefined,
|
||||||
|
prev: string | undefined,
|
||||||
|
isPartial: boolean,
|
||||||
|
isLoadingRecent: boolean,
|
||||||
|
) => {
|
||||||
const newIds = getStatusIds(statuses);
|
const newIds = getStatusIds(statuses);
|
||||||
|
|
||||||
return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => {
|
return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => {
|
||||||
timeline.set('isLoading', false);
|
timeline.set('isLoading', false);
|
||||||
timeline.set('loadingFailed', false);
|
timeline.set('loadingFailed', false);
|
||||||
timeline.set('isPartial', isPartial);
|
timeline.set('isPartial', isPartial);
|
||||||
|
timeline.set('next', next);
|
||||||
|
timeline.set('prev', prev);
|
||||||
|
|
||||||
if (!next && !isLoadingRecent) timeline.set('hasMore', false);
|
if (!next && !isLoadingRecent) timeline.set('hasMore', false);
|
||||||
|
|
||||||
|
@ -322,7 +334,15 @@ export default function timelines(state: State = initialState, action: AnyAction
|
||||||
case TIMELINE_EXPAND_FAIL:
|
case TIMELINE_EXPAND_FAIL:
|
||||||
return handleExpandFail(state, action.timeline);
|
return handleExpandFail(state, action.timeline);
|
||||||
case TIMELINE_EXPAND_SUCCESS:
|
case TIMELINE_EXPAND_SUCCESS:
|
||||||
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses) as ImmutableList<ImmutableMap<string, any>>, action.next, action.partial, action.isLoadingRecent);
|
return expandNormalizedTimeline(
|
||||||
|
state,
|
||||||
|
action.timeline,
|
||||||
|
fromJS(action.statuses) as ImmutableList<ImmutableMap<string, any>>,
|
||||||
|
action.next,
|
||||||
|
action.prev,
|
||||||
|
action.partial,
|
||||||
|
action.isLoadingRecent,
|
||||||
|
);
|
||||||
case TIMELINE_UPDATE:
|
case TIMELINE_UPDATE:
|
||||||
return updateTimeline(state, action.timeline, action.statusId);
|
return updateTimeline(state, action.timeline, action.statusId);
|
||||||
case TIMELINE_UPDATE_QUEUE:
|
case TIMELINE_UPDATE_QUEUE:
|
||||||
|
|
|
@ -597,6 +597,14 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can sign in using username instead of e-mail address.
|
||||||
|
*/
|
||||||
|
logInWithUsername: any([
|
||||||
|
v.software === PLEROMA,
|
||||||
|
v.software === TRUTHSOCIAL,
|
||||||
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can perform moderation actions with account and reports.
|
* Can perform moderation actions with account and reports.
|
||||||
* @see {@link https://docs.joinmastodon.org/methods/admin/}
|
* @see {@link https://docs.joinmastodon.org/methods/admin/}
|
||||||
|
|
Loading…
Reference in a new issue