Add connecting state
This commit is contained in:
7
TODO.md
7
TODO.md
@@ -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.
|
||||
|
||||
- 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.
|
||||
@@ -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}
|
||||
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -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,6 +1622,8 @@ 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>
|
||||
{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}>
|
||||
@@ -1621,6 +1633,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
|
||||
<ColoredIcon src={screenIcon} color="var(--header-secondary)" size="20px" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -3881,3 +3890,12 @@ 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user