Add connecting state

This commit is contained in:
Bryan1029384756
2026-02-16 19:06:17 -06:00
parent ee376b9ba3
commit fca2ed8da9
5 changed files with 84 additions and 22 deletions

View File

@@ -150,7 +150,7 @@ const getProviderClass = (url) => {
return '';
};
const LinkPreview = ({ url }) => {
export const LinkPreview = ({ url }) => {
const { links } = usePlatform();
const [metadata, setMetadata] = useState(metadataCache.get(url) || null);
const [loading, setLoading] = useState(!metadataCache.has(url));
@@ -219,7 +219,7 @@ const LinkPreview = ({ url }) => {
if (metadata.description === 'Image File' && metadata.image) {
return (
<div className="preview-image-standalone" style={{ marginTop: 8, display: 'inline-block', maxWidth: '100%', cursor: 'pointer' }}>
<img src={metadata.image} alt="Preview" style={{ maxWidth: '100%', maxHeight: '350px', borderRadius: '8px', display: 'block' }} />
<img src={metadata.image} alt="Preview" draggable="false" style={{ maxWidth: '100%', maxHeight: '350px', borderRadius: '8px', display: 'block' }} />
</div>
);
}
@@ -251,18 +251,18 @@ const LinkPreview = ({ url }) => {
)}
{isLargeImage && !isYouTube && metadata.image && (
<div className="preview-image-container large-image">
<img src={metadata.image} alt="Preview" className="preview-image" />
<img src={metadata.image} alt="Preview" draggable="false" className="preview-image" />
</div>
)}
</div>
{!isLargeImage && !isYouTube && metadata.image && (
<div className="preview-image-container">
<img src={metadata.image} alt="Preview" className="preview-image" />
<img src={metadata.image} alt="Preview" draggable="false" className="preview-image" />
</div>
)}
{isYouTube && metadata.image && !playing && (
<div className="preview-image-container" onClick={() => setPlaying(true)} style={{ cursor: 'pointer' }}>
<img src={metadata.image} alt="Preview" className="preview-image" />
<img src={metadata.image} alt="Preview" draggable="false" className="preview-image" />
<div className="play-icon"></div>
</div>
)}
@@ -322,7 +322,7 @@ const Attachment = ({ metadata, onLoad, onImageClick }) => {
if (error) return <div style={{ color: '#ed4245', fontSize: '12px' }}>{error}</div>;
if (metadata.mimeType.startsWith('image/')) {
return <img src={url} alt={metadata.filename} style={{ maxHeight: '300px', borderRadius: '4px', cursor: 'zoom-in' }} onLoad={onLoad} onClick={() => onImageClick(url)} />;
return <img src={url} alt={metadata.filename} draggable="false" style={{ maxHeight: '300px', borderRadius: '4px', cursor: 'zoom-in' }} onLoad={onLoad} onClick={() => onImageClick(url)} />;
}
if (metadata.mimeType.startsWith('video/')) {
const handlePlayClick = () => { setShowControls(true); if (videoRef.current) videoRef.current.play(); };
@@ -1074,9 +1074,13 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
const processFile = (file) => { setPendingFiles(prev => [...prev, file]); };
const handleFileSelect = (e) => { if (e.target.files && e.target.files.length > 0) Array.from(e.target.files).forEach(processFile); };
const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); if (!isDragging) setIsDragging(true); };
const isExternalFileDrag = (e) => {
const types = Array.from(e.dataTransfer.types);
return types.includes('Files') && !types.includes('text/uri-list') && !types.includes('text/html');
};
const handleDragOver = (e) => { if (!isExternalFileDrag(e)) return; e.preventDefault(); e.stopPropagation(); if (!isDragging) setIsDragging(true); };
const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget.contains(e.relatedTarget)) return; setIsDragging(false); };
const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) Array.from(e.dataTransfer.files).forEach(processFile); };
const handleDrop = (e) => { if (!isDragging) return; e.preventDefault(); e.stopPropagation(); setIsDragging(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) Array.from(e.dataTransfer.files).forEach(processFile); };
const uploadAndSendFile = async (file) => {
const fileKey = await crypto.randomBytes(32);
@@ -1477,6 +1481,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
{uploading ? <div className="spinner" style={{ width: 24, height: 24, borderRadius: '50%', border: '2px solid #b9bbbe', borderTopColor: 'transparent', animation: 'spin 1s linear infinite' }}></div> : <ColoredIcon src={AddIcon} color={ICON_COLOR_DEFAULT} size="24px" />}
</button>
<div ref={inputDivRef} contentEditable className="chat-input-richtext" role="textbox" aria-multiline="true"
onDrop={(e) => e.preventDefault()}
onBlur={saveSelection}
onMouseUp={saveSelection}
onKeyUp={saveSelection}

View File

@@ -2,6 +2,8 @@ import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useSearch } from '../contexts/SearchContext';
import { parseFilters } from '../utils/searchUtils';
import { usePlatform } from '../platform';
import { LinkPreview } from './ChatArea';
import { extractUrls } from './MessageItem';
function formatTime(ts) {
const d = new Date(ts);
@@ -16,6 +18,11 @@ function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function linkifyHtml(html) {
if (!html) return '';
return html.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" class="search-result-link">$1</a>');
}
function getAvatarColor(name) {
const colors = ['#5865F2', '#57F287', '#FEE75C', '#EB459E', '#ED4245', '#F47B67', '#E67E22', '#3498DB'];
let hash = 0;
@@ -198,6 +205,7 @@ const SearchResultFile = ({ metadata }) => {
const SearchPanel = ({ visible, onClose, channels, isDM, dmChannelId, onJumpToMessage, query, sortOrder, onSortChange }) => {
const { search, isReady } = useSearch() || {};
const { links } = usePlatform();
const [results, setResults] = useState([]);
const [searching, setSearching] = useState(false);
const [showSortMenu, setShowSortMenu] = useState(false);
@@ -382,7 +390,14 @@ const SearchPanel = ({ visible, onClose, channels, isDM, dmChannelId, onJumpToMe
{!(r.has_attachment && r.attachment_meta) && (
<div
className="search-result-content"
dangerouslySetInnerHTML={{ __html: r.snippet || escapeHtml(r.content) }}
dangerouslySetInnerHTML={{ __html: linkifyHtml(r.snippet || escapeHtml(r.content)) }}
onClick={(e) => {
if (e.target.tagName === 'A' && e.target.href) {
e.preventDefault();
e.stopPropagation();
links.openExternal(e.target.href);
}
}}
/>
)}
{r.has_attachment && r.attachment_meta ? (() => {
@@ -393,7 +408,10 @@ const SearchPanel = ({ visible, onClose, channels, isDM, dmChannelId, onJumpToMe
return <SearchResultFile metadata={meta} />;
} catch { return <span className="search-result-badge">File</span>; }
})() : r.has_attachment ? <span className="search-result-badge">File</span> : null}
{r.has_link && <span className="search-result-badge">Link</span>}
{r.has_link && r.content && (() => {
const urls = extractUrls(r.content);
return urls.map((url, i) => <LinkPreview key={i} url={url} />);
})()}
{r.pinned && <span className="search-result-badge">Pinned</span>}
</div>
</div>

View File

@@ -1589,7 +1589,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
{view === 'me' ? renderDMView() : renderServerView()}
</div>
{connectionState === 'connected' && (
{(connectionState === 'connected' || connectionState === 'connecting') && (
<div style={{
backgroundColor: 'var(--panel-bg)',
borderRadius: '8px 8px 0px 0px',
@@ -1600,7 +1600,17 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
borderBottom: '1px solid hsla(240, 4%, 60.784%, 0.039)'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
<div style={{ color: '#43b581', fontWeight: 'bold', fontSize: 13 }}>Voice Connected</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<svg className={connectionState === 'connecting' ? 'voice-connecting-icon' : ''} width="18" height="18" viewBox="0 0 24 24" fill="none">
<rect x="2" y="16" width="4" height="6" rx="1" fill={connectionState === 'connected' ? '#43b581' : '#faa61a'} />
<rect x="8" y="12" width="4" height="10" rx="1" fill={connectionState === 'connected' ? '#43b581' : '#faa61a'} />
<rect x="14" y="7" width="4" height="15" rx="1" fill={connectionState === 'connected' ? '#43b581' : '#faa61a'} opacity={connectionState === 'connecting' ? 0.4 : 1} />
<rect x="20" y="2" width="4" height="20" rx="1" fill={connectionState === 'connected' ? '#43b581' : '#faa61a'} opacity={connectionState === 'connecting' ? 0.4 : 1} />
</svg>
<div style={{ color: connectionState === 'connected' ? '#43b581' : '#faa61a', fontWeight: 'bold', fontSize: 13 }}>
{connectionState === 'connected' ? 'Voice Connected' : 'Voice Connecting'}
</div>
</div>
<button
onClick={disconnectVoice}
title="Disconnect"
@@ -1612,15 +1622,19 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
</button>
</div>
<div style={{ color: 'var(--text-normal)', fontSize: 12, marginBottom: 4 }}>{dmChannels?.some(dm => dm.channel_id === voiceChannelId) ? `Call with ${voiceChannelName}` : `${voiceChannelName} / ${serverName}`}</div>
<div style={{ marginBottom: 8 }}><VoiceTimer /></div>
<div style={{ display: 'flex', gap: 4 }}>
<button onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)} title="Turn On Camera" style={voicePanelButtonStyle}>
<ColoredIcon src={cameraIcon} color="var(--header-secondary)" size="20px" />
</button>
<button onClick={handleScreenShareClick} title="Share Screen" style={voicePanelButtonStyle}>
<ColoredIcon src={screenIcon} color="var(--header-secondary)" size="20px" />
</button>
</div>
{connectionState === 'connected' && (
<>
<div style={{ marginBottom: 8 }}><VoiceTimer /></div>
<div style={{ display: 'flex', gap: 4 }}>
<button onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)} title="Turn On Camera" style={voicePanelButtonStyle}>
<ColoredIcon src={cameraIcon} color="var(--header-secondary)" size="20px" />
</button>
<button onClick={handleScreenShareClick} title="Share Screen" style={voicePanelButtonStyle}>
<ColoredIcon src={screenIcon} color="var(--header-secondary)" size="20px" />
</button>
</div>
</>
)}
</div>
)}

View File

@@ -3693,6 +3693,15 @@ img.search-dropdown-avatar {
padding: 0 1px;
}
.search-result-link {
color: #00b0f4;
text-decoration: none;
cursor: pointer;
}
.search-result-link:hover {
text-decoration: underline;
}
.search-result-badge {
display: inline-block;
font-size: 10px;
@@ -3880,4 +3889,13 @@ img.search-dropdown-avatar {
.incoming-call-btn.reject {
background-color: #ed4245;
}
.voice-connecting-icon {
animation: voiceConnectingPulse 1.5s ease-in-out infinite;
}
@keyframes voiceConnectingPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}