Move toju-app into own its folder
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
EntityState,
|
||||
EntityAdapter,
|
||||
createEntityAdapter
|
||||
} from '@ngrx/entity';
|
||||
import { DELETED_MESSAGE_CONTENT, Message } from '../../shared-kernel';
|
||||
import { MessagesActions } from './messages.actions';
|
||||
|
||||
/** State shape for the messages feature slice, extending NgRx EntityState. */
|
||||
export interface MessagesState extends EntityState<Message> {
|
||||
/** Whether messages are being loaded from the database. */
|
||||
loading: boolean;
|
||||
/** Whether a peer-to-peer sync cycle is in progress. */
|
||||
syncing: boolean;
|
||||
/** Most recent error message from message operations. */
|
||||
error: string | null;
|
||||
/** ID of the room whose messages are currently loaded. */
|
||||
currentRoomId: string | null;
|
||||
}
|
||||
|
||||
export const messagesAdapter: EntityAdapter<Message> = createEntityAdapter<Message>({
|
||||
selectId: (message) => message.id,
|
||||
sortComparer: (messageA, messageB) => messageA.timestamp - messageB.timestamp
|
||||
});
|
||||
|
||||
export const initialState: MessagesState = messagesAdapter.getInitialState({
|
||||
loading: false,
|
||||
syncing: false,
|
||||
error: null,
|
||||
currentRoomId: null
|
||||
});
|
||||
|
||||
export const messagesReducer = createReducer(
|
||||
initialState,
|
||||
|
||||
// Load messages - clear stale messages when switching to a different room
|
||||
on(MessagesActions.loadMessages, (state, { roomId }) => {
|
||||
if (state.currentRoomId && state.currentRoomId !== roomId) {
|
||||
return messagesAdapter.removeAll({
|
||||
...state,
|
||||
loading: true,
|
||||
error: null,
|
||||
currentRoomId: roomId
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
error: null,
|
||||
currentRoomId: roomId
|
||||
};
|
||||
}),
|
||||
|
||||
on(MessagesActions.loadMessagesSuccess, (state, { messages }) =>
|
||||
messagesAdapter.setAll(messages, {
|
||||
...state,
|
||||
loading: false
|
||||
})
|
||||
),
|
||||
|
||||
on(MessagesActions.loadMessagesFailure, (state, { error }) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error
|
||||
})),
|
||||
|
||||
// Send message
|
||||
on(MessagesActions.sendMessage, (state) => ({
|
||||
...state,
|
||||
loading: true
|
||||
})),
|
||||
|
||||
on(MessagesActions.sendMessageSuccess, (state, { message }) =>
|
||||
messagesAdapter.addOne(message, {
|
||||
...state,
|
||||
loading: false
|
||||
})
|
||||
),
|
||||
|
||||
on(MessagesActions.sendMessageFailure, (state, { error }) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error
|
||||
})),
|
||||
|
||||
// Receive message from peer
|
||||
on(MessagesActions.receiveMessage, (state, { message }) =>
|
||||
messagesAdapter.upsertOne(message, state)
|
||||
),
|
||||
|
||||
// Edit message
|
||||
on(MessagesActions.editMessageSuccess, (state, { messageId, content, editedAt }) =>
|
||||
messagesAdapter.updateOne(
|
||||
{
|
||||
id: messageId,
|
||||
changes: { content,
|
||||
editedAt }
|
||||
},
|
||||
state
|
||||
)
|
||||
),
|
||||
|
||||
// Delete message
|
||||
on(MessagesActions.deleteMessageSuccess, (state, { messageId }) =>
|
||||
messagesAdapter.updateOne(
|
||||
{
|
||||
id: messageId,
|
||||
changes: {
|
||||
content: DELETED_MESSAGE_CONTENT,
|
||||
isDeleted: true,
|
||||
reactions: []
|
||||
}
|
||||
},
|
||||
state
|
||||
)
|
||||
),
|
||||
|
||||
// Add reaction
|
||||
on(MessagesActions.addReactionSuccess, (state, { reaction }) => {
|
||||
const message = state.entities[reaction.messageId];
|
||||
|
||||
if (!message)
|
||||
return state;
|
||||
|
||||
const existingReaction = message.reactions.find(
|
||||
(existing) => existing.emoji === reaction.emoji && existing.userId === reaction.userId
|
||||
);
|
||||
|
||||
if (existingReaction)
|
||||
return state;
|
||||
|
||||
return messagesAdapter.updateOne(
|
||||
{
|
||||
id: reaction.messageId,
|
||||
changes: {
|
||||
reactions: [...message.reactions, reaction]
|
||||
}
|
||||
},
|
||||
state
|
||||
);
|
||||
}),
|
||||
|
||||
// Remove reaction
|
||||
on(MessagesActions.removeReactionSuccess, (state, { messageId, emoji, oderId }) => {
|
||||
const message = state.entities[messageId];
|
||||
|
||||
if (!message)
|
||||
return state;
|
||||
|
||||
return messagesAdapter.updateOne(
|
||||
{
|
||||
id: messageId,
|
||||
changes: {
|
||||
reactions: message.reactions.filter(
|
||||
(existingReaction) => !(existingReaction.emoji === emoji && existingReaction.userId === oderId)
|
||||
)
|
||||
}
|
||||
},
|
||||
state
|
||||
);
|
||||
}),
|
||||
|
||||
// Sync lifecycle
|
||||
on(MessagesActions.startSync, (state) => ({
|
||||
...state,
|
||||
syncing: true
|
||||
})),
|
||||
|
||||
on(MessagesActions.syncComplete, (state) => ({
|
||||
...state,
|
||||
syncing: false
|
||||
})),
|
||||
|
||||
// Sync messages from peer (merge reactions to avoid losing local-only reactions)
|
||||
on(MessagesActions.syncMessages, (state, { messages }) => {
|
||||
const merged = messages.map(message => {
|
||||
const existing = state.entities[message.id];
|
||||
|
||||
if (existing?.reactions?.length) {
|
||||
const combined = [...(message.reactions ?? [])];
|
||||
|
||||
for (const existingReaction of existing.reactions) {
|
||||
const alreadyExists = combined.some((combinedReaction) =>
|
||||
combinedReaction.userId === existingReaction.userId &&
|
||||
combinedReaction.emoji === existingReaction.emoji &&
|
||||
combinedReaction.messageId === existingReaction.messageId
|
||||
);
|
||||
|
||||
if (!alreadyExists) {
|
||||
combined.push(existingReaction);
|
||||
}
|
||||
}
|
||||
|
||||
return { ...message,
|
||||
reactions: combined };
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
return messagesAdapter.upsertMany(merged, {
|
||||
...state,
|
||||
syncing: false
|
||||
});
|
||||
}),
|
||||
|
||||
// Clear messages
|
||||
on(MessagesActions.clearMessages, (state) =>
|
||||
messagesAdapter.removeAll({
|
||||
...state,
|
||||
currentRoomId: null
|
||||
})
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user