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) => (
))}