Правка импорта
This commit is contained in:
@@ -102,7 +102,7 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
||||
|
||||
if (filterType == "files")
|
||||
{
|
||||
return mType != "image" && mType != "video" && mType != "link";
|
||||
return mType != "image" && mType != "video" && mType != "link" && !isGif;
|
||||
}
|
||||
|
||||
if (filterType == "media")
|
||||
|
||||
@@ -43,15 +43,46 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
|
||||
var doc = await _parser.ParseDocumentAsync(htmlStream, ct);
|
||||
var messages = new List<TelegramMessage>();
|
||||
string? lastSenderName = null;
|
||||
DateTime? lastCreatedAt = null;
|
||||
|
||||
var messageNodes = doc.QuerySelectorAll(".message");
|
||||
foreach (var node in messageNodes)
|
||||
{
|
||||
// STOP ALL MERGING OF SEPARATE NODES.
|
||||
// Telegram HTML nodes represent visual bubbles. If we merge them, we break the original hierarchy
|
||||
// and often misplace captions or hide media.
|
||||
bool isJoined = node.ClassList.Contains("joined");
|
||||
var media = ExtractMedia(node, baseDirInZip);
|
||||
var textNode = node.QuerySelector(".text");
|
||||
var hasText = textNode != null && !string.IsNullOrWhiteSpace(textNode.TextContent);
|
||||
|
||||
var dateNode = node.QuerySelector(".date[title]") ?? node.QuerySelector("[title]");
|
||||
var dateStr = dateNode?.GetAttribute("title") ?? "";
|
||||
DateTime? currentAt = ParseDate(dateStr);
|
||||
|
||||
// REFINED ALBUM MERGING LOGIC:
|
||||
// 1. Current is .joined
|
||||
// 2. Current has media but NO text (important: if it has text, it's a new bubble group)
|
||||
// 3. Sender is the same as previous message
|
||||
// 4. Time difference is minimal (<= 2 seconds). Telegram albums usually share the exact same timestamp.
|
||||
if (isJoined && media.Count > 0 && !hasText && messages.Count > 0)
|
||||
{
|
||||
var prev = messages[messages.Count - 1];
|
||||
bool isSameSender = lastSenderName != null; // Since it's 'joined', it's the same visual group
|
||||
bool isCloseInTime = lastCreatedAt.HasValue && currentAt.HasValue &&
|
||||
Math.Abs((currentAt.Value - lastCreatedAt.Value).TotalSeconds) <= 2;
|
||||
|
||||
if (isSameSender && isCloseInTime)
|
||||
{
|
||||
var mergedMedia = (prev.Media ?? new List<TelegramMedia>()).Concat(media).ToList();
|
||||
messages[messages.Count - 1] = prev with { Media = mergedMedia };
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var msg = ParseSingleMessage(node, baseDirInZip, ref lastSenderName);
|
||||
if (msg != null) messages.Add(msg);
|
||||
if (msg != null)
|
||||
{
|
||||
messages.Add(msg);
|
||||
lastCreatedAt = currentAt;
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
@@ -82,11 +113,9 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
|
||||
if (idAttr.StartsWith("message")) long.TryParse(idAttr.Replace("message", ""), out numericId);
|
||||
|
||||
var fromNameNode = node.QuerySelector(".body > .from_name");
|
||||
// Ignore forwarded from_name as message sender
|
||||
if (fromNameNode != null && fromNameNode.Closest(".forwarded") != null) fromNameNode = null;
|
||||
|
||||
var senderName = fromNameNode != null ? CleanName(fromNameNode) : lastSenderName;
|
||||
// If it's a new bubble group (no "joined"), update lastSenderName
|
||||
if (senderName != null && !node.ClassList.Contains("joined")) lastSenderName = senderName;
|
||||
|
||||
var textNode = node.QuerySelector(".text");
|
||||
@@ -94,30 +123,10 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
|
||||
|
||||
var dateNode = node.QuerySelector(".date[title]") ?? node.QuerySelector("[title]");
|
||||
var dateStr = dateNode?.GetAttribute("title") ?? "";
|
||||
DateTime createdAt = DateTime.UtcNow;
|
||||
|
||||
if (!string.IsNullOrEmpty(dateStr))
|
||||
{
|
||||
var cleanDateStr = dateStr.Trim();
|
||||
var formats = new[] { "dd.MM.yyyy HH:mm:ss 'UTC'zzz", "dd.MM.yyyy HH:mm:ss", "d.MM.yyyy HH:mm:ss", "dd.M.yyyy HH:mm:ss", "d.M.yyyy HH:mm:ss" };
|
||||
|
||||
try
|
||||
{
|
||||
if (DateTimeOffset.TryParseExact(cleanDateStr, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto))
|
||||
{
|
||||
createdAt = dto.UtcDateTime;
|
||||
}
|
||||
else if (DateTimeOffset.TryParse(cleanDateStr.Replace("UTC", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto2))
|
||||
{
|
||||
createdAt = dto2.UtcDateTime;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
DateTime createdAt = ParseDate(dateStr) ?? DateTime.UtcNow;
|
||||
|
||||
var mediaList = ExtractMedia(node, baseDir);
|
||||
|
||||
// Replies
|
||||
string? replyToId = null;
|
||||
var replyNode = node.QuerySelector(".reply_to a[href^=\"#go_to_message\"]");
|
||||
if (replyNode != null)
|
||||
@@ -126,12 +135,10 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
|
||||
replyToId = href?.Replace("#go_to_message", "message");
|
||||
}
|
||||
|
||||
// Forwards
|
||||
string? forwardedFrom = null;
|
||||
var forwardNode = node.QuerySelector(".forwarded .from_name");
|
||||
if (forwardNode != null) forwardedFrom = CleanName(forwardNode);
|
||||
|
||||
// Reactions
|
||||
var reactions = new List<TelegramReaction>();
|
||||
var reactionNodes = node.QuerySelectorAll(".reactions .reaction");
|
||||
foreach (var r in reactionNodes)
|
||||
@@ -153,11 +160,20 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTime? ParseDate(string? dateStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dateStr)) return null;
|
||||
var cleanDateStr = dateStr.Trim();
|
||||
var formats = new[] { "dd.MM.yyyy HH:mm:ss 'UTC'zzz", "dd.MM.yyyy HH:mm:ss", "d.MM.yyyy HH:mm:ss", "dd.M.yyyy HH:mm:ss", "d.M.yyyy HH:mm:ss" };
|
||||
|
||||
if (DateTimeOffset.TryParseExact(cleanDateStr, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto)) return dto.UtcDateTime;
|
||||
if (DateTimeOffset.TryParse(cleanDateStr.Replace("UTC", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto2)) return dto2.UtcDateTime;
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<TelegramMedia> ExtractMedia(IElement node, string baseDir)
|
||||
{
|
||||
var mediaList = new List<TelegramMedia>();
|
||||
|
||||
// Look for links that represent media containers
|
||||
var allLinks = node.QuerySelectorAll("a[href]").ToList();
|
||||
|
||||
foreach (var link in allLinks)
|
||||
@@ -165,7 +181,6 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
|
||||
var href = NormalizeHref(link.GetAttribute("href"));
|
||||
if (href == null || !href.Contains("/")) continue;
|
||||
|
||||
// Skip internal pages
|
||||
if (href.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
var fullPath = BuildPath(baseDir, href);
|
||||
@@ -174,28 +189,15 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
|
||||
var fileName = Path.GetFileName(href);
|
||||
var mimeType = GetMimeType(href);
|
||||
|
||||
// ANIMATION DETECTION:
|
||||
// 1. Check classes (standard TG export)
|
||||
bool hasGifClass = link.ClassList.Contains("animated_wrap") || link.ClassList.Contains("media_video");
|
||||
|
||||
// 2. Check full text of the link container for "Animation" string
|
||||
// This is the most reliable way to find TG "GIFs" which are actually MP4s
|
||||
bool hasAnimationText = 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 (hasGifClass || hasAnimationText) mimeType = "image/gif";
|
||||
if (link.ClassList.Contains("media_voice_message") || href.Contains("voice_messages")) mimeType = "audio/ogg";
|
||||
|
||||
mediaList.Add(new TelegramMedia(fullPath, fileName, mimeType));
|
||||
}
|
||||
|
||||
// Alternative check for voice messages in audio tags
|
||||
foreach (var audioEl in node.QuerySelectorAll("audio[src]"))
|
||||
{
|
||||
var href = NormalizeHref(audioEl.GetAttribute("src"));
|
||||
|
||||
@@ -308,10 +308,10 @@ export default function GroupSettings({ chat, onClose, onGoToMessage }: GroupSet
|
||||
|
||||
const tabsConfig = [
|
||||
{ key: 'members' as const, label: t('membersCount') || 'Участники', icon: Users, count: chat.members.length },
|
||||
{ key: 'gifs' as const, label: t('gifs') || 'GIF', icon: Play, count: sortedGifs.length },
|
||||
{ key: 'media' as const, label: t('mediaTab'), icon: ImageIcon, count: sortedMedia.length },
|
||||
{ key: 'files' as const, label: t('filesTab'), icon: FileText, count: sortedFiles.flatMap(msg => msg.media || []).length },
|
||||
{ key: 'links' as const, label: t('linksTab'), icon: LinkIcon, count: sortedLinks.flatMap(msg => msg.links || []).length },
|
||||
{ key: 'gifs' as const, label: 'GIF', icon: Play, count: sortedGifs.length },
|
||||
{ key: 'media' as const, label: t('mediaTab') || 'Медиа', icon: ImageIcon, count: sortedMedia.length },
|
||||
{ key: 'files' as const, label: t('filesTab') || 'Файлы', icon: FileText, count: sortedFiles.flatMap(msg => msg.media || []).length },
|
||||
{ key: 'links' as const, label: t('linksTab') || 'Ссылки', icon: LinkIcon, count: sortedLinks.flatMap(msg => msg.links || []).length },
|
||||
];
|
||||
|
||||
const availableTabs = tabsConfig.filter(tab => tab.key === 'members' || !loadedTabs.has(tab.key) || tab.count > 0);
|
||||
@@ -336,7 +336,7 @@ export default function GroupSettings({ chat, onClose, onGoToMessage }: GroupSet
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, x: 50, scale: 0.95 }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||
className="fixed right-3 top-3 bottom-3 w-[650px] max-w-[calc(100%-24px)] bg-surface-secondary/90 backdrop-blur-3xl shadow-2xl shadow-black/80 border border-white/5 rounded-[2rem] z-50 flex flex-col overflow-hidden ring-1 ring-white/10"
|
||||
className="fixed right-3 top-3 bottom-3 w-[720px] max-w-[calc(100%-24px)] bg-surface-secondary/90 backdrop-blur-3xl shadow-2xl shadow-black/80 border border-white/5 rounded-[2rem] z-50 flex flex-col overflow-hidden ring-1 ring-white/10"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border/40">
|
||||
@@ -496,7 +496,7 @@ export default function GroupSettings({ chat, onClose, onGoToMessage }: GroupSet
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`flex-1 flex flex-col items-center justify-center gap-1 py-2 text-[10px] font-bold uppercase tracking-widest transition-all ${
|
||||
className={`flex-1 flex flex-col items-center justify-center gap-1.5 py-3 text-[10px] font-black uppercase tracking-wider transition-all border-r last:border-r-0 border-white/5 ${
|
||||
activeTab === tab.key
|
||||
? 'bg-white/5 text-knot-400'
|
||||
: 'text-zinc-500 hover:text-zinc-300'
|
||||
|
||||
Reference in New Issue
Block a user