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

View file

@ -45,7 +45,7 @@ const ConversationsList: React.FC = () => {
onLoadMore={handleLoadOlder} onLoadMore={handleLoadOlder}
id='direct-list' id='direct-list'
isLoading={isLoading} 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." />} 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) => ( {conversations.map((item: any) => (

View file

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

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
@ -24,7 +23,7 @@ const MentionsModal: React.FC<BaseModalProps & MentionsModalProps> = ({ onClose,
const getStatus = useCallback(makeGetStatus(), []); const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector((state) => getStatus(state, { id: statusId })); 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 = () => { const fetchData = () => {
dispatch(fetchStatusWithContext(statusId, intl)); 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 pick from 'lodash/pick';
import { create } from 'mutative';
import { import {
CONVERSATIONS_MOUNT, CONVERSATIONS_MOUNT,
@ -9,21 +9,27 @@ import {
CONVERSATIONS_FETCH_FAIL, CONVERSATIONS_FETCH_FAIL,
CONVERSATIONS_UPDATE, CONVERSATIONS_UPDATE,
CONVERSATIONS_READ, CONVERSATIONS_READ,
type ConversationsAction,
} from '../actions/conversations'; } from '../actions/conversations';
import { compareDate } from '../utils/comparators'; import { compareDate } from '../utils/comparators';
import type { Conversation, PaginatedResponse } from 'pl-api'; import type { Conversation, PaginatedResponse } from 'pl-api';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({ interface State {
items: ImmutableList<MinifiedConversation>(), items: Array<MinifiedConversation>;
isLoading: boolean;
hasMore: boolean;
next: (() => Promise<PaginatedResponse<Conversation>>) | null;
mounted: number;
}
const initialState: State = {
items: [],
isLoading: false, isLoading: false,
hasMore: true, hasMore: true,
next: null as (() => Promise<PaginatedResponse<Conversation>>) | null, next: null,
mounted: 0, mounted: 0,
}); };
type State = ReturnType<typeof ReducerRecord>;
const minifyConversation = (conversation: Conversation) => ({ const minifyConversation = (conversation: Conversation) => ({
...(pick(conversation, ['id', 'unread'])), ...(pick(conversation, ['id', 'unread'])),
@ -34,79 +40,85 @@ const minifyConversation = (conversation: Conversation) => ({
type MinifiedConversation = ReturnType<typeof minifyConversation>; type MinifiedConversation = ReturnType<typeof minifyConversation>;
const updateConversation = (state: State, item: Conversation) => state.update('items', list => { const updateConversation = (state: State, item: Conversation) => {
const index = list.findIndex(x => x.id === item.id); const index = state.items.findIndex(x => x.id === item.id);
const newItem = minifyConversation(item); const newItem = minifyConversation(item);
if (index === -1) { if (index === -1) {
return list.unshift(newItem); state.items = [newItem, ...state.items];
} else { } 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) { switch (action.type) {
case CONVERSATIONS_FETCH_REQUEST: case CONVERSATIONS_FETCH_REQUEST:
return state.set('isLoading', true); return create(state, (draft) => {
draft.isLoading = true;
});
case CONVERSATIONS_FETCH_FAIL: case CONVERSATIONS_FETCH_FAIL:
return state.set('isLoading', false); return create(state, (draft) => {
draft.isLoading = false;
});
case CONVERSATIONS_FETCH_SUCCESS: 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: case CONVERSATIONS_UPDATE:
return updateConversation(state, action.conversation); return create(state, (draft) => updateConversation(state, action.conversation));
case CONVERSATIONS_MOUNT: case CONVERSATIONS_MOUNT:
return state.update('mounted', count => count + 1); return create(state, (draft) => {
draft.mounted += 1;
});
case CONVERSATIONS_UNMOUNT: case CONVERSATIONS_UNMOUNT:
return state.update('mounted', count => count - 1); return create(state, (draft) => {
draft.mounted -= 1;
});
case CONVERSATIONS_READ: case CONVERSATIONS_READ:
return state.update('items', list => list.map(item => { return create(state, (draft) => {
if (item.id === action.conversationId) { state.items = state.items.map(item => {
return { ...item, unread: false }; if (item.id === action.conversationId) {
} return { ...item, unread: false };
}
return item; return item;
})); });
});
default: default:
return state; return state;
} }