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

@@ -9,3 +9,10 @@
- Can we add a way to tell the user they are connecting to voice. Like show them its connecting so the user knows something is happening instead of them clicking on the voice stage again and again. - Can we add a way to tell the user they are connecting to voice. Like show them its connecting so the user knows something is happening instead of them clicking on the voice stage again and again.
- Add photo / video albums like Commit https://commet.chat/ - Add photo / video albums like Commit https://commet.chat/
For the main admin can we keep track of how much space a user is taking in the database.
Is it possible with large files we do a torrent or peer to peer based file sharing. So users that want to share really large files dont store it on our servers but share it using peer to peer.

View File

@@ -150,7 +150,7 @@ const getProviderClass = (url) => {
return ''; return '';
}; };
const LinkPreview = ({ url }) => { export const LinkPreview = ({ url }) => {
const { links } = usePlatform(); const { links } = usePlatform();
const [metadata, setMetadata] = useState(metadataCache.get(url) || null); const [metadata, setMetadata] = useState(metadataCache.get(url) || null);
const [loading, setLoading] = useState(!metadataCache.has(url)); const [loading, setLoading] = useState(!metadataCache.has(url));
@@ -219,7 +219,7 @@ const LinkPreview = ({ url }) => {
if (metadata.description === 'Image File' && metadata.image) { if (metadata.description === 'Image File' && metadata.image) {
return ( return (
<div className="preview-image-standalone" style={{ marginTop: 8, display: 'inline-block', maxWidth: '100%', cursor: 'pointer' }}> <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> </div>
); );
} }
@@ -251,18 +251,18 @@ const LinkPreview = ({ url }) => {
)} )}
{isLargeImage && !isYouTube && metadata.image && ( {isLargeImage && !isYouTube && metadata.image && (
<div className="preview-image-container large-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>
)} )}
</div> </div>
{!isLargeImage && !isYouTube && metadata.image && ( {!isLargeImage && !isYouTube && metadata.image && (
<div className="preview-image-container"> <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> </div>
)} )}
{isYouTube && metadata.image && !playing && ( {isYouTube && metadata.image && !playing && (
<div className="preview-image-container" onClick={() => setPlaying(true)} style={{ cursor: 'pointer' }}> <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 className="play-icon"></div>
</div> </div>
)} )}
@@ -322,7 +322,7 @@ const Attachment = ({ metadata, onLoad, onImageClick }) => {
if (error) return <div style={{ color: '#ed4245', fontSize: '12px' }}>{error}</div>; if (error) return <div style={{ color: '#ed4245', fontSize: '12px' }}>{error}</div>;
if (metadata.mimeType.startsWith('image/')) { 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/')) { if (metadata.mimeType.startsWith('video/')) {
const handlePlayClick = () => { setShowControls(true); if (videoRef.current) videoRef.current.play(); }; 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 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 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 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 uploadAndSendFile = async (file) => {
const fileKey = await crypto.randomBytes(32); 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" />} {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> </button>
<div ref={inputDivRef} contentEditable className="chat-input-richtext" role="textbox" aria-multiline="true" <div ref={inputDivRef} contentEditable className="chat-input-richtext" role="textbox" aria-multiline="true"
onDrop={(e) => e.preventDefault()}
onBlur={saveSelection} onBlur={saveSelection}
onMouseUp={saveSelection} onMouseUp={saveSelection}
onKeyUp={saveSelection} onKeyUp={saveSelection}

View File

@@ -2,6 +2,8 @@ import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useSearch } from '../contexts/SearchContext'; import { useSearch } from '../contexts/SearchContext';
import { parseFilters } from '../utils/searchUtils'; import { parseFilters } from '../utils/searchUtils';
import { usePlatform } from '../platform'; import { usePlatform } from '../platform';
import { LinkPreview } from './ChatArea';
import { extractUrls } from './MessageItem';
function formatTime(ts) { function formatTime(ts) {
const d = new Date(ts); const d = new Date(ts);
@@ -16,6 +18,11 @@ function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); 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) { function getAvatarColor(name) {
const colors = ['#5865F2', '#57F287', '#FEE75C', '#EB459E', '#ED4245', '#F47B67', '#E67E22', '#3498DB']; const colors = ['#5865F2', '#57F287', '#FEE75C', '#EB459E', '#ED4245', '#F47B67', '#E67E22', '#3498DB'];
let hash = 0; let hash = 0;
@@ -198,6 +205,7 @@ const SearchResultFile = ({ metadata }) => {
const SearchPanel = ({ visible, onClose, channels, isDM, dmChannelId, onJumpToMessage, query, sortOrder, onSortChange }) => { const SearchPanel = ({ visible, onClose, channels, isDM, dmChannelId, onJumpToMessage, query, sortOrder, onSortChange }) => {
const { search, isReady } = useSearch() || {}; const { search, isReady } = useSearch() || {};
const { links } = usePlatform();
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [showSortMenu, setShowSortMenu] = 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) && ( {!(r.has_attachment && r.attachment_meta) && (
<div <div
className="search-result-content" 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 ? (() => { {r.has_attachment && r.attachment_meta ? (() => {
@@ -393,7 +408,10 @@ const SearchPanel = ({ visible, onClose, channels, isDM, dmChannelId, onJumpToMe
return <SearchResultFile metadata={meta} />; return <SearchResultFile metadata={meta} />;
} catch { return <span className="search-result-badge">File</span>; } } catch { return <span className="search-result-badge">File</span>; }
})() : r.has_attachment ? <span className="search-result-badge">File</span> : null} })() : 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>} {r.pinned && <span className="search-result-badge">Pinned</span>}
</div> </div>
</div> </div>

View File

@@ -1589,7 +1589,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
{view === 'me' ? renderDMView() : renderServerView()} {view === 'me' ? renderDMView() : renderServerView()}
</div> </div>
{connectionState === 'connected' && ( {(connectionState === 'connected' || connectionState === 'connecting') && (
<div style={{ <div style={{
backgroundColor: 'var(--panel-bg)', backgroundColor: 'var(--panel-bg)',
borderRadius: '8px 8px 0px 0px', 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)' borderBottom: '1px solid hsla(240, 4%, 60.784%, 0.039)'
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}> <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 <button
onClick={disconnectVoice} onClick={disconnectVoice}
title="Disconnect" title="Disconnect"
@@ -1612,15 +1622,19 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
</button> </button>
</div> </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={{ 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> {connectionState === 'connected' && (
<div style={{ display: 'flex', gap: 4 }}> <>
<button onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)} title="Turn On Camera" style={voicePanelButtonStyle}> <div style={{ marginBottom: 8 }}><VoiceTimer /></div>
<ColoredIcon src={cameraIcon} color="var(--header-secondary)" size="20px" /> <div style={{ display: 'flex', gap: 4 }}>
</button> <button onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)} title="Turn On Camera" style={voicePanelButtonStyle}>
<button onClick={handleScreenShareClick} title="Share Screen" style={voicePanelButtonStyle}> <ColoredIcon src={cameraIcon} color="var(--header-secondary)" size="20px" />
<ColoredIcon src={screenIcon} color="var(--header-secondary)" size="20px" /> </button>
</button> <button onClick={handleScreenShareClick} title="Share Screen" style={voicePanelButtonStyle}>
</div> <ColoredIcon src={screenIcon} color="var(--header-secondary)" size="20px" />
</button>
</div>
</>
)}
</div> </div>
)} )}

View File

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