Add audio attachments with a paperclip composer control and a styled timeline player

Hop-State: A_06FP9RVYP3SNM80AM7SMF98
Hop-Proposal: R_06FP9RTB5CCV1SZR9B7YSY0
Hop-Task: T_06FP9QHMNTRHD9BZ3B4BVMG
Hop-Attempt: AT_06FP9QHMNV944FQ20714908
This commit is contained in:
2026-07-15 01:27:38 -07:00
committed by Hop
parent d1442dc903
commit 7f8a97ffe2
8 changed files with 290 additions and 30 deletions
+15 -7
View File
@@ -3,15 +3,17 @@
import { useState, useEffect, useRef } from 'react';
import AutoTextarea from '@/components/AutoTextarea';
import { Post, Attachment } from '@/lib/types';
import { ImageIcon, AlertTriangle, Film } from 'lucide-react';
import { AlertTriangle, Music2, Paperclip } from 'lucide-react';
import { VideoEmbed } from '@/components/VideoEmbed';
import { useFormattedHandle } from '@/lib/utils/handle';
import { useAuth } from '@/lib/contexts/AuthContext';
import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPrompt';
import { getStorageProvider, MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
import { getMediaKind } from '@/lib/media/upload-policy';
interface MediaAttachment extends Attachment {
mimeType?: string;
filename?: string;
}
interface ComposeProps {
@@ -142,6 +144,7 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
url: media.url,
altText: media.altText ?? null,
mimeType: media.mimeType ?? file.type,
filename: file.name,
});
} catch (error) {
if (error instanceof MediaUploadError && error.code === 'STORAGE_NOT_CONFIGURED') {
@@ -229,11 +232,16 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
{attachments.length > 0 && (
<div className="compose-media-grid">
{attachments.map((item) => {
const isVideo = item.mimeType?.startsWith('video/');
const mediaKind = getMediaKind(item.mimeType);
return (
<div className="compose-media-item" key={item.id}>
{isVideo ? (
<div className={`compose-media-item ${mediaKind === 'audio' ? 'audio' : ''}`} key={item.id}>
{mediaKind === 'video' ? (
<video src={item.url} muted playsInline preload="metadata" />
) : mediaKind === 'audio' ? (
<div className="compose-audio-preview">
<Music2 size={22} />
<span>{item.filename || 'Audio track'}</span>
</div>
) : (
<img src={item.url} alt={item.altText || 'Upload preview'} />
)}
@@ -324,16 +332,16 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
<button
type="button"
className="compose-media-button"
title="Add media"
title="Attach media"
onClick={handleAddMedia}
disabled={isUploading || isCheckingStorage || attachments.length >= 4}
>
{isUploading || isCheckingStorage ? '...' : <ImageIcon size={20} />}
{isUploading || isCheckingStorage ? '...' : <Paperclip size={20} />}
</button>
<input
ref={mediaInputRef}
type="file"
accept="image/*,video/mp4,video/webm,video/quicktime"
accept="image/*,video/mp4,video/webm,video/quicktime,audio/mpeg,audio/mp4,audio/aac,audio/wav,audio/ogg,audio/flac"
multiple
onChange={handleMediaSelect}
disabled={isUploading || attachments.length >= 4}