feat: launch readiness — PWA, API hardening, memory leak fixes, sticker removal

- Add PWA infrastructure: vite-plugin-pwa, manifest, service worker,
  SW update prompt component, placeholder icons, Apple meta tags
- Harden API client: 401 auto-logout, AbortController timeouts
  (30s standard, 120s uploads), onUnauthorized callback
- Fix memory leaks: clear voice user status on leave, clean up all
  Maps (channelToSpaceMap, permissions, etc.) on removeSpace
- Upgrade error boundary to Aether Drift design with Try Again button,
  collapsible stack trace, and componentDidCatch logging
- Configure desktop icon paths in electron-builder.yml
- Remove sticker feature (server routes, schema, types, UI components)
- Fix Docker build: use **/node_modules in .dockerignore to prevent
  COPY from clobbering pnpm-installed workspace dependencies
- Add vite-env.d.ts declarations for noise suppressor wasm imports
- Exclude test files from tsc build via tsconfig
This commit is contained in:
Jannis Braun
2026-03-15 15:41:22 +01:00
parent b08f40feb7
commit 4d230711fc
36 changed files with 2740 additions and 1283 deletions
@@ -6,7 +6,7 @@ import { MentionPopover } from './MentionPopover';
import { TypingIndicator } from './TypingIndicator';
import { InputPopover, type InputPopoverTab } from './InputPopover';
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
import { MAX_MESSAGE_LENGTH, type MemberWithUser, type Sticker } from '@backspace/shared';
import { MAX_MESSAGE_LENGTH, type MemberWithUser } from '@backspace/shared';
import { useSettingsStore } from '../../stores/settingsStore';
interface MessageInputProps {
@@ -31,7 +31,6 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
const inputContainerRef = useRef<HTMLDivElement>(null);
const popoverAnchorRef = useRef<HTMLDivElement>(null);
const sendMessage = useChatStore((s) => s.sendMessage);
const sendStickerMessage = useChatStore((s) => s.sendStickerMessage);
const replyTo = useChatStore((s) => s.replyTo);
const setReplyTo = useChatStore((s) => s.setReplyTo);
const members = useSpaceStore((s) => s.members);
@@ -39,8 +38,6 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
// Feature flags
const gifEnabled = useSettingsStore((s) => s.gifEnabled);
const spaces = useSpaceStore((s) => s.spaces);
const stickersEnabled = spaces.length > 0; // stickers available if user is in any space
// Permission gating: DM channels always allow sending; space channels check SEND_MESSAGES
const channelPerms = useSpaceStore((s) => s.channelPermissions.get(channelId));
@@ -284,11 +281,6 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
sendMessage(channelId, url);
}, [channelId, sendMessage]);
const handleStickerSelect = useCallback((sticker: Sticker) => {
setActivePopover(null);
sendStickerMessage(channelId, sticker.id);
}, [channelId, sendStickerMessage]);
const togglePopover = useCallback((tab: InputPopoverTab) => {
setActivePopover((prev) => prev === tab ? null : tab);
}, []);
@@ -309,17 +301,15 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
<div ref={popoverAnchorRef} data-pip-obstacle="bottom" className="relative 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]">
<TypingIndicator channelId={channelId} />
{/* Input popover (emoji / gif / stickers) */}
{/* Input popover (emoji / gif) */}
{activePopover && (
<InputPopover
activeTab={activePopover}
onClose={() => setActivePopover(null)}
onEmojiSelect={handleEmojiSelect}
onGifSelect={handleGifSelect}
onStickerSelect={handleStickerSelect}
anchorRef={popoverAnchorRef}
gifEnabled={gifEnabled}
stickersEnabled={stickersEnabled}
onTabChange={setActivePopover}
/>
)}
@@ -460,19 +450,6 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
</button>
)}
{/* Sticker button */}
<button
onClick={() => togglePopover('stickers')}
className={`w-[34px] h-[34px] flex items-center justify-center rounded-[6px] transition-colors flex-shrink-0 ${
activePopover === 'stickers' ? 'text-accent-primary' : 'text-txt-tertiary hover:text-txt-secondary'
}`}
title="Stickers"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12.5 2C6.81 2 2 6.81 2 12.5S6.81 23 12.5 23c1.31 0 2.56-.25 3.73-.7l5.07-5.07c.45-1.17.7-2.42.7-3.73C22 7.81 17.19 2 12.5 2Zm0 19c-4.69 0-8.5-3.81-8.5-8.5S7.81 4 12.5 4 21 7.81 21 12.5c0 .89-.14 1.74-.4 2.54l-3.56 3.56c-.8.26-1.65.4-2.54.4ZM8 11.5c.83 0 1.5-.67 1.5-1.5S8.83 8.5 8 8.5 6.5 9.17 6.5 10s.67 1.5 1.5 1.5Zm6 0c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5-1.5.67-1.5 1.5.67 1.5 1.5 1.5Zm-1 3.5c-2.33 0-4.31-1.46-5.11-3.5h10.22c-.8 2.04-2.78 3.5-5.11 3.5Z" />
</svg>
</button>
{/* Emoji button */}
<button
onClick={() => togglePopover('emoji')}