ThundrPlus

avoid bots + more features

// ==UserScript==
// @name         ThundrPlus
// @namespace    http://tampermonkey.net/
// @version      2025-04-27
// @description  avoid bots + more features
// @author       YassinMi
// @match        https://thundr.com/text
// @icon         https://www.google.com/s2/favicons?sz=64&domain=thundr.com
// @grant        none
// @license MIT
// ==/UserScript==


//@ts-check



/**
 * @typedef {{ip:string,id:string,country:string,sex:string}} PartnerInfo
 */

/**
 * @typedef {{partnerInfo:PartnerInfo,matchedAt:Date,matchId:string}} Match
 */

/**
 * @typedef {{disconnectedAt:Date,matchId:string, from:string?, to:string}} DisconnectedArgs when disconection is pushed form me the from are nulll
 */
/**
 * @typedef {{from:"You"|"Stranger",content:string}[]} ConversationContent
 */
/**
 * @typedef {{match:Match,conversationContent:ConversationContent,disconnectedBy:"You"|"Stranger",disconnectedAt:Date}} Interaction
 */

class DBLayer {
    /**
     * 
     */
    static verifyReadyForNewMatchEntry() {
        if (this.matchesBuffer.length) {
            var latestMatchEntry = this.matchesBuffer[this.matchesBuffer.length - 1]
            if (latestMatchEntry.disconnectArgs === undefined) {
                console.warn("not ready for new match, the last match isn't properly disconnected, an implicit closure is made")
                latestMatchEntry.disconnectArgs = { disconnectedAt: new Date(), matchId: latestMatchEntry.match.matchId, from: "", to: "" }
            }
        }
    }

    /**
     * 
     * @param {string} matchId 
     * @returns {{match:Match,matchRaw:any,disconnectArgs:DisconnectedArgs?,scrapedConvo:ConversationContent?}|undefined}
     */
    static getMatchFromMatchBuffer(matchId) {
        return DBLayer.matchesBuffer.find(i => i.match.matchId === matchId);
    }

    /**
     * @type {DBLayer}
     */
    static DB = new DBLayer()
    /**
     * @type {{match:Match,matchRaw:any,disconnectArgs:DisconnectedArgs?,scrapedConvo:ConversationContent?}[]}
     */
    static matchesBuffer = []


    /**
     * @type {string?}
     */
    static latestKnownMatchId
    constructor() {

    }
    /**
     * 
     * @param {string} matchId 
     * @param {ConversationContent} scrapedConvo 
     */
    updateScrapedConvoForMatchId(matchId, scrapedConvo) {
        console.log(`DB Cache: updating match entry ${matchId} with scraped convo ${scrapedConvo?.length}`)
        var matchEntry = DBLayer.getMatchFromMatchBuffer(matchId)
        if (matchEntry === undefined) {
            console.log("no such match entry registred")
            throw new Error("no such match entry registred")
        }
        else {

        }

        var maybeExists = matchEntry.scrapedConvo
        if (maybeExists) {

            if ((!scrapedConvo) || (maybeExists.length > scrapedConvo.length)) {
                return;
            }
            else {

            }
        }

        matchEntry.scrapedConvo = scrapedConvo


    }
    getInteractionssByIP(ip) {
        var oldData = window.localStorage.getItem("convosOfIp_" + ip)
        var parsedOldData = JSON.parse(oldData ?? "[]")
        return parsedOldData;
    }
    /**
     * 
     * @param {Interaction} interaction 
     */
    addInteraction(interaction) {
        console.log("DB: saving interaction:", interaction)
        var oldData = window.localStorage.getItem("convosOfIp_" + interaction.match.partnerInfo.ip)
        var parsedOldData = JSON.parse(oldData ?? "[]")
        parsedOldData.push(interaction)
        window.localStorage.setItem("convosOfIp_" + interaction.match.partnerInfo.ip, JSON.stringify(parsedOldData))
    }
    /**
     * 
     * @param {string} ip 
     * @param {string} note 
     */
    storeIPNote(ip, note) {
        console.log("DB: saving note:", note, ip)
        window.localStorage.setItem("noteOfIp_" + ip, note)
    }
    /**
     * 
     * @param {string} ip 
     */
    getIPNote(ip) {
        return window.localStorage.getItem("noteOfIp_" + ip)
    }
}

/**
 * this layer requires maintainance, it injects code that intercepts events and fires them in the the standard
 *  TP_MATCHED, TP_MESSAGE, TP_DISCONNECTED, TP_INIT layer 
 */
function hookEvents() {
    console.log("####### hookEvents called")
    if (window["tp_hookEvents_called"]) {
        throw new Error("hook events already called")
    }
    window["tp_hookEvents_called"] = true
    const originalLog = console.log;

    var matchLogDetector = function (...args) {
        if (args.length >= 2 && typeof args[0] === "string" && args[0].includes("$$$ matched! $$$")) {
            var arg = args[1]
            if (typeof arg === "object" && arg !== null && "match_id" in arg && "room" in arg && "partner" in arg) {
                return arg
            }
        }
    }
    var disconnectedLogDetector = function (...args) {
        if (args.length >= 2 && typeof args[0] === "string" && args[0].includes("$$$ user disconnected $$$")) {
            var arg = args[1]
            if (typeof arg === "object" && arg !== null && "match_id" in arg && "from" in arg && "to" in arg) {
                return arg
            }
        }
        return undefined
    }
    var disconnectedPushedFromMeLogDetector = function (...args) {
        if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("$$$ push disconnect $$$")) {
            var concat = args.join("");
            var partnerId = (/**@type {string}*/(concat)).replace("$$$ push disconnect $$$", "").trim()
            return partnerId
        }
        return undefined
    }
    var experimentalPreDisconnectLog = function (...args) {
        if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("fired event start_text_m")) {
            return true
        }
        return false
    }
    var experimentalLefRoomLog = function (...args) {
        if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("left room")) {
            return true
        }
        return false
    }
    var startMatchLogDetector = function (...args) {
        if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("$$$ start match $$$")) {
            return true
        }
        return false
    }
    //
    //$$$ push disconnect $$$ 230d75c2-d3f8-4bbb-86c0-16462add3fe7
    console.log = function (...args) {
        originalLog("fake log")
        originalLog.apply(console, args);

        var maybeMatchLogArg = matchLogDetector(...args)
        var maybeDisconnectLogArg = disconnectedLogDetector(...args)
        var maybeDisconnectFromMeLogArg = disconnectedPushedFromMeLogDetector(...args)
        var experimentalPreDisconnectLog_ = experimentalPreDisconnectLog(...args)
        var experimentalLefRoomLog_ = experimentalLefRoomLog(...args)
        var startMatchLogDetector_ = startMatchLogDetector(...args)
        if (startMatchLogDetector_) {
            window.postMessage({ type: "TP_INIT", data: null }, "*");
        }
        else if (maybeMatchLogArg) {
            console.log(" log detected as maybeMatchLogArg")
            var arg = maybeMatchLogArg;
            const summary = {
                matchId: arg.match_id,
                matchedAt: new Date(),
                /**@type {PartnerInfo} */
                partnerInfo: { ip: arg.partner.ip, id: arg.partner.id, country: arg.partner?.country, sex: arg.partner.settings.profile.sex },

            };
            console.log("####### TP_MATCHED fired", summary)
            window.postMessage({ type: "TP_MATCHED", data: summary }, "*");
        }
        else if (maybeDisconnectLogArg) {
            console.log(" log detected as maybeDisconnectLogArg")
            pollUpdateScrapedConvoForLatestMatch()
            var arg = maybeDisconnectLogArg;
            /**
             * @type {DisconnectedArgs}
             */
            const summary = {
                matchId: arg.match_id,
                disconnectedAt: new Date(),
                from: arg.from,
                to: arg.to
            };
            console.log("####### TP_DISCONNECTED fired", summary)
            window.postMessage({ type: "TP_DISCONNECTED", data: summary }, "*");
        }
        else if (maybeDisconnectFromMeLogArg) {
            console.log(" log detected as maybeDisconnectFromMeLogArg")
            pollUpdateScrapedConvoForLatestMatch()
            var partnerId = maybeDisconnectFromMeLogArg;
            if (DBLayer.latestKnownMatchId === null) throw new Error("a disconnected pushed form me without a latest registred match")
            /**
             * @type {DisconnectedArgs}
             */
            const summary = {
                matchId: DBLayer.latestKnownMatchId,
                disconnectedAt: new Date(),
                from: null,
                to: partnerId
            };
            console.log("####### TP_DISCONNECTED fired", summary)
            window.postMessage({ type: "TP_DISCONNECTED", data: summary }, "*");
        }
        else if (experimentalPreDisconnectLog_) {
            console.log(" log detected as experimentalPreDisconnectLog_")
            pollUpdateScrapedConvoForLatestMatch()
        }
        else if (experimentalLefRoomLog_) {
            console.log(" log detected as experimentalLefRoomLog_")
            pollUpdateScrapedConvoForLatestMatch()
        }
    };

}
/**
 * the part of injecting UI that relys on thundr UI and needs maintainance, only styling depends on thudr, the returned html structure does not change
 */

function injectUI_impl() {
    /**
     * @type {HTMLDivElement}
     */
    const targetDiv = /**@type {HTMLDivElement}*/(document.evaluate("//div[@class='css-17vaizm']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue);
    var tpRoot = document.createElement("div")
    tpRoot.classList.add("css-1pum37")
    tpRoot.style.backgroundColor = "#00000066";
    tpRoot.style.width = "640px"
    tpRoot.innerHTML = `
    


    <div id="pt-panel-header" style="display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 0; gap: 16px;">
  <div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
    <div style="display: flex; flex-direction: column; gap:0px;">
    <style>
    /*todo: tp-nav-butn should be highlighted when hovered and should be grayed out when in disabled state*/
      .tp-nav-btn {
    background: none;
    border: none;
    font-size: 16px;
    color: white;
    cursor: pointer;
    padding: 0;
  }

  .tp-nav-btn:hover:not(:disabled) {
    color: var(--chakra-colors-color-palette-solid);
  }

  .tp-nav-btn:disabled {
    color: #aaa;
    cursor: default;
  }
    </style>
      <button class="tp-nav-btn" id="tp-prev-btn">▲</button>
      <button class="tp-nav-btn" disabled id="tp-next-btn" style="">▼</button>
    </div>
    <div id="tp-cursor-status">4/7</div>
  </div>
  <div style="display: flex; flex-direction: column; align-items: flex-end; font-size:13px;">
    <div id="tp-match-id-lbl">1254-5468-56548-54654</div>
    <div id="tp-connected-status" >Disconnected</div>
  </div>
</div>
      <strong>ip:</strong> <span id="ip-span">-</span><br>
      <strong>desc:</strong><span id="ipNote-span">-</span><br>
      <strong>notes:</strong><span id="notes-span">-</span><br>
      <strong>info:</strong><span id="info-span">-</span><br>
      <strong>Convo count:</strong><span id="convoCount-span">-</span><br>
      <div style="display:flex; flex-direction:row; align-items:center; ">
        <textarea rows="2" cols="50" placeholder="Write ip notes..." id="tp-ipnote-textarea"></textarea>
        <button type="button" class="chakra-button css-18tsh74" id="tp-save-ipnote-btn">Save</button>
    </div>
      <br>
      <span id="toast-status">ready</span><br>
      <table id="history-table" style="width: 100%; border-collapse: collapse; margin-top: 10px; color: white; font-family: Arial, sans-serif; font-size:12px; min-width:640px;">
    <thead>
      <tr>
        <th style="border: 1px solid white; padding: 4px;">Date</th>
        <th style="border: 1px solid white; padding: 4px;">Type</th>
        <th style="border: 1px solid white; padding: 4px;">Msg count</th>
        <th style="border: 1px solid white; padding: 4px;">Dur</th>
        <th style="border: 1px solid white; padding: 4px;">Lst. Msg</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="border: 1px solid white; padding: 4px;">2025-04-23</td>
        <td style="border: 1px solid white; padding: 4px;">They skipped</td>
        <td style="border: 1px solid white; padding: 4px;">12</td>
        <td style="border: 1px solid white; padding: 4px;">5m 42s</td>
        <td style="border: 1px solid white; padding: 4px;">They: Thanks, got..!</td>
      </tr>
    </tbody>
  </table>
<button type="button" class="chakra-button css-18tsh74 tp-toggle-convo-popup-btn" >Show/Hide Chat</button>
  

  <div id="tp-convo-popup" style="  position: absolute;  display: flex;  top: 80px;  width: 100%;  background-color: rgb(19, 4, 13);;  max-height: 50vh;  flex-direction: column;">
  <button type="button" class="chakra-button css-18tsh74 tp-toggle-convo-popup-btn" style=" align-self: flex-end;">Show/Hide Chat</button>
  
   <div  class="hide-scrollbar css-1qs2dh2 tp-convo-popup" style="  max-height: calc(100% - 40px);  min-height: 0;  ">
   
  </div>
  </div>
  
 
  
  
  `;
    targetDiv.appendChild(tpRoot)
    return tpRoot;
}

/**
 * 
 * @returns {ConversationContent}
 */
function scrapeConversationContent() {
    /**
     * @type {ConversationContent} 
     */
    var res = []
    /**
     * @type {HTMLDivElement}
     */
    const chatRootDiv = /**@type {HTMLDivElement}*/(document.evaluate("//div[@class='css-1sl5lif']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue);

    var messagesDivs = Array.from(chatRootDiv.querySelectorAll(".css-wtl4b5"))

    messagesDivs.forEach(d => {
        var from =/**@type {"Stranger"|"You"}*/(d.querySelector("b")?.innerHTML)
        var content = ""
        d.childNodes.forEach(node => {
            if (node.nodeType === Node.TEXT_NODE) {
                content += node.textContent;
            }
        });
        res.push({ from: from, content: content })
    })
    //div[@class='css-1sl5lif']
    return res;
}

class Convo {
    constructor(partnerInfo) {
        this.partner = partnerInfo;
        this.messages = []
    }

}

class TPUIComponent {
    /**
     * 
     * @param {Interaction} interaction 
     */
    notifyInteractionAdded(interaction) {
        this.pages.forEach(p => {
            if (p.match.partnerInfo.ip == interaction.match.partnerInfo.ip) {
                p.interactions.push(interaction)
                if (this.pages[this._current] === p) {
                    this.pushPartnerHistoryDetail(p.interactions, p.ipNote)
                }
            }
        })
    }
    /**
     * @typedef {{match:Match,interactions:Interaction[], ipNote:string?,dirtyIpNote:string?,pageIndex:number,disconnectedArg:DisconnectedArgs?}} UIPage
     */
    /**
     * @type {TPUIComponent}
     */
    static mainPanel

    /**
     * 
     * @param {HTMLDivElement} root 
     */
    constructor(root) {
        this._root = root;

        this._cursorStatusDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-cursor-status")))
        this._connectedStatusDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-connected-status")))
        this._navPrevBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-prev-btn")))
        this._navNextBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-next-btn")))
        this._tpMatchHeaderDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-match-id-lbl")))
        this._ipNoteTextarea = (/**@type {HTMLTextAreaElement} */ (this._root.querySelector("#tp-ipnote-textarea")))
        this._historyTable = (/**@type {HTMLTextAreaElement} */ (this._root.querySelector('#history-table')))

        this._saveIpnoteBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-save-ipnote-btn")))
        this._toggleConvoPopupVisibilityBtns = (/**@type {HTMLButtonElement[]} */ (Array.from(this._root.querySelectorAll(".tp-toggle-convo-popup-btn"))))
        this._convoPopupDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-convo-popup")))
        this._navPrevBtn.addEventListener("click", this._onPrevClick.bind(this))
        this._navNextBtn.addEventListener("click", this._onNextClick.bind(this))
        this._navNextBtn.addEventListener('contextmenu', (ev) => {
            ev.preventDefault();
            this._onNextToEndClick();
            return false;
        }, false);
        this._saveIpnoteBtn.addEventListener("click", this._onSaveIpnoteClick.bind(this))
        this._toggleConvoPopupVisibilityBtns.forEach(b => {
            b.addEventListener("click", this._onToggleConvoPopupClick.bind(this))
        })
        this._ipNoteTextarea.addEventListener("input", this._onIpNoteInputChange.bind(this));
        this.isAutoPaging = true;
        this._isTransparent = false;
        /**
         * @type {UIPage[]}
         */
        this.pages = []
        this._updateCursor(0)
        this._updateConvoPopupOpenStatus(false);

        this._root.querySelector("#pt-panel-header")?.addEventListener("dblclick", (ev) => {
            if (ev.srcElement !== this._root.querySelector("#pt-panel-header")) {
                return;
            }
            console.log(ev)
            this._isTransparent = !this._isTransparent;
            this._root.style.opacity = this._isTransparent ? "0.2" : "100%"
        })
    }

    _updateConvoPopupOpenStatus(open) {
        this._isConvoPopupOpen = open;
        this._convoPopupDiv.style.display = open ? "flex" : "none";
    }
    /**
     * 
     * @param {Interaction} interaction 
     */
    _populateConvoPopupWithContent(interaction) {
        var container = /**@type {HTMLDivElement} */ (this._convoPopupDiv.querySelector(".css-1qs2dh2"));
        container.innerHTML = ""
        interaction.conversationContent.forEach(m => {
            var messageDiv = document.createElement("div")
            messageDiv.classList.add("css-wtl4b5")
            messageDiv.innerHTML = (m.from?.includes("Stranger")) ? `<b style="color: var(--chakra-colors-messages-stranger);">Stranger:</b>`
                : `<b style="color: var(--chakra-colors-messages-you);">You:</b>`
            messageDiv.appendChild(document.createTextNode(m.content));

            container?.appendChild(messageDiv);


        })
    }
    _onToggleConvoPopupClick() {
        this._updateConvoPopupOpenStatus(!this._isConvoPopupOpen);
    }
    _onSaveIpnoteClick() {
        //save to db and update dirty note and note in the page
        if (!this._getCurrentPage()) { return }
        console.log("saving ipnote", this._getCurrentPage().dirtyIpNote)
        DBLayer.DB.storeIPNote(this._getCurrentPage().match.partnerInfo.ip, this._getCurrentPage()?.dirtyIpNote || "");
        this._getCurrentPage().ipNote = this._getCurrentPage().dirtyIpNote;
        this._refreshButtonsEnabledState()
        console.log("saved ipnote", this._getCurrentPage().dirtyIpNote)
        this.pushToast("saved note")

    }
    _onIpNoteInputChange() {
        if (!this._getCurrentPage()) {
            return;
        }
        var newNote = this._ipNoteTextarea.value
        this.pages[this._current].dirtyIpNote = newNote;

        this._refreshButtonsEnabledState()
        console.log("change detected", this._getCurrentPage()?.dirtyIpNote)
    }
    _updateCursor(current) {
        this._current = current;
        this._refreshButtonsEnabledState()
        this._refreshDisplayedCursor()

    }
    _refreshButtonsEnabledState() {
        this._navPrevBtn.disabled = !this._canClickPrev()
        this._navNextBtn.disabled = !this._canClickNext()
        this._saveIpnoteBtn.disabled = !this._canClickSaveIpnote()
    }
    _refreshDisplayedCursor() {
        if (this.pages.length > 0) {
            this._cursorStatusDiv.innerHTML = `${this._current + 1}/${this.pages.length}`
        }
        else {
            this._cursorStatusDiv.innerHTML = `(no matches)`
        }
    }
    _canClickPrev() {
        return (this.pages.length > 0 && this._current > 0)
    }
    _canClickNext() {
        return (this.pages.length > 0 && this._current < this.pages.length - 1)

    }
    _getCurrentPage() {
        return this.pages[this._current]
    }
    _canClickSaveIpnote() {
        if (!this._getCurrentPage()) {
            return false;
        }
        return this._getCurrentPage() && this._getCurrentPage().ipNote !== this._getCurrentPage().dirtyIpNote
    }
    _onPrevClick(ev) {
        ev.stopPropagation();
        if (!this._canClickPrev()) return;
        this.displayPageOfIndex(this._current - 1)
    }
    _onNextClick(ev) {
        ev.stopPropagation();
        if (!this._canClickNext()) return;
        this.displayPageOfIndex(this._current + 1)


    }
    _onNextToEndClick() {
        if (!this._canClickNext()) return;
        this.displayPageOfIndex(this.pages.length - 1)
    }
    /**
     * 
     * @param {string} date 
     * @param {*} type 
     * @param {*} msgCount 
     * @param {*} duration 
     * @param {string?} lastMsg 
     * @param {Interaction} interactionRef 
     */
    _addTableRow(date, type, msgCount, duration, lastMsg, interactionRef) {
        const table = this._historyTable.getElementsByTagName('tbody')[0];
        const row = table.insertRow();

        const cells = [date, type, msgCount, duration, lastMsg];
        for (let i = 0; i < cells.length; i++) {
            const cell = row.insertCell();
            if (i === 0) {
                //making the date clickable to open the chat popup
                cell.innerHTML = `<a href="javascript:;">${cells[i]}</a>`
                cell.querySelector("a")?.addEventListener("click", () => {
                    this._populateConvoPopupWithContent(interactionRef)
                    this._updateConvoPopupOpenStatus(true);
                })
            }
            else if (i === 1) {
                cell.innerHTML = cells[i];//for advanced convo type styling (comes as html)
            }
            else if (i === 4) {

                const div = document.createElement("div");
                div.textContent = lastMsg;
                div.title = cells[i];

                Object.assign(div.style, {
                    maxWidth: "100px",
                    whiteSpace: "normal",
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    display: "-webkit-box",
                    WebkitLineClamp: "2",
                    WebkitBoxOrient: "vertical"
                });

                cell.textContent = ""; // Clear cell's default text
                cell.appendChild(div);
            }
            else {
                cell.textContent = cells[i];
            }
            cell.style.border = "1px solid white";
            cell.style.padding = "4px";
        }
    }

    _clearTableRows() {
        const tbody = this._historyTable.getElementsByTagName('tbody')[0];
        while (tbody.firstChild) {
            tbody.removeChild(tbody.firstChild);
        }
    }

    _toggleTableVisibility(visible) {
        this._historyTable.style.display = (!!visible) ? 'table' : 'none';
    }
    /**
     * 
     * @param {string} ip 
     * @param {string} mId 
     * @param {string} sex 
     * @param {string} country 
     */
    _renderMatchDetails(ip, mId, sex, country) {

        (/**@type {HTMLSpanElement} */ (this._root.querySelector("#ip-span"))).innerHTML = ip;
        (/**@type {HTMLSpanElement} */ (this._root.querySelector("#info-span"))).innerHTML = `${sex?.toUpperCase() ?? "?"} - ${country?.toUpperCase()}`;
        this._tpMatchHeaderDiv.innerText = mId
    }
    /**
     * @param {string | number} convoCount
     * @param {Interaction?} lastInteraction
     * @param {string | null} [ipNote]
     */
    _renderPartnerHistory(convoCount, lastInteraction, ipNote) {
        (/**@type {HTMLDivElement} */ (this._root.querySelector("#convoCount-span"))).innerHTML = convoCount?.toString() ?? "never";
        if (convoCount) {
            (/**@type {HTMLDivElement} */ (this._root.querySelector("#convoCount-span"))).style.color = "green"

        }
        else {
            (/**@type {HTMLDivElement} */ (this._root.querySelector("#convoCount-span"))).style.color = ""
        }

        this._ipNoteTextarea.value = ipNote || "";


    }
    _isInLastPageOrEmptyPage() {
        return (this.pages.length === 0) || (this._current == this.pages.length - 1);
    }
    pushPage(match, interactions, ipNote) {
        var shouldAutoPage = this.isAutoPaging && this._isInLastPageOrEmptyPage();
        /**
         * @type {UIPage}
         */
        var newPag = { match: match, interactions: interactions, ipNote: ipNote, dirtyIpNote: null, pageIndex: this.pages.length, disconnectedArg: null }

        this.pages.push(newPag)

        if (shouldAutoPage) {
            this._updateCursor(this._current + 1)
            this.displayPageOfIndex(this._current)
        }
        else {
            this._updateCursor(this._current)
        }
    }
    _refreshPageConnectinStatus() {
        if (!this._getCurrentPage()) {
            this._connectedStatusDiv.innerText = ""
            return;
        }
        this._connectedStatusDiv.innerText = this._getCurrentPage().disconnectedArg ? "Disconnected" : "Connected";
    }
    _updatePageConnectionStatus(matchId, disconnectedArgs) {
        this.pages.forEach(p => {
            if (p.match.matchId === matchId) {
                p.disconnectedArg = disconnectedArgs
            }
        })
        this._refreshPageConnectinStatus()
    }
    displayPageOfIndex(ix) {
        if (ix > (this.pages.length - 1)) {
            console.log("requested to diplay out of range page ", ix, this.pages)
            return;
        }
        var page = this.pages[ix]
        this.clear()
        this.pushMatch(page.match)
        this.pushPartnerHistoryDetail(page.interactions, page.ipNote)

        if (page.dirtyIpNote !== page.ipNote) {
            this._ipNoteTextarea.value = page.dirtyIpNote || ""
        }
        this._updateCursor(ix)
        this._refreshPageConnectinStatus()

    }
    clear() {
        this._clearTableRows();
        this._toggleTableVisibility(false)
    }
    /**
     * 
     * @param {Match} match 
     */
    pushMatch(match) {

        this._renderMatchDetails(match.partnerInfo.ip, match.matchId, match.partnerInfo.sex, match.partnerInfo.country)
    }
    /**
     * 
     * @param {Interaction[]?} interactions 
     * @param {string?} ipNote 
     */
    pushPartnerHistoryDetail(interactions, ipNote) {
        console.log("push history ", interactions)
        this._clearTableRows()
        function truncateString(str) {
            return str.length > 20 ? str.slice(0, 19) + '…' : str;
        }
        if (interactions === null) {
            this._renderPartnerHistory("-", null, null)
            return;
        }
        var latestInteraction = interactions.length == 0 ? null : interactions.sort((a, b) => new Date(a.match.matchedAt).getTime() - new Date(b.match.matchedAt).getTime())[0]
        this._renderPartnerHistory(interactions.length, latestInteraction, ipNote)
        if (interactions && interactions.length) {
            this._toggleTableVisibility(true)
            interactions.forEach(i => {
                var msgCc = i.conversationContent.length
                var dur = getDurationString(i?.match.matchedAt, i?.disconnectedAt);
                var fullMessage = i.conversationContent.length > 0 ? (i.conversationContent[i.conversationContent.length - 1].from + i.conversationContent[i.conversationContent.length - 1].content) : null
                const date = new Date(i?.match.matchedAt);
                const friendlyDateTime = date.toLocaleString('en-GB', {
                    day: '2-digit',
                    month: 'long',
                    year: 'numeric',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                });
                this._addTableRow(friendlyDateTime, getInteractionType(i), msgCc, dur, fullMessage, i)
            })
        }
        else {
            this._toggleTableVisibility(false)
        }
    }
    onCloseConvoClick() {

    }
    /**
     * 
     * @param {string} message 
     */
    pushToast(message) {
        (/**@type {HTMLSpanElement} */ (this._root.querySelector("#toast-status"))).innerText = message;
    }

}

/**
 * set up the TP UI parts and injects them in the html and have the various standard events and TP UI events integrated,
 * this contains most of the app logic 
 */
function initTPUI() {
    if (window["tp_initTPUI_called"]) {
        throw new Error("initTPUI already called")
    }
    window["tp_initTPUI_called"] = true
    var tpRoot = injectUI_impl()
    TPUIComponent.mainPanel = new TPUIComponent(tpRoot)

    var testIp = "yassinMi"
    TPUIComponent.mainPanel.pushMatch({ matchedAt: new Date(), matchId: "yass", partnerInfo: { ip: testIp, country: "ma", sex: "M", id: "111" } })
    TPUIComponent.mainPanel.pushPartnerHistoryDetail([], null)

    window.addEventListener("message", (event) => {
        if (event.source !== window) return;
        if (event.data.type === "TP_MESSAGE") {

        }
        if (event.data.type === "TP_MATCHED") {
            /**
             * @type {Match}
             */
            const m = event.data.data
            console.log("matched recieved,", m)
            var interactions = DBLayer.DB.getInteractionssByIP(m.partnerInfo.ip);
            var ipNote = DBLayer.DB.getIPNote(m.partnerInfo.ip)
            TPUIComponent.mainPanel.pushPage(m, interactions, ipNote)
            DBLayer.verifyReadyForNewMatchEntry()
            DBLayer.matchesBuffer.push({ match: m, matchRaw: null, disconnectArgs: null, scrapedConvo: null })
            DBLayer.latestKnownMatchId = m.matchId
        }
        if (event.data.type === "TP_DISCONNECTED") {
            /**
             * @type {DisconnectedArgs}
             */
            const disconnectedArgs = event.data.data;

            pollUpdateScrapedConvoForLatestMatch()


            var matchEntry
            if (disconnectedArgs.from === null) {
                console.log("handeling diconnection from pushed")
                var latestMatchEntry = DBLayer.latestKnownMatchId === null ? null : DBLayer.getMatchFromMatchBuffer(DBLayer.latestKnownMatchId)
                if (!latestMatchEntry) {
                    throw new Error("a pushed disconnected without existing current match entry")
                }
                if (latestMatchEntry.match.partnerInfo.id !== disconnectedArgs.to) {
                    throw new Error("a pushed disconnected without existing current match entry matching the destinated partner")
                }
                matchEntry = latestMatchEntry;
            }
            else {
                console.log("handeling diconnection from user")
                matchEntry = DBLayer.getMatchFromMatchBuffer(disconnectedArgs.matchId)
                if (matchEntry === undefined) { throw new Error("cannot find match with id form disconnected args") }
            }

            if (matchEntry.disconnectArgs) {
                if (matchEntry.disconnectArgs.to !== "")
                    throw new Error("the match entry is already disconnected properly")
                else {
                    throw new Error("the match entry is already disconnected implecitly")

                }
            }
            matchEntry.disconnectArgs = disconnectedArgs;
            console.log("marked match entry as disconnected")
            TPUIComponent.mainPanel.pushToast(`disconnected from ${disconnectedArgs.matchId}`);
            /**
             * @type {"Stranger"|"You"}
             */
            var disconnectedBy;
            if (matchEntry.match.partnerInfo.id == disconnectedArgs.from) {
                disconnectedBy = "Stranger"
            }
            else if (matchEntry.match.partnerInfo.id === disconnectedArgs.to) {
                disconnectedBy = "You"

            }
            else {
                throw new Error("disconnected args from and to do not match partner id")
            }

            console.log(`the disconnected match entry had scraped convo of ${matchEntry.scrapedConvo?.length}`)
            /**
             * @type {Interaction}
             */
            var interaction = { conversationContent: matchEntry.scrapedConvo || [], disconnectedBy: disconnectedBy, match: matchEntry.match, disconnectedAt: disconnectedArgs.disconnectedAt }
            DBLayer.DB.addInteraction(interaction)
            TPUIComponent.mainPanel.notifyInteractionAdded(interaction)
            TPUIComponent.mainPanel._updatePageConnectionStatus(interaction.match.matchId, matchEntry.disconnectArgs)

        }
    });

}

(function () {
    console.log("tp script started")
    var initialized = false
    hookEvents();
    window.addEventListener("message", (event) => {
        if (initialized) return;
        if (event.source !== window) return;

        if (event.data.type === "TP_INIT") {
            initialized = true


            initTPUI();
            var pollingScrap = setInterval(() => {
                pollUpdateScrapedConvoForLatestMatch()
            }, 10000);
        }
    })

})();

class AnalyticsHelper {
    static function(params) {

    }
}

function pollUpdateScrapedConvoForLatestMatch() {
    try {
        if (DBLayer.latestKnownMatchId) {
            DBLayer.DB.updateScrapedConvoForMatchId(DBLayer.latestKnownMatchId, scrapeConversationContent())
        }
    }
    catch (e) {
        console.error(e);
    }

}
function getDurationString(startDate, endDate) {
    let diffMs = Math.abs(new Date(endDate).getTime() - new Date(startDate).getTime());

    let seconds = Math.floor(diffMs / 1000) % 60;
    let minutes = Math.floor(diffMs / (1000 * 60)) % 60;
    let hours = Math.floor(diffMs / (1000 * 60 * 60));

    return `${hours}h ${minutes}m ${seconds}s`;
}

/**
 * Returns a colored label with interaction type and who skipped.
 * @param {Interaction} interaction 
 */
function getInteractionType(interaction) {
    const whoSkipped = interaction.disconnectedBy === "Stranger" ? "they skipped" : "you skipped";
    const msgCount = interaction.conversationContent.length;

    if (msgCount === 0) {
        return `<span style="color:crimson;">empty — ${whoSkipped}</span>`;
    }

    if (msgCount < 10) {
        const color = interaction.disconnectedBy === "Stranger" ? "orangered" : "tomato";
        return `<span style="color:${color};">short — ${whoSkipped}</span>`;
    }

    const color = interaction.disconnectedBy === "Stranger" ? "mediumseagreen" : "limegreen";
    return `<span style="color:${color};">meaningful — ${whoSkipped}</span>`;
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址