ThundrPlus

avoid bots + more features

As of 10.10.2025. See ბოლო ვერსია.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ThundrPlus
// @namespace    http://tampermonkey.net/
// @version      2025-10-10
// @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        GM_getValue
// @grant        GM_setValue
// @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


    }
    /**
     * 
     * @param {string} id 
     * @returns 
     */
    getInteractionssByID(id) {
        var oldData = window.localStorage.getItem("convosOfId_" + id)
        var parsedOldData = JSON.parse(oldData ?? "[]")
        return parsedOldData;
    }
    /**
     * 
     * @param {string} id 
     * return boolean
     */
    getIsIdPresentInLegacyHistory(id){
        //returns true if the id string appeans in any way in the local strorage
        return JSON.stringify(window.localStorage).includes(id)
    }
    /**
     * 
     * @param {Interaction} interaction 
     */
    addInteraction(interaction) {
        console.log("DB: saving interaction for id:", interaction)
        var oldData = window.localStorage.getItem("convosOfId_" + interaction.match.partnerInfo.id)
        var parsedOldData = JSON.parse(oldData ?? "[]")
        parsedOldData.push(interaction)
        window.localStorage.setItem("convosOfId_" + interaction.match.partnerInfo.id, JSON.stringify(parsedOldData))
        // @ts-ignore
        GM_setValue("convosOfId_" + interaction.match.partnerInfo.id, 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)
        // @ts-ignore
        GM_setValue("noteOfIp_" + ip, note);
    }
    /**
     * 
     * @param {string} ip 
     */
    getIPNote(ip) {
        return window.localStorage.getItem("noteOfIp_" + ip)
    }
    /**
     * 
     * @param {string} id 
     * @param {string} note 
     */
    storeIDNote(id, note) {
        console.log("DB: saving note:", note, id)
        window.localStorage.setItem("noteOfId_" + id, note)
        // @ts-ignore
        GM_setValue("noteOfId_" + id, note);
    }
    /**
     * 
     * @param {string} id 
     */
    getIDNote(id) {
        return window.localStorage.getItem("noteOfId_" + id)
    }
}

/**
 * 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-has-legacy-history-indicator {
  display: none; /* hidden by default */
}

#tp-has-legacy-history-indicator.hasHistory {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: #28a745; /* green text */
  font-weight: 600;
  font-family: sans-serif;
}

#tp-has-legacy-history-indicator.hasHistory::before {
  content: "";
  width: 8px;
  height: 8px;
  background-color: #28a745;
  border-radius: 50%;
  display: inline-block;
}

  .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 id="tp-has-legacy-history-indicator" >history</div>

    
  </div>
</div>
      <strong>ip:</strong> <span id="ip-span">-</span><br>
      <strong>id:</strong> <span id="id-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>
     <div style="display:flex; flex-direction:row; align-items:center; ">
        <textarea rows="2" cols="50" placeholder="Write id notes..." id="tp-idnote-textarea"></textarea>
        <button type="button" class="chakra-button css-18tsh74" id="tp-save-idnote-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[contains(@class,'css-1qs2dh2')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue);
    if(!chatRootDiv) throw new Error("cannot find chatRootDiv, selector may need to be updated")
    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 {
    /**
     * 
     * @param {PartnerInfo} partnerInfo
     */
    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, p.idNote, p.hasLegacyHistory);
                }
            }
        })
    }
    /**
     * @typedef {{match:Match,interactions:Interaction[], ipNote:string?,idNote:string?,dirtyIpNote:string?,dirtyIdNote:string?,pageIndex:number,disconnectedArg:DisconnectedArgs?, hasLegacyHistory:boolean}} 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._idNoteTextarea = (/**@type {HTMLTextAreaElement} */ (this._root.querySelector("#tp-idnote-textarea")))
        this._historyTable = (/**@type {HTMLTextAreaElement} */ (this._root.querySelector('#history-table')))
        this._hasLegacyHistoryIndicator = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-has-legacy-history-indicator")))

        this._saveIpnoteBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-save-ipnote-btn")))
        this._saveIdnoteBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-save-idnote-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._saveIdnoteBtn.addEventListener("click", this._onSaveIdnoteClick.bind(this))
        this._toggleConvoPopupVisibilityBtns.forEach(b => {
            b.addEventListener("click", this._onToggleConvoPopupClick.bind(this))
        })
        this._ipNoteTextarea.addEventListener("input", this._onIpNoteInputChange.bind(this));
        this._idNoteTextarea.addEventListener("input", this._onIdNoteInputChange.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%"
        })
        this._current = 0;
    }

    /**
     * 
     * @param {boolean} open 
     */
    _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")

    }
    _onSaveIdnoteClick() {
        //save to db and update dirty note and note in the page
        if (!this._getCurrentPage()) { return }
        console.log("saving idnote", this._getCurrentPage().dirtyIdNote)
        DBLayer.DB.storeIDNote(this._getCurrentPage().match.partnerInfo.id, this._getCurrentPage()?.dirtyIdNote || "");
        this._getCurrentPage().idNote = this._getCurrentPage().dirtyIdNote;
        this._refreshButtonsEnabledState()
        console.log("saved idnote", this._getCurrentPage().dirtyIdNote)
        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)
    }
    _onIdNoteInputChange() {
        if (!this._getCurrentPage()) {
            return;


        }
        var newNote = this._idNoteTextarea.value
        this.pages[this._current].dirtyIdNote = newNote;

        this._refreshButtonsEnabledState()
        console.log("change detected", this._getCurrentPage()?.dirtyIdNote)
    }

    /**
     * 
     * @param {number} current 
     */
    _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
    }
    _canClickSaveIdnote() {
        if (!this._getCurrentPage()) {
            return false;
        }
        return this._getCurrentPage() && this._getCurrentPage().idNote !== this._getCurrentPage().dirtyIdNote
    }
    /**
     * 
     * @param {Event} ev
     */
    _onPrevClick(ev) {
        ev.stopPropagation();
        if (!this._canClickPrev()) return;
        this.displayPageOfIndex(this._current - 1)
    }
    /**
     * 
     * @param {Event} ev
     */
    _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);
        }
    }

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

        (/**@type {HTMLSpanElement} */ (this._root.querySelector("#ip-span"))).innerHTML = ip;
        (/**@type {HTMLSpanElement} */ (this._root.querySelector("#id-span"))).innerHTML = id;
        (/**@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
     * @param {string | null} idNote
     * @param {boolean} hasLegacyHistory
     */
    _renderPartnerHistory(convoCount, lastInteraction, ipNote, idNote, hasLegacyHistory) {
        (/**@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 || "";
        this._idNoteTextarea.value = idNote || "";
        this._hasLegacyHistoryIndicator.classList.toggle('hasHistory', hasLegacyHistory);


    }
    _isInLastPageOrEmptyPage() {
        return (this.pages.length === 0) || (this._current == this.pages.length - 1);
    }
    /**
     * 
     * @param {Match} match 
     * @param {Interaction[]} interactions 
     * @param {string?} ipNote 
     * @param {string?} idNote
     * @param {boolean} hasLegacyHistory
     */
    pushPage(match, interactions, ipNote, idNote, hasLegacyHistory) {
        var shouldAutoPage = this.isAutoPaging && this._isInLastPageOrEmptyPage();
        /**
         * @type {UIPage}
         */
        var newPag = { match: match, interactions: interactions, ipNote: ipNote, idNote: idNote, dirtyIpNote: null, dirtyIdNote: null, pageIndex: this.pages.length, disconnectedArg: null, hasLegacyHistory: hasLegacyHistory }

        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";
    }
    /**
     * 
     * @param {string} matchId 
     * @param {DisconnectedArgs} disconnectedArgs 
     */
    _updatePageConnectionStatus(matchId, disconnectedArgs) {
        this.pages.forEach(p => {
            if (p.match.matchId === matchId) {
                p.disconnectedArg = disconnectedArgs
            }
        })
        this._refreshPageConnectinStatus()
    }
    /**
     * 
     * @param {number} ix 
     */
    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, page.idNote, page.hasLegacyHistory)

        if (page.dirtyIpNote !== page.ipNote) {
            this._ipNoteTextarea.value = page.dirtyIpNote || ""
        }
        if (page.dirtyIdNote !== page.idNote) {
            this._idNoteTextarea.value = page.dirtyIdNote || ""
        }
        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.id, match.partnerInfo.sex, match.partnerInfo.country)
    }
    /**
     * 
     * @param {Interaction[]?} interactions 
     * @param {string?} ipNote 
     * @param {string?} idNote
     * @param {boolean} hasLegacyHistory
     */
    pushPartnerHistoryDetail(interactions, ipNote, idNote, hasLegacyHistory) {
        console.log("push history ", interactions)
        this._clearTableRows()
        /**
         * 
         * @param {string} str 
         */
        function truncateString(str) {
            return str.length > 20 ? str.slice(0, 19) + '…' : str;
        }
        if (interactions === null) {
            this._renderPartnerHistory("-", null, null, null, false)
            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, idNote, hasLegacyHistory)
        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, null, false)

    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.getInteractionssByID(m.partnerInfo.id);
            var hasHistory = DBLayer.DB.getIsIdPresentInLegacyHistory(m.partnerInfo.id)
            var idNote = DBLayer.DB.getIDNote(m.partnerInfo.id)
            var ipNote = DBLayer.DB.getIPNote(m.partnerInfo.ip)
            TPUIComponent.mainPanel.pushPage(m, interactions, ipNote, idNote, hasHistory)
            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 {
    
}

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

}
/**
 *
 * @param {string} startDate
 * @param {string} endDate
 * @returns
 */
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>`;
}