// ==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>`;
}