import { PayloadAction, createSelector, createSlice } from "@reduxjs/toolkit"; import { GetUnreadCountResponse, PrivateMessageView } from "lemmy-js-client"; import { AppDispatch, RootState } from "../../store"; import { InboxItemView } from "./InboxItem"; import { differenceBy, groupBy, sortBy, uniqBy } from "lodash"; import { receivedUsers } from "../user/userSlice"; import { clientSelector, jwtSelector } from "../auth/authSelectors"; interface PostState { counts: { mentions: number; messages: number; replies: number; }; lastUpdatedCounts: number; readByInboxItemId: Record; messageSyncState: "init" | "syncing" | "synced"; messages: PrivateMessageView[]; } const initialState: PostState = { counts: { mentions: 0, messages: 0, replies: 0, }, lastUpdatedCounts: 0, readByInboxItemId: {}, messageSyncState: "init", messages: [], }; export const inboxSlice = createSlice({ name: "inbox", initialState, reducers: { receivedInboxCounts: ( state, action: PayloadAction, ) => { state.counts.mentions = action.payload.mentions; state.counts.messages = action.payload.private_messages; state.counts.replies = action.payload.replies; state.lastUpdatedCounts = Date.now(); }, receivedInboxItems: (state, action: PayloadAction) => { for (const item of action.payload) { state.readByInboxItemId[getInboxItemId(item)] = getInboxItemReadStatus(item); } }, setReadStatus: ( state, action: PayloadAction<{ item: InboxItemView; read: boolean }>, ) => { state.readByInboxItemId[getInboxItemId(action.payload.item)] = action.payload.read; }, setAllReadStatus: (state) => { for (const [id, read] of Object.entries(state.readByInboxItemId)) { if (read) continue; state.readByInboxItemId[id] = true; } }, receivedMessages: (state, action: PayloadAction) => { state.messages = uniqBy( [...action.payload, ...state.messages], (m) => m.private_message.id, ); }, sync: (state) => { state.messageSyncState = "syncing"; }, syncComplete: (state) => { state.messageSyncState = "synced"; }, syncFail: (state) => { if (state.messageSyncState === "syncing") state.messageSyncState = "init"; }, resetInbox: () => initialState, resetMessages: (state) => { state.messageSyncState = "init"; state.messages = []; }, }, }); // Action creators are generated for each case reducer function export const { receivedInboxCounts, receivedInboxItems, setReadStatus, receivedMessages, resetInbox, resetMessages, sync, syncComplete, syncFail, setAllReadStatus: markAllReadInCache, } = inboxSlice.actions; export default inboxSlice.reducer; export const totalUnreadSelector = (state: RootState) => state.inbox.counts.mentions + state.inbox.counts.messages + state.inbox.counts.replies; export const getInboxCounts = () => async (dispatch: AppDispatch, getState: () => RootState) => { const jwt = jwtSelector(getState()); if (!jwt) { dispatch(resetInbox()); return; } const lastUpdatedCounts = getState().inbox.lastUpdatedCounts; if (Date.now() - lastUpdatedCounts < 3_000) return; const result = await clientSelector(getState()).getUnreadCount(); if (result) dispatch(receivedInboxCounts(result)); }; export const syncMessages = () => async (dispatch: AppDispatch, getState: () => RootState) => { const jwt = jwtSelector(getState()); if (!jwt) { dispatch(resetInbox()); return; } const syncState = getState().inbox.messageSyncState; switch (syncState) { case "syncing": break; case "init": case "synced": { dispatch(sync()); let page = 1; while (true) { let privateMessages; try { const results = await clientSelector(getState()).getPrivateMessages( { limit: (() => { if (syncState === "init") return 50; // initial sync, expect many messages if (page === 1) return 1; // poll to check for new messages return 20; // detected new messages, kick off sync })(), page, }, ); privateMessages = results.private_messages; } catch (e) { dispatch(syncFail()); throw e; } const newMessages = differenceBy( privateMessages, getState().inbox.messages, (msg) => msg.private_message.id, ); dispatch(receivedMessages(privateMessages)); dispatch(receivedUsers(privateMessages.map((msg) => msg.creator))); dispatch(receivedUsers(privateMessages.map((msg) => msg.recipient))); if (!newMessages.length || page > 10) break; page++; } dispatch(syncComplete()); } } }; export const markAllRead = () => async (dispatch: AppDispatch, getState: () => RootState) => { await clientSelector(getState()).markAllAsRead(); dispatch(markAllReadInCache()); dispatch(getInboxCounts()); }; export const conversationsByPersonIdSelector = createSelector( [ (state: RootState) => state.inbox.messages, (state: RootState) => state.site.response?.my_user?.local_user_view?.local_user?.person_id, ], (messages, myUserId) => { return sortBy( Object.values( groupBy(messages, (m) => m.private_message.creator_id === myUserId ? m.private_message.recipient_id : m.private_message.creator_id, ), ).map((messages) => sortBy(messages, (m) => -Date.parse(m.private_message.published)), ), (group) => -Date.parse(group[0]!.private_message.published), ); }, ); export function getInboxItemId(item: InboxItemView): string { if ("comment_reply" in item) { return `repl_${item.comment_reply.id}`; } if ("private_message" in item) { return `dm_${item.private_message.id}`; } return `mention_${item.person_mention.id}`; } export function getInboxItemReadStatus(item: InboxItemView): boolean { if ("comment_reply" in item) { return item.comment_reply.read; } if ("private_message" in item) { return item.private_message.read; } return item.person_mention.read; } export function getInboxItemPublished(item: InboxItemView): string { if ("comment_reply" in item) { return item.comment_reply.published; } if ("private_message" in item) { return item.private_message.published; } return item.person_mention.published; } export const markRead = (item: InboxItemView, read: boolean) => async (dispatch: AppDispatch, getState: () => RootState) => { const client = clientSelector(getState()); const initialRead = !!getState().inbox.readByInboxItemId[getInboxItemId(item)]; dispatch(setReadStatus({ item, read })); try { if ("person_mention" in item) { await client.markPersonMentionAsRead({ read, person_mention_id: item.person_mention.id, }); } else if ("comment_reply" in item) { await client.markCommentReplyAsRead({ read, comment_reply_id: item.comment_reply.id, }); } else if ("private_message" in item) { await client.markPrivateMessageAsRead({ read, private_message_id: item.private_message.id, }); } } catch (error) { dispatch(setReadStatus({ item, read: initialRead })); throw error; } dispatch(getInboxCounts()); };