Add date filter to timeline + fix #302

This commit is contained in:
viktorstrate
2021-09-25 17:11:54 +02:00
parent 2c0baa6bdf
commit 69bf8a01c4
16 changed files with 354 additions and 200 deletions

View File

@@ -0,0 +1,22 @@
package actions
import (
"github.com/photoview/photoview/api/graphql/models"
"gorm.io/gorm"
)
func MyMedia(db *gorm.DB, user *models.User, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
if err := user.FillAlbums(db); err != nil {
return nil, err
}
query := db.Where("media.album_id IN (SELECT user_albums.album_id FROM user_albums WHERE user_albums.user_id = ?)", user.ID)
query = models.FormatSQL(query, order, paginate)
var media []*models.Media
if err := query.Find(&media).Error; err != nil {
return nil, err
}
return media, nil
}

View File

@@ -0,0 +1,88 @@
package actions_test
import (
"testing"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/graphql/models/actions"
"github.com/photoview/photoview/api/test_utils"
"github.com/stretchr/testify/assert"
)
func TestMyMedia(t *testing.T) {
db := test_utils.DatabaseTest(t)
password := "1234"
user, err := models.RegisterUser(db, "user", &password, false)
assert.NoError(t, err)
rootAlbum := models.Album{
Title: "root",
Path: "/photos",
}
assert.NoError(t, db.Save(&rootAlbum).Error)
childAlbum := models.Album{
Title: "subalbum",
Path: "/photos/subalbum",
ParentAlbumID: &rootAlbum.ID,
}
assert.NoError(t, db.Save(&childAlbum).Error)
assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum))
assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum))
media := []models.Media{
{
Title: "pic1",
Path: "/photos/pic1",
AlbumID: rootAlbum.ID,
},
{
Title: "pic2",
Path: "/photos/pic2",
AlbumID: rootAlbum.ID,
},
{
Title: "pic3",
Path: "/photos/subalbum/pic3",
AlbumID: childAlbum.ID,
},
{
Title: "pic4",
Path: "/photos/subalbum/pic4",
AlbumID: childAlbum.ID,
},
}
assert.NoError(t, db.Save(&media).Error)
anotherUser, err := models.RegisterUser(db, "user2", &password, false)
assert.NoError(t, err)
anotherAlbum := models.Album{
Title: "AnotherAlbum",
Path: "/another",
}
assert.NoError(t, db.Save(&anotherAlbum).Error)
anotherMedia := models.Media{
Title: "anotherPic",
Path: "/another/anotherPic",
AlbumID: anotherAlbum.ID,
}
assert.NoError(t, db.Save(&anotherMedia).Error)
assert.NoError(t, db.Model(&anotherUser).Association("Albums").Append(&anotherAlbum))
t.Run("Simple query", func(t *testing.T) {
myMedia, err := actions.MyMedia(db, user, nil, nil)
assert.NoError(t, err)
assert.Len(t, myMedia, 4)
})
}

View File

@@ -8,6 +8,7 @@ import (
api "github.com/photoview/photoview/api/graphql"
"github.com/photoview/photoview/api/graphql/auth"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/graphql/models/actions"
"github.com/photoview/photoview/api/scanner/face_detection"
"github.com/pkg/errors"
"gorm.io/gorm/clause"
@@ -19,29 +20,7 @@ func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, pag
return nil, errors.New("unauthorized")
}
if err := user.FillAlbums(r.Database); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
var media []*models.Media
query := r.Database.
Joins("Album").
Where("albums.id IN (?)", userAlbumIDs).
Where("media.id IN (?)", r.Database.Model(&models.MediaURL{}).Select("id").Where("media_url.media_id = media.id"))
query = models.FormatSQL(query, order, paginate)
if err := query.Find(&media).Error; err != nil {
return nil, err
}
return media, nil
return actions.MyMedia(r.Database, user, order, paginate)
}
func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) {

View File

@@ -26,6 +26,10 @@ func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Paginat
query = query.Where("media.date_shot < ?", fromDate)
}
if onlyFavorites != nil && *onlyFavorites == true {
query = query.Where("media.id IN (?)", r.Database.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
}
query = models.FormatSQL(query, nil, paginate)
var media []*models.Media
@@ -35,127 +39,3 @@ func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Paginat
return media, nil
}
// func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool) ([]*models.TimelineGroup, error) {
// user := auth.UserFromContext(ctx)
// if user == nil {
// return nil, auth.ErrUnauthorized
// }
// var timelineGroups []*models.TimelineGroup
// transactionError := r.Database.Transaction(func(tx *gorm.DB) error {
// // album_id, year, month, day
// daysQuery := tx.Select(
// "albums.id AS album_id",
// fmt.Sprintf("%s AS year", database.DateExtract(tx, database.DateCompYear, "media.date_shot")),
// fmt.Sprintf("%s AS month", database.DateExtract(tx, database.DateCompMonth, "media.date_shot")),
// fmt.Sprintf("%s AS day", database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
// ).
// Table("media").
// Joins("JOIN albums ON media.album_id = albums.id").
// Where("albums.id IN (?)", tx.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID))
// if onlyFavorites != nil && *onlyFavorites == true {
// 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 paginate != nil {
// if paginate.Limit != nil {
// daysQuery.Limit(*paginate.Limit)
// }
// if paginate.Offset != nil {
// daysQuery.Offset(*paginate.Offset)
// }
// }
// rows, err := daysQuery.Group("albums.id").Group(
// fmt.Sprintf("%s, %s, %s",
// database.DateExtract(tx, database.DateCompYear, "media.date_shot"),
// database.DateExtract(tx, database.DateCompMonth, "media.date_shot"),
// database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
// ).
// Order(
// fmt.Sprintf("%s DESC, %s DESC, %s DESC",
// database.DateExtract(tx, database.DateCompYear, "media.date_shot"),
// database.DateExtract(tx, database.DateCompMonth, "media.date_shot"),
// database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
// ).Rows()
// defer rows.Close()
// if err != nil {
// return err
// }
// type group struct {
// albumID int
// year int
// month int
// day int
// }
// dbGroups := make([]group, 0)
// for rows.Next() {
// var g group
// rows.Scan(&g.albumID, &g.year, &g.month, &g.day)
// dbGroups = append(dbGroups, g)
// }
// timelineGroups = make([]*models.TimelineGroup, len(dbGroups))
// for i, group := range dbGroups {
// // Fill album
// var groupAlbum models.Album
// if err := tx.First(&groupAlbum, group.albumID).Error; err != nil {
// return err
// }
// // Fill media
// var groupMedia []*models.Media
// mediaQuery := tx.Model(&models.Media{}).
// Where("album_id = ?", group.albumID).
// Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompYear, "media.date_shot")), group.year).
// Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompMonth, "media.date_shot")), group.month).
// Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompDay, "media.date_shot")), group.day).
// Order("date_shot DESC")
// if onlyFavorites != nil && *onlyFavorites == true {
// mediaQuery.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 err := mediaQuery.Limit(5).Find(&groupMedia).Error; err != nil {
// return err
// }
// // Get total media count
// var totalMedia int64
// if err := mediaQuery.Count(&totalMedia).Error; err != nil {
// return err
// }
// var date time.Time = groupMedia[0].DateShot
// date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
// timelineGroup := models.TimelineGroup{
// Album: &groupAlbum,
// Media: groupMedia,
// MediaTotal: int(totalMedia),
// Date: date,
// }
// timelineGroups[i] = &timelineGroup
// }
// return nil
// })
// if transactionError != nil {
// return nil, transactionError
// }
// return timelineGroups, nil
// }

View File

@@ -3,16 +3,16 @@ import Layout from '../../components/layout/Layout'
import { useTranslation } from 'react-i18next'
import TimelineGallery from '../../components/timelineGallery/TimelineGallery'
const PhotosPage = () => {
const TimelinePage = () => {
const { t } = useTranslation()
return (
<>
<Layout title={t('photos_page.title', 'Photos')}>
<Layout title={t('photos_page.title', 'Timeline')}>
<TimelineGallery />
</Layout>
</>
)
}
export default PhotosPage
export default TimelinePage

View File

@@ -10,7 +10,7 @@ import { ReactComponent as DirectionIcon } from './icons/direction-arrow.svg'
import Dropdown from '../../primitives/form/Dropdown'
type FavoriteCheckboxProps = {
export type FavoriteCheckboxProps = {
onlyFavorites: boolean
setOnlyFavorites(favorites: boolean): void
}
@@ -79,7 +79,7 @@ const SortingOptions = ({ setOrdering, ordering }: SortingOptionsProps) => {
<fieldset>
<legend id="filter_group_sort-label" className="inline-block mb-1">
<SortingIcon
className="inline-block align-baseline mr-1"
className="inline-block align-baseline mr-1 mt-1"
aria-hidden="true"
/>
<span>{t('album_filter.sort', 'Sort')}</span>

View File

@@ -80,7 +80,7 @@ export const MainMenu = () => {
<div className="fixed w-full bottom-0 lg:bottom-auto lg:top-[84px] z-30 bg-white shadow-separator lg:shadow-none lg:w-[240px] lg:ml-8 lg:mr-5 flex-shrink-0">
<ul className="flex justify-around py-2 px-2 max-w-lg mx-auto lg:flex-col lg:p-0">
<MenuButton
to="/photos"
to="/timeline"
exact
label={t('sidemenu.photos', 'Timeline')}
background="#8ac5f4"

View File

@@ -12,7 +12,9 @@ const AlbumsPage = React.lazy(
() => import('../../Pages/AllAlbumsPage/AlbumsPage')
)
const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage'))
const PhotosPage = React.lazy(() => import('../../Pages/PhotosPage/PhotosPage'))
const TimelinePage = React.lazy(
() => import('../../Pages/TimelinePage/TimelinePage')
)
const PlacesPage = React.lazy(() => import('../../Pages/PlacesPage/PlacesPage'))
const SharePage = React.lazy(() => import('../../Pages/SharePage/SharePage'))
const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage'))
@@ -49,11 +51,17 @@ const Routes = () => {
<Route path="/share" component={SharePage} />
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
<AuthorizedRoute path="/photos" component={PhotosPage} />
<AuthorizedRoute path="/timeline" component={TimelinePage} />
<AuthorizedRoute path="/places" component={PlacesPage} />
<AuthorizedRoute path="/people/:person?" component={PeoplePage} />
<AuthorizedRoute path="/settings" component={SettingsPage} />
<Route path="/" exact render={() => <Redirect to="/photos" />} />
<Route path="/" exact render={() => <Redirect to="/timeline" />} />
{/* For backwards compatibility */}
<Route
path="/photos"
exact
render={() => <Redirect to="/timeline" />}
/>
<Route
render={() => (
<div>{t('routes.page_not_found', 'Page not found')}</div>

View File

@@ -0,0 +1,103 @@
import { useQuery } from '@apollo/client'
import gql from 'graphql-tag'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown'
import { FavoriteCheckboxProps, FavoritesCheckbox } from '../album/AlbumFilter'
import { ReactComponent as DateIcon } from './icons/date.svg'
import { earliestMedia } from './__generated__/earliestMedia'
const EARLIEST_MEDIA_QUERY = gql`
query earliestMedia {
myMedia(
order: { order_by: "date_shot", order_direction: ASC }
paginate: { limit: 1 }
) {
id
date
}
}
`
type DateSelectorProps = {
filterDate: string | null
setFilterDate(date: string | null): void
}
const DateSelector = ({ filterDate, setFilterDate }: DateSelectorProps) => {
const { t } = useTranslation()
const { data, loading } = useQuery<earliestMedia>(EARLIEST_MEDIA_QUERY)
let items: DropdownItem[] = [
{
value: 'all',
label: t('timeline_filter.date.dropdown_all', 'From today'),
},
]
if (data) {
const dateStr = data.myMedia[0].date
const date = new Date(dateStr)
const now = new Date()
const currentYear = now.getFullYear()
const earliestYear = date.getFullYear()
const years: number[] = []
for (let i = currentYear - 1; i >= earliestYear; i--) {
years.push(i)
}
const yearItems = years.map<DropdownItem>(x => ({
value: `${x}`,
label: `${x} and earlier`,
}))
items = [...items, ...yearItems]
}
return (
<fieldset>
<legend id="filter_group_date-label" className="inline-block mb-1">
<DateIcon
className="inline-block align-baseline mr-1"
aria-hidden="true"
/>
<span>{t('timeline_filter.date.label', 'Date')}</span>
</legend>
<div>
<Dropdown
aria-labelledby="filter_group_date-label"
setSelected={date =>
date == 'all' ? setFilterDate(null) : setFilterDate(date)
}
value={filterDate || 'all'}
items={items}
disabled={loading}
/>
</div>
</fieldset>
)
}
type TimelineFiltersProps = DateSelectorProps & FavoriteCheckboxProps
const TimelineFilters = ({
onlyFavorites,
setOnlyFavorites,
filterDate,
setFilterDate,
}: TimelineFiltersProps) => {
return (
<div className="flex items-end gap-4 flex-wrap mb-4">
<DateSelector filterDate={filterDate} setFilterDate={setFilterDate} />
<FavoritesCheckbox
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
/>
</div>
)
}
export default TimelineFilters

View File

@@ -4,7 +4,6 @@ import { useQuery, gql } from '@apollo/client'
import TimelineGroupDate from './TimelineGroupDate'
import PresentView from '../photoGallery/presentView/PresentView'
import useURLParameters from '../../hooks/useURLParameters'
import { FavoritesCheckbox } from '../album/AlbumFilter'
import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../PaginateLoader'
import { useTranslation } from 'react-i18next'
@@ -20,11 +19,19 @@ import {
import MediaSidebar from '../sidebar/MediaSidebar'
import { SidebarContext } from '../sidebar/Sidebar'
import { urlPresentModeSetupHook } from '../photoGallery/photoGalleryReducer'
import TimelineFilters from './TimelineFilters'
import client from '../../apolloClient'
const MY_TIMELINE_QUERY = gql`
query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) {
query myTimeline(
$onlyFavorites: Boolean
$limit: Int
$offset: Int
$fromDate: Time
) {
myTimeline(
onlyFavorites: $onlyFavorites
fromDate: $fromDate
paginate: { limit: $limit, offset: $offset }
) {
id
@@ -77,7 +84,10 @@ const TimelineGallery = () => {
const onlyFavorites = getParam('favorites') == '1' ? true : false
const setOnlyFavorites = (favorites: boolean) =>
setParam('favorites', favorites ? '1' : '0')
setParam('favorites', favorites ? '1' : null)
const filterDate = getParam('date')
const setFilterDate = (x: string) => setParam('date', x)
const favoritesNeedsRefresh = useRef(false)
@@ -97,6 +107,9 @@ const TimelineGallery = () => {
>(MY_TIMELINE_QUERY, {
variables: {
onlyFavorites,
fromDate: filterDate
? `${parseInt(filterDate) + 1}-01-01T00:00:00Z`
: undefined,
offset: 0,
limit: 200,
},
@@ -126,6 +139,20 @@ const TimelineGallery = () => {
}
}, [mediaState.activeIndex])
useEffect(() => {
;(async () => {
await client.resetStore()
await refetch({
onlyFavorites,
fromDate: filterDate
? `${parseInt(filterDate) + 1}-01-01T00:00:00Z`
: undefined,
offset: 0,
limit: 200,
})
})()
}, [filterDate])
urlPresentModeSetupHook({
dispatchMedia,
openPresentMode: event => {
@@ -158,12 +185,12 @@ const TimelineGallery = () => {
return (
<div className="overflow-x-hidden">
<div className="mb-2">
<FavoritesCheckbox
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
/>
</div>
<TimelineFilters
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
filterDate={filterDate}
setFilterDate={setFilterDate}
/>
<div className="-mx-3 flex flex-wrap" ref={containerElem}>
{timelineGroups}
</div>

View File

@@ -73,7 +73,7 @@ const TimelineGroupAlbum = ({
<Link to={`/album/${albumID}`} className="hover:underline">
{albumTitle}
</Link>
<div className="flex flex-wrap items-center relative -mx-1 pr-4 overflow-hidden">
<div className="flex flex-wrap items-center relative -mx-1 overflow-hidden">
{mediaElms}
<PhotoFiller />
</div>

View File

@@ -0,0 +1,24 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: earliestMedia
// ====================================================
export interface earliestMedia_myMedia {
__typename: 'Media'
id: string
/**
* The date the image was shot or the date it was imported as a fallback
*/
date: any
}
export interface earliestMedia {
/**
* List of media owned by the logged in user
*/
myMedia: earliestMedia_myMedia[]
}

View File

@@ -3,95 +3,96 @@
// @generated
// This file was automatically generated and should not be edited.
import { MediaType } from "./../../../__generated__/globalTypes";
import { MediaType } from './../../../__generated__/globalTypes'
// ====================================================
// GraphQL query operation: myTimeline
// ====================================================
export interface myTimeline_myTimeline_thumbnail {
__typename: "MediaURL";
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string;
url: string
/**
* Width of the image in pixels
*/
width: number;
width: number
/**
* Height of the image in pixels
*/
height: number;
height: number
}
export interface myTimeline_myTimeline_highRes {
__typename: "MediaURL";
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string;
url: string
/**
* Width of the image in pixels
*/
width: number;
width: number
/**
* Height of the image in pixels
*/
height: number;
height: number
}
export interface myTimeline_myTimeline_videoWeb {
__typename: "MediaURL";
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string;
url: string
}
export interface myTimeline_myTimeline_album {
__typename: "Album";
id: string;
title: string;
__typename: 'Album'
id: string
title: string
}
export interface myTimeline_myTimeline {
__typename: "Media";
id: string;
title: string;
type: MediaType;
__typename: 'Media'
id: string
title: string
type: MediaType
/**
* URL to display the media in a smaller resolution
*/
thumbnail: myTimeline_myTimeline_thumbnail | null;
thumbnail: myTimeline_myTimeline_thumbnail | null
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: myTimeline_myTimeline_highRes | null;
highRes: myTimeline_myTimeline_highRes | null
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: myTimeline_myTimeline_videoWeb | null;
favorite: boolean;
videoWeb: myTimeline_myTimeline_videoWeb | null
favorite: boolean
/**
* The album that holds the media
*/
album: myTimeline_myTimeline_album;
album: myTimeline_myTimeline_album
/**
* The date the image was shot or the date it was imported as a fallback
*/
date: any;
date: any
}
export interface myTimeline {
/**
* Get a list of media, ordered first by day, then by album if multiple media was found for the same day.
*/
myTimeline: myTimeline_myTimeline[];
myTimeline: myTimeline_myTimeline[]
}
export interface myTimelineVariables {
onlyFavorites?: boolean | null;
limit?: number | null;
offset?: number | null;
onlyFavorites?: boolean | null
limit?: number | null
offset?: number | null
fromDate?: any | null
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="13px" height="15px" viewBox="0 0 13 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M9.16666667,5.68434189e-14 C9.41979718,5.68434189e-14 9.62899397,0.188102588 9.66210226,0.432152962 L9.66666667,0.5 L9.666,1.333 L11.1666667,1.33333333 C12.1285626,1.33333333 12.9174396,2.07411552 12.9939226,3.01630483 L13,3.16666667 L13,12.5 C13,13.512522 12.1791887,14.3333333 11.1666667,14.3333333 L11.1666667,14.3333333 L1.83333333,14.3333333 C0.820811292,14.3333333 0,13.512522 0,12.5 L0,12.5 L0,3.16666667 C0,2.15414463 0.820811292,1.33333333 1.83333333,1.33333333 L1.83333333,1.33333333 L3.333,1.333 L3.33333333,0.5 C3.33333333,0.223857625 3.55719096,5.68434189e-14 3.83333333,5.68434189e-14 C4.08646384,5.68434189e-14 4.29566064,0.188102588 4.32876892,0.432152962 L4.33333333,0.5 L4.333,1.333 L8.666,1.333 L8.66666667,0.5 C8.66666667,0.223857625 8.89052429,5.68434189e-14 9.16666667,5.68434189e-14 Z M12,6.333 L1,6.333 L1,12.5 C1,12.9248344 1.31790432,13.2754183 1.72880177,13.3268405 L1.83333333,13.3333333 L11.1666667,13.3333333 C11.626904,13.3333333 12,12.9602373 12,12.5 L12,12.5 L12,6.333 Z M3.333,2.333 L1.83333333,2.33333333 C1.37309604,2.33333333 1,2.70642938 1,3.16666667 L1,3.16666667 L1,5.333 L12,5.333 L12,3.16666667 C12,2.74183224 11.6820957,2.39124835 11.2711982,2.33982618 L11.1666667,2.33333333 L9.666,2.333 L9.66666667,3.16666667 C9.66666667,3.44280904 9.44280904,3.66666667 9.16666667,3.66666667 C8.91353616,3.66666667 8.70433936,3.47856408 8.67123108,3.2345137 L8.66666667,3.16666667 L8.666,2.333 L4.333,2.333 L4.33333333,3.16666667 C4.33333333,3.44280904 4.10947571,3.66666667 3.83333333,3.66666667 C3.58020282,3.66666667 3.37100603,3.47856408 3.33789774,3.2345137 L3.33333333,3.16666667 L3.333,2.333 Z" fill="currentColor" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -90,6 +90,10 @@ function useScrollPagination<D>({
reconfigureIntersectionObserver()
}, [fetchMore, data, finished])
useEffect(() => {
setFinished(false)
}, [data])
return {
containerElem,
finished,

View File

@@ -1,10 +1,10 @@
import { useState } from 'react'
export type UrlKeyValuePair = { key: string; value: string }
export type UrlKeyValuePair = { key: string; value: string | null }
export type UrlParams = {
getParam(key: string, defaultValue?: string | null): string | null
setParam(key: string, value: string): void
setParam(key: string, value: string | null): void
setParams(pairs: UrlKeyValuePair[]): void
}
@@ -19,18 +19,30 @@ function useURLParameters(): UrlParams {
}
const updateParams = () => {
history.replaceState({}, '', url.pathname + '?' + params.toString())
if (params.toString()) {
history.replaceState({}, '', url.pathname + '?' + params.toString())
} else {
history.replaceState({}, '', url.pathname)
}
setUrlString(document.location.href)
}
const setParam = (key: string, value: string) => {
params.set(key, value)
const setParam = (key: string, value: string | null) => {
if (value) {
params.set(key, value)
} else {
params.delete(key)
}
updateParams()
}
const setParams = (pairs: UrlKeyValuePair[]) => {
for (const pair of pairs) {
params.set(pair.key, pair.value)
if (pair.value) {
params.set(pair.key, pair.value)
} else {
params.delete(pair.key)
}
}
updateParams()
}