diff --git a/src/features/inbox/inboxSlice.ts b/src/features/inbox/inboxSlice.ts index 01e6b2442b..b190dd0073 100644 --- a/src/features/inbox/inboxSlice.ts +++ b/src/features/inbox/inboxSlice.ts @@ -1,9 +1,9 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { PayloadAction, createSelector, createSlice } from "@reduxjs/toolkit"; import { GetUnreadCountResponse, PrivateMessageView } from "lemmy-js-client"; import { AppDispatch, RootState } from "../../store"; import { logoutAccount } from "../auth/authSlice"; import { InboxItemView } from "./InboxItem"; -import { differenceBy, uniqBy } from "lodash"; +import { differenceBy, groupBy, sortBy, uniqBy } from "lodash"; import { receivedUsers } from "../user/userSlice"; import { isLemmyError } from "../../helpers/lemmyErrors"; import { @@ -78,6 +78,10 @@ export const inboxSlice = createSlice({ if (state.messageSyncState === "syncing") state.messageSyncState = "init"; }, resetInbox: () => initialState, + resetMessages: (state) => { + state.messageSyncState = "init"; + state.messages = []; + }, }, }); @@ -88,7 +92,7 @@ export const { setReadStatus, receivedMessages, resetInbox, - + resetMessages, sync, syncComplete, syncFail, @@ -206,6 +210,28 @@ export const markAllRead = 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}`; diff --git a/src/features/inbox/messages/ConversationItem.tsx b/src/features/inbox/messages/ConversationItem.tsx index 79b8639009..a4d9227246 100644 --- a/src/features/inbox/messages/ConversationItem.tsx +++ b/src/features/inbox/messages/ConversationItem.tsx @@ -1,11 +1,22 @@ import { styled } from "@linaria/react"; -import { IonIcon, IonItem } from "@ionic/react"; +import { + IonIcon, + IonItem, + IonItemOption, + IonItemOptions, + IonItemSliding, + IonLoading, + useIonAlert, +} from "@ionic/react"; import { PrivateMessageView } from "lemmy-js-client"; -import { useAppSelector } from "../../../store"; +import { useAppDispatch, useAppSelector } from "../../../store"; import { getHandle } from "../../../helpers/lemmy"; import ItemIcon from "../../labels/img/ItemIcon"; import { chevronForwardOutline } from "ionicons/icons"; import Time from "./Time"; +import { useState } from "react"; +import { clientSelector } from "../../auth/authSelectors"; +import { blockUser } from "../../user/userSlice"; const StyledItemIcon = styled(ItemIcon)` margin: 0.75rem 0; @@ -92,10 +103,14 @@ interface ConversationItemProps { } export default function ConversationItem({ messages }: ConversationItemProps) { + const dispatch = useAppDispatch(); + const [present] = useIonAlert(); + const [loading, setLoading] = useState(false); const myUserId = useAppSelector( (state) => state.site.response?.my_user?.local_user_view?.local_user?.person_id, ); + const client = useAppSelector(clientSelector); const previewMsg = messages[0]!; // presorted, newest => oldest @@ -109,26 +124,94 @@ export default function ConversationItem({ messages }: ConversationItemProps) { !msg.private_message.read && msg.private_message.creator_id !== myUserId, ); + async function onDelete() { + const theirs = messages.filter((m) => m.creator.id !== myUserId); + + const theirPotentialRecentMessage = theirs.pop(); + + if (!theirPotentialRecentMessage) { + present( + "This user hasn't messaged you, so there's nothing to block/report.", + ); + return; + } + + await present("Block and report conversation?", [ + { + text: "Just block", + role: "destructive", + handler: () => { + blockAndReportIfNeeded(theirPotentialRecentMessage); + }, + }, + { + text: "Block + Report", + role: "destructive", + handler: () => { + blockAndReportIfNeeded(theirPotentialRecentMessage, true); + }, + }, + ]); + } + + async function blockAndReportIfNeeded( + theirRecentMessage: PrivateMessageView, + report = false, + ) { + setLoading(true); + + try { + if (report) { + await client.createPrivateMessageReport({ + private_message_id: theirRecentMessage.private_message.id, + reason: "Spam or abuse", + }); + } + + dispatch(blockUser(true, theirRecentMessage.creator.id)); + } finally { + setLoading(false); + } + } + return ( - -
- {unread ? : ""} - -
- - - {person.display_name ?? getHandle(person)} - - - - - - - - {previewMsg.private_message.content} - - -
+ <> + + + + + Block + + + + +
+ {unread ? : ""} + +
+ + + + {person.display_name ?? getHandle(person)} + + + + + + + + + {previewMsg.private_message.content} + + +
+
+ ); } diff --git a/src/features/labels/links/PersonLink.tsx b/src/features/labels/links/PersonLink.tsx index e5e0d50fe9..acf0e13037 100644 --- a/src/features/labels/links/PersonLink.tsx +++ b/src/features/labels/links/PersonLink.tsx @@ -16,10 +16,8 @@ import { useIonActionSheet } from "@ionic/react"; import { removeCircleOutline } from "ionicons/icons"; import { blockUser } from "../../user/userSlice"; import useAppToast from "../../../helpers/useAppToast"; -import { - buildBlocked, - problemBlockingUser, -} from "../../../helpers/toastMessages"; +import { buildBlocked } from "../../../helpers/toastMessages"; +import { getBlockUserErrorMessage } from "../../../helpers/lemmyErrors"; const Prefix = styled.span` font-weight: normal; @@ -76,7 +74,10 @@ export default function PersonLink({ try { await dispatch(blockUser(!isBlocked, person.id)); } catch (error) { - presentToast(problemBlockingUser); + presentToast({ + color: "danger", + message: getBlockUserErrorMessage(error, person), + }); throw error; } diff --git a/src/features/user/useUserDetails.tsx b/src/features/user/useUserDetails.tsx index b2637d6ec4..26591e2fcd 100644 --- a/src/features/user/useUserDetails.tsx +++ b/src/features/user/useUserDetails.tsx @@ -3,8 +3,9 @@ import { useAppDispatch, useAppSelector } from "../../store"; import { getHandle } from "../../helpers/lemmy"; import { PageContext } from "../auth/PageContext"; import { blockUser } from "./userSlice"; -import { buildBlocked, problemBlockingUser } from "../../helpers/toastMessages"; +import { buildBlocked } from "../../helpers/toastMessages"; import useAppToast from "../../helpers/useAppToast"; +import { getBlockUserErrorMessage } from "../../helpers/lemmyErrors"; export function useUserDetails(handle: string) { const blocks = useAppSelector( @@ -26,7 +27,10 @@ export function useUserDetails(handle: string) { try { await dispatch(blockUser(!isBlocked, user.id)); } catch (error) { - presentToast(problemBlockingUser); + presentToast({ + color: "danger", + message: getBlockUserErrorMessage(error, user), + }); throw error; } diff --git a/src/features/user/userSlice.tsx b/src/features/user/userSlice.tsx index c1bb7fe70d..9fb7f8acce 100644 --- a/src/features/user/userSlice.tsx +++ b/src/features/user/userSlice.tsx @@ -6,6 +6,7 @@ import { LIMIT } from "../../services/lemmy"; import { receivedComments } from "../comment/commentSlice"; import { BanFromCommunity, Person } from "lemmy-js-client"; import { getSite } from "../auth/siteSlice"; +import { resetMessages, syncMessages } from "../inbox/inboxSlice"; interface CommentState { userByHandle: Record; @@ -75,6 +76,21 @@ export const blockUser = dispatch(receivedUsers([response.person_view.person])); await dispatch(getSite()); + + // We have synced (or are syncing) messages, AND + // - We are either unblocking (may have messages from that user), OR + // - we are blocking the user and we have messages from this user, + // so refresh is needed + if ( + getState().inbox.messageSyncState !== "init" && + (!block || + getState().inbox.messages?.find( + (msg) => msg.creator.id === id || msg.recipient.id === id, + )) + ) { + dispatch(resetMessages()); + dispatch(syncMessages()); + } }; export const banUser = diff --git a/src/helpers/lemmyErrors.ts b/src/helpers/lemmyErrors.ts index e10035db8e..d795c959c7 100644 --- a/src/helpers/lemmyErrors.ts +++ b/src/helpers/lemmyErrors.ts @@ -1,4 +1,4 @@ -import { LemmyErrorType } from "lemmy-js-client"; +import { LemmyErrorType, Person } from "lemmy-js-client"; type LemmyErrorValue = LemmyErrorType["error"]; @@ -56,3 +56,19 @@ export function getVoteErrorMessage(error: unknown): string { "Problem voting, please try again.", ); } + +export function getBlockUserErrorMessage( + error: unknown, + blockingUser: Person, +): string { + return getErrorMessage( + error, + (message) => { + switch (message) { + case "cant_block_admin": + return `${blockingUser.name} is an admin. You can't block admins.`; + } + }, + "Problem blocking user. Please try again.", + ); +} diff --git a/src/helpers/toastMessages.ts b/src/helpers/toastMessages.ts index 58744b0dfd..f9a634217b 100644 --- a/src/helpers/toastMessages.ts +++ b/src/helpers/toastMessages.ts @@ -24,11 +24,6 @@ export const allNSFWHidden: AppToastOptions = { color: "success", }; -export const problemBlockingUser: AppToastOptions = { - message: "Problem blocking user. Please try again.", - color: "danger", -}; - export const problemFetchingTitle: AppToastOptions = { message: "Unable to fetch title", color: "warning", diff --git a/src/routes/pages/inbox/MessagesPage.tsx b/src/routes/pages/inbox/MessagesPage.tsx index 1953e88031..bd9842abc6 100644 --- a/src/routes/pages/inbox/MessagesPage.tsx +++ b/src/routes/pages/inbox/MessagesPage.tsx @@ -12,11 +12,13 @@ import { import { useAppDispatch, useAppSelector } from "../../../store"; import { useEffect, useMemo, useRef, useState } from "react"; import MarkAllAsReadButton from "./MarkAllAsReadButton"; -import { groupBy, sortBy } from "lodash"; import { jwtPayloadSelector } from "../../../features/auth/authSelectors"; import ConversationItem from "../../../features/inbox/messages/ConversationItem"; import { MaxWidthContainer } from "../../../features/shared/AppContent"; -import { syncMessages } from "../../../features/inbox/inboxSlice"; +import { + conversationsByPersonIdSelector, + syncMessages, +} from "../../../features/inbox/inboxSlice"; import ComposeButton from "./ComposeButton"; import { CenteredSpinner } from "../posts/PostPage"; import { useSetActivePage } from "../../../features/auth/AppContext"; @@ -27,11 +29,15 @@ export default function MessagesPage() { const dispatch = useAppDispatch(); const messages = useAppSelector((state) => state.inbox.messages); const jwtPayload = useAppSelector(jwtPayloadSelector); - const myUserId = useAppSelector( - (state) => - state.site.response?.my_user?.local_user_view?.local_user?.person_id, - ); const [loading, setLoading] = useState(false); + const conversationsByPersonId = useAppSelector( + conversationsByPersonIdSelector, + ); + + const groupedConversations = useMemo( + () => Object.values(conversationsByPersonId), + [conversationsByPersonId], + ); useSetActivePage(pageRef); @@ -49,23 +55,6 @@ export default function MessagesPage() { } } - const messagesByCreator = useMemo( - () => - 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), - ), - [messages, myUserId], - ); - return ( @@ -95,12 +84,12 @@ export default function MessagesPage() { > - {(!messages.length && loading) || !myUserId ? ( + {!messages.length && loading ? ( ) : ( - {messagesByCreator.map((conversationMessages, index) => ( + {groupedConversations.map((conversationMessages, index) => ( ))}