feat: default-popup infinite scroll for obj-view

This commit is contained in:
Michal Szczepanski 2023-09-26 21:01:00 +02:00
parent 8b28017e39
commit 892d8c63d6
6 changed files with 60 additions and 22 deletions

@ -18,7 +18,6 @@ import { BrowserStorage } from '@pinmenote/browser-api';
import { ICommand } from '../../../model/shared/common.dto'; import { ICommand } from '../../../model/shared/common.dto';
import { ObjectStoreKeys } from '../../../keys/object.store.keys'; import { ObjectStoreKeys } from '../../../keys/object.store.keys';
import { SegmentData } from '@pinmenote/page-compute'; import { SegmentData } from '@pinmenote/page-compute';
import { fnConsoleLog } from '../../../fn/fn-console';
export class PageSegmentAddCommand<T> implements ICommand<Promise<void>> { export class PageSegmentAddCommand<T> implements ICommand<Promise<void>> {
constructor(private content: SegmentData<T>, private ref = true) {} constructor(private content: SegmentData<T>, private ref = true) {}

@ -15,7 +15,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { BrowserStorage } from '@pinmenote/browser-api'; import { BrowserStorage } from '@pinmenote/browser-api';
import { fnConsoleLog } from '../fn/fn-console';
export class LinkOriginStore { export class LinkOriginStore {
static readonly NOTE_ORIGIN = 'note:origin'; static readonly NOTE_ORIGIN = 'note:origin';

@ -38,6 +38,7 @@ interface Props {
} }
export const ObjListComponent: FunctionComponent<Props> = (props) => { export const ObjListComponent: FunctionComponent<Props> = (props) => {
LogManager.log(`RENDER !!! ${props.objList.length}`);
const [reRender, setReRender] = useState(false); const [reRender, setReRender] = useState(false);
const handlePinRemove = async (data: ObjDto<ObjPinDto>) => { const handlePinRemove = async (data: ObjDto<ObjPinDto>) => {

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ObjDto, ObjPageDataDto, ObjTypeDto } from '../../../common/model/obj/obj.dto'; import { ObjDto, ObjPageDataDto, ObjTypeDto } from '../../../common/model/obj/obj.dto';
import React, { FunctionComponent, useEffect, useState } from 'react'; import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
import { BusMessageType } from '../../../common/model/bus.model'; import { BusMessageType } from '../../../common/model/bus.model';
import { ObjGetHrefCommand } from '../../../common/command/obj/url/obj-get-href.command'; import { ObjGetHrefCommand } from '../../../common/command/obj/url/obj-get-href.command';
import { ObjGetOriginCommand } from '../../../common/command/obj/url/obj-get-origin.command'; import { ObjGetOriginCommand } from '../../../common/command/obj/url/obj-get-origin.command';
@ -33,11 +33,6 @@ interface Props {
editNoteCallback: (obj: ObjDto<ObjPageNoteDto>) => void; editNoteCallback: (obj: ObjDto<ObjPageNoteDto>) => void;
} }
interface FetchObjectsResult {
objs: ObjDto<ObjPageDataDto>[];
index: number;
}
const hrefFilter = (obj: ObjDto<ObjPageDataDto>, href?: string) => { const hrefFilter = (obj: ObjDto<ObjPageDataDto>, href?: string) => {
if ([ObjTypeDto.PageSnapshot, ObjTypeDto.PageElementSnapshot].includes(obj.type)) { if ([ObjTypeDto.PageSnapshot, ObjTypeDto.PageElementSnapshot].includes(obj.type)) {
if ((obj.data as ObjPageDto).snapshot.info.url.href === href) return true; if ((obj.data as ObjPageDto).snapshot.info.url.href === href) return true;
@ -51,10 +46,28 @@ const hrefFilter = (obj: ObjDto<ObjPageDataDto>, href?: string) => {
return false; return false;
}; };
const fetchObjects = async (idList: number[], index: number, href?: string): Promise<FetchObjectsResult> => { class Store {
if (index >= idList.length) return { objs: [], index }; static readonly limit = 10;
static fetching = false;
static hrefObjs: ObjDto<ObjPageDataDto>[] = [];
static hrefIndex = 0;
static originObjs: ObjDto<ObjPageDataDto>[] = [];
static originIndex = 0;
static resetStore() {
this.hrefObjs = [];
this.hrefIndex = 0;
this.originObjs = [];
this.originIndex = 0;
}
}
const fetchObjects = async (idList: number[], type: 'href' | 'origin', href?: string): Promise<boolean> => {
let index = type === 'origin' ? Store.originIndex : Store.hrefIndex;
if (index >= idList.length) return false;
const objs: ObjDto<ObjPageDataDto>[] = []; const objs: ObjDto<ObjPageDataDto>[] = [];
for (index; index < idList.length; index++) { for (index; index < idList.length; index++) {
if (objs.length === Store.limit) break;
const obj = await new ObjGetCommand<ObjPageDataDto>(idList[index]).execute(); const obj = await new ObjGetCommand<ObjPageDataDto>(idList[index]).execute();
// TODO check it - something is not deleting // TODO check it - something is not deleting
if (!obj) { if (!obj) {
@ -64,36 +77,64 @@ const fetchObjects = async (idList: number[], index: number, href?: string): Pro
if (hrefFilter(obj, href)) continue; if (hrefFilter(obj, href)) continue;
objs.push(obj); objs.push(obj);
} }
return { objs, index }; if (type === 'origin') {
Store.originObjs.push(...objs);
Store.originIndex = index;
} else {
Store.hrefObjs.push(...objs);
Store.hrefIndex = index;
}
return true;
}; };
export const ObjViewComponent: FunctionComponent<Props> = (props) => { export const ObjViewComponent: FunctionComponent<Props> = (props) => {
const [originObjs, setOriginObjs] = useState<ObjDto<ObjPageDataDto>[]>([]); const [originObjs, setOriginObjs] = useState<ObjDto<ObjPageDataDto>[]>([]);
const [hrefObjs, setHrefObjs] = useState<ObjDto<ObjPageDataDto>[]>([]); const [hrefObjs, setHrefObjs] = useState<ObjDto<ObjPageDataDto>[]>([]);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
setTimeout(async () => await initUrl(), 100); setTimeout(async () => await initUrl(), 100);
const urlKey = TinyDispatcher.getInstance().addListener(BusMessageType.POP_UPDATE_URL, () => { const urlKey = TinyDispatcher.getInstance().addListener(BusMessageType.POP_UPDATE_URL, () => {
setTimeout(async () => await initUrl(), 100); setTimeout(async () => {
Store.resetStore();
await initUrl();
}, 100);
}); });
return () => { return () => {
TinyDispatcher.getInstance().removeListener(BusMessageType.POP_UPDATE_URL, urlKey); TinyDispatcher.getInstance().removeListener(BusMessageType.POP_UPDATE_URL, urlKey);
}; };
}, [props]); }, []);
const initUrl = async () => { const initUrl = async () => {
LogManager.log('initUrl'); LogManager.log('initUrl');
if (Store.fetching) return;
if (!PopupActiveTabStore.url) return; if (!PopupActiveTabStore.url) return;
Store.fetching = true;
const hrefIds = await new ObjGetHrefCommand(PopupActiveTabStore.url).execute(); const hrefIds = await new ObjGetHrefCommand(PopupActiveTabStore.url).execute();
const href = await fetchObjects(hrefIds, 0); const hasHref = await fetchObjects(hrefIds, 'href');
setHrefObjs(href.objs); if (hasHref) setHrefObjs(Store.hrefObjs.concat());
const originIds = await new ObjGetOriginCommand(PopupActiveTabStore.url).execute(); const originIds = await new ObjGetOriginCommand(PopupActiveTabStore.url).execute();
const origin = await fetchObjects(originIds, 0, PopupActiveTabStore.url.href); const hasOrigin = await fetchObjects(originIds, 'origin', PopupActiveTabStore.url.href);
setOriginObjs(origin.objs); if (hasOrigin) setOriginObjs(Store.originObjs.concat());
Store.fetching = false;
};
const handleScroll = () => {
if (!ref.current) return;
if (Store.fetching) return;
const bottom = ref.current.scrollHeight - ref.current.clientHeight;
if (bottom - ref.current.scrollTop > 10) return; // too much up
setTimeout(async () => {
await initUrl();
}, 200);
}; };
return ( return (
<div style={{ maxHeight: '420px', display: 'flex', flexDirection: 'column', overflow: 'auto' }}> <div
ref={ref}
style={{ maxHeight: '420px', display: 'flex', flexDirection: 'column', overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>On this page</div> <div style={{ fontWeight: 'bold', fontSize: '14px' }}>On this page</div>
<ObjListComponent objList={hrefObjs} editNoteCallback={props.editNoteCallback} /> <ObjListComponent objList={hrefObjs} editNoteCallback={props.editNoteCallback} />
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>On {PopupActiveTabStore.url?.origin}</div> <div style={{ fontWeight: 'bold', fontSize: '14px' }}>On {PopupActiveTabStore.url?.origin}</div>

@ -23,7 +23,6 @@ import { ObjPdfDto } from '../../../common/model/obj/obj-pdf.dto';
import { PageNoteElement } from './page-note/page-note.element'; import { PageNoteElement } from './page-note/page-note.element';
import { PageSnapshotElement } from './page-snapshot/page-snapshot.element'; import { PageSnapshotElement } from './page-snapshot/page-snapshot.element';
import { PdfElement } from './pdf/pdf.element'; import { PdfElement } from './pdf/pdf.element';
import { fnConsoleLog } from '../../../common/fn/fn-console';
export const BoardComponent: FunctionComponent = () => { export const BoardComponent: FunctionComponent = () => {
const [objData, setObjData] = useState<ObjDto[]>(BoardStore.objList); const [objData, setObjData] = useState<ObjDto[]>(BoardStore.objList);
@ -50,7 +49,6 @@ export const BoardComponent: FunctionComponent = () => {
}; };
const handleScroll = () => { const handleScroll = () => {
fnConsoleLog('handleScroll');
if (!ref.current) return; if (!ref.current) return;
if (BoardStore.isLast) return; // last element so return if (BoardStore.isLast) return; // last element so return
const bottom = ref.current.scrollHeight - ref.current.clientHeight; const bottom = ref.current.scrollHeight - ref.current.clientHeight;

@ -118,8 +118,8 @@ export class SyncSnapshotIncomingCommand implements ICommand<Promise<boolean>> {
try { try {
src = await UrlFactory.toDataUri(segmentData); src = await UrlFactory.toDataUri(segmentData);
// @vane WORKAROUND FIX convert image/svg -> image/svg+xml to render correctly inside <img> tag // @vane WORKAROUND FIX convert image/svg -> image/svg+xml to render correctly inside <img> tag
const check = 'data:image/svg'; if (child.mimeType == 'data:image/svg+xml' && !src.startsWith(child.mimeType))
if (src.startsWith(check)) src = 'data:image/svg+xml' + src.substring(check.length); src = 'data:image/svg+xml' + src.substring('data:image/svg'.length);
} catch (e) { } catch (e) {
const buffer = await segmentData.arrayBuffer(); const buffer = await segmentData.arrayBuffer();
const textData = new TextDecoder().decode(buffer); const textData = new TextDecoder().decode(buffer);