Skip to content

Commit

Permalink
feat: Add swipe left options (save and collapse chain) (#889)
Browse files Browse the repository at this point in the history
  • Loading branch information
gkasdorf authored Jul 29, 2023
1 parent 5aa6ac1 commit 4b8ba79
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 92 deletions.
48 changes: 24 additions & 24 deletions src/components/common/Comments/CommentItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Divider, HStack, View } from "@src/components/common/Gluestack";
import React, { useMemo } from "react";
import { Box, Divider, HStack, View } from "@src/components/common/Gluestack";
import React from "react";
import {
useSettingsStore,
useThemeOptions,
Expand All @@ -10,9 +10,7 @@ import { ILemmyVote } from "@src/types/lemmy/ILemmyVote";
import useComment from "../../../hooks/comments/useComment";
import ILemmyComment from "../../../types/lemmy/ILemmyComment";
import AvatarUsername from "../AvatarUsername";
import { ReplyOption } from "../SwipeableRow/ReplyOption";
import { SwipeableRow } from "../SwipeableRow/SwipeableRow";
import { VoteOption } from "../SwipeableRow/VoteOption";
import CommentActions from "./CommentActions";
import CommentBody from "./CommentBody";
import CommentCollapsed from "./CommentCollapsed";
Expand All @@ -24,17 +22,19 @@ import CommentWrapper from "./CommentWrapper";
interface IProps {
comment: ILemmyComment;
depth?: number;
isUnreadReply?: boolean;
voteOption?: React.ReactElement;
replyOption?: React.ReactElement;
onVote?: (value: ILemmyVote) => void;
onPress: () => unknown;
}

function CommentItem({
comment,
depth,
isUnreadReply,
onVote,
onPress,
voteOption,
replyOption,
}: IProps) {
const theme = useThemeOptions();

Expand All @@ -50,26 +50,9 @@ function CommentItem({
comment,
});

const voteOption = useMemo(
() =>
onVote ? (
<VoteOption onVote={onVote} vote={comment.comment.my_vote} />
) : undefined,
[comment.comment.comment.id, comment.comment.my_vote]
);

return (
<>
<SwipeableRow
leftOption={voteOption}
rightOption={
<ReplyOption
onReply={commentHook.onReply}
extraType={isUnreadReply ? "read" : undefined}
onExtra={isUnreadReply ? commentHook.onReadPress : undefined}
/>
}
>
<SwipeableRow leftOption={voteOption} rightOption={replyOption}>
<CommentContextMenu
isButton={false}
onPress={({ nativeEvent }) => {
Expand Down Expand Up @@ -117,6 +100,23 @@ function CommentItem({
)}
</CommentWrapper>
</CommentContextMenu>
{comment.comment.saved && (
<Box
style={{
position: "absolute",
top: 0,
right: 0,
backgroundColor: "transparent",
width: 0,
height: 0,
borderTopColor: theme.colors.bookmark,
borderTopWidth: 15,
borderLeftWidth: 15,
borderLeftColor: "transparent",
zIndex: 1,
}}
/>
)}
</SwipeableRow>
<View
style={{
Expand Down
19 changes: 12 additions & 7 deletions src/components/common/SwipeableRow/ReplyOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import { ICON_MAP } from "../../../constants/IconMap";
type Stops = [first: number, second: number];
const DEFAULT_STOPS: Stops = [-75, -150];

type Icon = "comment" | "save" | "read";
export type ReplyOptionIcon = "comment" | "Save" | "read" | "Collapse";

interface Props {
stops?: Stops;
onReply: () => unknown;
onExtra?: () => unknown;
extraType?: Icon | undefined;
extraType?: ReplyOptionIcon | undefined;
}

const buzz = () => {
Expand All @@ -41,6 +41,9 @@ const mailOpenedIcon = (
<SFIcon icon={ICON_MAP.MAIL_OPENED} color="white" size={14} />
);
const commentIcon = <SFIcon icon={ICON_MAP.REPLY} color="white" size={14} />;
const collapseIcon = (
<SFIcon icon={ICON_MAP.COLLAPSE} color="white" size={14} />
);

const screenWidth = Dimensions.get("screen").width;

Expand All @@ -54,10 +57,11 @@ export function ReplyOption({

const [firstStop, secondStop] = stops;

const secondColorMap: Record<Icon, string> = {
const secondColorMap: Record<ReplyOptionIcon, string> = {
comment: theme.colors.info,
save: theme.colors.bookmark,
Save: theme.colors.bookmark,
read: theme.colors.success,
Collapse: theme.colors.accent,
};

const secondColor = secondColorMap[extraType ?? "comment"];
Expand All @@ -74,7 +78,7 @@ export function ReplyOption({
const pulseTimer = useSharedValue(0);
const isFrozen = useSharedValue(false);
const [iconRect, setIconRect] = useState<LayoutRectangle | null>(null);
const [icon, setIcon] = useState<Icon>("comment");
const [icon, setIcon] = useState<ReplyOptionIcon>("comment");
const { subscribe, translateX } = useSwipeableRow();

useEffect(
Expand Down Expand Up @@ -212,8 +216,9 @@ export function ReplyOption({
}}
>
{(icon === "comment" && commentIcon) ||
(icon === "save" && bookmarkIcon) ||
(icon === "read" && mailOpenedIcon)}
(icon === "Save" && bookmarkIcon) ||
(icon === "read" && mailOpenedIcon) ||
(icon === "Collapse" && collapseIcon)}
</Animated.View>
</Animated.View>
</Animated.View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function CompactFeedItem({ postId }: { postId: number }) {
<ReplyOption
onReply={feedItem.doReply}
onExtra={feedItem.doSave}
extraType="save"
extraType="Save"
/>
),
[postId, postSaved]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function FeedItem({ postId, recycled }: FeedItemProps) {
<ReplyOption
onReply={feedItem.doReply}
onExtra={feedItem.doSave}
extraType="save"
extraType="Save"
/>
),
[postId, postSaved]
Expand Down
1 change: 0 additions & 1 deletion src/components/screens/Inbox/InboxScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ function InboxScreen({
<InboxReplyItem
commentId={item.comment.comment.id}
unread={inbox.topSelected === "unread"}
onPress={inbox.onCommentReplyPress}
/>
);

Expand Down
60 changes: 43 additions & 17 deletions src/components/screens/Inbox/components/InboxReplyItem.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,62 @@
import React from "react";
import CommentItem from "../../../common/Comments/CommentItem";
import React, { useCallback, useMemo } from "react";
import { useNavigation } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import useComment from "@src/hooks/comments/useComment";
import { lemmyAuthToken, lemmyInstance } from "@src/LemmyInstance";
import { addPost } from "@src/stores/posts/actions";
import { handleLemmyError } from "@src/helpers/LemmyErrorHelper";
import { ReplyOption } from "@src/components/common/SwipeableRow/ReplyOption";
import { useInboxReply } from "../../../../stores/inbox/inboxStore";
import CommentItem from "../../../common/Comments/CommentItem";

interface IProps {
commentId: number;
unread: boolean;
onPress: (postId: number, commentId: number) => Promise<void>;
}

function InboxReplyItem({ commentId, unread, onPress }: IProps) {
function InboxReplyItem({ commentId, unread }: IProps) {
const navigation = useNavigation<NativeStackNavigationProp<any>>();

const comment = useInboxReply(commentId);
const commentHook = useComment({ comment });

const onPress = useCallback(async () => {
try {
const res = await lemmyInstance.getPost({
auth: lemmyAuthToken,
id: comment.comment.post.id,
});

const onCommentPress = () => {
const commentPathArr = comment.comment.comment.path.split(".");
const postKey =
Date.now().toString() + comment.comment.post.id.toString();

if (commentPathArr.length === 2) {
onPress(comment.comment.post.id, comment.comment.comment.id).then();
} else {
onPress(
comment.comment.post.id,
Number(commentPathArr[commentPathArr.length - 2])
).then();
addPost(postKey, res.post_view, {
initialCommentId: comment.comment.comment.id,
});

navigation.push("Post", { postKey });
} catch (e) {
handleLemmyError(e.toString());
}
};
}, [comment]);

const replyOption = useMemo(
() => (
<ReplyOption
onReply={commentHook.onReply}
extraType={unread ? "read" : undefined}
onExtra={unread ? commentHook.onReadPress : undefined}
/>
),
[comment.comment.comment.id]
);

return (
<CommentItem
comment={comment}
isUnreadReply={unread}
onPress={onCommentPress}
depth={2}
onVote={null}
onPress={onPress}
replyOption={replyOption}
/>
);
}
Expand Down
89 changes: 51 additions & 38 deletions src/components/screens/Post/components/PostCommentItem.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import React, { useCallback } from "react";
import React, { useCallback, useMemo } from "react";
import { useRoute } from "@react-navigation/core";
import { onGenericHapticFeedback } from "@src/helpers/HapticFeedbackHelpers";
import { VoteOption } from "@src/components/common/SwipeableRow/VoteOption";
import {
ReplyOption,
ReplyOptionIcon,
} from "@src/components/common/SwipeableRow/ReplyOption";
import useComment from "@src/hooks/comments/useComment";
import setCollapsed from "@src/stores/posts/actions/setCollapsed";
import { useSettingsStore } from "@src/stores/settings/settingsStore";
import CommentItem from "../../../common/Comments/CommentItem";
import setPostCommentVote from "../../../../stores/posts/actions/setPostCommentVote";
import { ILemmyVote } from "../../../../types/lemmy/ILemmyVote";
import { determineVotes } from "../../../../helpers/VoteHelper";
import {
usePostComment,
usePostsStore,
} from "../../../../stores/posts/postsStore";
import { usePostComment } from "../../../../stores/posts/postsStore";

interface IProps {
commentId: number;
Expand All @@ -17,6 +22,11 @@ interface IProps {
function PostCommentItem({ commentId }: IProps) {
const { postKey } = useRoute<any>().params;
const comment = usePostComment(postKey, commentId);
const commentHook = useComment({ comment });

const swipeLeftSecond = useSettingsStore(
(state) => state.settings.commentSwipeLeftSecond
);

const onVote = useCallback(
(value: ILemmyVote) => {
Expand All @@ -35,43 +45,46 @@ function PostCommentItem({ commentId }: IProps) {
const onPress = useCallback(() => {
onGenericHapticFeedback();

usePostsStore.setState((state) => {
const prev = state.posts.get(postKey);
const prevComment = prev.commentsState.comments.find(
(c) => c.comment.comment.id === commentId
);
prevComment.collapsed = !prevComment.collapsed;
prev.rerenderComments = !prev.rerenderComments;
setCollapsed(postKey, commentId);
}, [comment.comment.comment.id]);

const prevToHide = prev.commentsState.comments.filter(
(c) =>
c.comment.comment.path.includes(prevComment.comment.comment.path) &&
c.comment.comment.id !== commentId
);
const voteOption = useMemo(
() => <VoteOption onVote={onVote} vote={comment.comment.my_vote} />,
[comment.comment.comment.id, comment.comment.my_vote]
);

if (!prevComment.collapsed) {
prevToHide.forEach((c) => {
const shouldUnhide =
prevToHide.findIndex(
(cc) =>
cc.collapsed &&
c.comment.comment.path.includes(cc.comment.comment.path) &&
c.comment.comment.id !== cc.comment.comment.id
) === -1;
const onSwipeLeftSecond = useCallback(() => {
if (swipeLeftSecond === "Collapse") {
commentHook.onCollapseChain();
} else if (swipeLeftSecond === "Save") {
commentHook.onSave().then();
}
}, [swipeLeftSecond]);

if (shouldUnhide) {
c.hidden = false;
}
});
} else {
prevToHide.forEach((c) => {
c.hidden = true;
});
}
});
}, [comment.comment.comment.id]);
const replyOption = useMemo(
() => (
<ReplyOption
onReply={commentHook.onReply}
extraType={
swipeLeftSecond !== "None"
? (swipeLeftSecond as ReplyOptionIcon)
: undefined
}
onExtra={swipeLeftSecond !== "None" ? onSwipeLeftSecond : undefined}
/>
),
[comment.comment.comment.id, comment.comment.my_vote, swipeLeftSecond]
);

return <CommentItem comment={comment} onVote={onVote} onPress={onPress} />;
return (
<CommentItem
comment={comment}
onVote={onVote}
onPress={onPress}
voteOption={voteOption}
replyOption={replyOption}
/>
);
}

export default React.memo(PostCommentItem);
Loading

0 comments on commit 4b8ba79

Please sign in to comment.