pl-fe: migrate conversations reducer

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-10 18:15:58 +01:00
parent 086f409ec8
commit 81149a891f
5 changed files with 102 additions and 71 deletions

View file

@ -21,10 +21,15 @@ const mountConversations = () => ({ type: CONVERSATIONS_MOUNT });
const unmountConversations = () => ({ type: CONVERSATIONS_UNMOUNT });
interface ConversationsReadAction {
type: typeof CONVERSATIONS_READ;
conversationId: string;
}
const markConversationRead = (conversationId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch({
dispatch<ConversationsReadAction>({
type: CONVERSATIONS_READ,
conversationId,
});
@ -71,18 +76,32 @@ const expandConversationsFail = (error: unknown) => ({
error,
});
interface ConversataionsUpdateAction {
type: typeof CONVERSATIONS_UPDATE;
conversation: Conversation;
}
const updateConversations = (conversation: Conversation) => (dispatch: AppDispatch) => {
dispatch(importEntities({
accounts: conversation.accounts,
statuses: [conversation.last_status],
}));
return dispatch({
return dispatch<ConversataionsUpdateAction>({
type: CONVERSATIONS_UPDATE,
conversation,
});
};
type ConversationsAction =
| ReturnType<typeof mountConversations>
| ReturnType<typeof unmountConversations>
| ConversationsReadAction
| ReturnType<typeof expandConversationsRequest>
| ReturnType<typeof expandConversationsSuccess>
| ReturnType<typeof expandConversationsFail>
| ConversataionsUpdateAction
export {
CONVERSATIONS_MOUNT,
CONVERSATIONS_UNMOUNT,
@ -99,4 +118,5 @@ export {
expandConversationsSuccess,
expandConversationsFail,
updateConversations,
type ConversationsAction,
};

View file

@ -45,7 +45,7 @@ const ConversationsList: React.FC = () => {
onLoadMore={handleLoadOlder}
id='direct-list'
isLoading={isLoading}
showLoading={isLoading && conversations.size === 0}
showLoading={isLoading && conversations.length === 0}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
>
{conversations.map((item: any) => (

View file

@ -142,7 +142,7 @@ const Notifications = () => {
'animate-pulse': notifications.size === 0,
})}
>
{scrollableContent as ImmutableList<JSX.Element>}
{scrollableContent!}
</ScrollableList>
);

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useCallback, useEffect, useRef } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
@ -24,7 +23,7 @@ const MentionsModal: React.FC<BaseModalProps & MentionsModalProps> = ({ onClose,
const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector((state) => getStatus(state, { id: statusId }));
const accountIds = status ? ImmutableOrderedSet(status.mentions.map(m => m.id)) : null;
const accountIds = status ? status.mentions.map(m => m.id) : null;
const fetchData = () => {
dispatch(fetchStatusWithContext(statusId, intl));

View file

@ -1,5 +1,5 @@
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
import pick from 'lodash/pick';
import { create } from 'mutative';
import {
CONVERSATIONS_MOUNT,
@ -9,21 +9,27 @@ import {
CONVERSATIONS_FETCH_FAIL,
CONVERSATIONS_UPDATE,
CONVERSATIONS_READ,
type ConversationsAction,
} from '../actions/conversations';
import { compareDate } from '../utils/comparators';
import type { Conversation, PaginatedResponse } from 'pl-api';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
items: ImmutableList<MinifiedConversation>(),
interface State {
items: Array<MinifiedConversation>;
isLoading: boolean;
hasMore: boolean;
next: (() => Promise<PaginatedResponse<Conversation>>) | null;
mounted: number;
}
const initialState: State = {
items: [],
isLoading: false,
hasMore: true,
next: null as (() => Promise<PaginatedResponse<Conversation>>) | null,
next: null,
mounted: 0,
});
type State = ReturnType<typeof ReducerRecord>;
};
const minifyConversation = (conversation: Conversation) => ({
...(pick(conversation, ['id', 'unread'])),
@ -34,79 +40,85 @@ const minifyConversation = (conversation: Conversation) => ({
type MinifiedConversation = ReturnType<typeof minifyConversation>;
const updateConversation = (state: State, item: Conversation) => state.update('items', list => {
const index = list.findIndex(x => x.id === item.id);
const updateConversation = (state: State, item: Conversation) => {
const index = state.items.findIndex(x => x.id === item.id);
const newItem = minifyConversation(item);
if (index === -1) {
return list.unshift(newItem);
state.items = [newItem, ...state.items];
} else {
return list.set(index, newItem);
state.items[index] = newItem;
}
});
const expandNormalizedConversations = (state: State, conversations: Conversation[], next: (() => Promise<PaginatedResponse<Conversation>>) | null, isLoadingRecent?: boolean) => {
let items = ImmutableList(conversations.map(minifyConversation));
return state.withMutations(mutable => {
if (!items.isEmpty()) {
mutable.update('items', list => {
list = list.map(oldItem => {
const newItemIndex = items.findIndex(x => x.id === oldItem.id);
if (newItemIndex === -1) {
return oldItem;
}
const newItem = items.get(newItemIndex);
items = items.delete(newItemIndex);
return newItem!;
});
list = list.concat(items);
return list.sortBy(x => x.last_status_created_at, (a, b) => {
if (a === null || b === null) {
return -1;
}
return compareDate(a, b);
});
});
}
if (!next && !isLoadingRecent) {
mutable.set('hasMore', false);
}
mutable.set('next', next);
mutable.set('isLoading', false);
});
};
const conversations = (state = ReducerRecord(), action: AnyAction) => {
const expandNormalizedConversations = (state: State, conversations: Conversation[], next: (() => Promise<PaginatedResponse<Conversation>>) | null, isLoadingRecent?: boolean) => {
let items = conversations.map(minifyConversation);
if (items.length) {
let list = state.items.map(oldItem => {
const newItemIndex = items.findIndex(x => x.id === oldItem.id);
if (newItemIndex === -1) {
return oldItem;
}
const newItem = items[newItemIndex];
items = items.filter((_, index) => index !== newItemIndex);
return newItem!;
});
list = list.concat(items);
state.items = list.toSorted((a, b) => {
if (a.last_status_created_at === null || b.last_status_created_at === null) {
return -1;
}
return compareDate(a.last_status_created_at, b.last_status_created_at);
});
}
if (!next && !isLoadingRecent) {
state.hasMore = false;
}
state.next = next;
state.isLoading = false;
};
const conversations = (state = initialState, action: ConversationsAction): State => {
switch (action.type) {
case CONVERSATIONS_FETCH_REQUEST:
return state.set('isLoading', true);
return create(state, (draft) => {
draft.isLoading = true;
});
case CONVERSATIONS_FETCH_FAIL:
return state.set('isLoading', false);
return create(state, (draft) => {
draft.isLoading = false;
});
case CONVERSATIONS_FETCH_SUCCESS:
return expandNormalizedConversations(state, action.conversations, action.next, action.isLoadingRecent);
return create(state, (draft) => expandNormalizedConversations(draft, action.conversations, action.next, action.isLoadingRecent));
case CONVERSATIONS_UPDATE:
return updateConversation(state, action.conversation);
return create(state, (draft) => updateConversation(state, action.conversation));
case CONVERSATIONS_MOUNT:
return state.update('mounted', count => count + 1);
return create(state, (draft) => {
draft.mounted += 1;
});
case CONVERSATIONS_UNMOUNT:
return state.update('mounted', count => count - 1);
return create(state, (draft) => {
draft.mounted -= 1;
});
case CONVERSATIONS_READ:
return state.update('items', list => list.map(item => {
if (item.id === action.conversationId) {
return { ...item, unread: false };
}
return create(state, (draft) => {
state.items = state.items.map(item => {
if (item.id === action.conversationId) {
return { ...item, unread: false };
}
return item;
}));
return item;
});
});
default:
return state;
}