feat: collapse big messages (#810)

* Updating Message component to add ability to collapse long running messages. Height is broadcast out to parent on height toggle.

See: https://github.com/gotify/server/issues/790

* Cleanup of the Message component including simplifying the read-more expand/collapse functionality.

* fix: cleanup & properly updating the height

---------

Co-authored-by: Jannis Mattheis <contact@jmattheis.de>
This commit is contained in:
Jeremy Gooch 2025-07-06 06:07:57 -05:00 committed by GitHub
parent 2498e6e19f
commit c1cb2e855a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 64 additions and 6 deletions

View File

@ -1,14 +1,18 @@
import {Button} from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import {createStyles, Theme, withStyles, WithStyles} from '@material-ui/core/styles'; import {createStyles, Theme, withStyles, WithStyles} from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import {ExpandLess, ExpandMore} from '@material-ui/icons';
import Delete from '@material-ui/icons/Delete'; import Delete from '@material-ui/icons/Delete';
import React from 'react'; import React, {RefObject} from 'react';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import Container from '../common/Container'; import Container from '../common/Container';
import * as config from '../config';
import {Markdown} from '../common/Markdown'; import {Markdown} from '../common/Markdown';
import {RenderMode, contentType} from './extras'; import * as config from '../config';
import {IMessageExtras} from '../types'; import {IMessageExtras} from '../types';
import {contentType, RenderMode} from './extras';
const PREVIEW_LENGTH = 500;
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -52,7 +56,12 @@ const styles = (theme: Theme) =>
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
}, },
content: { content: {
maxHeight: PREVIEW_LENGTH,
wordBreak: 'break-all', wordBreak: 'break-all',
overflowY: 'hidden',
'&.expanded': {
maxHeight: 'none',
},
'& p': { '& p': {
margin: 0, margin: 0,
}, },
@ -79,6 +88,11 @@ interface IProps {
height: (height: number) => void; height: (height: number) => void;
} }
interface IState {
expanded: boolean;
isOverflowing: boolean;
}
const priorityColor = (priority: number) => { const priorityColor = (priority: number) => {
if (priority >= 4 && priority <= 7) { if (priority >= 4 && priority <= 7) {
return 'rgba(230, 126, 34, 0.7)'; return 'rgba(230, 126, 34, 0.7)';
@ -89,14 +103,39 @@ const priorityColor = (priority: number) => {
} }
}; };
class Message extends React.PureComponent<IProps & WithStyles<typeof styles>> { class Message extends React.PureComponent<IProps & WithStyles<typeof styles>, IState> {
public state = {expanded: false, isOverflowing: false};
private node: HTMLDivElement | null = null; private node: HTMLDivElement | null = null;
private previewRef: RefObject<HTMLDivElement>;
public componentDidMount = () => constructor(props: IProps & WithStyles<typeof styles>) {
super(props);
this.previewRef = React.createRef();
}
public componentDidMount = () => {
if (this.previewRef.current) {
this.setState({
isOverflowing:
this.previewRef.current.scrollHeight > this.previewRef.current.clientHeight,
});
}
this.updateHeightInParent();
};
public togglePreviewHeight = () => {
this.setState(
(state) => ({expanded: !state.expanded}),
() => this.updateHeightInParent()
);
};
private updateHeightInParent = () =>
this.props.height(this.node ? this.node.getBoundingClientRect().height : 0); this.props.height(this.node ? this.node.getBoundingClientRect().height : 0);
private renderContent = () => { private renderContent = () => {
const content = this.props.content; const content = this.props.content;
switch (contentType(this.props.extras)) { switch (contentType(this.props.extras)) {
case RenderMode.Markdown: case RenderMode.Markdown:
return <Markdown>{content}</Markdown>; return <Markdown>{content}</Markdown>;
@ -114,6 +153,7 @@ class Message extends React.PureComponent<IProps & WithStyles<typeof styles>> {
<Container <Container
style={{ style={{
display: 'flex', display: 'flex',
flexWrap: 'wrap',
borderLeftColor: priorityColor(priority), borderLeftColor: priorityColor(priority),
borderLeftWidth: 6, borderLeftWidth: 6,
borderLeftStyle: 'solid', borderLeftStyle: 'solid',
@ -141,10 +181,28 @@ class Message extends React.PureComponent<IProps & WithStyles<typeof styles>> {
<Delete /> <Delete />
</IconButton> </IconButton>
</div> </div>
<Typography component="div" className={`${classes.content} content`}>
<Typography
component="div"
ref={this.previewRef}
className={`${classes.content} content ${
this.state.isOverflowing && this.state.expanded ? 'expanded' : ''
}`}>
{this.renderContent()} {this.renderContent()}
</Typography> </Typography>
</div> </div>
{this.state.isOverflowing && (
<Button
style={{marginTop: 16}}
onClick={() => this.togglePreviewHeight()}
variant="contained"
color="primary"
size="large"
fullWidth={true}
startIcon={this.state.expanded ? <ExpandLess /> : <ExpandMore />}>
{this.state.expanded ? 'Read Less' : 'Read More'}
</Button>
)}
</Container> </Container>
</div> </div>
); );