|
1 |
| -import React, { useEffect, useRef } from 'react' |
2 |
| -import { CellMeasurer, CellMeasurerCache, List, ListRowProps } from 'react-virtualized' |
3 |
| -import AutoSizer from 'react-virtualized-auto-sizer' |
| 1 | +import React, { useCallback, useEffect, useRef, useState } from 'react' |
| 2 | +import { ListChildComponentProps, ListOnScrollProps, VariableSizeList as List } from 'react-window' |
| 3 | +import { EuiButton, EuiIcon } from '@elastic/eui' |
4 | 4 |
|
5 | 5 | import { getFormatDateTime } from 'uiSrc/utils'
|
6 | 6 | import { IMessage } from 'apiSrc/modules/pub-sub/interfaces/message.interface'
|
| 7 | + |
7 | 8 | import styles from './styles.module.scss'
|
8 | 9 |
|
9 | 10 | export interface Props {
|
10 | 11 | items: IMessage[]
|
| 12 | + width: number |
| 13 | + height: number |
11 | 14 | }
|
12 | 15 |
|
13 | 16 | const PROTRUDING_OFFSET = 2
|
| 17 | +const MIN_ROW_HEIGHT = 30 |
14 | 18 |
|
15 | 19 | const MessagesList = (props: Props) => {
|
16 |
| - const { items = [] } = props |
17 |
| - |
18 |
| - const cache = new CellMeasurerCache({ |
19 |
| - defaultHeight: 17, |
20 |
| - fixedWidth: true, |
21 |
| - fixedHeight: false |
22 |
| - }) |
| 20 | + const { items = [], width = 0, height = 0 } = props |
23 | 21 |
|
| 22 | + const [showAnchor, setShowAnchor] = useState<boolean>(false) |
24 | 23 | const listRef = useRef<List>(null)
|
| 24 | + const followRef = useRef<boolean>(true) |
| 25 | + const hasMountedRef = useRef<boolean>(false) |
| 26 | + const rowHeights = useRef<{ [key: number]: number }>({}) |
| 27 | + const outerRef = useRef<HTMLDivElement>(null) |
| 28 | + |
| 29 | + useEffect(() => { |
| 30 | + scrollToBottom() |
| 31 | + }, []) |
25 | 32 |
|
26 | 33 | useEffect(() => {
|
27 |
| - clearCacheAndUpdate() |
| 34 | + if (items.length > 0 && followRef.current) { |
| 35 | + setTimeout(() => { |
| 36 | + scrollToBottom() |
| 37 | + }, 0) |
| 38 | + } |
28 | 39 | }, [items])
|
29 | 40 |
|
30 |
| - const clearCacheAndUpdate = () => { |
31 |
| - listRef?.current?.scrollToRow(items.length - 1) |
| 41 | + useEffect(() => { |
| 42 | + if (followRef.current) { |
| 43 | + setTimeout(() => { |
| 44 | + scrollToBottom() |
| 45 | + }, 0) |
| 46 | + } |
| 47 | + }, [width, height]) |
| 48 | + |
| 49 | + const getRowHeight = (index: number) => ( |
| 50 | + rowHeights.current[index] > MIN_ROW_HEIGHT ? (rowHeights.current[index] + 2) : MIN_ROW_HEIGHT |
| 51 | + ) |
| 52 | + |
| 53 | + const setRowHeight = (index: number, size: number) => { |
| 54 | + listRef.current?.resetAfterIndex(0) |
| 55 | + rowHeights.current = { ...rowHeights.current, [index]: size } |
| 56 | + } |
| 57 | + |
| 58 | + const scrollToBottom = () => { |
| 59 | + listRef.current?.scrollToItem(items.length - 1, 'end') |
32 | 60 | requestAnimationFrame(() => {
|
33 |
| - listRef?.current?.scrollToRow(items.length - 1) |
| 61 | + listRef.current?.scrollToItem(items.length - 1, 'end') |
34 | 62 | })
|
35 | 63 | }
|
36 | 64 |
|
37 |
| - const rowRenderer = ({ parent, index, key, style }: ListRowProps) => { |
38 |
| - const { time = 0, channel = '', message = '' } = items[index] |
| 65 | + // TODO: delete after manual tests |
| 66 | + // const scrollToBottomReserve = () => { |
| 67 | + // const { scrollHeight = 0, offsetHeight = 0 } = outerRef.current || {} |
| 68 | + |
| 69 | + // listRef.current?.scrollTo(scrollHeight - offsetHeight) |
| 70 | + // requestAnimationFrame(() => { |
| 71 | + // listRef.current?.scrollTo(scrollHeight - offsetHeight) |
| 72 | + // }) |
| 73 | + // } |
| 74 | + |
| 75 | + const handleAnchorClick = () => { |
| 76 | + scrollToBottom() |
| 77 | + } |
| 78 | + |
| 79 | + const handleScroll = useCallback((e: ListOnScrollProps) => { |
| 80 | + if (!hasMountedRef.current) { |
| 81 | + hasMountedRef.current = true |
| 82 | + return |
| 83 | + } |
| 84 | + |
| 85 | + if (e.scrollUpdateWasRequested === false) { |
| 86 | + followRef.current = false |
| 87 | + setShowAnchor(true) |
| 88 | + } |
| 89 | + |
| 90 | + if (!outerRef.current) { |
| 91 | + return |
| 92 | + } |
| 93 | + |
| 94 | + if (e.scrollOffset + outerRef.current.offsetHeight === outerRef.current.scrollHeight) { |
| 95 | + followRef.current = true |
| 96 | + setShowAnchor(false) |
| 97 | + } |
| 98 | + }, []) |
| 99 | + |
| 100 | + const Row = ({ index, style }: ListChildComponentProps) => { |
| 101 | + const rowRef = useRef<HTMLDivElement>(null) |
| 102 | + |
| 103 | + useEffect(() => { |
| 104 | + if (rowRef.current) { |
| 105 | + setRowHeight(index, rowRef.current?.clientHeight) |
| 106 | + } |
| 107 | + }, [rowRef]) |
| 108 | + |
| 109 | + const { channel, message, time } = items[index] |
| 110 | + |
39 | 111 | return (
|
40 |
| - <CellMeasurer |
41 |
| - cache={cache} |
42 |
| - columnIndex={0} |
43 |
| - key={key} |
44 |
| - parent={parent} |
45 |
| - rowIndex={index} |
46 |
| - > |
47 |
| - {({ registerChild, measure }) => ( |
48 |
| - <div onLoad={measure} className={styles.item} ref={registerChild} style={style}> |
49 |
| - <div className={styles.time}>{getFormatDateTime(time)}</div> |
50 |
| - <div className={styles.channel}>{channel}</div> |
51 |
| - <div className={styles.message}>{message}</div> |
52 |
| - </div> |
53 |
| - )} |
54 |
| - </CellMeasurer> |
| 112 | + <div style={style} className={styles.item} data-testid={`row-${index}`}> |
| 113 | + <div className={styles.time}>{getFormatDateTime(time)}</div> |
| 114 | + <div className={styles.channel}>{channel}</div> |
| 115 | + <div className={styles.message} ref={rowRef}>{message}</div> |
| 116 | + </div> |
55 | 117 | )
|
56 | 118 | }
|
57 | 119 |
|
58 | 120 | return (
|
59 | 121 | <>
|
60 |
| - <div className={styles.header} data-testid="messages-list"> |
61 |
| - <div className={styles.time}>Timestamp</div> |
62 |
| - <div className={styles.channel}>Channel</div> |
63 |
| - <div className={styles.message}>Message</div> |
64 |
| - </div> |
65 |
| - <AutoSizer> |
66 |
| - {({ width, height }) => ( |
67 |
| - <List |
68 |
| - ref={listRef} |
69 |
| - width={width - PROTRUDING_OFFSET} |
70 |
| - height={height - PROTRUDING_OFFSET} |
71 |
| - rowCount={items.length} |
72 |
| - rowHeight={cache.rowHeight} |
73 |
| - rowRenderer={rowRenderer} |
74 |
| - overscanRowCount={30} |
75 |
| - className={styles.listWrapper} |
76 |
| - deferredMeasurementCache={cache} |
77 |
| - /> |
78 |
| - )} |
79 |
| - </AutoSizer> |
| 122 | + <List |
| 123 | + height={height} |
| 124 | + itemCount={items.length} |
| 125 | + itemSize={getRowHeight} |
| 126 | + ref={listRef} |
| 127 | + width={width - PROTRUDING_OFFSET} |
| 128 | + className={styles.listContent} |
| 129 | + outerRef={outerRef} |
| 130 | + onScroll={handleScroll} |
| 131 | + overscanCount={30} |
| 132 | + > |
| 133 | + {Row} |
| 134 | + </List> |
| 135 | + {showAnchor && ( |
| 136 | + <EuiButton |
| 137 | + fill |
| 138 | + color="secondary" |
| 139 | + className={styles.anchorBtn} |
| 140 | + onClick={handleAnchorClick} |
| 141 | + data-testid="messages-list-anchor-btn" |
| 142 | + > |
| 143 | + New messages |
| 144 | + <EuiIcon type="sortDown" /> |
| 145 | + </EuiButton> |
| 146 | + )} |
80 | 147 | </>
|
81 | 148 | )
|
82 | 149 | }
|
|
0 commit comments