merge ends and elements in scrolling subscription

This commit is contained in:
Michael Peters 2022-01-23 15:12:42 -06:00
parent 7e703f84fd
commit ce9b17535b
2 changed files with 137 additions and 44 deletions

View File

@ -20,7 +20,6 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
fetchAboveCallable,
fetchBelowCallable,
setScrollRatio,
ends,
messagesResult,
messagesFetchError,
messagesFetchAboveError,
@ -29,10 +28,10 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
const messageElements = useMemo(() => {
const result = [];
if (messagesResult && messagesResult.value) {
for (let i = 0; i < messagesResult.value.length; ++i) {
const prevMessage = messagesResult.value[i - 1] ?? null;
const message = messagesResult.value[i] as Message;
if (messagesResult) {
for (let i = 0; i < messagesResult.value.elements.length; ++i) {
const prevMessage = messagesResult.value.elements[i - 1] ?? null;
const message = messagesResult.value.elements[i] as Message;
result.push(<MessageElement key={guild.id + message.id} guild={guild} message={message} prevMessage={prevMessage} messageGuild={messagesResult.guild} setOverlay={setOverlay} />);
}
}
@ -46,7 +45,7 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
fetchAboveCallable={fetchAboveCallable}
fetchBelowCallable={fetchBelowCallable}
setScrollRatio={setScrollRatio}
ends={ends}
ends={messagesResult?.value.ends ?? null}
fetchError={messagesFetchError}
fetchAboveError={messagesFetchAboveError}
fetchBelowError={messagesFetchBelowError}

View File

@ -59,6 +59,14 @@ export interface SubscriptionResult<T> {
guild: CombinedGuild;
}
export interface ScrollingSubscriptionResult<T> {
value: {
ends: { hasMoreAbove: boolean, hasMoreBelow: boolean };
elements: T[];
},
guild: CombinedGuild;
}
export function isNonNullAndHasValue<T>(subscriptionResult: SubscriptionResult<T | null> | null): subscriptionResult is SubscriptionResult<T> {
return !!(subscriptionResult !== null && subscriptionResult.value !== null);
}
@ -347,8 +355,7 @@ function useMultipleGuildSubscriptionScrolling<
fetchAboveCallable: () => Promise<void>,
fetchBelowCallable: () => Promise<void>,
setScrollRatio: Dispatch<SetStateAction<number>>,
ends: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null,
lastResult: SubscriptionResult<T[]> | null,
lastResult: ScrollingSubscriptionResult<T> | null,
fetchError: unknown | null,
fetchAboveError: unknown | null,
fetchBelowError: unknown | null,
@ -364,14 +371,12 @@ function useMultipleGuildSubscriptionScrolling<
const isMounted = useIsMountedRef();
// TODO: lastResult.value should really be only T[] instead of | null since we set it to [] anyway in the onUpdate, etc functions
const [ lastResult, setLastResult ] = useState<{ value: T[], guild: CombinedGuild } | null>(null);
const [ lastResult, setLastResult ] = useState<ScrollingSubscriptionResult<T> | null>(null);
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
const [ fetchAboveError, setFetchAboveError ] = useState<unknown | null>(null);
const [ fetchBelowError, setFetchBelowError ] = useState<unknown | null>(null);
const [ ends, setEnds ] = useState<{ hasMoreAbove: boolean, hasMoreBelow: boolean } | null>(null);
// Percentage of scroll from top. i.e. 300px from top of 1000px scroll = 0.3 scroll ratio
const [ scrollRatio, setScrollRatio ] = useState<number>(0.5);
@ -389,18 +394,18 @@ function useMultipleGuildSubscriptionScrolling<
}
}, [ scrollRatio ]);
function removeByCounts<T>(elements: T[], counts: { fromTop: number, fromBottom: number }): T[] {
function removeByCounts<T>(elements: T[], counts: { fromTop: number, fromBottom: number }): T[] {
const { fromTop, fromBottom } = counts;
return elements.slice(fromTop, elements.length - fromBottom);
}
const fetchAboveCallable = useCallback(async (): Promise<void> => {
if (!isMounted.current) return;
if (!lastResult || !lastResult.value || lastResult.value.length === 0) return;
if (!lastResult || lastResult.value.elements.length === 0) return;
if (guild !== lastResult.guild) return;
try {
const reference = lastResult.value[0] as T;
const reference = lastResult.value.elements[0] as T;
const aboveElements = await fetchAboveFunc(reference);
const lastResultAfterAwait = getStateAfterAwait(setLastResult);
if (!isMounted.current) return;
@ -411,16 +416,30 @@ function useMultipleGuildSubscriptionScrolling<
let removedFromBottom = false;
setLastResult((lastResult) => {
if (!lastResult) return null;
let newValue = aboveElements.concat(lastResult.value ?? []).sort(sortFunc);
if (newValue.length > maxElements) {
newValue = newValue.slice(0, maxElements);
let newElements = aboveElements.concat(lastResult.value.elements ?? []).sort(sortFunc);
if (newElements.length > maxElements) {
newElements = newElements.slice(0, maxElements);
removedFromBottom = true;
}
return { value: newValue, guild: lastResult.guild };
return {
value: {
elements: newElements,
ends: { hasMoreBelow: removedFromBottom || lastResult.value.ends.hasMoreBelow, hasMoreAbove }
},
guild: lastResult.guild
};
});
setEnds(prev => (prev && { hasMoreBelow: removedFromBottom || prev.hasMoreBelow, hasMoreAbove: prev.hasMoreAbove }));
} else {
setEnds(prev => (prev && { hasMoreBelow: prev.hasMoreBelow, hasMoreAbove: false }));
setLastResult((lastResult) => {
if (!lastResult) return null;
return {
value: {
elements: lastResult.value.elements,
ends: { hasMoreBelow: lastResult.value.ends.hasMoreBelow, hasMoreAbove: false }
},
guild: lastResult.guild
};
})
}
} catch (e: unknown) {
LOG.error('error fetching above for subscription', e);
@ -428,16 +447,25 @@ function useMultipleGuildSubscriptionScrolling<
if (!isMounted.current) return;
if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return;
setFetchAboveError(e);
setEnds(prev => (prev && { hasMoreBelow: prev.hasMoreBelow, hasMoreAbove: true }))
setLastResult((lastResult) => {
if (!lastResult) return null;
return {
value: {
elements: lastResult.value.elements,
ends: { hasMoreBelow: lastResult.value.ends.hasMoreBelow, hasMoreAbove: true },
},
guild: lastResult.guild
};
});
}
}, [ guild, lastResult, fetchAboveFunc, maxFetchElements ]);
const fetchBelowCallable = useCallback(async (): Promise<void> => {
if (!isMounted.current) return;
if (!lastResult || !lastResult.value || lastResult.value.length === 0) return;
if (!lastResult || !lastResult.value || lastResult.value.elements.length === 0) return;
if (guild !== lastResult.guild) return;
try {
const reference = lastResult.value[lastResult.value.length - 1] as T;
const reference = lastResult.value.elements[lastResult.value.elements.length - 1] as T;
const belowElements = await fetchBelowFunc(reference);
const lastResultAfterAwait = getStateAfterAwait(setLastResult);
if (!isMounted.current) return;
@ -448,16 +476,30 @@ function useMultipleGuildSubscriptionScrolling<
let removedFromTop = false;
setLastResult((lastResult) => {
if (!lastResult) return null;
let newValue = (lastResult.value ?? []).concat(belowElements).sort(sortFunc);
if (newValue.length > maxElements) {
newValue = newValue.slice(Math.max(newValue.length - maxElements, 0));
let newElements = (lastResult.value.elements ?? []).concat(belowElements).sort(sortFunc);
if (newElements.length > maxElements) {
newElements = newElements.slice(Math.max(newElements.length - maxElements, 0));
removedFromTop = true;
}
return { value: newValue, guild: lastResult.guild };
return {
value: {
elements: newElements,
ends: { hasMoreBelow, hasMoreAbove: removedFromTop || lastResult.value.ends.hasMoreAbove }
},
guild: lastResult.guild
};
});
setEnds(prev => (prev && { hasMoreBelow, hasMoreAbove: removedFromTop || prev.hasMoreAbove })); // :)
} else {
setEnds(prev => (prev && { hasMoreBelow: false, hasMoreAbove: prev.hasMoreAbove }))
setLastResult((lastResult) => {
if (!lastResult) return null;
return {
value: {
elements: lastResult.value.elements,
ends: { hasMoreBelow: false, hasMoreAbove: lastResult.value.ends.hasMoreAbove }
},
guild: lastResult.guild
};
});
}
} catch (e: unknown) {
LOG.error('error fetching below for subscription', e);
@ -465,7 +507,16 @@ function useMultipleGuildSubscriptionScrolling<
if (!isMounted.current) return;
if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return;
setFetchBelowError(e);
setEnds(prev => (prev && { hasMoreBelow: true, hasMoreAbove: prev.hasMoreAbove }));
setLastResult((lastResult) => {
if (!lastResult) return null;
return {
value: {
elements: lastResult.value.elements,
ends: { hasMoreBelow: true, hasMoreAbove: lastResult.value.ends.hasMoreAbove }
},
guild: lastResult.guild
};
});
}
}, [ lastResult, fetchBelowFunc, maxFetchElements ]);
@ -476,8 +527,13 @@ function useMultipleGuildSubscriptionScrolling<
fetchValue = fetchValue.slice(Math.max(fetchValue.length - maxElements)).sort(sortFunc);
}
//LOG.debug('Got items: ', { fetchValueLength: fetchValue?.length ?? '<empty>' })
setEnds({ hasMoreAbove, hasMoreBelow: false });
setLastResult({ value: fetchValue, guild: fetchValueGuild });
setLastResult({
value: {
elements: fetchValue,
ends: { hasMoreAbove, hasMoreBelow: false }
},
guild: fetchValueGuild
});
setFetchError(null);
}, [ sortFunc, maxFetchElements, maxElements ]);
@ -494,13 +550,25 @@ function useMultipleGuildSubscriptionScrolling<
const newElements = newEventArgsMap(...args);
setLastResult((lastResult) => {
if (lastResult === null) return null;
if (ends?.hasMoreBelow) return lastResult; // Don't add to bottom if we are not at the bottom
let newValue = (lastResult.value ?? []).concat(newElements).sort(sortFunc);
if (newValue.length > maxElements) {
if (lastResult.value.ends.hasMoreBelow) return lastResult; // Don't add to bottom if we are not at the bottom
let newResultElements = (lastResult.value.elements ?? []).concat(newElements).sort(sortFunc);
let newEnds = lastResult.value.ends;
if (newResultElements.length > maxElements) {
// Remove in a way that tries to keep the scrollbar position consistent
newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements));
const removeCounts = getRemoveCounts(newResultElements.length - maxElements);
newResultElements = removeByCounts(newResultElements, removeCounts);
newEnds = {
hasMoreBelow: removeCounts.fromBottom > 0 || lastResult.value.ends.hasMoreBelow,
hasMoreAbove: removeCounts.fromTop > 0 || lastResult.value.ends.hasMoreAbove
};
}
return { value: newValue, guild: guild };
return {
value: {
elements: newResultElements,
ends: newEnds
},
guild: guild
};
});
}, [ guild, newEventArgsMap ]) as (Connectable & Conflictable)[NE];
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
@ -509,7 +577,15 @@ function useMultipleGuildSubscriptionScrolling<
const updatedElements = updatedEventArgsMap(...args);
setLastResult((lastResult) => {
if (lastResult === null) return null;
return { value: (lastResult.value ?? []).map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc), guild: guild };
return {
value: {
elements: (lastResult.value.elements ?? [])
.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element)
.sort(sortFunc),
ends: lastResult.value.ends
},
guild: guild
};
});
}, [ guild, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE];
const boundRemovedFunc = useCallback((...args: Arguments<Connectable[RE]>): void => {
@ -519,7 +595,14 @@ function useMultipleGuildSubscriptionScrolling<
setLastResult((lastResult) => {
if (lastResult === null) return null;
const deletedIds = new Set(removedElements.map(removedElement => removedElement.id));
return { value: (lastResult.value ?? []).filter(element => !deletedIds.has(element.id)), guild: guild };
return {
value: {
elements: (lastResult.value.elements ?? [])
.filter(element => !deletedIds.has(element.id)),
ends: lastResult.value.ends
},
guild: guild
};
});
}, [ guild, removedEventArgsMap ]) as (Connectable & Conflictable)[RE];
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
@ -529,15 +612,27 @@ function useMultipleGuildSubscriptionScrolling<
setLastResult((lastResult) => {
if (lastResult === null) return null;
const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id));
let newValue = (lastResult.value ?? [])
let newResultElements = (lastResult.value.elements ?? [])
.concat(changes.added)
.map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element)
.filter(element => !deletedIds.has(element.id))
.sort(sortFunc);
if (newValue.length > maxElements) {
newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements));
let newEnds = lastResult.value.ends;
if (newResultElements.length > maxElements) {
const removeCounts = getRemoveCounts(newResultElements.length - maxElements);
newResultElements = removeByCounts(newResultElements, removeCounts);
newEnds = {
hasMoreBelow: removeCounts.fromBottom > 0 || lastResult.value.ends.hasMoreBelow,
hasMoreAbove: removeCounts.fromTop > 0 || lastResult.value.ends.hasMoreAbove
};
}
return { value: newValue, guild: guild };
return {
value: {
elements: newResultElements,
ends: newEnds
},
guild: guild
};
});
}, [ guild, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE];
@ -567,7 +662,6 @@ function useMultipleGuildSubscriptionScrolling<
fetchAboveCallable,
fetchBelowCallable,
setScrollRatio,
ends,
lastResult,
fetchError,
fetchAboveError,