diff --git a/TODO.md b/TODO.md index 6b1ce5a..c70761d 100644 --- a/TODO.md +++ b/TODO.md @@ -8,4 +8,11 @@ - 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/ \ No newline at end of file +- 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. \ No newline at end of file diff --git a/packages/shared/src/components/ChatArea.jsx b/packages/shared/src/components/ChatArea.jsx index 55fab2f..a759bba 100644 --- a/packages/shared/src/components/ChatArea.jsx +++ b/packages/shared/src/components/ChatArea.jsx @@ -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 (
- Preview + Preview
); } @@ -251,18 +251,18 @@ const LinkPreview = ({ url }) => { )} {isLargeImage && !isYouTube && metadata.image && (
- Preview + Preview
)} {!isLargeImage && !isYouTube && metadata.image && (
- Preview + Preview
)} {isYouTube && metadata.image && !playing && (
setPlaying(true)} style={{ cursor: 'pointer' }}> - Preview + Preview
)} @@ -322,7 +322,7 @@ const Attachment = ({ metadata, onLoad, onImageClick }) => { if (error) return
{error}
; if (metadata.mimeType.startsWith('image/')) { - return {metadata.filename} onImageClick(url)} />; + return {metadata.filename} 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 ?
: }
e.preventDefault()} onBlur={saveSelection} onMouseUp={saveSelection} onKeyUp={saveSelection} diff --git a/packages/shared/src/components/SearchPanel.jsx b/packages/shared/src/components/SearchPanel.jsx index 31a913d..3745a3e 100644 --- a/packages/shared/src/components/SearchPanel.jsx +++ b/packages/shared/src/components/SearchPanel.jsx @@ -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, '>'); } +function linkifyHtml(html) { + if (!html) return ''; + return html.replace(/(https?:\/\/[^\s<]+)/g, '$1'); +} + 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) && (
{ + 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 ; } catch { return File; } })() : r.has_attachment ? File : null} - {r.has_link && Link} + {r.has_link && r.content && (() => { + const urls = extractUrls(r.content); + return urls.map((url, i) => ); + })()} {r.pinned && Pinned}
diff --git a/packages/shared/src/components/Sidebar.jsx b/packages/shared/src/components/Sidebar.jsx index 2614fd5..52823ed 100644 --- a/packages/shared/src/components/Sidebar.jsx +++ b/packages/shared/src/components/Sidebar.jsx @@ -1589,7 +1589,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam {view === 'me' ? renderDMView() : renderServerView()} - {connectionState === 'connected' && ( + {(connectionState === 'connected' || connectionState === 'connecting') && (
-
Voice Connected
+
+ + + + + + +
+ {connectionState === 'connected' ? 'Voice Connected' : 'Voice Connecting'} +
+
{dmChannels?.some(dm => dm.channel_id === voiceChannelId) ? `Call with ${voiceChannelName}` : `${voiceChannelName} / ${serverName}`}
-
-
- - -
+ {connectionState === 'connected' && ( + <> +
+
+ + +
+ + )}
)} diff --git a/packages/shared/src/index.css b/packages/shared/src/index.css index 067f361..c9bc786 100644 --- a/packages/shared/src/index.css +++ b/packages/shared/src/index.css @@ -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; } } \ No newline at end of file