Skip to content

Commit

Permalink
Add block swipe option to conversations list (#450)
Browse files Browse the repository at this point in the history
* Update block error messages
  • Loading branch information
aeharding authored Mar 28, 2024
1 parent b2e2931 commit 5dd332c
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 63 deletions.
32 changes: 29 additions & 3 deletions src/features/inbox/inboxSlice.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -78,6 +78,10 @@ export const inboxSlice = createSlice({
if (state.messageSyncState === "syncing") state.messageSyncState = "init";
},
resetInbox: () => initialState,
resetMessages: (state) => {
state.messageSyncState = "init";
state.messages = [];
},
},
});

Expand All @@ -88,7 +92,7 @@ export const {
setReadStatus,
receivedMessages,
resetInbox,

resetMessages,
sync,
syncComplete,
syncFail,
Expand Down Expand Up @@ -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}`;
Expand Down
127 changes: 105 additions & 22 deletions src/features/inbox/messages/ConversationItem.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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

Expand All @@ -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 (
<IonItem routerLink={`/inbox/messages/${getHandle(person)}`} detail={false}>
<div slot="start">
{unread ? <Dot /> : ""}
<StyledItemIcon item={person} size={44} />
</div>
<MessageContent>
<MessageLine>
<PersonLabel>{person.display_name ?? getHandle(person)}</PersonLabel>
<OpenDetails>
<span>
<Time date={previewMsg.private_message.published} />
</span>
<IonIcon icon={chevronForwardOutline} />
</OpenDetails>
</MessageLine>
<MessagePreview color="medium">
{previewMsg.private_message.content}
</MessagePreview>
</MessageContent>
</IonItem>
<>
<IonLoading isOpen={loading} />
<IonItemSliding>
<IonItemOptions side="end" onIonSwipe={onDelete}>
<IonItemOption color="danger" expandable onClick={onDelete}>
Block
</IonItemOption>
</IonItemOptions>

<IonItem
routerLink={`/inbox/messages/${getHandle(person)}`}
href={undefined}
draggable={false}
detail={false}
>
<div slot="start">
{unread ? <Dot /> : ""}
<StyledItemIcon item={person} size={44} />
</div>
<MessageContent>
<MessageLine>
<PersonLabel>
{person.display_name ?? getHandle(person)}
</PersonLabel>
<OpenDetails>
<span>
<Time date={previewMsg.private_message.published} />
</span>
<IonIcon icon={chevronForwardOutline} />
</OpenDetails>
</MessageLine>
<MessagePreview color="medium">
{previewMsg.private_message.content}
</MessagePreview>
</MessageContent>
</IonItem>
</IonItemSliding>
</>
);
}
11 changes: 6 additions & 5 deletions src/features/labels/links/PersonLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
8 changes: 6 additions & 2 deletions src/features/user/useUserDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
}
Expand Down
16 changes: 16 additions & 0 deletions src/features/user/userSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Person>;
Expand Down Expand Up @@ -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 =
Expand Down
18 changes: 17 additions & 1 deletion src/helpers/lemmyErrors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LemmyErrorType } from "lemmy-js-client";
import { LemmyErrorType, Person } from "lemmy-js-client";

type LemmyErrorValue = LemmyErrorType["error"];

Expand Down Expand Up @@ -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.",
);
}
5 changes: 0 additions & 5 deletions src/helpers/toastMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 5dd332c

Please sign in to comment.