Update virtual lists

This commit is contained in:
Bill 2022-10-23 23:53:38 -04:00
parent b91f5eaed8
commit 1ef9190fab
11 changed files with 202 additions and 292 deletions

View File

@ -17,6 +17,7 @@
"@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@nitrots/nitro-renderer": "^1.3.4", "@nitrots/nitro-renderer": "^1.3.4",
"@tanstack/react-virtual": "^3.0.0-beta.18",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -27,7 +28,6 @@
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-slider": "^2.0.0", "react-slider": "^2.0.0",
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",
"react-virtualized": "^9.22.3",
"react-youtube": "^7.13.1", "react-youtube": "^7.13.1",
"sass": "^1.53.0", "sass": "^1.53.0",
"typescript": "^4.3.5", "typescript": "^4.3.5",
@ -44,7 +44,6 @@
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/react-slider": "^1.3.1", "@types/react-slider": "^1.3.1",
"@types/react-transition-group": "^4.4.5", "@types/react-transition-group": "^4.4.5",
"@types/react-virtualized": "^9.21.21",
"@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7", "@typescript-eslint/parser": "^5.30.7",
"eslint": "^8.20.0", "eslint": "^8.20.0",

View File

@ -0,0 +1,64 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { FC, ReactElement, useEffect, useRef } from 'react';
import { Base } from './Base';
interface InfiniteScrollProps<T = any>
{
rows: T[];
estimateSize: number;
overscan?: number;
rowRender: (row: T) => ReactElement;
}
export const InfiniteScroll: FC<InfiniteScrollProps> = props =>
{
const { rows = [], estimateSize = 0, overscan = 5, rowRender = null } = props;
const elementRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => elementRef?.current,
estimateSize: () => estimateSize,
overscan: overscan
});
useEffect(() =>
{
let timeout: ReturnType<typeof setTimeout> = null;
const resizeObserver = new ResizeObserver(() =>
{
if(timeout) clearTimeout(timeout);
if(!elementRef.current) return;
timeout = setTimeout(() => rowVirtualizer.getVirtualItems().forEach((virtualItem, index) => virtualItem.measureElement(elementRef?.current?.children?.[0]?.children[index])), 10);
});
if(elementRef.current) resizeObserver.observe(elementRef.current);
return () =>
{
if(timeout) clearTimeout();
timeout = null;
resizeObserver.disconnect();
}
}, [ rowVirtualizer ]);
return (
<Base fit innerRef={ elementRef } position="relative" overflow="auto">
<Base style={ { height: rowVirtualizer.getTotalSize() } }>
{ rowVirtualizer.getVirtualItems().map(virtualRow =>
{
return (
<div key={ virtualRow.index } ref={ virtualRow.measureElement } style={ { transform: `translateY(${ virtualRow.start }px)`, position: 'absolute', width: '100%' } }>
{ rowRender(rows[virtualRow.index]) }
</div>
);
}) }
</Base>
</Base>
);
}

View File

@ -12,6 +12,7 @@ export * from './FormGroup';
export * from './Grid'; export * from './Grid';
export * from './GridContext'; export * from './GridContext';
export * from './HorizontalRule'; export * from './HorizontalRule';
export * from './InfiniteScroll';
export * from './layout'; export * from './layout';
export * from './Text'; export * from './Text';
export * from './transitions'; export * from './transitions';

View File

@ -1,8 +1,7 @@
import { ILinkEventTracker } from '@nitrots/nitro-renderer'; import { ILinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer } from 'react-virtualized';
import { AddEventLinkTracker, ChatEntryType, LocalizeText, RemoveLinkEventTracker } from '../../api'; import { AddEventLinkTracker, ChatEntryType, LocalizeText, RemoveLinkEventTracker } from '../../api';
import { Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { Flex, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
import { useChatHistory } from '../../hooks'; import { useChatHistory } from '../../hooks';
export const ChatHistoryView: FC<{}> = props => export const ChatHistoryView: FC<{}> = props =>
@ -10,9 +9,6 @@ export const ChatHistoryView: FC<{}> = props =>
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ searchText, setSearchText ] = useState<string>(''); const [ searchText, setSearchText ] = useState<string>('');
const { chatHistory = [] } = useChatHistory(); const { chatHistory = [] } = useChatHistory();
const elementRef = useRef<List>(null);
const cache = useMemo(() => new CellMeasurerCache({ defaultHeight: 35, fixedWidth: true }), []);
const filteredChatHistory = useMemo(() => const filteredChatHistory = useMemo(() =>
{ {
@ -23,10 +19,10 @@ export const ChatHistoryView: FC<{}> = props =>
return chatHistory.filter(entry => ((entry.message && entry.message.toLowerCase().includes(text))) || (entry.name && entry.name.toLowerCase().includes(text))); return chatHistory.filter(entry => ((entry.message && entry.message.toLowerCase().includes(text))) || (entry.name && entry.name.toLowerCase().includes(text)));
}, [ chatHistory, searchText ]); }, [ chatHistory, searchText ]);
useEffect(() => /* useEffect(() =>
{ {
if(elementRef && elementRef.current && isVisible) elementRef.current.scrollToRow(-1); if(elementRef && elementRef.current && isVisible) elementRef.current.scrollTop = elementRef.current.scrollHeight;
}, [ isVisible ]); }, [ isVisible ]); */
useEffect(() => useEffect(() =>
{ {
@ -60,68 +56,39 @@ export const ChatHistoryView: FC<{}> = props =>
if(!isVisible) return null; if(!isVisible) return null;
const RowRenderer: ListRowRenderer = (props: ListRowProps) =>
{
const item = filteredChatHistory[props.index];
if (!item) return null;
return (
<CellMeasurer cache={ cache } columnIndex={ 0 } key={ props.key } parent={ props.parent } rowIndex={ props.index }>
<Flex alignItems="center" style={ props.style } className="p-1" gap={ 2 }>
<Text variant="muted">{ item.timestamp }</Text>
{ (item.type === ChatEntryType.TYPE_CHAT) &&
<div className="bubble-container" style={ { position: 'relative' } }>
{ (item.style === 0) &&
<div className="user-container-bg" style={ { backgroundColor: item.color } } /> }
<div className={ `chat-bubble bubble-${ item.style } type-${ item.chatType }` } style={ { maxWidth: '100%' } }>
<div className="user-container">
{ item.imageUrl && (item.imageUrl.length > 0) &&
<div className="user-image" style={ { backgroundImage: `url(${ item.imageUrl })` } } /> }
</div>
<div className="chat-content">
<b className="username mr-1" dangerouslySetInnerHTML={ { __html: `${ item.name }: ` } } />
<span className="message" dangerouslySetInnerHTML={ { __html: `${ item.message }` } } />
</div>
</div>
</div> }
{ (item.type === ChatEntryType.TYPE_ROOM_INFO) &&
<>
<i className="icon icon-small-room" />
<Text textBreak wrap grow>{ item.name }</Text>
</> }
</Flex>
</CellMeasurer>
);
};
return ( return (
<NitroCardView uniqueKey="chat-history" className="nitro-chat-history" theme="primary-slim"> <NitroCardView uniqueKey="chat-history" className="nitro-chat-history" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('room.chathistory.button.text') } onCloseClick={ event => setIsVisible(false) }/> <NitroCardHeaderView headerText={ LocalizeText('room.chathistory.button.text') } onCloseClick={ event => setIsVisible(false) }/>
<NitroCardContentView overflow="hidden"> <NitroCardContentView overflow="hidden" gap={ 2 }>
<Flex column fullHeight gap={ 2 }> <input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('generic.search') } value={ searchText } onChange={ event => setSearchText(event.target.value) } />
<input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('generic.search') } value={ searchText } onChange={ event => setSearchText(event.target.value) } /> <InfiniteScroll rows={ filteredChatHistory } estimateSize={ 35 } rowRender={ row =>
<div className="h-100"> {
<AutoSizer defaultWidth={ 300 } defaultHeight={ 170 }> return (
{ ({ height, width }) => <Flex alignItems="center" className="p-1" gap={ 2 }>
{ <Text variant="muted">{ row.timestamp }</Text>
cache.clearAll(); { (row.type === ChatEntryType.TYPE_CHAT) &&
<div className="bubble-container" style={ { position: 'relative' } }>
return ( { (row.style === 0) &&
<List <div className="user-container-bg" style={ { backgroundColor: row.color } } /> }
ref={ elementRef } <div className={ `chat-bubble bubble-${ row.style } type-${ row.chatType }` } style={ { maxWidth: '100%' } }>
width={ width } <div className="user-container">
height={ height } { row.imageUrl && (row.imageUrl.length > 0) &&
rowCount={ filteredChatHistory.length } <div className="user-image" style={ { backgroundImage: `url(${ row.imageUrl })` } } /> }
rowHeight={ 35 } </div>
className={ 'chat-history-list' } <div className="chat-content">
rowRenderer={ RowRenderer } <b className="username mr-1" dangerouslySetInnerHTML={ { __html: `${ row.name }: ` } } />
deferredMeasurementCache={ cache } /> <span className="message" dangerouslySetInnerHTML={ { __html: `${ row.message }` } } />
) </div>
} } </div>
</AutoSizer> </div> }
</div> { (row.type === ChatEntryType.TYPE_ROOM_INFO) &&
</Flex> <>
<i className="icon icon-small-room" />
<Text textBreak wrap grow>{ row.name }</Text>
</> }
</Flex>
)
} } />
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -23,12 +23,12 @@
.table { .table {
color: $black; color: $black;
> :not(caption) > * > * { > :not(caption)>*>* {
box-shadow: none; box-shadow: none;
border-bottom: 1px solid rgba(0, 0, 0, .2); border-bottom: 1px solid rgba(0, 0, 0, .2);
} }
&.table-striped > tbody > tr:nth-of-type(odd) { &.table-striped>tbody>tr:nth-of-type(odd) {
color: $black; color: $black;
background: rgba(0, 0, 0, .05); background: rgba(0, 0, 0, .05);
} }
@ -41,10 +41,12 @@
.nitro-mod-tools-chatlog { .nitro-mod-tools-chatlog {
width: 400px; width: 400px;
height: 250px;
} }
.nitro-mod-tools-user-visits { .nitro-mod-tools-user-visits {
width: 250px; width: 250px;
height: 250px;
} }
.nitro-mod-tools-tickets { .nitro-mod-tools-tickets {
@ -59,26 +61,10 @@
.nitro-mod-tools-chatlog, .nitro-mod-tools-chatlog,
.nitro-mod-tools-user-visits { .nitro-mod-tools-user-visits {
.log-container { .log-entry {
min-height: 200px;
height: 100%;
.log-entry-container { &.highlighted {
border: 1px solid $red;
.log-entry {
&.highlighted {
border: 1px solid $red;
}
}
&.highlighted {
border: 1px solid $red;
}
}
&:first-child {
padding-top: 0;
} }
} }
} }

View File

@ -0,0 +1,11 @@
export interface ChatlogRecord
{
timestamp?: string;
habboId?: number;
username?: string;
message?: string;
hasHighlighting?: boolean;
isRoomInfo?: boolean;
roomId?: number;
roomName?: string;
}

View File

@ -1,9 +1,9 @@
import { ChatRecordData } from '@nitrots/nitro-renderer'; import { ChatRecordData } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, Key, useCallback } from 'react'; import { FC, useMemo } from 'react';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps } from 'react-virtualized';
import { CreateLinkEvent, TryVisitRoom } from '../../../../api'; import { CreateLinkEvent, TryVisitRoom } from '../../../../api';
import { Base, Button, Column, Flex, Grid, Text } from '../../../../common'; import { Base, Button, Column, Flex, Grid, InfiniteScroll, Text } from '../../../../common';
import { useModTools } from '../../../../hooks'; import { useModTools } from '../../../../hooks';
import { ChatlogRecord } from './ChatlogRecord';
interface ChatlogViewProps interface ChatlogViewProps
{ {
@ -15,94 +15,38 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
const { records = null } = props; const { records = null } = props;
const { openRoomInfo = null } = useModTools(); const { openRoomInfo = null } = useModTools();
const rowRenderer = (props: ListRowProps) => const allRecords = useMemo(() =>
{ {
let chatlogEntry = records[0].chatlog[props.index]; const results: ChatlogRecord[] = [];
return ( records.forEach(record =>
<CellMeasurer
cache={ cache }
columnIndex={ 0 }
key={ props.key }
parent={ props.parent }
rowIndex={ props.index }
>
<Grid key={ props.key } fullHeight={ false } style={ props.style } gap={ 1 } alignItems="center" className="log-entry py-1 border-bottom">
<Text className="g-col-2">{ chatlogEntry.timestamp }</Text>
<Text className="g-col-3" bold underline pointer onClick={ event => CreateLinkEvent(`mod-tools/open-user-info/${ chatlogEntry.userId }`) }>{ chatlogEntry.userName }</Text>
<Text textBreak wrap className="g-col-7">{ chatlogEntry.message }</Text>
</Grid>
</CellMeasurer>
);
};
const advancedRowRenderer = (props: ListRowProps) =>
{
let chatlogEntry = null;
let currentRecord: ChatRecordData = null;
let isRoomInfo = false;
let totalIndex = 0;
for(let i = 0; i < records.length; i++)
{ {
currentRecord = records[i]; results.push({
isRoomInfo: true,
roomId: record.roomId,
roomName: record.roomName
});
totalIndex++; // row for room info record.chatlog.forEach(chatlog =>
totalIndex = (totalIndex + currentRecord.chatlog.length);
if(props.index > (totalIndex - 1)) continue;
if((props.index + 1) === (totalIndex - currentRecord.chatlog.length))
{ {
isRoomInfo = true; results.push({
timestamp: chatlog.timestamp,
habboId: chatlog.userId,
username: chatlog.userName,
hasHighlighting: chatlog.hasHighlighting,
message: chatlog.message,
isRoomInfo: false
});
});
});
break; return results;
}
const index = (props.index - (totalIndex - currentRecord.chatlog.length));
chatlogEntry = currentRecord.chatlog[index];
break;
}
return (
<CellMeasurer
cache={ cache }
columnIndex={ 0 }
key={ props.key }
parent={ props.parent }
rowIndex={ props.index }
>
{ (isRoomInfo && currentRecord) &&
<RoomInfo roomId={ currentRecord.roomId } roomName={ currentRecord.roomName } uniqueKey={ props.key } style={ props.style } /> }
{ !isRoomInfo &&
<Grid key={ props.key } fullHeight={ false } style={ props.style } gap={ 1 } alignItems="center" className="log-entry py-1 border-bottom">
<Text className="g-col-2">{ chatlogEntry.timestamp }</Text>
<Text className="g-col-3" bold underline pointer onClick={ event => CreateLinkEvent(`mod-tools/open-user-info/${ chatlogEntry.userId }`) }>{ chatlogEntry.userName }</Text>
<Text textBreak wrap className="g-col-7">{ chatlogEntry.message }</Text>
</Grid> }
</CellMeasurer>
);
}
const getNumRowsForAdvanced = useCallback(() =>
{
let count = 0;
for(let i = 0; i < records.length; i++)
{
count++; // add room info row
count = count + records[i].chatlog.length;
}
return count;
}, [ records ]); }, [ records ]);
const RoomInfo = (props: { roomId: number, roomName: string, uniqueKey: Key, style: CSSProperties }) => const RoomInfo = (props: { roomId: number, roomName: string }) =>
{ {
return ( return (
<Flex key={ props.uniqueKey } gap={ 2 } alignItems="center" justifyContent="between" className="room-info bg-muted rounded p-1" style={ props.style }> <Flex gap={ 2 } alignItems="center" justifyContent="between" className="bg-muted rounded p-1">
<Flex gap={ 1 }> <Flex gap={ 1 }>
<Text bold>Room name:</Text> <Text bold>Room name:</Text>
<Text>{ props.roomName }</Text> <Text>{ props.roomName }</Text>
@ -115,15 +59,8 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
); );
} }
const cache = new CellMeasurerCache({
defaultHeight: 25,
fixedWidth: true
});
return ( return (
<> <>
{ (records && (records.length === 1)) &&
<RoomInfo roomId={ records[0].roomId } roomName={ records[0].roomName } uniqueKey={ null } style={ {} } /> }
<Column fit gap={ 0 } overflow="hidden"> <Column fit gap={ 0 } overflow="hidden">
<Column gap={ 2 }> <Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1"> <Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
@ -133,25 +70,21 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
</Grid> </Grid>
</Column> </Column>
{ (records && (records.length > 0)) && { (records && (records.length > 0)) &&
<Column className="log-container striped-children" overflow="auto" gap={ 0 }> <InfiniteScroll rows={ allRecords } estimateSize={ 25 } rowRender={ (row: ChatlogRecord) =>
<AutoSizer defaultWidth={ 400 } defaultHeight={ 200 }> {
{ ({ height, width }) => return (
{ <>
cache.clearAll(); { row.isRoomInfo &&
<RoomInfo roomId={ row.roomId } roomName={ row.roomName } /> }
return ( { !row.isRoomInfo &&
<List <Grid fullHeight={ false } gap={ 1 } alignItems="center" className="log-entry py-1 border-bottom">
width={ width } <Text className="g-col-2">{ row.timestamp }</Text>
height={ height } <Text className="g-col-3" bold underline pointer onClick={ event => CreateLinkEvent(`mod-tools/open-user-info/${ row.habboId }`) }>{ row.username }</Text>
rowCount={ (records.length > 1) ? getNumRowsForAdvanced() : records[0].chatlog.length } <Text textBreak wrap className="g-col-7">{ row.message }</Text>
rowHeight={ cache.rowHeight } </Grid> }
className={ 'log-entry-container' } </>
rowRenderer={ (records.length > 1) ? advancedRowRenderer : rowRenderer } );
deferredMeasurementCache={ cache } /> } } /> }
);
} }
</AutoSizer>
</Column> }
</Column> </Column>
</> </>
); );

View File

@ -35,7 +35,7 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
return ( return (
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `Room Chatlog ${ roomChatlog.roomName }` } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ `Room Chatlog ${ roomChatlog.roomName }` } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-100"> <NitroCardContentView className="text-black" overflow="hidden">
{ roomChatlog && { roomChatlog &&
<ChatlogView records={ [ roomChatlog ] } /> } <ChatlogView records={ [ roomChatlog ] } /> }
</NitroCardContentView> </NitroCardContentView>

View File

@ -1,8 +1,7 @@
import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer'; import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { AutoSizer, List, ListRowProps } from 'react-virtualized';
import { SendMessageComposer, TryVisitRoom } from '../../../../api'; import { SendMessageComposer, TryVisitRoom } from '../../../../api';
import { Base, Column, DraggableWindowPosition, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { Base, Column, DraggableWindowPosition, Grid, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent } from '../../../../hooks'; import { useMessageEvent } from '../../../../hooks';
interface ModToolsUserRoomVisitsViewProps interface ModToolsUserRoomVisitsViewProps
@ -32,19 +31,6 @@ export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = p
if(!userId) return null; if(!userId) return null;
const RowRenderer = (props: ListRowProps) =>
{
const item = roomVisitData.rooms[props.index];
return (
<Grid key={ props.key } fullHeight={ false } style={ props.style } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Text className="g-col-2">{ item.enterHour.toString().padStart(2, '0') }: { item.enterMinute.toString().padStart(2, '0') }</Text>
<Text className="g-col-7">{ item.roomName }</Text>
<Text bold underline pointer variant="primary" className="g-col-3" onClick={ event => TryVisitRoom(item.roomId) }>Visit Room</Text>
</Grid>
);
}
return ( return (
<NitroCardView className="nitro-mod-tools-user-visits" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-user-visits" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } />
@ -57,24 +43,16 @@ export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = p
<Base className="g-col-3">Visit</Base> <Base className="g-col-3">Visit</Base>
</Grid> </Grid>
</Column> </Column>
<Column className="log-container striped-children" overflow="auto" gap={ 0 }> <InfiniteScroll rows={ roomVisitData?.rooms ?? [] } estimateSize={ 25 } rowRender={ row =>
{ roomVisitData && {
<AutoSizer defaultWidth={ 400 } defaultHeight={ 200 }> return (
{ ({ height, width }) => <Grid fullHeight={ false } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
{ <Text className="g-col-2">{ row.enterHour.toString().padStart(2, '0') }: { row.enterMinute.toString().padStart(2, '0') }</Text>
return ( <Text className="g-col-7">{ row.roomName }</Text>
<List <Text bold underline pointer variant="primary" className="g-col-3" onClick={ event => TryVisitRoom(row.roomId) }>Visit Room</Text>
width={ width } </Grid>
height={ height } );
rowCount={ roomVisitData.rooms.length } } } />
rowHeight={ 25 }
className={ 'log-entry-container' }
rowRenderer={ RowRenderer }
/>
);
} }
</AutoSizer> }
</Column>
</Column> </Column>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>

View File

@ -1,7 +1,7 @@
import classNames from 'classnames';
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { AutoSizer, List, ListRowProps, ListRowRenderer } from 'react-virtualized';
import { GetSessionDataManager, LocalizeText, RoomObjectItem } from '../../../../api'; import { GetSessionDataManager, LocalizeText, RoomObjectItem } from '../../../../api';
import { Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { Flex, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
interface ChooserWidgetViewProps interface ChooserWidgetViewProps
{ {
@ -25,17 +25,6 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
return items.filter(item => item.name.toLocaleLowerCase().includes(value)); return items.filter(item => item.name.toLocaleLowerCase().includes(value));
}, [ items, searchValue ]); }, [ items, searchValue ]);
const rowRenderer: ListRowRenderer = (props: ListRowProps) =>
{
const item = filteredItems[props.index];
return (
<Flex key={ props.key } alignItems="center" position="absolute" className={ 'rounded px-1' + ((selectedItem === item) ? ' bg-muted' : '') } pointer style={ props.style } onClick={ event => setSelectedItem(item) }>
<Text truncate>{ item.name } { canSeeId && (' - ' + item.id) }</Text>
</Flex>
);
}
useEffect(() => useEffect(() =>
{ {
if(!selectedItem) return; if(!selectedItem) return;
@ -46,21 +35,16 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
return ( return (
<NitroCardView className="nitro-chooser-widget" theme="primary-slim"> <NitroCardView className="nitro-chooser-widget" theme="primary-slim">
<NitroCardHeaderView headerText={ title } onCloseClick={ onClose } /> <NitroCardHeaderView headerText={ title } onCloseClick={ onClose } />
<NitroCardContentView overflow="hidden"> <NitroCardContentView overflow="hidden" gap={ 2 }>
<input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('generic.search') } value={ searchValue } onChange={ event => setSearchValue(event.target.value) } /> <input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('generic.search') } value={ searchValue } onChange={ event => setSearchValue(event.target.value) } />
<Column fullHeight overflow="auto"> <InfiniteScroll rows={ filteredItems } estimateSize={ 25 } rowRender={ row =>
<AutoSizer defaultWidth={ 0 } defaultHeight={ 0 }> {
{ ({ width, height }) => return (
{ <Flex alignItems="center" className={ classNames('rounded p-1', (selectedItem === row) && 'bg-muted') } pointer onClick={ event => setSelectedItem(row) }>
return (<List <Text truncate>{ row.name } { canSeeId && (' - ' + row.id) }</Text>
width={ width } </Flex>
height={ height } );
rowCount={ filteredItems.length } } } />
rowHeight={ 20 }
rowRenderer={ rowRenderer } />)
} }
</AutoSizer>
</Column>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -1043,7 +1043,7 @@
core-js-pure "^3.20.2" core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.18.9" version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
@ -1614,9 +1614,9 @@
integrity sha512-4eMqkns+NL2/DmdezjbVG4TW+eII3hvgDM3koDQNoO4yjMgU+55TTptPU9jJL/JJwntRiUECLSIHg8eZxmA5mA== integrity sha512-4eMqkns+NL2/DmdezjbVG4TW+eII3hvgDM3koDQNoO4yjMgU+55TTptPU9jJL/JJwntRiUECLSIHg8eZxmA5mA==
"@pixi/filter-adjustment@^4.1.3": "@pixi/filter-adjustment@^4.1.3":
version "4.1.3" version "4.2.0"
resolved "https://registry.yarnpkg.com/@pixi/filter-adjustment/-/filter-adjustment-4.1.3.tgz#61e34b4dd9766ccf40463f0538201bf68f78df66" resolved "https://registry.yarnpkg.com/@pixi/filter-adjustment/-/filter-adjustment-4.2.0.tgz#1648f705f856619835184dae42299138bf964c83"
integrity sha512-W+NhPiZRYKoRToa5+tkU95eOw8gnS5dfIp3ZP+pLv2mdER9RI+4xHxp1uLHMqUYZViTaMdZIIoVOuCgHFPYCbQ== integrity sha512-3Pvo5WyUb8X5GQ69X3/Tj6rHl0q7gArvcIYKeV2+/PzHaMypVF4Lsr3I3zYSyWWokxOQ0bFCAtNWm1WwDRA41g==
"@pixi/filter-alpha@~6.4.2": "@pixi/filter-alpha@~6.4.2":
version "6.4.2" version "6.4.2"
@ -1932,6 +1932,18 @@
"@svgr/plugin-svgo" "^5.5.0" "@svgr/plugin-svgo" "^5.5.0"
loader-utils "^2.0.0" loader-utils "^2.0.0"
"@tanstack/react-virtual@^3.0.0-beta.18":
version "3.0.0-beta.18"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.18.tgz#b97b2019f7d6a5770fb88ee1f7591da55b9059b4"
integrity sha512-mnyCZT6htcRNw1jVb+WyfMUMbd1UmXX/JWPuMf6Bmj92DB/V7Ogk5n5rby5Y5aste7c7mlsBeMF8HtpwERRvEQ==
dependencies:
"@tanstack/virtual-core" "3.0.0-beta.18"
"@tanstack/virtual-core@3.0.0-beta.18":
version "3.0.0-beta.18"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.18.tgz#d4b0738c1d0aada922063c899675ff4df9f696b2"
integrity sha512-tcXutY05NpN9lp3+AXI9Sn85RxSPV0EJC0XMim9oeQj/E7bjXoL0qZ4Er4wwnvIbv/hZjC91EmbIQGjgdr6nZg==
"@tootallnate/once@1": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@ -2123,15 +2135,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-virtualized@^9.21.21": "@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.0.15":
version "9.21.21"
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.21.tgz#65c96f25314f0fb3d40536929dc78112753b49e1"
integrity sha512-Exx6I7p4Qn+BBA1SRyj/UwQlZ0I0Pq7g7uhAp0QQ4JWzZunqEqNBGTmCmMmS/3N9wFgAGWuBD16ap7k8Y14VPA==
dependencies:
"@types/prop-types" "*"
"@types/react" "^17"
"@types/react@*", "@types/react@>=16.9.11", "@types/react@^17", "@types/react@^18.0.15":
version "18.0.15" version "18.0.15"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe"
integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow== integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==
@ -3715,11 +3719,6 @@ cliui@^6.0.0:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^6.2.0" wrap-ansi "^6.2.0"
clsx@^1.0.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
co@^4.6.0: co@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@ -4567,7 +4566,7 @@ dom-converter@^0.2.0:
dependencies: dependencies:
utila "~0.4" utila "~0.4"
dom-helpers@^5.0.1, dom-helpers@^5.1.3, dom-helpers@^5.2.0, dom-helpers@^5.2.1: dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
@ -9420,7 +9419,7 @@ prop-types@15.7.2:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.8.1" react-is "^16.8.1"
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -9757,18 +9756,6 @@ react-transition-group@^4.4.2:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" prop-types "^15.6.2"
react-virtualized@^9.22.3:
version "9.22.3"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421"
integrity sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==
dependencies:
"@babel/runtime" "^7.7.2"
clsx "^1.0.4"
dom-helpers "^5.1.3"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.4"
react-youtube@^7.13.1: react-youtube@^7.13.1:
version "7.14.0" version "7.14.0"
resolved "https://registry.yarnpkg.com/react-youtube/-/react-youtube-7.14.0.tgz#0505d86491521ca94ef0afb74af3f7936dc7bc86" resolved "https://registry.yarnpkg.com/react-youtube/-/react-youtube-7.14.0.tgz#0505d86491521ca94ef0afb74af3f7936dc7bc86"