Skip to content

Commit 178419e

Browse files
committed
Readded a virtual section for messages
ReactInfinite is replaced by virtuoso as ReactInfinite is not maintained anymore and does not provide a working version for react 18 or 19 Fix test: count elements with css class `.message` to count amount of messages displayed
1 parent 4d0f9bf commit 178419e

File tree

5 files changed

+75
-90
lines changed

5 files changed

+75
-90
lines changed

ui/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"react-router": "7.1.1",
2525
"react-router-dom": "7.1.1",
2626
"react-timeago": "7.2.0",
27+
"react-virtuoso": "4.12.3",
2728
"react-window": "1.8.11",
2829
"remark-gfm": "4.0.0",
2930
"remove-markdown": "0.6.0",
@@ -33,7 +34,7 @@
3334
"start": "vite",
3435
"build": "vite build",
3536
"preview": "vite preview",
36-
"test": "vitest",
37+
"test": "vitest --reporter=basic --disable-console-intercept",
3738
"lint": "eslint \"src/**/*.{ts,tsx}\"",
3839
"format": "prettier \"src/**/*.{ts,tsx}\" --write",
3940
"testformat": "prettier \"src/**/*.{ts,tsx}\" --list-different"

ui/src/message/Message.tsx

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useEffect, useRef} from 'react';
1+
import React from 'react';
22
import IconButton from '@mui/material/IconButton';
33
import {Theme} from '@mui/material/styles';
44
import Typography from '@mui/material/Typography';
@@ -78,7 +78,6 @@ interface IProps {
7878
priority: number;
7979
fDelete: VoidFunction;
8080
extras?: IMessageExtras;
81-
height: (height: number) => void;
8281
}
8382

8483
const priorityColor = (priority: number) => {
@@ -91,14 +90,8 @@ const priorityColor = (priority: number) => {
9190
}
9291
};
9392

94-
const Message = ({fDelete, title, date, image, priority, content, extras, height}: IProps) => {
93+
const Message = ({fDelete, title, date, image, priority, content, extras}: IProps) => {
9594
const {classes} = useStyles();
96-
const node = useRef<HTMLDivElement>(null);
97-
98-
useEffect(() => {
99-
// TODO: fix this
100-
// height(node ? node.getBoundingClientRect().height : 0);
101-
}, []);
10295

10396
const renderContent = () => {
10497
switch (contentType(extras)) {
@@ -111,7 +104,7 @@ const Message = ({fDelete, title, date, image, priority, content, extras, height
111104
};
112105

113106
return (
114-
<div className={`${classes.wrapperPadding} message`} ref={node}>
107+
<div className={`${classes.wrapperPadding} message`}>
115108
<Container
116109
style={{
117110
display: 'flex',

ui/src/message/Messages.tsx

+59-73
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import Grid from '@mui/material/Grid2';
22
import Typography from '@mui/material/Typography';
33
import React, {useEffect, useState} from 'react';
44
import {useParams} from 'react-router';
5+
import {Virtuoso} from 'react-virtuoso';
56
import DefaultPage from '../common/DefaultPage';
67
import Button from '@mui/material/Button';
78
import {useAppDispatch, useAppSelector} from '../store';
89
import {getAppName} from '../application/app-actions.ts';
10+
import {IMessage} from '../types.ts';
911
import {fetchMessages, removeMessagesByApp, removeSingleMessage} from './message-actions.ts';
1012
import Message from './Message';
1113
import ConfirmDialog from '../common/ConfirmDialog';
@@ -18,20 +20,35 @@ const Messages = () => {
1820

1921
const [toDeleteAll, setToDeleteAll] = useState<boolean>(false);
2022

21-
const heights: Record<string, number> = {};
22-
2323
const selectedApp = useAppSelector((state) => state.app.items.find((app) => app.id === appId));
2424
const apps = useAppSelector((state) => state.app.items);
25-
const messages = useAppSelector((state) => appId === -1 ? state.message.items : state.message.items.filter((item) => item.appid === appId));
25+
const messages = useAppSelector((state) =>
26+
appId === -1
27+
? state.message.items
28+
: state.message.items.filter((item) => item.appid === appId)
29+
);
2630
const hasMore = useAppSelector((state) => state.message.hasMore);
2731
const name = dispatch(getAppName(appId));
2832
const messagesLoaded = useAppSelector((state) => state.message.loaded);
2933
const hasMessages = messages.length !== 0;
3034

3135
useEffect(() => {
36+
window.onscroll = () => {
37+
if (
38+
window.innerHeight + window.scrollY >=
39+
document.body.offsetHeight - window.innerHeight * 2
40+
) {
41+
checkIfLoadMore();
42+
}
43+
};
44+
3245
dispatch(fetchMessages());
3346
}, [dispatch]);
3447

48+
const checkIfLoadMore = () => {
49+
console.log('checkIfLoadMore');
50+
};
51+
3552
const label = (text: string) => (
3653
<Grid size={12}>
3754
<Typography variant="caption" component="div" gutterBottom align="center">
@@ -40,6 +57,44 @@ const Messages = () => {
4057
</Grid>
4158
);
4259

60+
const messageFooter = () => {
61+
if (hasMore) {
62+
return <LoadingSpinner />;
63+
}
64+
if (hasMessages) {
65+
return label("You've reached the end");
66+
}
67+
return null;
68+
};
69+
70+
const renderMessages = () => (
71+
<Virtuoso
72+
id="messages"
73+
style={{width: '100%'}}
74+
useWindowScroll
75+
totalCount={messages.length}
76+
data={messages}
77+
itemContent={(index, message) => renderMessage(index, message)}
78+
components={{
79+
Footer: messageFooter,
80+
EmptyPlaceholder: () => label('No messages'),
81+
}}
82+
/>
83+
);
84+
85+
const renderMessage = (index: number, message: IMessage) => (
86+
<Message
87+
key={index}
88+
fDelete={() => dispatch(removeSingleMessage(message))}
89+
title={message.title}
90+
date={message.date}
91+
content={message.message}
92+
image={apps.find((app) => app.id == message.appid)?.image}
93+
extras={message.extras}
94+
priority={message.priority}
95+
/>
96+
);
97+
4398
return (
4499
<DefaultPage
45100
title={name}
@@ -63,41 +118,7 @@ const Messages = () => {
63118
</Button>
64119
</div>
65120
}>
66-
{!messagesLoaded ? (
67-
<LoadingSpinner />
68-
) : hasMessages ? (
69-
<div style={{width: '100%'}} id="messages">
70-
{/* TODO: maybe replace ReactInfitite with react-window, which is also documented here: https://mui.com/material-ui/react-list/#virtualized-list */}
71-
{/*<ReactInfinite*/}
72-
{/* key={appId}*/}
73-
{/* useWindowAsScrollContainer*/}
74-
{/* preloadBatchSize={window.innerHeight * 3}*/}
75-
{/* elementHeight={messages.map((m) => heights[m.id] || 1)}>*/}
76-
{/* {messages.map(renderMessage)}*/}
77-
{/*</ReactInfinite>*/}
78-
{messages.map((message) => (
79-
<Message
80-
key={message.id}
81-
height={(height: number) => {
82-
if (!heights[message.id]) {
83-
heights[message.id] = height;
84-
}
85-
}}
86-
fDelete={() => dispatch(removeSingleMessage(message))}
87-
title={message.title}
88-
date={message.date}
89-
content={message.message}
90-
image={apps.find((app) => app.id == message.appid)?.image}
91-
extras={message.extras}
92-
priority={message.priority}
93-
/>
94-
))}
95-
96-
{hasMore ? <LoadingSpinner /> : label("You've reached the end")}
97-
</div>
98-
) : (
99-
label('No messages')
100-
)}
121+
{!messagesLoaded ? <LoadingSpinner /> : renderMessages()}
101122

102123
{toDeleteAll && (
103124
<ConfirmDialog
@@ -113,39 +134,8 @@ const Messages = () => {
113134
/*
114135
@observer
115136
class Messages_old extends Component<IProps & Stores<'messagesStore' | 'appStore'>, IState> {
116-
@observable
117-
private heights: Record<string, number> = {};
118-
@observable
119-
private deleteAll = false;
120-
121-
private static appId(props: IProps) {
122-
if (props === undefined) {
123-
return -1;
124-
}
125-
const {match} = props;
126-
return match.params.id !== undefined ? parseInt(match.params.id, 10) : -1;
127-
}
128-
129-
public state = {appId: -1};
130-
131137
private isLoadingMore = false;
132138
133-
public componentWillReceiveProps(nextProps: IProps & Stores<'messagesStore' | 'appStore'>) {
134-
this.updateAllWithProps(nextProps);
135-
}
136-
137-
public componentWillMount() {
138-
window.onscroll = () => {
139-
if (
140-
window.innerHeight + window.pageYOffset >=
141-
document.body.offsetHeight - window.innerHeight * 2
142-
) {
143-
this.checkIfLoadMore();
144-
}
145-
};
146-
this.updateAll();
147-
}
148-
149139
private updateAllWithProps = (props: IProps & Stores<'messagesStore'>) => {
150140
const appId = Messages.appId(props);
151141
this.setState({appId});
@@ -154,10 +144,6 @@ class Messages_old extends Component<IProps & Stores<'messagesStore' | 'appStore
154144
}
155145
};
156146
157-
private updateAll = () => this.updateAllWithProps(this.props);
158-
159-
160-
161147
private checkIfLoadMore() {
162148
const {appId} = this.state;
163149
if (!this.isLoadingMore && this.props.messagesStore.canLoadMore(appId)) {

ui/src/tests/message.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe.sequential('Messages', () => {
5858
it('has no applications', async () => {
5959
expect(await count(page, `${naviId} .item`)).toBe(0);
6060
});
61-
describe('create apps', () => {
61+
describe.sequential('create apps', () => {
6262
it('Windows', async () => {
6363
windowsServerToken = await createApp('Windows');
6464
await page.reload();
@@ -84,11 +84,11 @@ describe.sequential('Messages', () => {
8484
await navigate('All Messages');
8585
});
8686
it('has no messages', async () => {
87-
expect(await count(page, '#messages')).toBe(0);
87+
expect(await count(page, '#messages .message')).toBe(0);
8888
});
8989
it('has no messages in app', async () => {
9090
await navigate('Windows');
91-
expect(await count(page, '#messages')).toBe(0);
91+
expect(await count(page, '#messages .message')).toBe(0);
9292
await navigate('All Messages');
9393
});
9494

ui/yarn.lock

+8-3
Original file line numberDiff line numberDiff line change
@@ -2031,9 +2031,9 @@ balanced-match@^1.0.0:
20312031
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
20322032

20332033
bare-events@^2.0.0, bare-events@^2.2.0:
2034-
version "2.5.0"
2035-
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc"
2036-
integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==
2034+
version "2.5.3"
2035+
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.3.tgz#899d7e4b2598a7978ef7ca315ce67eff0a16a727"
2036+
integrity sha512-pCO3aoRJ0MBiRMu8B7vUga0qL3L7gO1+SW7ku6qlSsMLwuhaawnuvZDyzJY/kyC63Un0XAB0OPUcfF1eTO/V+Q==
20372037

20382038
bare-fs@^2.1.1:
20392039
version "2.3.5"
@@ -5077,6 +5077,11 @@ react-transition-group@^4.4.5:
50775077
loose-envify "^1.4.0"
50785078
prop-types "^15.6.2"
50795079

5080+
react-virtuoso@4.12.3:
5081+
version "4.12.3"
5082+
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.12.3.tgz#beecf0582b31058c5a6ed3ec58fc43fd780e5844"
5083+
integrity sha512-6X1p/sU7hecmjDZMAwN+r3go9EVjofKhwkUbVlL8lXhBZecPv9XVCkZ/kBPYOr0Mv0Vl5+Ziwgexg9Kh7+NNXQ==
5084+
50805085
react-window@1.8.11:
50815086
version "1.8.11"
50825087
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525"

0 commit comments

Comments
 (0)