feat: dynamic PiP collision avoidance, reaction pill redesign, channel dedup fix

PiP positioning:
- Replace hardcoded layout constants with dynamic DOM measurement system
- Obstacle elements declare themselves via data-pip-obstacle="left|bottom"
- getPipBounds() queries actual element rects at runtime via getBoundingClientRect
- MutationObserver + ResizeObserver re-clamp PiP when obstacles appear/resize
- PiP reappears after close when navigating away from voice channel

Reactions:
- Align reaction pills with Aether Drift prototype
- Own-reaction accent: purple → mint (bg, border, count color)
- Default pills: subtle white overlay bg + border-soft border
- Count text: 12px/semibold/txt-secondary per prototype spec

Bug fix:
- Deduplicate channel insertion in createChannel store action
- Prevents double-add race between REST response and WS broadcast
This commit is contained in:
Jannis Braun
2026-03-02 02:09:52 +01:00
parent f79575ffa7
commit ddeb72101e
6 changed files with 124 additions and 29 deletions
@@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback, useMemo } from 'react';
import React, { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { useChatStore } from '../../stores/chatStore';
import { isDmChannel, useServerStore } from '../../stores/serverStore';
import { wsSend } from '../../hooks/useWebSocket';
@@ -30,6 +30,18 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
const members = useServerStore((s) => s.members);
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
// Auto-focus textarea on channel navigation
useEffect(() => {
textareaRef.current?.focus();
}, [channelId]);
// Auto-focus textarea when replying
useEffect(() => {
if (replyTo) {
textareaRef.current?.focus();
}
}, [replyTo]);
// Filter members for the mention popover
const filteredMembers = useMemo(() => {
if (!mentionState) return [];
@@ -216,7 +228,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
};
return (
<div className="px-3 pb-3 flex-shrink-0 md:absolute md:bottom-3 md:left-3 md:right-3 md:z-[110] md:px-0 md:pb-0 md:glass-bubble md:rounded-[14px]">
<div data-pip-obstacle="bottom" className="px-3 pb-3 flex-shrink-0 md:absolute md:bottom-3 md:left-3 md:right-3 md:z-[110] md:px-0 md:pb-0 md:glass-bubble md:rounded-[14px]">
{replyTo && (
<div className="bg-interactive-hover rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-border-hard/50">
<div className="flex items-center gap-1 text-[14px] text-txt-message truncate">