Greasy Fork

SnapTagger (Improved)

Tag and save Snapchat chats seamlessly using localStorage.

// ==UserScript==
// @name         SnapTagger (Improved)
// @namespace    http://tampermonkey.net/
// @version      0.3 // Increment version for the change
// @description  Tag and save Snapchat chats seamlessly using localStorage.
// @author       You & Gemini
// @license      MIT // Added MIT License
// @match        https://web.snapchat.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue // Optional: For clearing data if needed
// ==/UserScript==

(function () {
    'use strict';

    // --- Constants ---
    const STORAGE_KEY = 'snapTaggerData'; // Key for localStorage
    // IMPORTANT: These selectors might change if Snapchat updates their website.
    // You may need to inspect the elements on web.snapchat.com and update these.
    const SNAP_ICON_SELECTOR = 'svg[aria-label="Snapchat"]'; // Try targeting the SVG element more specifically
    const CHAT_HEADER_SELECTOR = '[data-testid="conversation-header-title"]'; // Selector for the chat header element containing the friend's name

    // --- State ---
    let uiInjected = false;

    // --- Styling ---
    // Use GM_addStyle for CSS to keep it separate and potentially more maintainable
    GM_addStyle(`
        .snaptagger-container {
            position: relative;
            cursor: pointer;
            display: inline-block; /* Adjust display as needed */
            vertical-align: middle; /* Align with other header items */
        }
        .snaptagger-menu {
            position: absolute;
            top: 100%; /* Position below the icon */
            right: 0;
            background-color: #2f2f2f; /* Slightly lighter dark grey */
            border: 1px solid #555; /* Softer border */
            border-radius: 8px; /* Match Snapchat's rounding */
            padding: 12px;
            z-index: 10000; /* Ensure it's on top */
            display: none; /* Hidden by default */
            color: #ffffff;
            font-size: 14px;
            min-width: 180px; /* Give it some width */
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); /* Add subtle shadow */
            font-family: "Inter", sans-serif; /* Try to match font */
        }
        .snaptagger-menu.visible {
            display: block;
        }
        .snaptagger-title {
            font-weight: 600;
            margin-bottom: 10px;
            color: #eee; /* Lighter title color */
            border-bottom: 1px solid #444;
            padding-bottom: 6px;
        }
        .snaptagger-button {
            display: block;
            width: 100%;
            background-color: #4a4a4a; /* Button background */
            color: #ffffff;
            border: none;
            padding: 8px 12px;
            margin-bottom: 6px;
            border-radius: 6px;
            cursor: pointer;
            text-align: left;
            font-size: 13px;
            transition: background-color 0.2s ease;
        }
        .snaptagger-button:hover {
            background-color: #5c5c5c; /* Slightly lighter on hover */
        }
        .snaptagger-button:last-child {
            margin-bottom: 0;
        }
        .snaptagger-feedback {
            font-size: 12px;
            color: #999;
            margin-top: 8px;
            text-align: center;
            min-height: 15px; /* Reserve space */
        }
    `);

    // --- Utility Functions ---

    /**
     * Gets the currently tagged data from storage.
     * @returns {Array<Object>} An array of tag objects or an empty array.
     */
    function getStoredTags() {
        // Use GM_getValue for Tampermonkey/Greasemonkey storage (more robust than raw localStorage)
        return GM_getValue(STORAGE_KEY, []); // Default to empty array if nothing stored
    }

    /**
     * Saves the tagged data to storage.
     * @param {Array<Object>} tags - The array of tag objects to save.
     */
    function saveTags(tags) {
        GM_setValue(STORAGE_KEY, tags);
    }

    /**
     * Gets the name of the currently open chat.
     * @returns {string | null} The chat name or null if not found.
     */
    function getCurrentChatName() {
        // This selector might need adjustment based on Snapchat's current structure
        const headerElement = document.querySelector(CHAT_HEADER_SELECTOR);
        return headerElement ? headerElement.textContent.trim() : null;
    }

    /**
     * Shows temporary feedback message in the menu.
     * @param {string} message - The message to display.
     */
    function showFeedback(message) {
        const feedbackEl = document.getElementById('snaptagger-feedback');
        if (feedbackEl) {
            feedbackEl.textContent = message;
            setTimeout(() => {
                if (feedbackEl.textContent === message) { // Only clear if it's the same message
                     feedbackEl.textContent = '';
                }
            }, 2500); // Clear after 2.5 seconds
        }
    }


    // --- Core Logic ---

    /**
     * Tags the currently open chat conversation.
     */
    function tagCurrentChat() {
        const chatName = getCurrentChatName();
        if (!chatName) {
            showFeedback("Error: Couldn't find chat name.");
            console.error("SnapTagger: Could not find chat header element with selector:", CHAT_HEADER_SELECTOR);
            return;
        }

        const tag = prompt(`Enter a tag for the chat with "${chatName}":`);
        if (tag === null || tag.trim() === '') {
            showFeedback("Tagging cancelled.");
            return; // User cancelled or entered empty tag
        }

        const newTagEntry = {
            chatName: chatName,
            tag: tag.trim(),
            timestamp: new Date().toISOString(),
            // Future: Add specific messages here if needed
            // messages: getSelectedMessages() // Placeholder for more advanced functionality
        };

        try {
            const currentTags = getStoredTags();
            currentTags.push(newTagEntry);
            saveTags(currentTags);
            console.log("SnapTagger: Chat tagged:", newTagEntry);
            showFeedback(`Tagged "${chatName}" as "${tag.trim()}"`);
        } catch (error) {
            console.error("SnapTagger: Error saving tag:", error);
            showFeedback("Error saving tag.");
            alert("SnapTagger Error: Could not save tag to storage. Storage might be full or disabled.\n\n" + error);
        }
    }

    /**
     * Exports the stored tags as a JSON file.
     */
    function exportTaggedChats() {
        try {
            const tags = getStoredTags();
            if (tags.length === 0) {
                showFeedback("No tags to export.");
                return;
            }

            const jsonData = JSON.stringify(tags, null, 2); // Pretty print JSON
            const blob = new Blob([jsonData], { type: 'application/json' });
            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = url;
            const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
            a.download = `snaptagger_export_${timestamp}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url); // Clean up

            showFeedback("Tags exported successfully!");
            console.log("SnapTagger: Exported tags.", tags);

        } catch (error) {
            console.error("SnapTagger: Error exporting tags:", error);
             showFeedback("Error during export.");
            alert("SnapTagger Error: Could not export tags.\n\n" + error);
        }
    }

    /**
     * Injects the SnapTagger UI elements into the page.
     * @param {Element} iconElement - The Snapchat icon element to replace/wrap.
     */
    function injectUI(iconElement) {
        if (uiInjected) return; // Prevent multiple injections

        // --- Create Container ---
        const container = document.createElement('div');
        container.className = 'snaptagger-container';

        // --- Clone Icon ---
        // Cloning helps maintain the original icon's appearance and any associated listeners
        const clonedIcon = iconElement.cloneNode(true);
        container.appendChild(clonedIcon);

        // --- Create Dropdown Menu ---
        const menu = document.createElement('div');
        menu.className = 'snaptagger-menu'; // Use class for styling
        menu.id = 'snaptagger-menu'; // Add ID for easier selection
        menu.innerHTML = `
            <div class="snaptagger-title">📌 SnapTagger</div>
            <button id="snaptagger-tag-chat" class="snaptagger-button">Tag this chat</button>
            <button id="snaptagger-export-chat" class="snaptagger-button">Export tagged chats</button>
            <div id="snaptagger-feedback" class="snaptagger-feedback"></div>
        `;
        container.appendChild(menu);

        // --- Replace Original Icon ---
        // Replace the original icon with our container that includes the icon and menu
        iconElement.parentNode.replaceChild(container, iconElement);
        uiInjected = true; // Mark UI as injected
        console.log("SnapTagger: UI Injected.");

        // --- Add Event Listeners ---

        // Toggle dropdown visibility
        container.addEventListener('click', (event) => {
            // Prevent clicks on buttons inside the menu from closing it immediately
            if (!menu.contains(event.target) || event.target === container || event.target === clonedIcon) {
                 menu.classList.toggle('visible');
            }
        });

        // Close dropdown if clicking outside
        document.addEventListener('click', (event) => {
            if (!container.contains(event.target) && menu.classList.contains('visible')) {
                menu.classList.remove('visible');
            }
        });

        // Button actions
        document.getElementById('snaptagger-tag-chat').addEventListener('click', (event) => {
            event.stopPropagation(); // Prevent container click listener from firing
            tagCurrentChat();
            // Optionally close menu after action:
            // menu.classList.remove('visible');
        });

        document.getElementById('snaptagger-export-chat').addEventListener('click', (event) => {
            event.stopPropagation(); // Prevent container click listener from firing
            exportTaggedChats();
            // Optionally close menu after action:
             menu.classList.remove('visible');
        });
    }

    // --- Initialization ---

    // Use MutationObserver for potentially better performance and reliability than setInterval
    const observer = new MutationObserver((mutationsList, obs) => {
        const snapIcon = document.querySelector(SNAP_ICON_SELECTOR);
        if (snapIcon && !uiInjected) {
            console.log("SnapTagger: Snapchat icon found. Injecting UI.");
            // Small delay to ensure surrounding elements are likely stable
            setTimeout(() => injectUI(snapIcon), 500);
            obs.disconnect(); // Stop observing once the icon is found and UI is injected
        }
        // Add a timeout safeguard in case the observer fails or the element never appears
        // setTimeout(() => {
        //     if (!uiInjected) {
        //         console.warn("SnapTagger: Timed out waiting for icon. Script might not work.");
        //         obs.disconnect();
        //     }
        // }, 15000); // Stop trying after 15 seconds
    });

    // Start observing the document body for added nodes
    console.log("SnapTagger: Initializing observer...");
    observer.observe(document.body, { childList: true, subtree: true });

    // Fallback using setInterval (less ideal but can work)
    // const checkInterval = 2000; // Check every 2 seconds
    // const maxAttempts = 10; // Try for 20 seconds
    // let attempts = 0;
    // const fallbackInterval = setInterval(() => {
    //     if (uiInjected) {
    //         clearInterval(fallbackInterval);
    //         return;
    //     }
    //     attempts++;
    //     const snapIcon = document.querySelector(SNAP_ICON_SELECTOR);
    //     if (snapIcon) {
    //         console.log("SnapTagger (Fallback): Snapchat icon found. Injecting UI.");
    //         clearInterval(fallbackInterval);
    //         injectUI(snapIcon);
    //     } else if (attempts >= maxAttempts) {
    //         clearInterval(fallbackInterval);
    //         console.warn(`SnapTagger (Fallback): Could not find Snapchat icon (${SNAP_ICON_SELECTOR}) after ${maxAttempts} attempts. Script may not work.`);
    //     }
    // }, checkInterval);


})();