From 32c9bc43cfba4fe04695f59b6f488cc91096f6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Mon, 6 Apr 2026 23:35:45 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D1=87=D0=B8=D0=BD=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B0,=20=D0=BF=D0=BB?= =?UTF-8?q?=D0=B5=D0=B5=D1=80=20=D0=B4=D0=BB=D1=8F=20=D0=B0=D1=83=D0=B4?= =?UTF-8?q?=D0=B8=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Messaging/Domain/MediaMessage.cs | 2 +- .../GetSharedMedia/GetSharedMediaQuery.cs | 18 +- .../TelegramImport/DTOs/TelegramMessage.cs | 2 +- .../Background/TelegramImportWorker.cs | 2 +- .../Parser/TelegramHtmlParser.cs | 85 +++++- .../components/ui/ImageLightbox.tsx | 6 +- .../presentation/components/ChatView.tsx | 257 +++++++++++------- .../presentation/components/GroupSettings.tsx | 18 +- .../presentation/components/MessageBubble.tsx | 128 ++++++--- .../presentation/components/UserProfile.tsx | 53 ++-- 10 files changed, 375 insertions(+), 196 deletions(-) diff --git a/backend/src/Contracts/Messaging/Domain/MediaMessage.cs b/backend/src/Contracts/Messaging/Domain/MediaMessage.cs index c1e3c70..b276e93 100644 --- a/backend/src/Contracts/Messaging/Domain/MediaMessage.cs +++ b/backend/src/Contracts/Messaging/Domain/MediaMessage.cs @@ -21,7 +21,7 @@ public class MediaMessage : Message public MediaMessage(Guid id, Guid chatId, Guid senderId, string mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported) : this(id, chatId, senderId, Enum.TryParse(mediaType, true, out var mt) ? mt : MediaType.File, caption, replyToId, forwardedFromId, createdAt, isImported) { } - public void AddMedia(string type, string url, string? filename, long? size) => _media.Add(new Media { Type = type, Url = url, FileId = filename, Size = size }); + public void AddMedia(string type, string url, string? filename, long? size, string? duration = null) => _media.Add(new Media { Type = type, Url = url, Filename = filename, FileId = filename, Size = size, Duration = duration }); public override void Edit(string newCaption) => base.Edit(newCaption); diff --git a/backend/src/Modules/Conversations/Application/Messages/GetSharedMedia/GetSharedMediaQuery.cs b/backend/src/Modules/Conversations/Application/Messages/GetSharedMedia/GetSharedMediaQuery.cs index 1e73ab6..0721292 100644 --- a/backend/src/Modules/Conversations/Application/Messages/GetSharedMedia/GetSharedMediaQuery.cs +++ b/backend/src/Modules/Conversations/Application/Messages/GetSharedMedia/GetSharedMediaQuery.cs @@ -95,20 +95,10 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler UserNames); diff --git a/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs b/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs index d616491..fb5008e 100644 --- a/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs +++ b/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs @@ -175,7 +175,7 @@ public class TelegramImportWorker : BackgroundService else if (mediaItem.MimeType.Contains("voice") || mediaItem.MimeType.Contains("audio")) itemType = "audio"; var fileId = await fileStorage.UploadFileAsync(mediaStream, mediaItem.FileName, mimeToUpload); - mediaMsg.AddMedia(itemType, $"/api/files/{fileId}", mediaItem.FileName, zipEntry.Length); + mediaMsg.AddMedia(itemType, $"/api/files/{fileId}", mediaItem.FileName, zipEntry.Length, mediaItem.Duration); } else { diff --git a/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs b/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs index 2c011c6..80684d6 100644 --- a/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs +++ b/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs @@ -186,16 +186,82 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser var fullPath = BuildPath(baseDir, href); if (mediaList.Any(m => m.FilePath == fullPath)) continue; - var fileName = Path.GetFileName(href); + // Priority 1: Extract real filename from .title or .name or .description or specific body part + string fileName = ""; + var titleNode = link.QuerySelector(".title") ?? link.QuerySelector(".name") ?? link.QuerySelector(".description"); + + if (titleNode != null) + { + fileName = titleNode.TextContent.Trim(); + } + else + { + // Try body but exclude status + var bodyNode = link.QuerySelector(".body"); + if (bodyNode != null) + { + var clone = (IElement)bodyNode.Clone(); + foreach (var s in clone.QuerySelectorAll(".status, .details, .pull_right")) s.Remove(); + fileName = clone.TextContent.Trim(); + } + + // Final desperate attempt: link's own content excluding status tags + if (string.IsNullOrWhiteSpace(fileName)) + { + var clone = (IElement)link.Clone(); + foreach (var s in clone.QuerySelectorAll(".status, .details, .pull_right, .details_icon")) s.Remove(); + fileName = clone.TextContent.Trim(); + } + } + + // Cleanup & Russian Support + if (!string.IsNullOrWhiteSpace(fileName)) + { + // Decrypt potential HTML entities + fileName = System.Net.WebUtility.HtmlDecode(fileName); + + // Remove extra lines if any + var lines = fileName.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + fileName = lines.Length > 0 ? lines[0].Trim() : ""; + + // If the "name" is just "Animation" or generic, we might want to fallback to path + if (fileName.Equals("Animation", StringComparison.OrdinalIgnoreCase)) fileName = ""; + } + + // Priority 2: Fallback to Path filename (e.g. file_1.mp3) + if (string.IsNullOrWhiteSpace(fileName) || fileName.Length < 2) + { + var rawName = Path.GetFileName(href); + fileName = System.Net.WebUtility.UrlDecode(rawName); + } + var mimeType = GetMimeType(href); - bool hasGifClass = link.ClassList.Contains("animated_wrap") || link.ClassList.Contains("media_video"); - bool hasAnimationText = link.TextContent.Contains("Animation", StringComparison.OrdinalIgnoreCase); + bool isVoice = link.ClassList.Contains("media_voice_message") || href.Contains("voice_messages"); + bool isAudio = link.ClassList.Contains("media_audio_file") || (mimeType.StartsWith("audio") && !isVoice); + bool isVideo = link.ClassList.Contains("media_video") || mimeType.StartsWith("video"); + bool isGif = link.ClassList.Contains("animated_wrap") || link.TextContent.Contains("Animation", StringComparison.OrdinalIgnoreCase); - if (hasGifClass || hasAnimationText) mimeType = "image/gif"; - if (link.ClassList.Contains("media_voice_message") || href.Contains("voice_messages")) mimeType = "audio/ogg"; + if (isGif) mimeType = "image/gif"; + else if (isVoice) mimeType = "audio/ogg"; + else if (isAudio && mimeType == "application/octet-stream") mimeType = "audio/mpeg"; - mediaList.Add(new TelegramMedia(fullPath, fileName, mimeType)); + // Extract Duration from .status + string? duration = null; + var statusNode = link.QuerySelector(".status") ?? link.QuerySelector(".details"); + if (statusNode != null) + { + var text = statusNode.TextContent.Trim(); + // Status is often: "01:23, 1.2 MB" or just "01:23" + var parts = text.Split(','); + var first = parts[0].Trim(); + if (first.Contains(":") && first.All(c => char.IsDigit(c) || c == ':')) + { + duration = first; + } + } + + mediaList.Add(new TelegramMedia(fullPath, fileName, mimeType, duration)); } foreach (var audioEl in node.QuerySelectorAll("audio[src]")) @@ -204,7 +270,7 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser if (href == null) continue; var fullPath = BuildPath(baseDir, href); if (!mediaList.Any(m => m.FilePath == fullPath)) - mediaList.Add(new TelegramMedia(fullPath, Path.GetFileName(href), "audio/ogg")); + mediaList.Add(new TelegramMedia(fullPath, System.Net.WebUtility.UrlDecode(Path.GetFileName(href)), "audio/ogg")); } return mediaList; @@ -244,6 +310,11 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser ".mp4" => "video/mp4", ".mov" => "video/quicktime", ".webm" => "video/webm", + ".mp3" => "audio/mpeg", + ".ogg" => "audio/ogg", + ".wav" => "audio/wav", + ".m4a" => "audio/mp4", + ".aac" => "audio/aac", _ => "application/octet-stream" }; } diff --git a/client-web/src/core/presentation/components/ui/ImageLightbox.tsx b/client-web/src/core/presentation/components/ui/ImageLightbox.tsx index 14a9a54..d8fcc28 100644 --- a/client-web/src/core/presentation/components/ui/ImageLightbox.tsx +++ b/client-web/src/core/presentation/components/ui/ImageLightbox.tsx @@ -102,11 +102,13 @@ export default function ImageLightbox({ url, images, initialIndex = 0, onClose } onClick={(e) => e.stopPropagation()} className="absolute inset-x-0 inset-y-12 flex items-center justify-center p-4" > - {currentType === 'video' ? ( + {(currentType === 'video' || currentType === 'gif') ? (