diff --git a/src/App.css b/src/App.css index 619467a..a4b4c9d 100644 --- a/src/App.css +++ b/src/App.css @@ -399,10 +399,10 @@ body, html { .media-box { position: relative; - background: rgba(255, 255, 255, 0.05); + background: transparent; border-radius: 12px; - padding: 8px; - border: 1px solid rgba(255, 255, 255, 0.1); + padding: 0; + border: none; } .image-box { @@ -415,7 +415,7 @@ body, html { } .video-box, .audio-box { - background: rgba(0, 0, 0, 0.3); + background: transparent; } .media-image { @@ -439,20 +439,24 @@ body, html { .video-controls { display: flex; - gap: 6px; - margin-top: 8px; + gap: 4px; + margin-top: 4px; flex-wrap: wrap; align-items: center; + background: rgba(0, 0, 0, 0.7); + padding: 6px; + border-radius: 8px; } .speed-btn, .fullscreen-btn { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.15); + border: none; color: white; - padding: 4px 10px; - border-radius: 6px; + padding: 6px 12px; + border-radius: 4px; cursor: pointer; - font-size: 12px; + font-size: 11px; + font-weight: 500; transition: all 0.2s ease; display: flex; align-items: center; @@ -460,8 +464,7 @@ body, html { } .speed-btn:hover, .fullscreen-btn:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.25); } .fullscreen-btn { @@ -566,4 +569,294 @@ body, html { color: white; border: none; padding: 5px; -} \ No newline at end of file + border-radius: 4px; +} + +/* Reaction Bubbles */ +.message-reactions { + display: flex; + gap: 2px; + margin-top: 2px; + margin-left: 4px; + flex-wrap: wrap; + position: relative; + bottom: -2px; +} + +.reaction-bubble { + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 10px; + padding: 1px 6px; + font-size: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 18px; +} + +/* Instagram Link Embed */ +.instagram-embed { + margin: 0; + padding: 0; + background: transparent; + border: none; +} + +.instagram-link { + text-decoration: none; + color: inherit; + display: block; + border: none; + outline: none; + background: transparent; + padding: 0; + margin: 0; +} + +.instagram-link:hover, +.instagram-link:focus, +.instagram-link:active { + text-decoration: none; + border: none; + outline: none; + background: transparent; +} + +.instagram-iframe { + width: 326px; + height: 400px; + border: none; + border-radius: 0; + overflow: hidden; + background: transparent; + display: block; + padding: 0; + margin: 0; +} + +/* Settings Modal */ +.settings-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.settings-content { + background: var(--ig-bg-primary); + border: 1px solid var(--ig-border); + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; +} + +.settings-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid var(--ig-border); +} + +.settings-header h2 { + margin: 0; + font-size: 24px; +} + +.settings-body { + padding: 20px; +} + +.setting-item { + margin-bottom: 24px; +} + +.setting-item label { + display: block; + margin-bottom: 8px; + font-weight: 600; + font-size: 14px; + color: var(--ig-text-primary); +} + +.setting-item select, +.setting-item input { + width: 100%; + padding: 10px; + background: var(--ig-bg-secondary); + color: var(--ig-text-primary); + border: 1px solid var(--ig-border); + border-radius: 8px; + font-size: 14px; +} + +.theme-toggle { + display: flex; + gap: 8px; +} + +.theme-toggle button { + flex: 1; + padding: 10px; + background: var(--ig-bg-secondary); + color: var(--ig-text-primary); + border: 1px solid var(--ig-border); + border-radius: 8px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.theme-toggle button.active { + background: var(--ig-bubble-mine); + border-color: var(--ig-bubble-mine); +} + +.theme-toggle button:hover { + background: var(--ig-bg-secondary); + opacity: 0.8; +} + +.search-container { + display: flex; + gap: 8px; +} + +.search-container input { + flex: 1; +} + +.search-container button { + padding: 10px 16px; + background: var(--ig-bubble-mine); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; + transition: opacity 0.2s; +} + +.search-container button:hover { + opacity: 0.8; +} + +.search-results { + margin-top: 16px; + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--ig-border); + border-radius: 8px; +} + +.search-result-chat { + border-bottom: 1px solid var(--ig-border); + padding: 12px; +} + +.search-result-chat:last-child { + border-bottom: none; +} + +.search-result-header { + cursor: pointer; + padding: 8px; + border-radius: 6px; + margin-bottom: 8px; + transition: background 0.2s; +} + +.search-result-header:hover { + background: var(--ig-bg-secondary); +} + +.search-result-messages { + padding-left: 12px; +} + +.search-result-message { + padding: 6px; + font-size: 13px; + color: var(--ig-text-secondary); + border-left: 2px solid var(--ig-border); + margin: 4px 0; + padding-left: 8px; + cursor: pointer; + transition: background 0.2s; + border-radius: 4px; +} + +.search-result-message:hover { + background: var(--ig-bg-secondary); +} + +.search-result-message .sender { + color: var(--ig-bubble-mine); + font-weight: 600; +} + +/* Highlight animation for jumped-to message */ +.highlight-message { + animation: highlightPulse 2s ease-in-out; +} + +@keyframes highlightPulse { + 0%, 100% { + background: transparent; + } + 50% { + background: rgba(55, 151, 240, 0.3); + } +} + +.search-result-more { + padding: 6px 8px; + font-size: 12px; + color: var(--ig-text-secondary); + font-style: italic; +} + +.upload-new-btn { + width: 100%; + padding: 12px; + background: var(--ig-bubble-mine); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: opacity 0.2s; +} + +.upload-new-btn:hover { + opacity: 0.8; +} + +/* Light Theme */ +body.light-theme { + --ig-bg-primary: #FFFFFF; + --ig-bg-secondary: #F0F0F0; + --ig-text-primary: #000000; + --ig-text-secondary: #737373; + --ig-bubble-mine: #3797F0; + --ig-bubble-theirs: #EFEFEF; + --ig-border: #DBDBDB; +} diff --git a/src/App.js b/src/App.js index e07424b..b0e7f24 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; -import { ArrowLeft, Phone, Video, Info, Maximize2, X } from 'lucide-react'; +import { ArrowLeft, Phone, Video, Info, Maximize2, X, Settings, Search, Upload } from 'lucide-react'; import './App.css'; import { parseInstagramHTML } from './utils/parser'; @@ -15,14 +15,16 @@ const TIMEZONES = [ function App() { // Multi-chat state - const [chats, setChats] = useState([]); // Array of chat objects + const [chats, setChats] = useState([]); // Array of chat metadata objects const [selectedChatId, setSelectedChatId] = useState(null); + const [loadedChatData, setLoadedChatData] = useState(null); // Currently loaded chat's full data // Single chat state (when viewing a specific chat) const [messages, setMessages] = useState([]); const [myUsername, setMyUsername] = useState(''); const [fileLoaded, setFileLoaded] = useState(false); const [timezone, setTimezone] = useState('UTC'); + const [theme, setTheme] = useState('dark'); // 'dark' or 'light' const [error, setError] = useState(''); const [mediaFiles, setMediaFiles] = useState({}); const [fullscreenMedia, setFullscreenMedia] = useState(null); @@ -30,6 +32,9 @@ function App() { const [allVideos, setAllVideos] = useState([]); const [isDragging, setIsDragging] = useState(false); const [folderName, setFolderName] = useState(''); + const [showSettings, setShowSettings] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); const chatEndRef = useRef(null); const videoRefs = useRef({}); const fileInputRef = useRef(null); @@ -52,15 +57,26 @@ function App() { }; }, [mediaFiles]); + // Apply theme to body + useEffect(() => { + if (theme === 'light') { + document.body.classList.add('light-theme'); + } else { + document.body.classList.remove('light-theme'); + } + }, [theme]); + // Helper function to strip Instagram path prefix const stripInstagramPrefix = (path) => { if (!path) return path; - // Remove "your_instagram_activity/messages/inbox/" prefix + // Remove "your_instagram_activity/messages/inbox/" prefix or just "inbox/" prefix const prefixes = [ 'your_instagram_activity/messages/inbox/', 'your_instagram_activity\\messages\\inbox\\', '../your_instagram_activity/messages/inbox/', - '../your_instagram_activity\\messages\\inbox\\' + '../your_instagram_activity\\messages\\inbox\\', + 'inbox/', + 'inbox\\' ]; let cleanPath = path; @@ -73,7 +89,136 @@ function App() { return cleanPath; }; - // Process entire inbox folder with multiple chat folders + // Helper function to mark messages as mine/theirs + const markMessagesOwnership = (messages, username) => { + messages.forEach(msg => { + msg.isMine = msg.sender.toLowerCase() === username.toLowerCase(); + }); + }; + + // Auto-detect username by finding the most common sender name across all chat metadata + const autoDetectUsername = (allChatMetadata) => { + const senderInChatCounts = {}; + + // Count senders across all chat previews + allChatMetadata.forEach(chatMeta => { + if (chatMeta.senderCounts) { + Object.keys(chatMeta.senderCounts).forEach(sender => { + senderInChatCounts[sender] = (senderInChatCounts[sender] || 0) + 1; + }); + } + }); + + // The username is likely the person who appears in ALL or most chats + let detectedUsername = ''; + let maxChatCount = 0; + + Object.keys(senderInChatCounts).forEach(sender => { + if (senderInChatCounts[sender] > maxChatCount) { + maxChatCount = senderInChatCounts[sender]; + detectedUsername = sender; + } + }); + + console.log('Auto-detected username:', detectedUsername, 'appears in', maxChatCount, 'chats'); + return detectedUsername; + }; + + // Extract metadata from chat folder without fully parsing messages + const extractChatMetadata = (chatFolderName, files) => { + return new Promise((resolve) => { + console.log(`Extracting metadata for: ${chatFolderName}`); + + // Find the HTML file + const htmlFile = files.find(f => f.name.toLowerCase().endsWith('.html')); + if (!htmlFile) { + console.log('No HTML file found in', chatFolderName); + resolve(null); + return; + } + + // Read HTML to extract basic info + const reader = new FileReader(); + reader.onload = (event) => { + try { + const htmlContent = event.target.result; + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlContent, 'text/html'); + + // Count message nodes + const messageSelectors = [ + '.pam._3-95._2ph-._a6-g.uiBoxWhite.noborder', + '.pam._3-95._2pi0._2lej.uiBoxWhite.noborder', + '.pam.uiBoxWhite.noborder' + ]; + + let messageNodes = []; + for (const selector of messageSelectors) { + messageNodes = Array.from(doc.querySelectorAll(selector)); + if (messageNodes.length > 0) break; + } + + const messageCount = messageNodes.length; + + // Extract senders for username detection + const senderCounts = {}; + let lastMessageText = ''; + + if (messageNodes.length > 0) { + // Get last message + const lastNode = messageNodes[messageNodes.length - 1]; + const contentElem = lastNode.querySelector('._3-95._a6-p'); + if (contentElem) { + lastMessageText = (contentElem.innerText || contentElem.textContent || '').trim().substring(0, 100); + } + + // Count senders (sample first 10 and last 10 messages for efficiency) + const sampleNodes = [ + ...messageNodes.slice(0, Math.min(10, messageNodes.length)), + ...messageNodes.slice(Math.max(0, messageNodes.length - 10)) + ]; + + sampleNodes.forEach(node => { + const senderElem = node.querySelector('._3-95._2pim._a6-h._a6-i'); + if (senderElem) { + const sender = (senderElem.innerText || senderElem.textContent || '').trim(); + if (sender) { + senderCounts[sender] = (senderCounts[sender] || 0) + 1; + } + } + }); + } + + // Generate chat ID + let chatId; + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + chatId = crypto.randomUUID(); + } else { + chatId = chatFolderName + '_' + Date.now() + '_' + Math.random().toString(36).substring(7); + } + + const metadata = { + id: chatId, + folderName: chatFolderName, + messageCount: messageCount, + lastMessageText: lastMessageText, + senderCounts: senderCounts, + files: files, // Store files for lazy loading + htmlFile: htmlFile, + loaded: false // Mark as not loaded + }; + + resolve(metadata); + } catch (err) { + console.error('Error extracting metadata for', chatFolderName, ':', err); + resolve(null); + } + }; + reader.readAsText(htmlFile); + }); + }; + + // Process entire inbox folder with multiple chat folders (metadata only) const processInboxFolder = (files) => { console.log('Processing inbox folder with', files.length, 'files'); @@ -110,30 +255,29 @@ function App() { console.log('Found chat folders:', Object.keys(chatFolders)); console.log('Chat folder details:', Object.keys(chatFolders).map(k => `${k}: ${chatFolders[k].length} files`)); - // Process each chat folder - const chatPromises = Object.keys(chatFolders).map(chatFolderName => { - return processSingleChatFolder(chatFolderName, chatFolders[chatFolderName]); + // Extract metadata for each chat folder (without parsing all messages) + const metadataPromises = Object.keys(chatFolders).map(chatFolderName => { + return extractChatMetadata(chatFolderName, chatFolders[chatFolderName]); }); - Promise.all(chatPromises).then(processedChats => { - const validChats = processedChats.filter(chat => chat !== null); - console.log('Processed chats:', validChats.length); + Promise.all(metadataPromises).then(chatMetadata => { + const validChats = chatMetadata.filter(chat => chat !== null); + console.log('Extracted metadata for', validChats.length, 'chats'); if (validChats.length === 0) { setError('No valid chats found. Please check the folder structure.'); } else { + // Auto-detect username from metadata + const detectedUsername = autoDetectUsername(validChats); + setMyUsername(detectedUsername); + setChats(validChats); setFileLoaded(true); setError(''); - // Select first chat by default - directly set state since chats hasn't updated yet + + // Load the first chat by default if (validChats.length > 0) { - const firstChat = validChats[0]; - setSelectedChatId(firstChat.id); - setMessages(firstChat.messages); - setMediaFiles(firstChat.mediaFiles); - setAllImages(firstChat.images); - setAllVideos(firstChat.videos); - setFolderName(firstChat.folderName); + loadChat(validChats[0].id, validChats, detectedUsername); } } }).catch(err => { @@ -144,7 +288,7 @@ function App() { }; // Process a single chat folder - const processSingleChatFolder = (chatFolderName, files) => { + const processSingleChatFolder = (chatFolderName, files, username = '') => { return new Promise((resolve) => { console.log(`Processing chat folder: ${chatFolderName} with ${files.length} files`); @@ -213,7 +357,7 @@ function App() { reader.onload = (event) => { try { const htmlContent = event.target.result; - const parsedMessages = parseInstagramHTML(htmlContent, myUsername, mediaFileMap); + const parsedMessages = parseInstagramHTML(htmlContent, username, mediaFileMap); if (parsedMessages.length === 0) { console.log('No messages found in', chatFolderName); @@ -249,18 +393,75 @@ function App() { reader.readAsText(htmlFile); }); }; + + // Load a chat's full data on demand + const loadChat = (chatId, chatsList = null, username = null) => { + const chatsToUse = chatsList || chats; + const usernameToUse = username || myUsername; + const chatMeta = chatsToUse.find(c => c.id === chatId); + + if (!chatMeta) { + console.error('Chat not found:', chatId); + return; + } + + console.log('Loading chat:', chatMeta.folderName); + + // If already loaded, just set the state + if (chatMeta.loaded && chatMeta.messages) { + setSelectedChatId(chatId); + setMessages(chatMeta.messages); + setMediaFiles(chatMeta.mediaFiles || {}); + setAllImages(chatMeta.images || []); + setAllVideos(chatMeta.videos || []); + setFolderName(chatMeta.folderName); + setLoadedChatData(chatMeta); + return; + } + + // Parse the chat fully + processSingleChatFolder(chatMeta.folderName, chatMeta.files, usernameToUse).then(chatData => { + if (chatData) { + // Mark messages as mine/theirs + markMessagesOwnership(chatData.messages, usernameToUse); + + // Update the chat metadata with full data + chatMeta.messages = chatData.messages; + chatMeta.mediaFiles = chatData.mediaFiles; + chatMeta.images = chatData.images; + chatMeta.videos = chatData.videos; + chatMeta.loaded = true; + + // Set state + setSelectedChatId(chatId); + setMessages(chatData.messages); + setMediaFiles(chatData.mediaFiles); + setAllImages(chatData.images); + setAllVideos(chatData.videos); + setFolderName(chatMeta.folderName); + setLoadedChatData(chatMeta); + + // Update chats array + setChats([...chatsToUse]); + } + }); + }; // Select and display a specific chat const selectChat = (chatId) => { - const chat = chats.find(c => c.id === chatId); - if (chat) { - setSelectedChatId(chatId); - setMessages(chat.messages); - setMediaFiles(chat.mediaFiles); - setAllImages(chat.images); - setAllVideos(chat.videos); - setFolderName(chat.folderName); + // Unload previous chat to free memory + if (loadedChatData && loadedChatData.id !== chatId) { + console.log('Unloading previous chat:', loadedChatData.folderName); + // Clear all data from memory but keep metadata + loadedChatData.messages = null; + loadedChatData.mediaFiles = null; + loadedChatData.images = null; + loadedChatData.videos = null; + loadedChatData.loaded = false; } + + // Load the new chat + loadChat(chatId); }; // Original process files function for backward compatibility @@ -342,12 +543,39 @@ function App() { reader.onload = (event) => { try { const htmlContent = event.target.result; - const parsedMessages = parseInstagramHTML(htmlContent, myUsername, mediaFileMap); + // First parse with empty username to get all messages + const parsedMessages = parseInstagramHTML(htmlContent, '', mediaFileMap); if (parsedMessages.length === 0) { setError('No messages found in the file. Please check the file format.'); setFileLoaded(false); } else { + // Auto-detect username from single chat + const senderCounts = {}; + parsedMessages.forEach(msg => { + if (msg.sender) { + senderCounts[msg.sender] = (senderCounts[msg.sender] || 0) + 1; + } + }); + + // The username is likely the most frequent sender + let detectedUsername = ''; + let maxCount = 0; + Object.keys(senderCounts).forEach(sender => { + if (senderCounts[sender] > maxCount) { + maxCount = senderCounts[sender]; + detectedUsername = sender; + } + }); + + setMyUsername(detectedUsername); + console.log('Auto-detected username:', detectedUsername); + + // Re-mark messages with correct username + parsedMessages.forEach(msg => { + msg.isMine = msg.sender.toLowerCase() === detectedUsername.toLowerCase(); + }); + setMessages(parsedMessages); setFileLoaded(true); setError(''); @@ -463,15 +691,7 @@ function App() { >

Instagram Chat Viewer

- - setMyUsername(e.target.value)} - /> - - +
{ - if (!chat || !chat.messages) return 'Unknown Chat'; + if (!chat) return 'Unknown Chat'; + // For metadata, just use folder name for now + if (!chat.messages || !chat.loaded) { + return chat.folderName || 'Unknown Chat'; + } return determineChatName(chat.messages, chat.folderName); }; @@ -552,32 +776,27 @@ function App() { return name.substring(0, 2).toUpperCase(); }; - // Check if we have multiple chats loaded - const hasMultipleChats = chats.length > 1; + // Check if we have any chats loaded + const hasChats = chats.length > 0; return (
- {/* Chat List Sidebar (only show if multiple chats) */} - {hasMultipleChats && ( + {/* Chat List Sidebar (only show if any chats) */} + {hasChats && (
Messages - { - setFileLoaded(false); - setChats([]); - setSelectedChatId(null); - }} + onClick={() => setShowSettings(true)} />
{chats.map(chat => { const chatName = getChatDisplayName(chat); - const lastMsg = chat.lastMessage; const isActive = chat.id === selectedChatId; return ( @@ -592,7 +811,7 @@ function App() {
{chatName}
- {lastMsg?.content || (lastMsg?.media?.length > 0 ? '📷 Photo/Video' : 'No messages')} + {chat.lastMessageText || 'No messages'}
@@ -614,12 +833,12 @@ function App() { size={24} style={{cursor:'pointer'}} onClick={() => { - if (hasMultipleChats) { - // In multi-chat view, just deselect + if (hasChats) { + // In chat view with sidebar, go back to chat list setSelectedChatId(null); setMessages([]); } else { - // In single chat view, go back to setup + // Fallback: go back to setup (shouldn't normally happen) setFileLoaded(false); setChats([]); } @@ -635,15 +854,6 @@ function App() {
- {/* Timezone Switcher Overlay */} -
- -
- {/* Chat Area */}
{messages.length === 0 ? ( @@ -661,7 +871,30 @@ function App() {
{msg.sender}
)}
- {msg.content &&
{msg.content}
} + {/* Check if message contains Instagram reel/post link */} + {(() => { + const instagramLinkMatch = msg.content?.match(/(https:\/\/www\.instagram\.com\/(reel|p)\/([a-zA-Z0-9_-]+))/); + if (instagramLinkMatch) { + const [fullUrl, , type, code] = instagramLinkMatch; + // For reels/posts, embed using Instagram's oEmbed API + const embedUrl = `https://www.instagram.com/${type}/${code}/embed/`; + return ( +
+ +