rework internal order system

This commit is contained in:
Matthias Hochmeister
2026-03-24 14:02:16 +01:00
parent 90944ca5f6
commit abb337c683
9 changed files with 261 additions and 75 deletions

View File

@@ -10,7 +10,7 @@ import type { NextcloudMessage } from '../../types/nextcloud.types';
import FileMessageContent from './FileMessageContent';
import RichMessageText from './RichMessageText';
import PollMessageContent from './PollMessageContent';
import MessageReactions from './MessageReactions';
import { ReactionChips, AddReactionButton } from './MessageReactions';
const SENDER_COLORS = [
'#E53935', '#D81B60', '#8E24AA', '#5E35B1',
@@ -44,9 +44,24 @@ function hasPollParam(params?: Record<string, any>): boolean {
const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneToOne, onReplyClick, onReactionToggled, reactionsOverride }) => {
const [hovered, setHovered] = useState(false);
const hoverDelayTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseEnter = () => {
hoverDelayTimer.current = setTimeout(() => {
setHovered(true);
}, 200);
};
const handleMouseLeave = () => {
if (hoverDelayTimer.current) {
clearTimeout(hoverDelayTimer.current);
hoverDelayTimer.current = null;
}
setHovered(false);
};
const handleTouchStart = () => {
longPressTimer.current = setTimeout(() => {
setHovered(true);
@@ -73,6 +88,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
}
setHovered(false);
};
const time = new Date(message.timestamp * 1000).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
@@ -96,7 +112,43 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
const reactions = reactionsOverride?.reactions ?? message.reactions ?? {};
const reactionsSelf = reactionsOverride?.reactionsSelf ?? message.reactionsSelf ?? [];
const hasReactions = Object.keys(reactions).length > 0;
/* Vertical toolbar with reply + add-reaction */
const toolbar = (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 0.25,
opacity: hovered ? 0.7 : 0,
visibility: hovered ? 'visible' : 'hidden',
transition: 'opacity 0.15s, visibility 0.15s',
alignSelf: 'center',
flexShrink: 0,
}}
>
{onReplyClick && (
<Tooltip title="Antworten">
<IconButton
size="small"
onClick={() => onReplyClick(message)}
sx={{ width: 24, height: 24 }}
>
<ReplyIcon sx={{ fontSize: '0.9rem', transform: isOwnMessage ? 'scaleX(-1)' : undefined }} />
</IconButton>
</Tooltip>
)}
{onReactionToggled && (
<AddReactionButton
token={message.token}
messageId={message.id}
reactionsSelf={reactionsSelf}
onReactionToggled={() => onReactionToggled(message.id)}
/>
)}
</Box>
);
return (
<Box
@@ -105,27 +157,17 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
justifyContent: isOwnMessage ? 'flex-end' : 'flex-start',
my: 0.5,
px: 1,
alignItems: 'flex-end',
alignItems: 'flex-start',
gap: 0.5,
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onTouchMove={handleTouchMove}
>
{/* Reply button for own messages (shown on left) */}
{isOwnMessage && onReplyClick && (
<Tooltip title="Antworten">
<IconButton
size="small"
onClick={() => onReplyClick(message)}
sx={{ opacity: hovered ? 0.7 : 0, visibility: hovered ? 'visible' : 'hidden', transition: 'opacity 0.15s, visibility 0.15s', mb: 0.5 }}
>
<ReplyIcon sx={{ fontSize: '0.9rem', transform: 'scaleX(-1)' }} />
</IconButton>
</Tooltip>
)}
{/* Own messages: toolbar on LEFT */}
{isOwnMessage && toolbar}
<Box sx={{ maxWidth: '80%' }}>
<Paper
@@ -212,29 +254,18 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
</Typography>
</Paper>
{/* Reactions — always rendered to avoid layout shift */}
<MessageReactions
{/* Reaction chips below the bubble */}
<ReactionChips
token={message.token}
messageId={message.id}
reactions={reactions}
reactionsSelf={reactionsSelf}
onReactionToggled={() => onReactionToggled?.(message.id)}
visible={hovered || hasReactions}
/>
</Box>
{/* Reply button for other messages (shown on right) */}
{!isOwnMessage && onReplyClick && (
<Tooltip title="Antworten">
<IconButton
size="small"
onClick={() => onReplyClick(message)}
sx={{ opacity: hovered ? 0.7 : 0, visibility: hovered ? 'visible' : 'hidden', transition: 'opacity 0.15s, visibility 0.15s', mb: 0.5 }}
>
<ReplyIcon sx={{ fontSize: '0.9rem' }} />
</IconButton>
</Tooltip>
)}
{/* Partner messages: toolbar on RIGHT */}
{!isOwnMessage && toolbar}
</Box>
);
};

View File

@@ -34,24 +34,16 @@ const EXTENDED_EMOJI_CATEGORIES: { label: string; emojis: string[] }[] = [
},
];
interface MessageReactionsProps {
token: string;
messageId: number;
reactions: Record<string, number>;
reactionsSelf: string[];
onReactionToggled: () => void;
visible?: boolean;
}
/* ------------------------------------------------------------------ */
/* Shared toggle helper */
/* ------------------------------------------------------------------ */
const MessageReactions: React.FC<MessageReactionsProps> = ({
token,
messageId,
reactions,
reactionsSelf,
onReactionToggled,
visible = true,
}) => {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
function useReactionToggle(
token: string,
messageId: number,
reactionsSelf: string[],
onReactionToggled: () => void,
) {
const [loading, setLoading] = useState<string | null>(null);
const handleToggle = async (emoji: string) => {
@@ -70,8 +62,33 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
}
};
return { loading, handleToggle };
}
/* ------------------------------------------------------------------ */
/* ReactionChips — existing reactions rendered below the bubble */
/* ------------------------------------------------------------------ */
interface ReactionChipsProps {
token: string;
messageId: number;
reactions: Record<string, number>;
reactionsSelf: string[];
onReactionToggled: () => void;
}
const ReactionChips: React.FC<ReactionChipsProps> = ({
token,
messageId,
reactions,
reactionsSelf,
onReactionToggled,
}) => {
const { loading, handleToggle } = useReactionToggle(token, messageId, reactionsSelf, onReactionToggled);
const hasReactions = Object.keys(reactions).length > 0;
if (!hasReactions) return null;
return (
<Box sx={{
display: 'flex',
@@ -79,12 +96,8 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
gap: 0.25,
mt: 0.25,
alignItems: 'center',
minHeight: 24,
visibility: visible ? 'visible' : 'hidden',
opacity: visible ? 1 : 0,
transition: 'opacity 0.15s, visibility 0.15s',
}}>
{hasReactions && Object.entries(reactions).map(([emoji, count]) => (
{Object.entries(reactions).map(([emoji, count]) => (
<Chip
key={emoji}
label={`${emoji} ${count}`}
@@ -102,11 +115,37 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
}}
/>
))}
</Box>
);
};
/* ------------------------------------------------------------------ */
/* AddReactionButton — icon button + emoji popover */
/* ------------------------------------------------------------------ */
interface AddReactionButtonProps {
token: string;
messageId: number;
reactionsSelf: string[];
onReactionToggled: () => void;
}
const AddReactionButton: React.FC<AddReactionButtonProps> = ({
token,
messageId,
reactionsSelf,
onReactionToggled,
}) => {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const { handleToggle } = useReactionToggle(token, messageId, reactionsSelf, onReactionToggled);
return (
<>
<Tooltip title="Reaktion hinzuf\u00FCgen">
<IconButton
size="small"
onClick={(e) => setAnchorEl(e.currentTarget)}
sx={{ width: 20, height: 20, opacity: 0.6, '&:hover': { opacity: 1 } }}
sx={{ width: 24, height: 24, opacity: 0.6, '&:hover': { opacity: 1 } }}
>
<AddReactionOutlinedIcon sx={{ fontSize: '0.85rem' }} />
</IconButton>
@@ -157,8 +196,50 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
</Box>
</Box>
</Popover>
</>
);
};
/* ------------------------------------------------------------------ */
/* Legacy default export — kept for backward compatibility */
/* (combines chips + add button, same interface as before) */
/* ------------------------------------------------------------------ */
interface MessageReactionsProps {
token: string;
messageId: number;
reactions: Record<string, number>;
reactionsSelf: string[];
onReactionToggled: () => void;
visible?: boolean;
}
const MessageReactions: React.FC<MessageReactionsProps> = (props) => {
const { visible = true, ...rest } = props;
const hasReactions = Object.keys(rest.reactions).length > 0;
return (
<Box sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 0.25,
mt: 0.25,
alignItems: 'center',
minHeight: hasReactions ? 24 : 0,
visibility: visible ? 'visible' : 'hidden',
opacity: visible ? 1 : 0,
transition: 'opacity 0.15s, visibility 0.15s',
}}>
<ReactionChips {...rest} />
<AddReactionButton
token={rest.token}
messageId={rest.messageId}
reactionsSelf={rest.reactionsSelf}
onReactionToggled={rest.onReactionToggled}
/>
</Box>
);
};
export { ReactionChips, AddReactionButton };
export default MessageReactions;