A social knowledge tool for researchers built on ATProto
45
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'development' into feature/personal-url-cards

+124 -75
+47
src/webapp/components/contentDisplay/infiniteScroll/InfiniteScroll.tsx
··· 1 + 'use client'; 2 + 3 + import { ReactNode, useEffect, startTransition, useRef } from 'react'; 4 + import { Center, Button, Stack } from '@mantine/core'; 5 + import { useIntersection } from '@mantine/hooks'; 6 + 7 + interface Props { 8 + children: ReactNode; 9 + dataLength: number; 10 + hasMore: boolean; 11 + isInitialLoading: boolean; 12 + isLoading: boolean; 13 + loadMore: () => void; 14 + loader?: ReactNode; 15 + manualLoadButton?: boolean; 16 + } 17 + 18 + export default function InfiniteScroll(props: Props) { 19 + const containerRef = useRef<HTMLDivElement>(null); 20 + const { ref, entry } = useIntersection({ 21 + root: containerRef.current, 22 + threshold: 0, 23 + }); 24 + 25 + useEffect(() => { 26 + startTransition(() => { 27 + if (entry?.isIntersecting && props.hasMore && !props.isLoading) { 28 + props.loadMore(); 29 + } 30 + }); 31 + }, [entry?.isIntersecting, props.hasMore, props.isLoading, props.loadMore]); 32 + 33 + return ( 34 + <Stack> 35 + {props.children} 36 + {props.isLoading && props.loader} 37 + 38 + <Center ref={ref}> 39 + {!props.isLoading && props.hasMore && props.manualLoadButton && ( 40 + <Button loading={props.isLoading} onClick={props.loadMore}> 41 + Load more 42 + </Button> 43 + )} 44 + </Center> 45 + </Stack> 46 + ); 47 + }
+1 -1
src/webapp/components/navigation/navbar/Navbar.tsx
··· 30 30 const { data: profile } = useMyProfile(); 31 31 32 32 return ( 33 - <AppShellNavbar px={'xs'} pb={'md'} pt={'xs'} style={{ zIndex: 3 }}> 33 + <AppShellNavbar p={'xs'} style={{ zIndex: 3 }}> 34 34 <Group justify="space-between" ml={'sm'}> 35 35 <Anchor component={Link} href={'/home'}> 36 36 <Image src={SembleLogo.src} alt="Semble logo" w={20.84} h={28} />
+2 -1
src/webapp/features/cards/components/addCardToModal/CardToBeAddedPreview.tsx
··· 16 16 import { LuLibrary } from 'react-icons/lu'; 17 17 import { getDomain } from '@/lib/utils/link'; 18 18 import useMyProfile from '@/features/profile/lib/queries/useMyProfile'; 19 + import { getRecordKey } from '@/lib/utils/atproto'; 19 20 20 21 interface Props { 21 22 cardId: string; ··· 95 96 <Menu.Item 96 97 key={c.id} 97 98 component={Link} 98 - href={`/profile/${c.createdBy.handle}/collections/${c.id}`} 99 + href={`/profile/${c.createdBy.handle}/collections/${getRecordKey(c.uri!)}`} 99 100 target="_blank" 100 101 c="blue" 101 102 fw={600}
+11 -1
src/webapp/features/cards/components/urlCard/UrlCard.tsx
··· 8 8 Group, 9 9 Anchor, 10 10 AspectRatio, 11 + Skeleton, 11 12 } from '@mantine/core'; 12 13 import Link from 'next/link'; 13 14 import UrlCardActions from '../urlCardActions/UrlCardActions'; ··· 69 70 )} 70 71 </Group> 71 72 72 - <Suspense> 73 + <Suspense 74 + fallback={ 75 + <Group justify="space-between"> 76 + <Group gap={'xs'}> 77 + <Skeleton w={22} h={22} /> 78 + </Group> 79 + <Skeleton w={22} h={22} /> 80 + </Group> 81 + } 82 + > 73 83 <UrlCardActions 74 84 cardContent={props.cardContent} 75 85 id={props.id}
+30 -35
src/webapp/features/cards/containers/cardsContainer/CardsContainer.tsx
··· 1 1 'use client'; 2 2 3 - import { Container, Grid, Stack, Button, Text, Center } from '@mantine/core'; 3 + import { Center, Container, Grid, Loader, Stack } from '@mantine/core'; 4 4 import useCards from '../../lib/queries/useCards'; 5 5 import UrlCard from '@/features/cards/components/urlCard/UrlCard'; 6 6 import CardsContainerError from './Error.CardsContainer'; 7 7 import CardsContainerSkeleton from './Skeleton.CardsContainer'; 8 - import { Fragment, useState } from 'react'; 9 8 import ProfileEmptyTab from '@/features/profile/components/profileEmptyTab/ProfileEmptyTab'; 10 9 import { FaRegNoteSticky } from 'react-icons/fa6'; 10 + import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; 11 11 12 12 interface Props { 13 13 handle: string; ··· 43 43 44 44 return ( 45 45 <Container p="xs" size="xl"> 46 - <Stack> 47 - <Fragment> 48 - <Grid gutter="md"> 49 - {allCards.map((card) => ( 50 - <Grid.Col key={card.id} span={{ base: 12, xs: 6, sm: 4, lg: 3 }}> 51 - <UrlCard 52 - id={card.id} 53 - url={card.url} 54 - cardContent={card.cardContent} 55 - note={card.note} 56 - collections={card.collections} 57 - authorHandle={props.handle} 58 - /> 59 - </Grid.Col> 60 - ))} 61 - </Grid> 62 - 63 - {hasNextPage && ( 64 - <Center> 65 - <Button 66 - onClick={() => fetchNextPage()} 67 - disabled={isFetchingNextPage} 68 - loading={isFetchingNextPage} 69 - variant="light" 70 - color="gray" 71 - mt="md" 72 - > 73 - Load More 74 - </Button> 75 - </Center> 76 - )} 77 - </Fragment> 78 - </Stack> 46 + <InfiniteScroll 47 + dataLength={allCards.length} 48 + hasMore={!!hasNextPage} 49 + isInitialLoading={isPending} 50 + isLoading={isFetchingNextPage} 51 + loadMore={fetchNextPage} 52 + manualLoadButton={false} 53 + loader={ 54 + <Center> 55 + <Loader /> 56 + </Center> 57 + } 58 + > 59 + <Grid gutter="md"> 60 + {allCards.map((card) => ( 61 + <Grid.Col key={card.id} span={{ base: 12, xs: 6, sm: 4, lg: 3 }}> 62 + <UrlCard 63 + id={card.id} 64 + url={card.url} 65 + cardContent={card.cardContent} 66 + note={card.note} 67 + collections={card.collections} 68 + authorHandle={props.handle} 69 + /> 70 + </Grid.Col> 71 + ))} 72 + </Grid> 73 + </InfiniteScroll> 79 74 </Container> 80 75 ); 81 76 }
+12 -18
src/webapp/features/collections/containers/collectionsContainer/CollectionsContainer.tsx
··· 1 1 'use client'; 2 2 3 - import { Button, Container, Stack, SimpleGrid, Center } from '@mantine/core'; 3 + import { Button, Container, Stack, SimpleGrid } from '@mantine/core'; 4 4 import useCollections from '../../lib/queries/useCollections'; 5 5 import CollectionCard from '../../components/collectionCard/CollectionCard'; 6 6 import CreateCollectionDrawer from '../../components/createCollectionDrawer/CreateCollectionDrawer'; 7 7 import { useState } from 'react'; 8 8 import ProfileEmptyTab from '@/features/profile/components/profileEmptyTab/ProfileEmptyTab'; 9 9 import { BiCollection } from 'react-icons/bi'; 10 + import InfiniteLoadTrigger from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; 10 11 11 12 interface Props { 12 13 handle: string; ··· 32 33 return ( 33 34 <Container p="xs" size="xl"> 34 35 <Stack> 35 - <Stack> 36 + <InfiniteLoadTrigger 37 + dataLength={collections.length} 38 + hasMore={!!hasNextPage} 39 + isInitialLoading={false} // or you can manage initial loading state if needed 40 + isLoading={isFetchingNextPage} 41 + loadMore={fetchNextPage} 42 + manualLoadButton={false} // automatic infinite scroll; set true if you want manual button 43 + loader={<div>Loading...</div>} // replace with your loader component if you want 44 + > 36 45 <SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md"> 37 46 {collections.map((collection) => ( 38 47 <CollectionCard key={collection.id} collection={collection} /> 39 48 ))} 40 49 </SimpleGrid> 41 - 42 - {hasNextPage && ( 43 - <Center> 44 - <Button 45 - onClick={() => fetchNextPage()} 46 - disabled={isFetchingNextPage} 47 - loading={isFetchingNextPage} 48 - variant="light" 49 - color="gray" 50 - mt="md" 51 - > 52 - Load More 53 - </Button> 54 - </Center> 55 - )} 56 - </Stack> 50 + </InfiniteLoadTrigger> 57 51 </Stack> 58 52 59 53 <CreateCollectionDrawer
+21 -19
src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
··· 2 2 3 3 import useMyFeed from '@/features/feeds/lib/queries/useMyFeed'; 4 4 import FeedItem from '@/features/feeds/components/feedItem/FeedItem'; 5 - import { Button, Stack, Title, Text, Center, Container } from '@mantine/core'; 5 + import { Stack, Title, Text, Center, Container, Loader } from '@mantine/core'; 6 6 import MyFeedContainerSkeleton from './Skeleton.MyFeedContainer'; 7 7 import MyFeedContainerError from './Error.MyFeedContainer'; 8 + import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; 8 9 9 10 export default function MyFeedContainer() { 10 11 const { ··· 39 40 </Text> 40 41 </Center> 41 42 ) : ( 42 - <Stack gap={'xl'} mx={'auto'} maw={600}> 43 - <Stack gap={60}> 44 - {allActivities.map((item, i) => ( 45 - <FeedItem key={item.id} item={item} /> 46 - ))} 47 - </Stack> 48 - 49 - {hasNextPage && ( 43 + <InfiniteScroll 44 + dataLength={allActivities.length} 45 + hasMore={!!hasNextPage} 46 + isInitialLoading={isPending} 47 + isLoading={isFetchingNextPage} 48 + loadMore={fetchNextPage} 49 + manualLoadButton={false} 50 + loader={ 50 51 <Center> 51 - <Button 52 - onClick={() => fetchNextPage()} 53 - loading={isFetchingNextPage} 54 - variant="light" 55 - color="gray" 56 - > 57 - Load More 58 - </Button> 52 + <Loader /> 59 53 </Center> 60 - )} 61 - </Stack> 54 + } 55 + > 56 + <Stack gap={'xl'} mx={'auto'} maw={600}> 57 + <Stack gap={60}> 58 + {allActivities.map((item) => ( 59 + <FeedItem key={item.id} item={item} /> 60 + ))} 61 + </Stack> 62 + </Stack> 63 + </InfiniteScroll> 62 64 )} 63 65 </Stack> 64 66 </Container>