Skip to content

Commit

Permalink
feat: Ability to hide posts from feeds (#63)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Harding <2166114+aeharding@users.noreply.github.com>
  • Loading branch information
burakcan and aeharding authored Jul 4, 2023
1 parent 9d3495d commit 9bfce54
Show file tree
Hide file tree
Showing 12 changed files with 607 additions and 94 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"lint:formatting": "prettier --check ."
},
"dependencies": {
"dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.6",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6",
"react-textarea-autosize": "^8.5.0",
Expand Down
7 changes: 7 additions & 0 deletions src/TabbedRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import CommunitySidebarPage from "./pages/shared/CommunitySidebarPage";
import ApolloMigratePage from "./pages/settings/ApolloMigratePage";
import PostAppearancePage from "./pages/settings/PostAppearancePage";
import ProfilePage from "./pages/profile/ProfilePage";
import ProfileFeedHiddenPostsPage from "./pages/profile/ProfileFeedHiddenPostsPage";
import { PageContextProvider } from "./features/auth/PageContext";

const Interceptor = styled.div`
Expand Down Expand Up @@ -209,6 +210,12 @@ export default function TabbedRoutes() {
<ProfileFeedItemsPage type="Comments" />
</ActorRedirect>
</Route>,
// eslint-disable-next-line react/jsx-key
<Route exact path={`/${tab}/:actor/u/:handle/hidden`}>
<ActorRedirect>
<ProfileFeedHiddenPostsPage />
</ActorRedirect>
</Route>,
];
}

Expand Down
56 changes: 49 additions & 7 deletions src/features/feed/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import React, {
ComponentType,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
import { Virtuoso, VirtuosoHandle, VirtuosoProps } from "react-virtuoso";
import {
IonRefresher,
IonRefresherContent,
RefresherCustomEvent,
useIonToast,
} from "@ionic/react";
import { LIMIT } from "../../services/lemmy";
import { LIMIT as DEFAULT_LIMIT } from "../../services/lemmy";
import { CenteredSpinner } from "../post/detail/PostDetail";
import { pullAllBy } from "lodash";
import { useSetActivePage } from "../auth/AppContext";
Expand All @@ -22,17 +23,23 @@ export type FetchFn<I> = (page: number) => Promise<I[]>;

export interface FeedProps<I> {
fetchFn: FetchFn<I>;
filterFn?: (item: I) => boolean;
getIndex?: (item: I) => number | string;
renderItemContent: (item: I) => React.ReactNode;
header?: ComponentType<{ context?: unknown }>;
limit?: number;

communityName?: string;
}

export default function Feed<I>({
fetchFn,
filterFn,
renderItemContent,
header,
communityName,
getIndex,
limit = DEFAULT_LIMIT,
}: FeedProps<I>) {
const [page, setPage] = useState(0);
const [items, setitems] = useState<I[]>([]);
Expand All @@ -41,6 +48,32 @@ export default function Feed<I>({
const [atEnd, setAtEnd] = useState(false);
const [present] = useIonToast();

const filteredItems = useMemo(
() => (filterFn ? items.filter(filterFn) : items),
[filterFn, items]
);

// Fetch more items if there are less than FETCH_MORE_THRESHOLD items left due to filtering
useEffect(() => {
const fetchMoreThreshold = limit / 2;
const currentPageItems = items.slice((page - 1) * limit, page * limit);

const currentPageFilteredItems = filteredItems.filter(
(item) => currentPageItems.indexOf(item) !== -1
);

if (
loading ||
currentPageItems.length - currentPageFilteredItems.length <
fetchMoreThreshold
)
return;

fetchMore();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filteredItems, filteredItems, items, items, page, loading]);

const virtuosoRef = useRef<VirtuosoHandle>(null);

useSetActivePage(virtuosoRef);
Expand Down Expand Up @@ -85,8 +118,8 @@ export default function Feed<I>({
} else {
setitems((existingPosts) => {
const result = [...existingPosts];
const newPosts = pullAllBy(items, existingPosts, "post.id");
result.splice(currentPage * LIMIT, LIMIT, ...newPosts);
const newPosts = pullAllBy(items.slice(), existingPosts, "post.id");
result.splice(currentPage * limit, limit, ...newPosts);
return result;
});
}
Expand All @@ -104,7 +137,15 @@ export default function Feed<I>({
}
}

if ((loading && !items.length) || loading === undefined)
// TODO looks like a Virtuoso bug where virtuoso checks if computeItemKey exists,
// not if it's not undefined (needs report)
const computeProp: Partial<VirtuosoProps<unknown, unknown>> = getIndex
? {
computeItemKey: (index) => getIndex(filteredItems[index]),
}
: {};

if ((loading && !filteredItems.length) || loading === undefined)
return <CenteredSpinner />;

return (
Expand All @@ -121,9 +162,10 @@ export default function Feed<I>({
ref={virtuosoRef}
style={{ height: "100%" }}
atTopStateChange={setIsListAtTop}
totalCount={items.length}
{...computeProp}
totalCount={filteredItems.length}
itemContent={(index) => {
const item = items[index];
const item = filteredItems[index];

return renderItemContent(item);
}}
Expand Down
27 changes: 24 additions & 3 deletions src/features/feed/PostCommentFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import FeedComment from "../comment/inFeed/FeedComment";
import { CommentView, PostView } from "lemmy-js-client";
import { useAppDispatch, useAppSelector } from "../../store";
import { css } from "@emotion/react";
import { receivedPosts } from "../post/postSlice";
import { postHiddenByIdSelector, receivedPosts } from "../post/postSlice";
import { receivedComments } from "../comment/commentSlice";
import Post from "../post/inFeed/Post";
import CommentHr from "../comment/CommentHr";
Expand All @@ -26,17 +26,20 @@ export function isComment(item: PostCommentItem): item is CommentView {
interface PostCommentFeed
extends Omit<FeedProps<PostCommentItem>, "renderItemContent"> {
communityName?: string;
filterHiddenPosts?: boolean;
}

export default function PostCommentFeed({
communityName,
fetchFn: _fetchFn,
filterHiddenPosts = true,
...rest
}: PostCommentFeed) {
const dispatch = useAppDispatch();
const postAppearanceType = useAppSelector(
(state) => state.appearance.posts.type
);
const postHiddenById = useAppSelector(postHiddenByIdSelector);

const borderCss = (() => {
switch (postAppearanceType) {
Expand Down Expand Up @@ -78,15 +81,33 @@ export default function PostCommentFeed({
async (page) => {
const items = await _fetchFn(page);

dispatch(receivedPosts(items.filter(isPost)));
/* receivedPosts needs to be awaited so that we fetch post metadatas
from the db before showing them to prevent flickering
*/
await dispatch(receivedPosts(items.filter(isPost)));
dispatch(receivedComments(items.filter(isComment)));

return items;
},
[_fetchFn, dispatch]
);

const filterFn = useCallback(
(item: PostCommentItem) => !postHiddenById[item.post.id],
[postHiddenById]
);

return (
<Feed fetchFn={fetchFn} renderItemContent={renderItemContent} {...rest} />
<Feed
fetchFn={fetchFn}
filterFn={filterHiddenPosts ? filterFn : undefined}
getIndex={(item) =>
"comment" in item
? `comment-${item.comment.id}`
: `post-${item.post.id}`
}
renderItemContent={renderItemContent}
{...rest}
/>
);
}
64 changes: 52 additions & 12 deletions src/features/post/inFeed/Post.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { PostView } from "lemmy-js-client";
import LargePost from "./large/LargePost";
import { useAppSelector } from "../../../store";
import { useAppDispatch, useAppSelector } from "../../../store";
import CompactPost from "./compact/CompactPost";
import SlidingVote from "../../shared/sliding/SlidingPostVote";
import { IonItem } from "@ionic/react";
import styled from "@emotion/styled";
import { useBuildGeneralBrowseLink } from "../../../helpers/routes";
import { getHandle } from "../../../helpers/lemmy";
import { useCallback, useEffect, useRef, useState } from "react";
import { postHiddenByIdSelector, hidePost, unhidePost } from "../postSlice";
import AnimateHeight from "react-animate-height";

const CustomIonItem = styled(IonItem)`
--padding-start: 0;
Expand All @@ -29,6 +32,31 @@ export interface PostProps {
}

export default function Post(props: PostProps) {
const dispatch = useAppDispatch();
const [shouldHide, setShouldHide] = useState(false);
const isHidden = useAppSelector(postHiddenByIdSelector)[props.post.post.id];
const hideCompleteRef = useRef(false);

const onFinishHide = useCallback(() => {
hideCompleteRef.current = true;

if (isHidden) {
dispatch(unhidePost(props.post.post.id));
} else {
dispatch(hidePost(props.post.post.id));
}
}, [dispatch, props.post.post.id, isHidden]);

useEffect(() => {
return () => {
if (!shouldHide) return;
if (hideCompleteRef.current) return;

onFinishHide();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const buildGeneralBrowseLink = useBuildGeneralBrowseLink();
const postAppearanceType = useAppSelector(
(state) => state.appearance.posts.type
Expand All @@ -44,17 +72,29 @@ export default function Post(props: PostProps) {
})();

return (
<SlidingVote item={props.post} className={props.className}>
{/* href=undefined: Prevent drag failure on firefox */}
<CustomIonItem
detail={false}
routerLink={buildGeneralBrowseLink(
`/c/${getHandle(props.post.community)}/comments/${props.post.post.id}`
)}
href={undefined}
<AnimateHeight
duration={200}
height={shouldHide ? 1 : "auto"}
onHeightAnimationEnd={onFinishHide}
>
<SlidingVote
item={props.post}
className={props.className}
onHide={() => setShouldHide(true)}
>
{postBody}
</CustomIonItem>
</SlidingVote>
{/* href=undefined: Prevent drag failure on firefox */}
<CustomIonItem
detail={false}
routerLink={buildGeneralBrowseLink(
`/c/${getHandle(props.post.community)}/comments/${
props.post.post.id
}`
)}
href={undefined}
>
{postBody}
</CustomIonItem>
</SlidingVote>
</AnimateHeight>
);
}
Loading

0 comments on commit 9bfce54

Please sign in to comment.