Move toju-app into own its folder

This commit is contained in:
2026-03-29 23:55:24 +02:00
parent 0467a7b612
commit 8162e0444a
287 changed files with 42 additions and 34 deletions
@@ -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
})
)
);