/** * MattermostImport — modal dialog for importing media from Mattermost. * * Allows pulling images from a Mattermost thread/channel URL and managing * linked channels for auto-sync. */ import React, { useState, useEffect, useCallback } from 'react'; import api from '../api'; interface MattermostImportProps { boardId: string; onClose: () => void; onMediaArrived?: (assets: { assetKey: string; w: number; h: number }[]) => void; } interface LinkedChannel { id: string; channelName: string; channelId: string; linkedAt: string; } export default function MattermostImport({ boardId, onClose, onMediaArrived }: MattermostImportProps) { const [url, setUrl] = useState(''); const [pulling, setPulling] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [linkedChannels, setLinkedChannels] = useState([]); const [loadingChannels, setLoadingChannels] = useState(false); const [linkUrl, setLinkUrl] = useState(''); const [linking, setLinking] = useState(false); // Load linked channels on mount const loadLinkedChannels = useCallback(async () => { setLoadingChannels(true); try { const res = await api.get(`/api/boards/${boardId}/mm-links`); setLinkedChannels(res.data?.links || []); } catch { // Endpoint may not exist yet — silently ignore setLinkedChannels([]); } finally { setLoadingChannels(false); } }, [boardId]); useEffect(() => { loadLinkedChannels(); }, [loadLinkedChannels]); // Close on Escape useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); // Pull images from URL const handlePull = async () => { if (!url.trim()) return; setPulling(true); setError(''); setSuccess(''); try { const res = await api.post(`/api/boards/${boardId}/mm-pull`, { url: url.trim() }); const assets = res.data?.assets || []; const count = assets.length; setSuccess(`Pulled ${count} image${count !== 1 ? 's' : ''}`); setUrl(''); if (count > 0 && onMediaArrived) { onMediaArrived(assets); } // Auto-close after brief delay on success if (count > 0) { setTimeout(onClose, 800); } } catch (err: any) { setError(err.response?.data?.error || 'Failed to pull images'); } finally { setPulling(false); } }; // Link a channel const handleLink = async () => { if (!linkUrl.trim()) return; setLinking(true); setError(''); try { await api.post(`/api/boards/${boardId}/mm-links`, { url: linkUrl.trim() }); setLinkUrl(''); loadLinkedChannels(); } catch (err: any) { setError(err.response?.data?.error || 'Failed to link channel'); } finally { setLinking(false); } }; // Unlink a channel const handleUnlink = async (linkId: string) => { try { await api.delete(`/api/boards/${boardId}/mm-links/${linkId}`); loadLinkedChannels(); } catch (err: any) { setError(err.response?.data?.error || 'Failed to unlink channel'); } }; return (
e.stopPropagation()} style={{ background: '#141414', border: '1px solid #222', borderRadius: '16px', width: '100%', maxWidth: '480px', maxHeight: '85vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 24px 64px rgba(0,0,0,0.6)', }} > {/* Header */}

Import from Mattermost

{/* Body */}
{/* Error / Success */} {error && (
{error}
)} {success && (
{success}
)} {/* Pull images section */}
setUrl(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handlePull(); }} placeholder="https://chat.metalfinger.xyz/team/pl/..." style={{ flex: 1, padding: '8px 12px', background: '#1a1a1a', border: '1px solid #333', borderRadius: '8px', color: '#e0e0e0', fontSize: '13px', outline: 'none', }} onFocus={(e) => { e.currentTarget.style.borderColor = '#4a9eff'; }} onBlur={(e) => { e.currentTarget.style.borderColor = '#333'; }} />
{/* Linked channels section */}
{loadingChannels ? (
Loading...
) : linkedChannels.length === 0 ? (
No linked channels. Link one below for auto-sync.
) : (
{linkedChannels.map((ch) => (
{ch.channelName || ch.channelId}
Linked {new Date(ch.linkedAt).toLocaleDateString()}
))}
)}
{/* Link new channel */}
setLinkUrl(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleLink(); }} placeholder="Channel URL to link..." style={{ flex: 1, padding: '8px 12px', background: '#1a1a1a', border: '1px solid #333', borderRadius: '8px', color: '#e0e0e0', fontSize: '13px', outline: 'none', }} onFocus={(e) => { e.currentTarget.style.borderColor = '#4a9eff'; }} onBlur={(e) => { e.currentTarget.style.borderColor = '#333'; }} />
); } function SectionLabel({ text }: { text: string }) { return (
{text}
); }