From a9edb148f1ff99d10acce534ff501179d1a1d8d7 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 12 Feb 2021 17:44:32 +0100 Subject: [PATCH] Start on infinite scroll pagination --- api/graphql/generated.go | 38 ++++++++--- api/graphql/resolvers/timeline.go | 10 ++- api/graphql/schema.graphql | 2 +- ui/src/Pages/AlbumPage/AlbumPage.js | 2 +- ui/src/apolloClient.js | 10 ++- .../timelineGallery/TimelineGallery.js | 32 +++++++-- ui/src/hooks/useScrollPagination.js | 65 +++++++++++++++++++ .../{components => hooks}/useURLParameters.js | 0 8 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 ui/src/hooks/useScrollPagination.js rename ui/src/{components => hooks}/useURLParameters.js (100%) diff --git a/api/graphql/generated.go b/api/graphql/generated.go index bc1be1b9..1268a467 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -152,7 +152,7 @@ type ComplexityRoot struct { MyAlbums func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int MyMedia func(childComplexity int, filter *models.Filter) int MyMediaGeoJSON func(childComplexity int) int - MyTimeline func(childComplexity int, onlyFavorites *bool) int + MyTimeline func(childComplexity int, limit *int, offset *int, onlyFavorites *bool) int MyUser func(childComplexity int) int Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int ShareToken func(childComplexity int, token string, password *string) int @@ -272,7 +272,7 @@ type QueryResolver interface { MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error) Media(ctx context.Context, id int) (*models.Media, error) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) - MyTimeline(ctx context.Context, onlyFavorites *bool) ([]*models.TimelineGroup, error) + MyTimeline(ctx context.Context, limit *int, offset *int, onlyFavorites *bool) ([]*models.TimelineGroup, error) MyMediaGeoJSON(ctx context.Context) (interface{}, error) MapboxToken(ctx context.Context) (*string, error) ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) @@ -950,7 +950,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.MyTimeline(childComplexity, args["onlyFavorites"].(*bool)), true + return e.complexity.Query.MyTimeline(childComplexity, args["limit"].(*int), args["offset"].(*int), args["onlyFavorites"].(*bool)), true case "Query.myUser": if e.complexity.Query.MyUser == nil { @@ -1400,7 +1400,7 @@ type Query { "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! - myTimeline(onlyFavorites: Boolean): [TimelineGroup!]! + myTimeline(limit: Int, offset: Int, onlyFavorites: Boolean): [TimelineGroup!]! "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! @@ -2195,15 +2195,33 @@ func (ec *executionContext) field_Query_myMedia_args(ctx context.Context, rawArg func (ec *executionContext) field_Query_myTimeline_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 *bool - if tmp, ok := rawArgs["onlyFavorites"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyFavorites")) - arg0, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) + var arg0 *int + if tmp, ok := rawArgs["limit"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) + arg0, err = ec.unmarshalOInt2ᚖint(ctx, tmp) if err != nil { return nil, err } } - args["onlyFavorites"] = arg0 + args["limit"] = arg0 + var arg1 *int + if tmp, ok := rawArgs["offset"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("offset")) + arg1, err = ec.unmarshalOInt2ᚖint(ctx, tmp) + if err != nil { + return nil, err + } + } + args["offset"] = arg1 + var arg2 *bool + if tmp, ok := rawArgs["onlyFavorites"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyFavorites")) + arg2, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) + if err != nil { + return nil, err + } + } + args["onlyFavorites"] = arg2 return args, nil } @@ -5231,7 +5249,7 @@ func (ec *executionContext) _Query_myTimeline(ctx context.Context, field graphql fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyTimeline(rctx, args["onlyFavorites"].(*bool)) + return ec.resolvers.Query().MyTimeline(rctx, args["limit"].(*int), args["offset"].(*int), args["onlyFavorites"].(*bool)) }) if err != nil { ec.Error(ctx, err) diff --git a/api/graphql/resolvers/timeline.go b/api/graphql/resolvers/timeline.go index 5fcdb3ed..ba8819a4 100644 --- a/api/graphql/resolvers/timeline.go +++ b/api/graphql/resolvers/timeline.go @@ -9,7 +9,7 @@ import ( "gorm.io/gorm" ) -func (r *queryResolver) MyTimeline(ctx context.Context, onlyFavorites *bool) ([]*models.TimelineGroup, error) { +func (r *queryResolver) MyTimeline(ctx context.Context, limit *int, offset *int, onlyFavorites *bool) ([]*models.TimelineGroup, error) { user := auth.UserFromContext(ctx) if user == nil { return nil, auth.ErrUnauthorized @@ -33,6 +33,14 @@ func (r *queryResolver) MyTimeline(ctx context.Context, onlyFavorites *bool) ([] daysQuery.Where("media.id IN (?)", tx.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1")) } + if limit != nil { + daysQuery.Limit(*limit) + } + + if offset != nil { + daysQuery.Offset(*offset) + } + rows, err := daysQuery.Group("albums.id, YEAR(media.date_shot), MONTH(media.date_shot), DAY(media.date_shot)"). Order("media.date_shot DESC"). Rows() diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index aac782d4..4317c95b 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -44,7 +44,7 @@ type Query { "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! - myTimeline(onlyFavorites: Boolean): [TimelineGroup!]! + myTimeline(limit: Int, offset: Int, onlyFavorites: Boolean): [TimelineGroup!]! "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js index 22fd6876..157e7124 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.js @@ -4,7 +4,7 @@ import { useQuery, gql } from '@apollo/client' import AlbumGallery from '../../components/albumGallery/AlbumGallery' import PropTypes from 'prop-types' import Layout from '../../Layout' -import useURLParameters from '../../components/useURLParameters' +import useURLParameters from '../../hooks/useURLParameters' const albumQuery = gql` query albumQuery( diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.js index cbd423ca..f56eb780 100644 --- a/ui/src/apolloClient.js +++ b/ui/src/apolloClient.js @@ -5,7 +5,10 @@ import { ApolloLink, HttpLink, } from '@apollo/client' -import { getMainDefinition } from '@apollo/client/utilities' +import { + getMainDefinition, + offsetLimitPagination, +} from '@apollo/client/utilities' import { onError } from '@apollo/client/link/error' import { WebSocketLink } from '@apollo/client/link/ws' @@ -111,6 +114,11 @@ const memoryCache = new InMemoryCache({ SiteInfo: { merge: true, }, + Query: { + fields: { + myTimeline: offsetLimitPagination(['onlyFavorites']), + }, + }, }, }) diff --git a/ui/src/components/timelineGallery/TimelineGallery.js b/ui/src/components/timelineGallery/TimelineGallery.js index f6197c87..870c6f55 100644 --- a/ui/src/components/timelineGallery/TimelineGallery.js +++ b/ui/src/components/timelineGallery/TimelineGallery.js @@ -5,12 +5,13 @@ import TimelineGroupDate from './TimelineGroupDate' import styled from 'styled-components' import PresentView from '../photoGallery/presentView/PresentView' import { Loader } from 'semantic-ui-react' -import useURLParameters from '../useURLParameters' +import useURLParameters from '../../hooks/useURLParameters' import { FavoritesCheckbox } from '../AlbumFilter' +import useScrollPagination from '../../hooks/useScrollPagination' const MY_TIMELINE_QUERY = gql` - query myTimeline($onlyFavorites: Boolean) { - myTimeline(onlyFavorites: $onlyFavorites) { + query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) { + myTimeline(onlyFavorites: $onlyFavorites, limit: $limit, offset: $offset) { album { id title @@ -123,9 +124,26 @@ const TimelineGallery = () => { }) }, [activeIndex]) - const { data, error, loading, refetch } = useQuery(MY_TIMELINE_QUERY, { - variables: { - onlyFavorites, + const { data, error, loading, refetch, fetchMore } = useQuery( + MY_TIMELINE_QUERY, + { + variables: { + onlyFavorites, + offset: 0, + limit: 10, + }, + } + ) + + const { containerElem } = useScrollPagination({ + loading, + fetchMore: () => { + console.log('fetching more') + fetchMore({ + variables: { + offset: data.myTimeline.length, + }, + }) }, }) @@ -190,7 +208,7 @@ const TimelineGallery = () => { onlyFavorites={onlyFavorites} setOnlyFavorites={setOnlyFavorites} /> - {timelineGroups} + {timelineGroups} {presenting && ( { + const observer = useRef(null) + const observerElem = useRef(null) + + const reconfigureIntersectionObserver = () => { + var options = { + root: null, + rootMargin: '-100% 0px 0px 0px', + threshold: 0, + } + + // delete old observer + if (observer.current) observer.current.disconnect() + + // configure new observer + observer.current = new IntersectionObserver(entities => { + console.log('Observing', entities) + if (entities.find(x => x.isIntersecting == false)) { + console.log('load more') + fetchMore() + } + }, options) + + // activate new observer + if (observerElem.current && !loading) + observer.current.observe(observerElem.current) + } + + const containerElem = useCallback(node => { + observerElem.current = node + + // cleanup + if (observer.current != null) { + observer.current.disconnect() + } + + if (node != null) { + reconfigureIntersectionObserver() + } + }, []) + + // only observe when not loading + useEffect(() => { + if (observer.current != null) { + if (loading) { + observer.current.unobserve(observerElem.current) + } else { + observer.current.observe(observerElem.current) + } + } + }, [loading]) + + // reconfigure observer if fetchMore function changes + useEffect(() => { + reconfigureIntersectionObserver() + }, [fetchMore]) + + return { + containerElem, + } +} + +export default useScrollPagination diff --git a/ui/src/components/useURLParameters.js b/ui/src/hooks/useURLParameters.js similarity index 100% rename from ui/src/components/useURLParameters.js rename to ui/src/hooks/useURLParameters.js