pl-fe: Move user lists away from immutable
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
563e5288fb
commit
0811af7ad7
20 changed files with 197 additions and 157 deletions
|
@ -43,7 +43,7 @@ const expandDirectory = (params: Record<string, any>) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(expandDirectoryRequest());
|
dispatch(expandDirectoryRequest());
|
||||||
|
|
||||||
const loadedItems = getState().user_lists.directory.items.size;
|
const loadedItems = getState().user_lists.directory.items.length;
|
||||||
|
|
||||||
return getClient(getState()).instance.profileDirectory({ ...params, offset: loadedItems, limit: 20 }).then((data) => {
|
return getClient(getState()).instance.profileDirectory({ ...params, offset: loadedItems, limit: 20 }).then((data) => {
|
||||||
dispatch(importEntities({ accounts: data }));
|
dispatch(importEntities({ accounts: data }));
|
||||||
|
|
|
@ -268,7 +268,7 @@ const fetchEventParticipationsFail = (statusId: string, error: unknown) => ({
|
||||||
|
|
||||||
const expandEventParticipations = (statusId: string) =>
|
const expandEventParticipations = (statusId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const next = getState().user_lists.event_participations.get(statusId)?.next || null;
|
const next = getState().user_lists.event_participations[statusId]?.next || null;
|
||||||
|
|
||||||
if (next === null) {
|
if (next === null) {
|
||||||
return dispatch(noOp);
|
return dispatch(noOp);
|
||||||
|
@ -337,7 +337,7 @@ const fetchEventParticipationRequestsFail = (statusId: string, error: unknown) =
|
||||||
|
|
||||||
const expandEventParticipationRequests = (statusId: string) =>
|
const expandEventParticipationRequests = (statusId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const next = getState().user_lists.event_participation_requests.get(statusId)?.next || null;
|
const next = getState().user_lists.event_participation_requests[statusId]?.next || null;
|
||||||
|
|
||||||
if (next === null) {
|
if (next === null) {
|
||||||
return dispatch(noOp);
|
return dispatch(noOp);
|
||||||
|
|
|
@ -28,7 +28,7 @@ const useAccountList = (listKey: string[], entityFn: EntityFn<void>) => {
|
||||||
getNextPageParam: (config) => config.next ? config : undefined,
|
getNextPageParam: (config) => config.next ? config : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = flattenPages<Account>(queryInfo.data as any)?.toReversed() || [];
|
const data = flattenPages<Account>(queryInfo.data as any) || [];
|
||||||
|
|
||||||
const { relationships } = useRelationships(
|
const { relationships } = useRelationships(
|
||||||
listKey,
|
listKey,
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Avatar from 'pl-fe/components/ui/avatar';
|
import Avatar from 'pl-fe/components/ui/avatar';
|
||||||
import HStack from 'pl-fe/components/ui/hstack';
|
import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||||
import { makeGetAccount } from 'pl-fe/selectors';
|
import { selectAccounts } from 'pl-fe/selectors';
|
||||||
|
|
||||||
import type { Account } from 'pl-fe/normalizers/account';
|
|
||||||
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
interface IAvatarStack {
|
interface IAvatarStack {
|
||||||
accountIds: ImmutableOrderedSet<string>;
|
accountIds: Array<string>;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AvatarStack: React.FC<IAvatarStack> = ({ accountIds, limit = 3 }) => {
|
const AvatarStack: React.FC<IAvatarStack> = ({ accountIds, limit = 3 }) => {
|
||||||
const accounts = useAppSelector(state => ImmutableList(accountIds.slice(0, limit).map(accountId => getAccount(state, accountId)).filter(account => account))) as ImmutableList<Account>;
|
const accounts = useAppSelector(state => selectAccounts(state, accountIds.slice(0, limit)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack className='relative' aria-hidden>
|
<HStack className='relative' aria-hidden>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ interface IBirthdayPanel {
|
||||||
const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const birthdays: ImmutableOrderedSet<string> = useAppSelector(state => state.user_lists.birthday_reminders.get(state.me as string)?.items || ImmutableOrderedSet());
|
const birthdays = useAppSelector(state => state.user_lists.birthday_reminders[state.me as string]?.items || []);
|
||||||
const birthdaysToRender = birthdays.slice(0, limit);
|
const birthdaysToRender = birthdays.slice(0, limit);
|
||||||
|
|
||||||
const timeout = useRef<NodeJS.Timeout>();
|
const timeout = useRef<NodeJS.Timeout>();
|
||||||
|
@ -48,7 +47,7 @@ const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (birthdaysToRender.isEmpty()) {
|
if (!birthdaysToRender.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||||
import { useUiStore } from 'pl-fe/stores/ui';
|
import { useUiStore } from 'pl-fe/stores/ui';
|
||||||
import sourceCode from 'pl-fe/utils/code';
|
import sourceCode from 'pl-fe/utils/code';
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
|
||||||
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
|
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -95,9 +94,9 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const me = useAppSelector((state) => state.me);
|
const me = useAppSelector((state) => state.me);
|
||||||
const { account } = useAccount(me || undefined);
|
const { account } = useAccount(me || undefined);
|
||||||
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state));
|
const otherAccounts = useAppSelector((state) => getOtherAccounts(state));
|
||||||
const { settings } = useSettingsStore();
|
const { settings } = useSettingsStore();
|
||||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.length);
|
||||||
const interactionRequestsCount = useInteractionRequestsCount().data || 0;
|
const interactionRequestsCount = useInteractionRequestsCount().data || 0;
|
||||||
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
||||||
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
||||||
|
|
|
@ -48,7 +48,7 @@ const SidebarNavigation = () => {
|
||||||
const logoSrc = useLogo();
|
const logoSrc = useLogo();
|
||||||
|
|
||||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.length);
|
||||||
const interactionRequestsCount = useInteractionRequestsCount().data || 0;
|
const interactionRequestsCount = useInteractionRequestsCount().data || 0;
|
||||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.length + state.admin.awaitingApproval.length);
|
const dashboardCount = useAppSelector((state) => state.admin.openReports.length + state.admin.awaitingApproval.length);
|
||||||
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
||||||
|
|
|
@ -70,7 +70,7 @@ interface IManagePendingParticipants {
|
||||||
const ManagePendingParticipants: React.FC<IManagePendingParticipants> = ({ statusId }) => {
|
const ManagePendingParticipants: React.FC<IManagePendingParticipants> = ({ statusId }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const accounts = useAppSelector((state) => state.user_lists.event_participation_requests.get(statusId!)?.items);
|
const accounts = useAppSelector((state) => state.user_lists.event_participation_requests[statusId]?.items);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusId) dispatch(fetchEventParticipationRequests(statusId));
|
if (statusId) dispatch(fetchEventParticipationRequests(statusId));
|
||||||
|
|
|
@ -66,7 +66,7 @@ const GroupBlockedMembers: React.FC<IGroupBlockedMembers> = ({ params }) => {
|
||||||
const groupId = params?.groupId;
|
const groupId = params?.groupId;
|
||||||
|
|
||||||
const { group } = useGroup(groupId);
|
const { group } = useGroup(groupId);
|
||||||
const accountIds = useAppSelector((state) => state.user_lists.group_blocks.get(groupId)?.items);
|
const accountIds = useAppSelector((state) => state.user_lists.group_blocks[groupId]?.items);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchGroupBlocks(groupId));
|
dispatch(fetchGroupBlocks(groupId));
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||||
import type { BaseModalProps } from '../modal-root';
|
import type { BaseModalProps } from '../modal-root';
|
||||||
|
|
||||||
const BirthdaysModal = ({ onClose }: BaseModalProps) => {
|
const BirthdaysModal = ({ onClose }: BaseModalProps) => {
|
||||||
const accountIds = useAppSelector(state => state.user_lists.birthday_reminders.get(state.me as string)?.items);
|
const accountIds = useAppSelector(state => state.user_lists.birthday_reminders[state.me as string]?.items);
|
||||||
|
|
||||||
const onClickClose = () => {
|
const onClickClose = () => {
|
||||||
onClose('BIRTHDAYS');
|
onClose('BIRTHDAYS');
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface DislikesModalProps {
|
||||||
const DislikesModal: React.FC<BaseModalProps & DislikesModalProps> = ({ onClose, statusId }) => {
|
const DislikesModal: React.FC<BaseModalProps & DislikesModalProps> = ({ onClose, statusId }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const accountIds = useAppSelector((state) => state.user_lists.disliked_by.get(statusId)?.items);
|
const accountIds = useAppSelector((state) => state.user_lists.disliked_by[statusId]?.items);
|
||||||
|
|
||||||
const fetchData = () => {
|
const fetchData = () => {
|
||||||
dispatch(fetchDislikes(statusId));
|
dispatch(fetchDislikes(statusId));
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface EventParticipantsModalProps {
|
||||||
const EventParticipantsModal: React.FC<BaseModalProps & EventParticipantsModalProps> = ({ onClose, statusId }) => {
|
const EventParticipantsModal: React.FC<BaseModalProps & EventParticipantsModalProps> = ({ onClose, statusId }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const accountIds = useAppSelector((state) => state.user_lists.event_participations.get(statusId)?.items);
|
const accountIds = useAppSelector((state) => state.user_lists.event_participations[statusId]?.items);
|
||||||
|
|
||||||
const fetchData = () => {
|
const fetchData = () => {
|
||||||
dispatch(fetchEventParticipations(statusId));
|
dispatch(fetchEventParticipations(statusId));
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
@ -21,7 +20,7 @@ interface FamiliarFollowersModalProps {
|
||||||
const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalProps> = ({ accountId, onClose }) => {
|
const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalProps> = ({ accountId, onClose }) => {
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const account = useAppSelector(state => getAccount(state, accountId));
|
const account = useAppSelector(state => getAccount(state, accountId));
|
||||||
const familiarFollowerIds: ImmutableOrderedSet<string> = useAppSelector(state => state.user_lists.familiar_followers.get(accountId)?.items || ImmutableOrderedSet());
|
const familiarFollowerIds = useAppSelector(state => state.user_lists.familiar_followers[accountId]?.items || []);
|
||||||
|
|
||||||
const onClickClose = () => {
|
const onClickClose = () => {
|
||||||
onClose('FAMILIAR_FOLLOWERS');
|
onClose('FAMILIAR_FOLLOWERS');
|
||||||
|
|
|
@ -19,8 +19,8 @@ const FavouritesModal: React.FC<BaseModalProps & FavouritesModalProps> = ({ onCl
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const accountIds = useAppSelector((state) => state.user_lists.favourited_by.get(statusId)?.items);
|
const accountIds = useAppSelector((state) => state.user_lists.favourited_by[statusId]?.items);
|
||||||
const next = useAppSelector((state) => state.user_lists.favourited_by.get(statusId)?.next);
|
const next = useAppSelector((state) => state.user_lists.favourited_by[statusId]?.next);
|
||||||
|
|
||||||
const fetchData = () => {
|
const fetchData = () => {
|
||||||
dispatch(fetchFavourites(statusId));
|
dispatch(fetchFavourites(statusId));
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
@ -36,7 +35,7 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [reaction, setReaction] = useState(initialReaction);
|
const [reaction, setReaction] = useState(initialReaction);
|
||||||
const reactions = useAppSelector((state) => state.user_lists.reactions.get(statusId)?.items);
|
const reactions = useAppSelector((state) => state.user_lists.reactions[statusId]?.items);
|
||||||
|
|
||||||
const onClickClose = () => {
|
const onClickClose = () => {
|
||||||
onClose('REACTIONS');
|
onClose('REACTIONS');
|
||||||
|
@ -65,15 +64,15 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
|
||||||
return <Tabs items={items} activeItem={reaction || 'all'} />;
|
return <Tabs items={items} activeItem={reaction || 'all'} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const accounts = useMemo((): ImmutableList<IAccountWithReaction> | undefined => {
|
const accounts = useMemo((): Array<IAccountWithReaction> | undefined => {
|
||||||
if (!reactions) return;
|
if (!reactions) return;
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
const reactionRecord = reactions.find(({ name }) => name === reaction);
|
const reactionRecord = reactions.find(({ name }) => name === reaction);
|
||||||
|
|
||||||
if (reactionRecord) return reactionRecord.accounts.map(account => ({ id: account, reaction: reaction, reactionUrl: reactionRecord.url || undefined })).toList();
|
if (reactionRecord) return reactionRecord.accounts.map(account => ({ id: account, reaction: reaction, reactionUrl: reactionRecord.url || undefined }));
|
||||||
} else {
|
} else {
|
||||||
return reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url }))).flatten() as ImmutableList<IAccountWithReaction>;
|
return reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url || undefined }))).flat();
|
||||||
}
|
}
|
||||||
}, [reactions, reaction]);
|
}, [reactions, reaction]);
|
||||||
|
|
||||||
|
@ -89,11 +88,11 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
|
||||||
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
||||||
|
|
||||||
body = (<>
|
body = (<>
|
||||||
{reactions.size > 0 && renderFilterBar()}
|
{reactions.length > 0 && renderFilterBar()}
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
listClassName={clsx('max-w-full', {
|
listClassName={clsx('max-w-full', {
|
||||||
'mt-4': reactions.size > 0,
|
'mt-4': reactions.length > 0,
|
||||||
})}
|
})}
|
||||||
itemClassName='pb-3'
|
itemClassName='pb-3'
|
||||||
style={{ height: 'calc(80vh - 88px)' }}
|
style={{ height: 'calc(80vh - 88px)' }}
|
||||||
|
|
|
@ -19,8 +19,8 @@ interface ReblogsModalProps {
|
||||||
const ReblogsModal: React.FC<BaseModalProps & ReblogsModalProps> = ({ onClose, statusId }) => {
|
const ReblogsModal: React.FC<BaseModalProps & ReblogsModalProps> = ({ onClose, statusId }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const accountIds = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.items);
|
const accountIds = useAppSelector((state) => state.user_lists.reblogged_by[statusId]?.items);
|
||||||
const next = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.next);
|
const next = useAppSelector((state) => state.user_lists.reblogged_by[statusId]?.next);
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const fetchData = () => {
|
const fetchData = () => {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
@ -19,13 +18,13 @@ interface IPinnedAccountsPanel {
|
||||||
|
|
||||||
const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit }) => {
|
const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const pinned = useAppSelector((state) => state.user_lists.pinned.get(account.id)?.items || ImmutableOrderedSet<string>()).slice(0, limit);
|
const pinned = useAppSelector((state) => state.user_lists.pinned[account.id]?.items || []).slice(0, limit);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchPinnedAccounts(account.id));
|
dispatch(fetchPinnedAccounts(account.id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (pinned.isEmpty()) {
|
if (!pinned.length) {
|
||||||
return (
|
return (
|
||||||
<WhoToFollowPanel limit={limit} />
|
<WhoToFollowPanel limit={limit} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormattedList, FormattedMessage } from 'react-intl';
|
import { FormattedList, FormattedMessage } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -29,8 +28,8 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const me = useAppSelector((state) => state.me);
|
const me = useAppSelector((state) => state.me);
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const familiarFollowerIds = useAppSelector(state => state.user_lists.familiar_followers.get(account.id)?.items || ImmutableOrderedSet<string>());
|
const familiarFollowerIds = useAppSelector(state => state.user_lists.familiar_followers[account.id]?.items || []);
|
||||||
const familiarFollowers: ImmutableOrderedSet<Account | null> = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId)));
|
const familiarFollowers = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId)));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (me && features.familiarFollowers) {
|
if (me && features.familiarFollowers) {
|
||||||
|
@ -44,7 +43,7 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (familiarFollowerIds.size === 0) {
|
if (familiarFollowerIds.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,15 +59,15 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
|
||||||
</HStack>
|
</HStack>
|
||||||
</Link>
|
</Link>
|
||||||
</HoverAccountWrapper>
|
</HoverAccountWrapper>
|
||||||
)).toArray().filter(Boolean);
|
)).filter(Boolean);
|
||||||
|
|
||||||
if (familiarFollowerIds.size > 2) {
|
if (familiarFollowerIds.length > 2) {
|
||||||
accounts.push(
|
accounts.push(
|
||||||
<span key='_' className='cursor-pointer hover:underline' role='presentation' onClick={openFamiliarFollowersModal}>
|
<span key='_' className='cursor-pointer hover:underline' role='presentation' onClick={openFamiliarFollowersModal}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='account.familiar_followers.more'
|
id='account.familiar_followers.more'
|
||||||
defaultMessage='{count, plural, one {# other} other {# others}} you follow'
|
defaultMessage='{count, plural, one {# other} other {# others}} you follow'
|
||||||
values={{ count: familiarFollowerIds.size - familiarFollowers.size }}
|
values={{ count: familiarFollowerIds.length - familiarFollowers.length }}
|
||||||
/>
|
/>
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { create } from 'mutative';
|
||||||
Map as ImmutableMap,
|
|
||||||
OrderedSet as ImmutableOrderedSet,
|
|
||||||
Record as ImmutableRecord,
|
|
||||||
} from 'immutable';
|
|
||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -44,95 +40,123 @@ import {
|
||||||
FAVOURITES_EXPAND_SUCCESS,
|
FAVOURITES_EXPAND_SUCCESS,
|
||||||
DISLIKES_FETCH_SUCCESS,
|
DISLIKES_FETCH_SUCCESS,
|
||||||
REACTIONS_FETCH_SUCCESS,
|
REACTIONS_FETCH_SUCCESS,
|
||||||
|
InteractionsAction,
|
||||||
} from 'pl-fe/actions/interactions';
|
} from 'pl-fe/actions/interactions';
|
||||||
import { NOTIFICATIONS_UPDATE } from 'pl-fe/actions/notifications';
|
import { NOTIFICATIONS_UPDATE } from 'pl-fe/actions/notifications';
|
||||||
|
|
||||||
import type { Account, Notification, PaginatedResponse } from 'pl-api';
|
import type { Account, EmojiReaction, Notification, PaginatedResponse } from 'pl-api';
|
||||||
import type { APIEntity } from 'pl-fe/types/entities';
|
import type { APIEntity } from 'pl-fe/types/entities';
|
||||||
|
|
||||||
const ListRecord = ImmutableRecord({
|
interface List {
|
||||||
next: null as (() => Promise<PaginatedResponse<Account>>) | null,
|
next: (() => Promise<PaginatedResponse<Account>>) | null;
|
||||||
items: ImmutableOrderedSet<string>(),
|
items: Array<string>;
|
||||||
isLoading: false,
|
isLoading: boolean;
|
||||||
});
|
}
|
||||||
|
|
||||||
const ReactionRecord = ImmutableRecord({
|
interface Reaction {
|
||||||
accounts: ImmutableOrderedSet<string>(),
|
accounts: Array<string>;
|
||||||
count: 0,
|
count: number;
|
||||||
name: '',
|
name: string;
|
||||||
url: null as string | null,
|
url: string | null;
|
||||||
});
|
}
|
||||||
|
|
||||||
const ReactionListRecord = ImmutableRecord({
|
interface ReactionList {
|
||||||
next: null as (() => Promise<PaginatedResponse<Reaction>>) | null,
|
next: (() => Promise<PaginatedResponse<Reaction>>) | null;
|
||||||
items: ImmutableOrderedSet<Reaction>(),
|
items: Array<Reaction>;
|
||||||
isLoading: false,
|
isLoading: boolean;
|
||||||
});
|
}
|
||||||
|
|
||||||
const ParticipationRequestRecord = ImmutableRecord({
|
interface ParticipationRequest {
|
||||||
account: '',
|
account: string;
|
||||||
participation_message: null as string | null,
|
participation_message: string | null;
|
||||||
});
|
}
|
||||||
|
|
||||||
const ParticipationRequestListRecord = ImmutableRecord({
|
interface ParticipationRequestList {
|
||||||
next: null as (() => Promise<PaginatedResponse<any>>) | null,
|
next: (() => Promise<PaginatedResponse<any>>) | null;
|
||||||
items: ImmutableOrderedSet<ParticipationRequest>(),
|
items: Array<ParticipationRequest>;
|
||||||
isLoading: false,
|
isLoading: boolean;
|
||||||
});
|
}
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
type ListKey = 'follow_requests' | 'directory';
|
||||||
followers: ImmutableMap<string, List>(),
|
type NestedListKey = 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'membership_requests' | 'group_blocks';
|
||||||
following: ImmutableMap<string, List>(),
|
|
||||||
reblogged_by: ImmutableMap<string, List>(),
|
|
||||||
favourited_by: ImmutableMap<string, List>(),
|
|
||||||
disliked_by: ImmutableMap<string, List>(),
|
|
||||||
reactions: ImmutableMap<string, ReactionList>(),
|
|
||||||
follow_requests: ListRecord(),
|
|
||||||
mutes: ListRecord(),
|
|
||||||
directory: ListRecord({ isLoading: true }),
|
|
||||||
pinned: ImmutableMap<string, List>(),
|
|
||||||
birthday_reminders: ImmutableMap<string, List>(),
|
|
||||||
familiar_followers: ImmutableMap<string, List>(),
|
|
||||||
event_participations: ImmutableMap<string, List>(),
|
|
||||||
event_participation_requests: ImmutableMap<string, ParticipationRequestList>(),
|
|
||||||
membership_requests: ImmutableMap<string, List>(),
|
|
||||||
group_blocks: ImmutableMap<string, List>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type State = ReturnType<typeof ReducerRecord>;
|
type State = Record<ListKey, List> & Record<NestedListKey, Record<string, List>> & {
|
||||||
type List = ReturnType<typeof ListRecord>;
|
reactions: Record<string, ReactionList>;
|
||||||
type Reaction = ReturnType<typeof ReactionRecord>;
|
event_participation_requests: Record<string, ParticipationRequestList>;
|
||||||
type ReactionList = ReturnType<typeof ReactionListRecord>;
|
};
|
||||||
type ParticipationRequest = ReturnType<typeof ParticipationRequestRecord>;
|
|
||||||
type ParticipationRequestList = ReturnType<typeof ParticipationRequestListRecord>;
|
|
||||||
type Items = ImmutableOrderedSet<string>;
|
|
||||||
type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'event_participation_requests' | 'membership_requests' | 'group_blocks', string];
|
|
||||||
type ListPath = ['follow_requests' | 'mutes' | 'directory'];
|
|
||||||
|
|
||||||
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next?: (() => any) | null) =>
|
const initialState: State = {
|
||||||
state.setIn(path, ListRecord({
|
reblogged_by: {},
|
||||||
next,
|
favourited_by: {},
|
||||||
items: ImmutableOrderedSet(accounts.map(item => item.id)),
|
disliked_by: {},
|
||||||
}));
|
reactions: {},
|
||||||
|
follow_requests: { next: null, items: [], isLoading: false },
|
||||||
|
directory: { next: null, items: [], isLoading: true },
|
||||||
|
pinned: {},
|
||||||
|
birthday_reminders: {},
|
||||||
|
familiar_followers: {},
|
||||||
|
event_participations: {},
|
||||||
|
event_participation_requests: {},
|
||||||
|
membership_requests: {},
|
||||||
|
group_blocks: {},
|
||||||
|
};
|
||||||
|
|
||||||
const appendToList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => any) | null) =>
|
type NestedListPath = [NestedListKey, string];
|
||||||
state.updateIn(path, map => (map as List)
|
type ListPath = [ListKey];
|
||||||
.set('next', next)
|
|
||||||
.set('isLoading', false)
|
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => Promise<PaginatedResponse<any>>) | null = null) =>
|
||||||
.update('items', list => (list as Items).concat(accounts.map(item => item.id))),
|
create(state, (draft) => {
|
||||||
);
|
let list: List;
|
||||||
|
|
||||||
|
if (path.length === 1) {
|
||||||
|
list = draft[path[0]];
|
||||||
|
} else {
|
||||||
|
list = draft[path[0]][path[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newList = { ...list, next, items: accounts.map(item => item.id), isLoading: false };
|
||||||
|
|
||||||
|
if (path.length === 1) {
|
||||||
|
draft[path[0]] = newList;
|
||||||
|
} else {
|
||||||
|
draft[path[0]][path[1]] = newList;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendToList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => any) | null = null) =>
|
||||||
|
create(state, (draft) => {
|
||||||
|
let list: List;
|
||||||
|
|
||||||
|
if (path.length === 1) {
|
||||||
|
list = draft[path[0]];
|
||||||
|
} else {
|
||||||
|
list = draft[path[0]][path[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
list.next = next;
|
||||||
|
list.isLoading = false;
|
||||||
|
list.items = [...new Set([...list.items, ...accounts.map(item => item.id)])];
|
||||||
|
});
|
||||||
|
|
||||||
const removeFromList = (state: State, path: NestedListPath | ListPath, accountId: string) =>
|
const removeFromList = (state: State, path: NestedListPath | ListPath, accountId: string) =>
|
||||||
state.updateIn(path, map =>
|
create(state, (draft) => {
|
||||||
(map as List).update('items', list => (list as Items).filterNot(item => item === accountId)),
|
let list: List;
|
||||||
);
|
|
||||||
|
if (path.length === 1) {
|
||||||
|
list = draft[path[0]];
|
||||||
|
} else {
|
||||||
|
list = draft[path[0]][path[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
list.items = list.items.filter(item => item !== accountId);
|
||||||
|
});
|
||||||
|
|
||||||
const normalizeFollowRequest = (state: State, notification: Notification) =>
|
const normalizeFollowRequest = (state: State, notification: Notification) =>
|
||||||
state.updateIn(['follow_requests', 'items'], list =>
|
create(state, (draft) => {
|
||||||
ImmutableOrderedSet([notification.account.id]).union(list as Items),
|
draft.follow_requests.items = [...new Set([notification.account.id, ...draft.follow_requests.items])];
|
||||||
);
|
});
|
||||||
|
|
||||||
const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction) => {
|
const userLists = (state = initialState, action: DirectoryAction | InteractionsAction | AnyAction): State => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case REBLOGS_FETCH_SUCCESS:
|
case REBLOGS_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['reblogged_by', action.statusId], action.accounts, action.next);
|
return normalizeList(state, ['reblogged_by', action.statusId], action.accounts, action.next);
|
||||||
|
@ -145,12 +169,13 @@ const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction)
|
||||||
case DISLIKES_FETCH_SUCCESS:
|
case DISLIKES_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['disliked_by', action.statusId], action.accounts);
|
return normalizeList(state, ['disliked_by', action.statusId], action.accounts);
|
||||||
case REACTIONS_FETCH_SUCCESS:
|
case REACTIONS_FETCH_SUCCESS:
|
||||||
return state.setIn(['reactions', action.statusId], ReactionListRecord({
|
return create(state, (draft) => {
|
||||||
items: ImmutableOrderedSet<Reaction>(action.reactions.map(({ accounts, ...reaction }: APIEntity) => ReactionRecord({
|
draft.reactions[action.statusId] = {
|
||||||
...reaction,
|
items: action.reactions.map((reaction: EmojiReaction) => ({ ...reaction, accounts: reaction.accounts.map(({ id }) => id) })),
|
||||||
accounts: ImmutableOrderedSet(accounts.map((account: APIEntity) => account.id)),
|
next: null,
|
||||||
}))),
|
isLoading: false,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
case NOTIFICATIONS_UPDATE:
|
case NOTIFICATIONS_UPDATE:
|
||||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||||
|
@ -166,10 +191,14 @@ const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction)
|
||||||
return appendToList(state, ['directory'], action.accounts, null);
|
return appendToList(state, ['directory'], action.accounts, null);
|
||||||
case DIRECTORY_FETCH_REQUEST:
|
case DIRECTORY_FETCH_REQUEST:
|
||||||
case DIRECTORY_EXPAND_REQUEST:
|
case DIRECTORY_EXPAND_REQUEST:
|
||||||
return state.setIn(['directory', 'isLoading'], true);
|
return create(state, (draft) => {
|
||||||
|
draft.directory.isLoading = true;
|
||||||
|
});
|
||||||
case DIRECTORY_FETCH_FAIL:
|
case DIRECTORY_FETCH_FAIL:
|
||||||
case DIRECTORY_EXPAND_FAIL:
|
case DIRECTORY_EXPAND_FAIL:
|
||||||
return state.setIn(['directory', 'isLoading'], false);
|
return create(state, (draft) => {
|
||||||
|
draft.directory.isLoading = false;
|
||||||
|
});
|
||||||
case PINNED_ACCOUNTS_FETCH_SUCCESS:
|
case PINNED_ACCOUNTS_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['pinned', action.accountId], action.accounts, action.next);
|
return normalizeList(state, ['pinned', action.accountId], action.accounts, action.next);
|
||||||
case BIRTHDAY_REMINDERS_FETCH_SUCCESS:
|
case BIRTHDAY_REMINDERS_FETCH_SUCCESS:
|
||||||
|
@ -181,43 +210,60 @@ const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction)
|
||||||
case EVENT_PARTICIPATIONS_EXPAND_SUCCESS:
|
case EVENT_PARTICIPATIONS_EXPAND_SUCCESS:
|
||||||
return appendToList(state, ['event_participations', action.statusId], action.accounts, action.next);
|
return appendToList(state, ['event_participations', action.statusId], action.accounts, action.next);
|
||||||
case EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS:
|
case EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS:
|
||||||
return state.setIn(['event_participation_requests', action.statusId], ParticipationRequestListRecord({
|
return create(state, (draft) => {
|
||||||
next: action.next,
|
draft.event_participation_requests[action.statusId] = {
|
||||||
items: ImmutableOrderedSet(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({
|
next: action.next,
|
||||||
account: account.id,
|
items: action.participations.map(({ account, participation_message }: APIEntity) => ({
|
||||||
participation_message,
|
|
||||||
}))),
|
|
||||||
}));
|
|
||||||
case EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS:
|
|
||||||
return state.updateIn(
|
|
||||||
['event_participation_requests', action.statusId, 'items'],
|
|
||||||
(items) => (items as ImmutableOrderedSet<ParticipationRequest>)
|
|
||||||
.union(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({
|
|
||||||
account: account.id,
|
account: account.id,
|
||||||
participation_message,
|
participation_message,
|
||||||
}))),
|
})),
|
||||||
);
|
isLoading: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
case EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS:
|
||||||
|
return create(state, (draft) => {
|
||||||
|
const list = draft.event_participation_requests[action.statusId];
|
||||||
|
list.next = action.next;
|
||||||
|
list.items = [...list.items, ...action.participations.map(({ account, participation_message }: APIEntity) => ({
|
||||||
|
account: account.id,
|
||||||
|
participation_message,
|
||||||
|
}))];
|
||||||
|
list.isLoading = false;
|
||||||
|
});
|
||||||
case EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS:
|
case EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS:
|
||||||
case EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS:
|
case EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS:
|
||||||
return state.updateIn(
|
return create(state, (draft) => {
|
||||||
['event_participation_requests', action.statusId, 'items'],
|
const list = draft.event_participation_requests[action.statusId];
|
||||||
items => (items as ImmutableOrderedSet<ParticipationRequest>).filter(({ account }) => account !== action.accountId),
|
if (list.items) list.items = list.items.filter(item => item !== action.accountId);
|
||||||
);
|
});
|
||||||
case GROUP_BLOCKS_FETCH_SUCCESS:
|
case GROUP_BLOCKS_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['group_blocks', action.groupId], action.accounts, action.next);
|
return normalizeList(state, ['group_blocks', action.groupId], action.accounts, action.next);
|
||||||
case GROUP_BLOCKS_FETCH_REQUEST:
|
case GROUP_BLOCKS_FETCH_REQUEST:
|
||||||
return state.setIn(['group_blocks', action.groupId, 'isLoading'], true);
|
return create(state, (draft) => {
|
||||||
|
draft.group_blocks[action.groupId] = {
|
||||||
|
items: [],
|
||||||
|
next: null,
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
case GROUP_BLOCKS_FETCH_FAIL:
|
case GROUP_BLOCKS_FETCH_FAIL:
|
||||||
return state.setIn(['group_blocks', action.groupId, 'isLoading'], false);
|
return create(state, (draft) => {
|
||||||
|
draft.group_blocks[action.groupId] = {
|
||||||
|
items: [],
|
||||||
|
next: null,
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
case GROUP_UNBLOCK_SUCCESS:
|
case GROUP_UNBLOCK_SUCCESS:
|
||||||
return state.updateIn(['group_blocks', action.groupId, 'items'], list => (list as ImmutableOrderedSet<string>).filterNot(item => item === action.accountId));
|
return create(state, (draft) => {
|
||||||
|
const list = draft.group_blocks[action.groupId];
|
||||||
|
if (list.items) list.items = list.items.filter(item => item !== action.accountId);
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ListRecord,
|
|
||||||
ReducerRecord,
|
|
||||||
userLists as default,
|
userLists as default,
|
||||||
};
|
};
|
||||||
|
|
|
@ -51,6 +51,8 @@ const makeGetAccount = () => createSelector([
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type SelectedAccount = Exclude<ReturnType<ReturnType<typeof makeGetAccount>>, null>;
|
||||||
|
|
||||||
const toServerSideType = (columnType: string): Filter['context'][0] => {
|
const toServerSideType = (columnType: string): Filter['context'][0] => {
|
||||||
switch (columnType) {
|
switch (columnType) {
|
||||||
case 'home':
|
case 'home':
|
||||||
|
@ -277,11 +279,12 @@ const makeGetOtherAccounts = () => createSelector([
|
||||||
getAuthUserIds,
|
getAuthUserIds,
|
||||||
(state: RootState) => state.me,
|
(state: RootState) => state.me,
|
||||||
], (accounts, authUserIds, me) =>
|
], (accounts, authUserIds, me) =>
|
||||||
authUserIds.reduce((list: ImmutableList<any>, id: string) => {
|
authUserIds.reduce<Array<Account>>((list, id) => {
|
||||||
if (id === me) return list;
|
if (id === me) return list;
|
||||||
const account = accounts?.[id];
|
const account = accounts?.[id];
|
||||||
return account ? list.push(account) : list;
|
if (account) list.push(account);
|
||||||
}, ImmutableList()),
|
return list;
|
||||||
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getSimplePolicy = createSelector([
|
const getSimplePolicy = createSelector([
|
||||||
|
@ -353,8 +356,10 @@ const makeGetStatusIds = () => createSelector([
|
||||||
export {
|
export {
|
||||||
type RemoteInstance,
|
type RemoteInstance,
|
||||||
selectAccount,
|
selectAccount,
|
||||||
|
selectAccounts,
|
||||||
selectOwnAccount,
|
selectOwnAccount,
|
||||||
makeGetAccount,
|
makeGetAccount,
|
||||||
|
type SelectedAccount,
|
||||||
getFilters,
|
getFilters,
|
||||||
regexFromFilters,
|
regexFromFilters,
|
||||||
makeGetStatus,
|
makeGetStatus,
|
||||||
|
|
Loading…
Reference in a new issue