Skip to content

Commit

Permalink
641 add sequence navigation buttons (#642)
Browse files Browse the repository at this point in the history
* Add picture sequence navigation buttons

* Prefetch picture sequence neighbors

* Fix ambiguous selector in test

* Small refactorings
  • Loading branch information
MariusDoe authored Aug 23, 2024
1 parent 329b01e commit 724d0e4
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 52 deletions.
4 changes: 2 additions & 2 deletions projects/bp-gallery/cypress/e2e/picture-grid.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ describe('picture grid', () => {
cy.visit('/archives/1');
cy.get('.overview-container .picture-preview').first().click();
cy.url().should('contain', '/picture/');
cy.get('.picture-navigation-buttons [data-testid="ChevronRightIcon"]').click();
cy.get('.picture-navigation-buttons [data-testid="ChevronRightIcon"]').click();
cy.get('.picture-navigation-buttons [data-testid="next"]').click();
cy.get('.picture-navigation-buttons [data-testid="next"]').click();
cy.contains('Zurück').click();
urlIs('/archives/1');
});
Expand Down
111 changes: 81 additions & 30 deletions projects/bp-gallery/src/components/views/picture/PictureView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface PictureViewContextFields {
navigatePicture?: (target: PictureNavigationTarget) => void;
hasNext?: boolean;
hasPrevious?: boolean;
hasNextInSequence?: boolean;
hasPreviousInSequence?: boolean;
sideBarOpen?: boolean;
noDistractionMode?: boolean;
setSideBarOpen?: Dispatch<SetStateAction<boolean>>;
Expand All @@ -45,6 +47,11 @@ export const PictureViewContext = createContext<PictureViewContextFields>({ img:
// Used for the sidebar (in px) --> same as in shared.scss
const MOBILE_BREAKPOINT = 750;

export type PictureIds = {
pictureInSiblingsId: string;
pictureInSequenceId: string;
};

const PictureView = ({
initialPictureId,
siblingIds,
Expand All @@ -60,7 +67,9 @@ const PictureView = ({

const containerRef = useRef<HTMLDivElement>(null);

const [pictureId, setPictureId] = useState<string>(initialPictureId);
const [pictureInSiblingsId, setPictureInSiblingsId] = useState<string>(initialPictureId);
const [pictureInSequenceId, setPictureInSequenceId] = useState<string>(initialPictureId);

const [sideBarOpen, setSideBarOpen] = useState<boolean>(false);

useEffect(() => {
Expand All @@ -70,10 +79,14 @@ const PictureView = ({
}, []);

useEffect(() => {
if (fetchMore && siblingIds && siblingIds.indexOf(pictureId) === siblingIds.length - 1) {
fetchMore(pictureId);
if (
fetchMore &&
siblingIds &&
siblingIds.indexOf(pictureInSiblingsId) === siblingIds.length - 1
) {
fetchMore(pictureInSiblingsId);
}
}, [fetchMore, siblingIds, pictureId]);
}, [fetchMore, siblingIds, pictureInSiblingsId]);

const search = window.location.search;
const [sessionId, isPresentationMode] = useMemo((): [string, boolean] => {
Expand All @@ -85,39 +98,75 @@ const PictureView = ({
];
}, [search]);

const [hasPrevious, hasNext] = useMemo(() => {
return [
Boolean(getPreviousPictureId(pictureId, siblingIds)),
Boolean(getNextPictureId(pictureId, siblingIds)),
];
}, [pictureId, siblingIds]);

// Api connection
usePrefetchPictureHook(pictureId, siblingIds);

const { data, loading, error } = useGetPictureInfoQuery({ variables: { pictureId } });
const { data, loading, error } = useGetPictureInfoQuery({
variables: { pictureId: pictureInSequenceId },
});
const picture: FlatPicture | undefined = useSimplifiedQueryResponseData(data)?.picture;
const pictureLink = asUploadPath(picture?.media);
const pictureSequenceIds = useMemo(
() => picture?.picture_sequence?.pictures?.map(picture => picture.id),
[picture]
);

const onNavigateMessage = useCallback((pictureId: string) => {
replaceHistoryWithoutRouter(`/picture/${pictureId}${window.location.search}`);
setPictureId(pictureId);
}, []);
usePrefetchPictureHook(
{ pictureInSiblingsId, pictureInSequenceId },
siblingIds,
pictureSequenceIds
);

const [hasPrevious, hasNext, hasPreviousInSequence, hasNextInSequence] = useMemo(
() => [
Boolean(getPreviousPictureId(pictureInSiblingsId, siblingIds)),
Boolean(getNextPictureId(pictureInSiblingsId, siblingIds)),
Boolean(getPreviousPictureId(pictureInSequenceId, pictureSequenceIds)),
Boolean(getNextPictureId(pictureInSequenceId, pictureSequenceIds)),
],
[pictureInSequenceId, pictureInSiblingsId, pictureSequenceIds, siblingIds]
);

const onNavigateMessage = useCallback(
({ pictureInSiblingsId, pictureInSequenceId }: PictureIds) => {
replaceHistoryWithoutRouter(`/picture/${pictureInSequenceId}${window.location.search}`);
setPictureInSiblingsId(pictureInSiblingsId);
setPictureInSequenceId(pictureInSequenceId);
},
[]
);

const navigateToPicture = usePresentationChannel(sessionId, onNavigateMessage);

// Call the previous or next picture
const navigatePicture = useCallback(
(target: PictureNavigationTarget) => {
const targetId =
target === PictureNavigationTarget.NEXT
? getNextPictureId(pictureId, siblingIds)
: getPreviousPictureId(pictureId, siblingIds);
if (targetId) {
navigateToPicture(targetId);
const targetIds: Partial<PictureIds> = {
pictureInSequenceId,
pictureInSiblingsId,
};
switch (target) {
case PictureNavigationTarget.NEXT:
targetIds.pictureInSiblingsId = getNextPictureId(pictureInSiblingsId, siblingIds);
break;
case PictureNavigationTarget.PREVIOUS:
targetIds.pictureInSiblingsId = getPreviousPictureId(pictureInSiblingsId, siblingIds);
break;
case PictureNavigationTarget.NEXT_IN_SEQUENCE:
targetIds.pictureInSequenceId = getNextPictureId(pictureInSequenceId, pictureSequenceIds);
break;
case PictureNavigationTarget.PREVIOUS_IN_SEQUENCE:
targetIds.pictureInSequenceId = getPreviousPictureId(
pictureInSequenceId,
pictureSequenceIds
);
break;
}
if (targetIds.pictureInSiblingsId && targetIds.pictureInSequenceId) {
if (targetIds.pictureInSiblingsId !== pictureInSiblingsId) {
targetIds.pictureInSequenceId = targetIds.pictureInSiblingsId;
}
navigateToPicture(targetIds as PictureIds);
}
},
[pictureId, siblingIds, navigateToPicture]
[pictureInSequenceId, pictureInSiblingsId, siblingIds, pictureSequenceIds, navigateToPicture]
);

const [img, setImg] = useState<HTMLImageElement | null>(null);
Expand All @@ -131,6 +180,8 @@ const PictureView = ({
navigatePicture,
hasNext,
hasPrevious,
hasNextInSequence,
hasPreviousInSequence,
sideBarOpen,
noDistractionMode,
setSideBarOpen,
Expand All @@ -144,20 +195,20 @@ const PictureView = ({
const unblock = history.block(() => {
setSideBarOpen(false);
if (onBack) {
onBack(pictureId);
onBack(pictureInSiblingsId);
}
});
return () => {
unblock();
};
}, [history, pictureId, onBack]);
}, [history, pictureInSiblingsId, onBack]);

const onImageContextMenu = useBlockImageContextMenuByPictureId(pictureId);
const onImageContextMenu = useBlockImageContextMenuByPictureId(pictureInSequenceId);

return (
<div className={`picture-view-container ${noDistractionMode ? 'cursor-none' : ''}`}>
<PictureViewContext.Provider value={contextValue}>
<FaceTaggingProvider pictureId={pictureId}>
<FaceTaggingProvider pictureId={pictureInSequenceId}>
<div className={`picture-view`} ref={containerRef}>
<ZoomWrapper blockScroll={true} pictureId={picture?.id ?? ''}>
<div className='picture-wrapper w-full h-full'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import { ChevronLeft, ChevronRight, Filter } from '@mui/icons-material';
import { IconButton } from '@mui/material';
import { useContext, useEffect } from 'react';
import { PictureViewContext } from '../PictureView';
Expand All @@ -7,10 +7,13 @@ import { useNoDistractionModeStyle } from '../helpers/no-distraction-mode-style'
export enum PictureNavigationTarget {
NEXT,
PREVIOUS,
NEXT_IN_SEQUENCE,
PREVIOUS_IN_SEQUENCE,
}

const PictureNavigationButtons = () => {
const { navigatePicture, hasNext, hasPrevious } = useContext(PictureViewContext);
const { navigatePicture, hasNext, hasPrevious, hasNextInSequence, hasPreviousInSequence } =
useContext(PictureViewContext);

useEffect(() => {
const navigateKeyboardAction = (event: KeyboardEvent) => {
Expand All @@ -29,23 +32,53 @@ const PictureNavigationButtons = () => {
const noDistractionModeStyle = useNoDistractionModeStyle();

return (
<div className={`picture-navigation-buttons ${noDistractionModeStyle}`}>
<div className={`picture-navigation-buttons ${noDistractionModeStyle} relative`}>
<IconButton
style={{ visibility: hasPrevious ? 'visible' : 'hidden' }}
onClick={
navigatePicture ? () => navigatePicture(PictureNavigationTarget.PREVIOUS) : undefined
}
size='large'
data-testid='previous'
>
<ChevronLeft />
</IconButton>
<IconButton
style={{ visibility: hasNext ? 'visible' : 'hidden' }}
onClick={navigatePicture ? () => navigatePicture(PictureNavigationTarget.NEXT) : undefined}
size='large'
data-testid='next'
>
<ChevronRight />
</IconButton>
<div className='absolute w-full top-1/2 mt-10 flex flex-row justify-between'>
<IconButton
style={{ visibility: hasPreviousInSequence ? 'visible' : 'hidden' }}
onClick={
navigatePicture
? () => navigatePicture(PictureNavigationTarget.PREVIOUS_IN_SEQUENCE)
: undefined
}
size='large'
data-testid='previous-in-sequence'
>
<ChevronLeft />
<Filter />
</IconButton>
<IconButton
style={{ visibility: hasNextInSequence ? 'visible' : 'hidden' }}
onClick={
navigatePicture
? () => navigatePicture(PictureNavigationTarget.NEXT_IN_SEQUENCE)
: undefined
}
size='large'
data-testid='next-in-sequence'
>
<Filter />
<ChevronRight />
</IconButton>
</div>
</div>
);
};
Expand Down
54 changes: 44 additions & 10 deletions projects/bp-gallery/src/hooks/prefetch.hook.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,68 @@
import { useEffect } from 'react';
import { useGetPictureInfoLazyQuery } from '../graphql/APIConnector';
import { PictureIds } from '../components/views/picture/PictureView';
import {
getNextPictureId,
getPreviousPictureId,
} from '../components/views/picture/helpers/next-prev-picture';
import { useGetPictureInfoLazyQuery } from '../graphql/APIConnector';

const usePrefetchPictureHook = (id: string, siblings?: string[]) => {
const [previousQuery] = useGetPictureInfoLazyQuery();
const [nextQuery] = useGetPictureInfoLazyQuery();
const usePrefetchPictureHook = (
{ pictureInSiblingsId, pictureInSequenceId }: PictureIds,
siblings?: string[],
pictureSequenceIds?: string[]
) => {
const [previousInSiblingsQuery] = useGetPictureInfoLazyQuery();
const [nextInSiblingsQuery] = useGetPictureInfoLazyQuery();
const [previousInSequenceQuery] = useGetPictureInfoLazyQuery();
const [nextInSequenceQuery] = useGetPictureInfoLazyQuery();

useEffect(() => {
if (siblings?.includes(id)) {
const previousId = getPreviousPictureId(id, siblings);
if (siblings?.includes(pictureInSiblingsId)) {
const previousId = getPreviousPictureId(pictureInSiblingsId, siblings);
if (previousId) {
previousInSiblingsQuery({
variables: {
pictureId: previousId,
},
});
}
const nextId = getNextPictureId(pictureInSiblingsId, siblings);
if (nextId) {
nextInSiblingsQuery({
variables: {
pictureId: nextId,
},
});
}
}
if (pictureSequenceIds?.includes(pictureInSequenceId)) {
const previousId = getPreviousPictureId(pictureInSequenceId, pictureSequenceIds);
if (previousId) {
previousQuery({
previousInSequenceQuery({
variables: {
pictureId: previousId,
},
});
}
const nextId = getNextPictureId(id, siblings);
const nextId = getNextPictureId(pictureInSequenceId, pictureSequenceIds);
if (nextId) {
nextQuery({
nextInSequenceQuery({
variables: {
pictureId: nextId,
},
});
}
}
}, [id, siblings, previousQuery, nextQuery]);
}, [
pictureInSiblingsId,
siblings,
previousInSiblingsQuery,
nextInSiblingsQuery,
pictureSequenceIds,
pictureInSequenceId,
previousInSequenceQuery,
nextInSequenceQuery,
]);
};

export default usePrefetchPictureHook;
13 changes: 6 additions & 7 deletions projects/bp-gallery/src/hooks/presentation-channel.hook.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useCallback, useEffect, useRef } from 'react';
import { PictureIds } from '../components/views/picture/PictureView';
import { FallbackChannel, channelFactory } from '../helpers/channel-helpers';

const usePresentationChannel = (id: string, onNavigate: (pictureId: string) => void) => {
const usePresentationChannel = (id: string, onNavigate: (ids: PictureIds) => void) => {
const producer = useRef<FallbackChannel | BroadcastChannel | null>(null);

useEffect(() => {
const consumer = channelFactory(id);
producer.current = channelFactory(id);
consumer.onmessage = (event: MessageEvent<{ pictureId: string }>) => {
onNavigate(event.data.pictureId);
consumer.onmessage = (event: MessageEvent<PictureIds>) => {
onNavigate(event.data);
};

return () => {
Expand All @@ -19,10 +20,8 @@ const usePresentationChannel = (id: string, onNavigate: (pictureId: string) => v
}, [id, onNavigate]);

return useCallback(
(targetId: string) => {
producer.current?.postMessage({
pictureId: targetId,
});
(ids: PictureIds) => {
producer.current?.postMessage(ids);
},
[producer]
);
Expand Down

0 comments on commit 724d0e4

Please sign in to comment.