Untitled
Anonymous
Plain Text
09/30/2026 2:56 PM
128.5 KB
11
No Index
Never
// ==UserScript==
// @name TopicLive+ Reworked TEST
// @namespace TopicLive+JVC
// @description Affiche les nouveaux messages d'un topic en direct.
// @author StrangerFruit, Meshuggah_93, khhyrt2, sur une base de : moyaona, lantea/atlantis, kiwec
// @match https://www.jeuxvideo.com/forums/*
// @run-at document-end
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @icon https://image.noelshack.com/fichiers/2026/25/5/1781893261-logo.png
// @version 1.0.3
// @grant GM_xmlhttpRequest
// @connect raw.githubusercontent.com
// @noframes
// ==/UserScript==
/**
* Représente une page de topic et gère l'analyse du DOM pour en extraire les messages.
*/
async function extractPayloadGzip() {
const scripts = document.getElementsByTagName('script');
for (let s of scripts) {
const content = s.textContent || '';
if (content.includes('forumsAppPayload')) {
const match = content.match(/forumsAppPayload\s*=\s*["']?([^"']+)["']?/);
if (match && match[1]) {
try {
const binaryString = atob(match[1]);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
const decompressed = await new Response(stream).text();
return JSON.parse(decompressed);
} catch(e) {
return null;
}
}
}
}
return null;
}
class Page {
constructor($page) {
this.$page = $page;
}
obtenirMessages() {
const msgs = [];
this.trouver(`${TL.class_msg}:not(.msg-pseudo-blacklist)`).each(function() {
msgs.push(new Message($(this)));
});
return msgs;
}
maj() {
if (TL.options.sound) {
try {
TL.son.play();
} catch (err) {
console.error(`[TopicLive+] Erreur son : ${err}`);
}
}
try {
if (!TL.ongletActif) {
TL.updateCounters();
}
} catch (err) {
console.error(`[TopicLive+] Erreur favicon (maj) : ${err}`);
}
try {
this.Transformation();
} catch (err) {
console.error(`[TopicLive+] Erreur jsli.Transformation() : ${err}`);
}
const nb_messages = $(`${TL.class_msg}:not(.msg-pseudo-blacklist)`).size();
if (nb_messages > 100) {
let messagesASupprimer = nb_messages - 100;
if (messagesASupprimer % 2 !== 0) messagesASupprimer++;
$(`${TL.class_msg}:not(.msg-pseudo-blacklist)`).slice(0, messagesASupprimer).remove();
}
dispatchEvent(new CustomEvent('topiclive:doneprocessing', {
'detail': { jvcake: TL.jvCake }
}));
}
performScroll() {
const $firstUnreadMessage = TL.unreadMessageAnchors[0];
if (!$firstUnreadMessage || $firstUnreadMessage.length === 0) return;
const targetScrollTop = $firstUnreadMessage.offset().top - 100;
$('html, body').animate({ scrollTop: targetScrollTop }, 800);
}
scan() {
TL.ajaxTs = this.trouver('#ajax_timestamp_liste_messages').val();
TL.ajaxHash = this.trouver('#ajax_hash_liste_messages').val();
const connectesText = $('.userCount').filter(function() {
return $(this).text().includes('connecté');
}).find('.userCount__number').text().trim();
if (connectesText && TL.$tl_connected_counter) {
TL.$tl_connected_counter.text(connectesText.split(' ')[0]);
}
const nbConnectes = this.trouver('.nb-connect-fofo').text().trim();
if (nbConnectes.length) {
$('.nb-connect-fofo').text(nbConnectes);
}
const isTextareaFocused = $(TL.formu.obtenirMessage()).is(':focus');
let distanceFromBottom;
if (isTextareaFocused) {
distanceFromBottom = document.documentElement.scrollHeight - $(window).scrollTop();
}
if ($(TL.class_msg).length === 0) {
TL.majUrl(this);
TL.loop();
return;
}
let messages_a_afficher = [];
const nvMsgs = this.obtenirMessages();
// ── NOUVEAU : détection dernière page avec nouvelle pagination React ──
const isOnLastPage = TL.estSurDernierePage;
try {
for (let nvMsg of nvMsgs) {
let nv = true;
for (let ancienMsg of TL.messages) {
if (ancienMsg.id_message == nvMsg.id_message) {
nv = false;
ancienMsg.update(nvMsg);
break;
}
}
if (nv && isOnLastPage) {
TL.messages.push(nvMsg);
TL.nvxMessages++;
// Le nouveau message doit rester dans #listMessages
$('#listMessages').append(nvMsg.$message);
nvMsg.$message.hide();
nvMsg.fixAvatar();
nvMsg.fixBlacklist();
setTimeout(() => { nvMsg.fixCitation(TL.ajaxTs, TL.ajaxHash); }, 1500);
nvMsg.initPartialQuote();
nvMsg.fixDeroulerCitation();
// nvMsg.fixImages();
const entry = {
message: nvMsg,
cancelled: false
};
messages_a_afficher.push(entry);
dispatchEvent(new CustomEvent('topiclive:newmessage', {
detail: {
id: nvMsg.id_message,
jvcake: TL.jvCake,
cancel: () => {
entry.cancelled = true;
}
}
}));
}
}
} catch (err) {
console.error(`[TopicLive+] Erreur nouveaux messages : ${err}`);
}
TL.majUrl(this);
if (messages_a_afficher.length > 0) {
setTimeout(() => {
let maj = false;
let $firstNewMessageToShow = null;
for (let msg of messages_a_afficher) {
if (msg.cancelled) {
TL.nvxMessages--;
} else {
if (!$firstNewMessageToShow) $firstNewMessageToShow = msg.message.$message;
msg.message.$message.show();
msg.message.fixImages();
TL.addUnreadAnchor(msg.message.$message);
maj = true;
}
}
if (isTextareaFocused) {
const newScrollTop = document.documentElement.scrollHeight - distanceFromBottom;
$(window).scrollTop(newScrollTop);
}
if (maj) {
this.maj();
if (TL.justPostedMessageId) {
const messageSelector = `${TL.class_msg}[id="message-${TL.justPostedMessageId}"]`;
const $myNewMessage = $(messageSelector);
if ($myNewMessage.length > 0) {
TL.isChatModeActive = true;
const targetScrollTop = $myNewMessage.offset().top - 100;
$('html, body').stop().animate({ scrollTop: targetScrollTop }, 800);
TL.updateCounters();
}
TL.justPostedMessageId = null;
} else if (TL.isChatModeActive && !isTextareaFocused) {
if ($firstNewMessageToShow) {
const targetScrollTop = $firstNewMessageToShow.offset().top - 100;
$('html, body').animate({ scrollTop: targetScrollTop }, 800);
}
} else {
TL.updateCounters();
}
}
}, 1000);
}
TL.loop();
}
Transformation() {
$('.JvCare').each(function() {
const $span = $(this);
let classes = $span.attr('class');
const href = TL.jvCake(classes);
classes = classes.split(' ');
const index = classes.indexOf('JvCare');
classes.splice(index, 2);
classes.unshift('xXx');
classes = classes.join(' ');
$span.replaceWith(`<a href="${href}" class="${classes}" target="_blank" rel="noopener noreferrer">${$span.html()}</a>`);
});
$('.user-avatar-msg').each(function() {
const $elem = $(this);
const newsrc = $elem.attr('data-srcset');
if (newsrc != 'undefined') {
$elem.attr('src', newsrc);
$elem.removeAttr('data-srcset');
}
});
}
trouver(chose) {
return this.$page.find(chose);
}
}
/**
* Représente un message unique du forum.
*/
class Message {
constructor($message) {
if (TL.estMP) {
this.id_message = 'MP';
} else {
// ── NOUVEAU : l'ID est dans l'attribut id="message-XXXX" ──
const rawId = $message.attr('id') || '';
this.id_message = parseInt(rawId.replace('message-', ''), 10);
}
// ── NOUVEAU : nouveaux sélecteurs React ──
this.date = $('.messageUser__date', $message).text().replace(/[\r\n]|#[0-9]+$/g, '');
this.edition = $message.find('.info-edition-msg').text();
this.$message = $message;
// Sauvegarde du contenu original pour pouvoir restaurer le message
this.originalHtml = this.$message.find('.messageUser__main').html();
this.isDeleted = false;
this.pseudo = $('.messageUser__label', $message).text().replace(/[\r\n]/g, '');
this.supprime = false;
}
fixAvatar() {
let avatar = this.trouver('.user-avatar-msg, .avatar__image');
avatar.attr('src', avatar.data('src') || avatar.attr('src'));
}
setDeleted(data) {
this.isDeleted = true;
this.deletedData = data;
this.$message.addClass('messageUser--deleted');
const $btn = this.$message.find('.tl-delete-btn');
$btn
.removeClass('tl-delete-btn')
.addClass('tl-restore-btn')
.attr('title', 'Restaurer le message')
.find('.messageUser__actionLabel')
.text('Restaurer');
$btn.find('i')
.removeClass('icon-trash')
.addClass('icon-topic-restore');
const restoreUrl = data.actions?.restore?.url;
if (restoreUrl) {
this.$message.find('.tl-restore-btn')
.off('click')
.on('click', async () => {
try {
const res = await fetch(restoreUrl, {
method: 'POST',
credentials: 'include',
headers: {
'x-requested-with': 'XMLHttpRequest'
}
});
const json = await res.json();
if (!json.success) return;
TL.showToast(json.success[0]);
this.restore();
} catch (e) {
console.error(e);
}
});
}
}
restore() {
this.isDeleted = false;
this.$message.removeClass('messageUser--deleted');
const $btn = this.$message.find('.tl-restore-btn');
$btn
.removeClass('tl-restore-btn')
.addClass('tl-delete-btn')
.attr('title', 'Supprimer le message')
.find('.messageUser__actionLabel').text('Supprimer');
$btn.find('i')
.removeClass('icon-topic-restore')
.addClass('icon-trash');
}
fixBlacklist() {
this.trouver('.bloc-options-msg > .picto-msg-tronche, .msg-pseudo-blacklist .btn-blacklist-cancel').on('click', () => {
$.ajax({
url: '/forums/ajax_forum_blacklist.php',
data: {
id_alias_msg: this.$message.attr('data-id-alias'),
action: this.$message.attr('data-action'),
ajax_hash: $('#ajax_hash_preference_user').val()
},
dataType: 'json',
success: ({ erreur }) => {
if (erreur && erreur.length) {
TL.alert(erreur);
} else {
document.location.reload();
}
}
});
});
}
fixCitation(timestamp, hash) {
if (this.$message.find('.messageUser__action[title="Citer le message"]').length === 0) {
this.buildActionButtons();
}
this.$message.find('.messageUser__action[title="Citer le message"]').off('click').on('click', () => {
const $msg = TL.formu.obtenirMessage();
const datePropre = this.date.trim().replace(/\s+/g, ' ');
const pseudoPropre = this.pseudo.trim().replace(/\s+/g, ' ');
const texteSource = TL.messagesTextMap ? TL.messagesTextMap[this.id_message] : undefined;
let nvmsg;
if (texteSource !== undefined) {
// On a le vrai texte source (avec ses > d'origine) : on ajoute juste un niveau de citation
nvmsg = `> Le ${datePropre} ${pseudoPropre} a écrit :\n> ${texteSource.split('\n').join('\n> ')}\n\n`;
} else {
// Fallback : ancienne méthode si le texte source n'est pas encore disponible
const contentNode = this.trouver(TL.class_contenu)[0];
const txt = contentNode ? contentNode.innerText.trim() : '';
nvmsg = `> Le ${datePropre} ${pseudoPropre} a écrit :\n>${txt.split('\n').join('\n> ')}\n\n`;
}
if ($msg[0].value === '') {
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value").set.call($msg[0], `${nvmsg}\n`);
} else {
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value").set.call($msg[0], `${$msg[0].value}\n\n${nvmsg}`);
}
$msg[0].dispatchEvent(new Event("input", { bubbles: true }));
location.hash = '#forums-post-message-editor';
setTimeout(() => { $msg[0].focus(); }, 50);
});
}
buildActionButtons() {
const actions = TL.messagesActionsMap ? TL.messagesActionsMap[this.id_message] : null;
const pseudoPropre = this.pseudo.trim().replace(/\s+/g, ' ');
const pmUrl = actions?.privateMessage?.url || `https://www.jeuxvideo.com/messages-prives/nouveau.php?all_dest=${encodeURIComponent(pseudoPropre)}`;
const blacklistUrl = actions?.blacklist?.url || null;
const reportUrl = actions?.report?.url || null;
const kickUrl = TL.isModerator ? actions?.kick?.url : null;
const deleteUrl = TL.isModerator ? actions?.delete?.url : null;
const reportDirect = TL.isModerator ? reportUrl : null;
const innerHtml = `
<div class="tl-inline-actions" style="display:flex; align-items:center; gap:8px;">
<button type="button" class="messageUser__action tl-quote-btn" title="Citer le message">
<i class="messageUser__actionIcon icon-quotes"></i>
<span class="messageUser__actionLabel">Citer le message</span>
</button>
${kickUrl ? `<button type="button" class="messageUser__action tl-kick-btn" title="Kicker l'alias">
<i class="messageUser__actionIcon icon-kick"></i>
<span class="messageUser__actionLabel">Kicker l'alias</span>
</button>` : ''}
${reportDirect ? `
<button type="button" class="messageUser__action tl-report-btn" title="Faire un signalement">
<i class="messageUser__actionIcon icon-signaler"></i>
<span class="messageUser__actionLabel">Faire un signalement</span>
</button>
` : ''}
${deleteUrl ? `<button type="button" class="messageUser__action tl-delete-btn" title="Supprimer le message">
<i class="messageUser__actionIcon icon-trash"></i>
<span class="messageUser__actionLabel">Supprimer</span>
</button>` : ''}
</div>
<div class="messageUser__moreContainer">
<button class="messageUser__moreButton"
type="button"
aria-label="Plus d'actions">
<i class="messageUser__actionIcon icon-more"></i>
</button>
<div class="messageUser__modalContainer">
<div class="messageUser__fills">
${reportUrl && !TL.isModerator ? `
<button class="messageUser__action tl-report-btn"
type="button"
title="Faire un signalement">
<i class="messageUser__actionIcon icon-signaler"></i>
<span class="messageUser__actionLabel messageUser__actionLabel--showLabel">
Faire un signalement
</span>
</button>
` : ''}
${blacklistUrl ? `
<button class="messageUser__action tl-blacklist-btn"
type="button"
title="Blacklister">
<i class="messageUser__actionIcon icon-black-list"></i>
<span class="messageUser__actionLabel messageUser__actionLabel--showLabel">
Blacklister
</span>
</button>
` : ''}
<a href="${pmUrl}"
target="_blank"
rel="noopener noreferrer"
class="messageUser__action"
title="Envoyer un message privé">
<i class="messageUser__actionIcon icon-pm"></i>
<span class="messageUser__actionLabel messageUser__actionLabel--showLabel">
Envoyer un message privé
</span>
</a>
<button type="button" class="messageUser__cancel">
Annuler
</button>
</div>
</div>
<div class="messageUser__overlay" aria-hidden="true"></div>
</div>
${TL.isModerator ? `
<input
type="checkbox"
class="checkboxButton messageUser__checkbox tl-checkbox">
` : ''}`;
let $headerActions = this.$message.find('.messageUser__headerActions');
if ($headerActions.length === 0) {
const $header = this.$message.find('.messageUser__header');
if ($header.length > 0) {
$header.append('<div class="messageUser__headerActions js-message-user-actions"></div>');
$headerActions = this.$message.find('.messageUser__headerActions');
}
}
if ($headerActions.length > 0 && $headerActions.find('.tl-quote-btn').length === 0) {
$headerActions.html(innerHtml);
const $checkbox = $headerActions.find('.tl-checkbox');
if ($checkbox.length) {
$checkbox.data('message', this);
$checkbox.off('change').on('change', () => {
TL.updateSelectionBar();
});
}
const $wrap = $headerActions.find('.messageUser__moreContainer');
const $moreButton = $wrap.find('.messageUser__moreButton');
const $menu = $wrap.find('.messageUser__modalContainer');
const $overlay = $wrap.find('.messageUser__overlay');
const openModal = () => {
$menu.addClass('messageUser__modalContainer--isOpen');
};
const closeModal = () => {
$menu.removeClass('messageUser__modalContainer--isOpen');
};
$moreButton.off('click').on('click', (e) => {
e.preventDefault();
e.stopPropagation();
if ($menu.hasClass('messageUser__modalContainer--isOpen')) {
closeModal();
} else {
openModal();
}
});
$overlay.off('click').on('click', closeModal);
$(document)
.off(`click.tlmenu-${this.id_message}`)
.on(`click.tlmenu-${this.id_message}`, (e) => {
if (!$(e.target).closest($wrap).length) {
closeModal();
}
});
$wrap.find('.messageUser__cancel').off('click').on('click', closeModal);
if (kickUrl) {
$headerActions.find('.tl-kick-btn').off('click').on('click', () => {
const ajaxHash = (blacklistUrl || reportUrl || '').match(/ajax_hash=([^&]+)/)?.[1] || TL.ajaxHash;
const url = `https://www.jeuxvideo.com/forums/author/kick?message_id=${this.id_message}&forum_id=${TL.currentForumId}&ajax_hash=${ajaxHash}`;
this.openKickForm(url);
});
}
if (reportUrl) {
this.$message.find('.tl-report-btn')
.off('click')
.on('click', () => {
closeModal();
this.openReportForm(reportUrl);
});
}
if (deleteUrl) {
$headerActions.find('.tl-delete-btn').off('click').on('click', async () => {
try {
const res = await fetch(deleteUrl, {
method: 'POST',
credentials: 'include',
headers: {
'x-requested-with': 'XMLHttpRequest'
}
});
const json = await res.json();
if (!json.success) return;
TL.showToast(json.success[0]);
const response = await fetch(
`https://www.jeuxvideo.com/forums/list_messages/?permalink=1&ids=${this.id_message}`,
{
credentials: 'include'
}
);
const data = await response.json();
if (!data.success || !data.items?.length) return;
this.setDeleted(data.items[0]);
} catch (e) {
console.error(e);
}
});
}
if (blacklistUrl) {
$wrap.find('.tl-blacklist-btn').off('click').on('click', () => {
const separator = blacklistUrl.includes('?') ? '&' : '?';
const fullUrl = blacklistUrl.includes('action=') ? blacklistUrl : `${blacklistUrl}${separator}action=add`;
const $btn = $wrap.find('.tl-blacklist-btn');
$btn
.prop('disabled', true)
.attr('aria-busy', 'true');
if (!$btn.find('.messageUser__loader').length) {
$btn.prepend('<span class="loader messageUser__loader"></span>');
}
fetch(fullUrl, {
method: 'POST',
credentials: 'include',
headers: {
'x-requested-with': 'XMLHttpRequest'
}
})
.then(r => r.json())
.then(data => {
closeModal();
if (data && data.success) {
const aliasId = blacklistUrl.match(/alias_id=(\d+)/)?.[1];
const ajaxHashMatch = blacklistUrl.match(/ajax_hash=([^&]+)/)?.[1];
const removeUrl = `https://www.jeuxvideo.com/forums/author/blacklist?alias_id=${aliasId}&ajax_hash=${ajaxHashMatch}&action=delete`;
const $card = this.$message.find('.messageUser__card');
const $header = $card.find('.messageUser__header');
const $main = $card.find('.messageUser__main');
$header.hide();
$main.hide();
$card.append(`
<div class="tl-blacklist-box">
<div class="messageUser__header">
<div class="messageUser__profil">
<div class="messageUser__label">Auteur blacklisté</div>
<div class="messageUser__date">${this.date.trim()}</div>
</div>
</div>
<div class="messageUser__main">
<span class="messageUser__blacklistedText">
Ce pseudo figure dans votre blacklist
</span>
<button type="button"
class="messageUser__link tl-view-blacklisted-btn">
Voir le message
</button>
|
<button type="button"
class="messageUser__link tl-unblacklist-btn">
Retirer de la blacklist
</button>
</div>
</div>
`);
this.$message.addClass('messageUser--blacklisted');
this.$message.find('.tl-view-blacklisted-btn').on('click', () => {
$card.find('.tl-blacklist-box').remove();
$header.show();
$main.show();
this.$message.removeClass('messageUser--blacklisted');
});
this.$message.find('.tl-unblacklist-btn').on('click', () => {
const $removeBtn = this.$message.find('.tl-unblacklist-btn');
$removeBtn.prop('disabled', true);
if (!$removeBtn.find('.messageUser__loaderInline').length) {
$removeBtn.prepend('<span class="loader messageUser__loaderInline"></span>');
}
fetch(removeUrl, {
method: 'POST',
credentials: 'include',
headers: {
'x-requested-with': 'XMLHttpRequest'
}
})
.then(r => r.json())
.then(data => {
if (data && data.success) {
$card.find('.tl-blacklist-box').remove();
$header.show();
$main.show();
this.$message.removeClass('messageUser--blacklisted');
}
})
.catch(err => {
console.error('[TopicLive+] Erreur retrait blacklist:', err);
})
.finally(() => {
$removeBtn.find('.messageUser__loaderInline').remove();
$removeBtn.prop('disabled', false);
});
});
}
})
.catch(err => {
closeModal();
console.error('[TopicLive+] Erreur blacklist:', err);
})
.finally(() => {
$btn.find('.messageUser__loader').remove();
$btn
.prop('disabled', false)
.removeAttr('aria-busy');
});
});
}
}
}
openReportForm(reportUrl) {
fetch(reportUrl, { credentials: 'include' })
.then(r => r.json())
.then(data => {
if (!data || !data.reasons) return;
let optionsHtml = '';
const pseudoMessage = this.pseudo.trim();
for (const category in data.reasons) {
optionsHtml += `<optgroup label="${category}">`;
for (const reason of data.reasons[category]) {
optionsHtml += `<option value="${reason.id}" data-label="${reason.label}">${reason.label}</option>`;
}
optionsHtml += `</optgroup>`;
}
const $overlay = $('<div class="tl-report-overlay" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:999999;display:flex;align-items:center;justify-content:center;"></div>');
const $modal = $('<div class="modalWrapper__main" style="background:rgb(39,42,48);color:rgb(242,242,242);font-size:15px;padding:20px;border-radius:12px;width:440px;max-width:90vw;box-shadow:0 8px 32px rgba(0,0,0,0.5);"><div class="modalWrapper__header" style="background:transparent;padding:0 0 20px;margin:0 0 20px;display:flex;justify-content:space-between;align-items:center;"><div class="modalWrapper__title" style="font-size:17px;font-weight:700;text-transform:uppercase;">Faire un signalement</div><button class="tl-report-close" style="background:transparent;border:none;color:rgb(242,242,242);font-size:18px;cursor:pointer;"><span class="icon-close"></span></button></div><div class="modalWrapper__content"><form action="#" class="report"><div class="report__field" style="margin:0 0 15px;"><label class="report__label" style="font-weight:700;display:inline;">Pseudo :</label><span class="report__value" style="color:rgb(61,135,245);font-weight:700;margin-left:10px;">' + pseudoMessage + '</span></div><div class="report__field" style="margin:0 0 15px;"><label for="motif" class="report__label" style="font-weight:700;display:block;">Motif :</label><select name="motif" id="motif" class="report__select tl-report-motif" style="background:rgb(48,50,54);color:rgb(242,242,242);padding:7px 12px;margin:5px 0 0;border-radius:12px;border:1px solid rgb(99,101,105);width:100%;font-size:15px;"><option value="">Sélectionnez un motif</option>' + optionsHtml + '</select></div><div class="report__field" style="margin:0 0 15px;"><label for="reason" class="report__label" style="font-weight:700;display:block;">Remarques :</label><textarea name="reason" id="reason" rows="4" placeholder="Merci de saisir les remarques" class="report__textarea tl-report-remarque" style="background:rgb(48,50,54);color:rgb(242,242,242);padding:7px 12px;margin:5px 0 0;border-radius:12px;border:1px solid rgb(99,101,105);width:100%;font-size:15px;resize:vertical;"></textarea></div><div class="report__field" style="margin:0 0 15px;"><div class="report__description" style="color:rgb(158,158,158);font-size:13px;margin:5px 0 15px;">En cliquant sur le bouton d\'envoi, je déclare penser de bonne foi que les informations et allégations que ma notification contient sont exactes et complètes.</div></div><div class="tl-report-msg" style="margin-bottom:12px;font-size:13px;"></div><div class="report__actions" style="display:flex;justify-content:center;"><button class="report__submit tl-report-submit" type="button" style="background:rgb(61,135,245);color:rgb(0,0,0);padding:8px 20px;border-radius:18px;border:1px solid rgb(61,135,245);font-size:15px;cursor:pointer;">Valider</button></div></form></div></div>');
$overlay.append($modal);
$('body').append($overlay);
$modal.css({ 'opacity': '1', 'visibility': 'visible' });
$overlay.find('.tl-report-close').on('click', () => $overlay.remove());
$overlay.on('click', (e) => {
if (e.target === $overlay[0]) {
$overlay.remove();
}
});
$overlay.find('.tl-report-submit').on('click', () => {
const $select = $overlay.find('.tl-report-motif');
const motifId = $select.val();
const motifLabel = $select.find('option:selected').data('label');
if (!motifId) {
$overlay.find('.tl-report-msg')
.css('color', '#ef4444')
.text('Le motif est obligatoire');
return;
}
const formData = new FormData();
formData.append('motif', motifId);
formData.append('reason', motifLabel || '');
formData.append('formType', 'signalement');
for (const key in data.formSession) {
formData.append(key, data.formSession[key]);
}
fetch(reportUrl, {
method: 'POST',
credentials: 'include',
headers: {
'x-requested-with': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: formData
})
.then(r => r.json())
.then(res => {
if (res.success && res.success.length) {
TL.showToast(res.success[0]);
$overlay.remove();
} else {
$overlay.find('.tl-report-msg')
.css('color', '#ef4444')
.text('Erreur lors du signalement.');
}
})
.catch(() => {
$overlay.find('.tl-report-msg')
.css('color', '#ef4444')
.text("Erreur lors de l'envoi.");
});
});
})
.catch(err => console.error('[TopicLive+] Erreur chargement formulaire signalement:', err));
}
openKickForm(kickUrl) {
fetch(kickUrl, { credentials: 'include' })
.then(r => r.json())
.then(data => {
if (!data || !data.reasons) return;
let optionsHtml = '';
const pseudoKick = data.pseudo || this.pseudo.trim();
for (const category in data.reasons) {
optionsHtml += '<optgroup label="' + category + '">';
for (const reason of data.reasons[category]) {
const desc = reason.description ? ' — ' + reason.description : '';
optionsHtml += '<option value="' + reason.id + '" data-label="' + reason.label + '" title="' + (reason.description || '') + '">' + reason.label + '</option>';
}
optionsHtml += '</optgroup>';
}
const $overlay = $('<div class="tl-report-overlay" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:999999;display:flex;align-items:center;justify-content:center;"></div>');
const $modal = $('<div class="modalWrapper__main" style="background:rgb(39,42,48);color:rgb(242,242,242);font-size:15px;padding:20px;border-radius:12px;width:440px;max-width:90vw;box-shadow:0 8px 32px rgba(0,0,0,0.5);"><div class="modalWrapper__header" style="background:transparent;padding:0 0 20px;margin:0 0 20px;display:flex;justify-content:space-between;align-items:center;"><div class="modalWrapper__title" style="font-size:17px;font-weight:700;text-transform:uppercase;">Kick d\'un utilisateur</div><button class="tl-kick-close" style="background:transparent;border:none;color:rgb(242,242,242);font-size:18px;cursor:pointer;"><span class="icon-close"></span></button></div><div class="modalWrapper__content"><form class="report"><div class="report__field" style="margin:0 0 15px;"><label class="report__label" style="font-weight:700;display:inline;">Pseudo :</label><span class="report__value" style="color:rgb(61,135,245);font-weight:700;margin-left:10px;">' + pseudoKick + '</span></div><div class="report__field" style="margin:0 0 15px;"><label class="report__label" style="font-weight:700;display:block;">Motif :</label><select class="report__select tl-kick-motif" style="background:rgb(48,50,54);color:rgb(242,242,242);padding:7px 12px;margin:5px 0 0;border-radius:12px;border:1px solid rgb(99,101,105);width:100%;font-size:15px;"><option value="">Sélectionnez un motif</option>' + optionsHtml + '</select><div class="tl-kick-desc" style="color:rgb(158,158,158);font-size:13px;margin:8px 0 15px;line-height:1.4;min-height:40px;font-style:italic;"></div></div><div class="report__field" style="margin:0 0 15px;"><label class="report__label" style="font-weight:700;display:block;">Raison du kick (obligatoire) :</label><textarea rows="4" placeholder="Merci de saisir la raison du kick" class="report__textarea tl-kick-raison" style="background:rgb(48,50,54);color:rgb(242,242,242);padding:7px 12px;margin:5px 0 0;border-radius:12px;border:1px solid rgb(99,101,105);width:100%;font-size:15px;resize:vertical;"></textarea></div><div class="tl-kick-msg" style="margin-bottom:12px;font-size:13px;"></div><div style="display:flex;justify-content:center;"><button class="report__submit tl-kick-submit" type="button" style="background:rgb(61,135,245);color:rgb(0,0,0);padding:8px 20px;border-radius:18px;border:1px solid rgb(61,135,245);font-size:15px;cursor:pointer;">Valider</button></div></form></div></div>');
$overlay.append($modal);
$('body').append($overlay);
$modal.css({ 'opacity': '1', 'visibility': 'visible' });
$overlay.find('.tl-kick-motif').on('change', function() {
const desc = $overlay.find('.tl-kick-motif option:selected').attr('title') || '';
$overlay.find('.tl-kick-desc').text(desc);
});
$overlay.find('.tl-kick-close').on('click', () => $overlay.remove());
$overlay.on('click', (e) => { if (e.target === $overlay[0]) $overlay.remove(); });
$overlay.find('.tl-kick-submit').on('click', () => {
const motifId = $overlay.find('.tl-kick-motif').val();
const raison = $overlay.find('.tl-kick-raison').val().trim();
if (!motifId) { $overlay.find('.tl-kick-msg').css('color', '#ef4444').text('Le motif est obligatoire.'); return; }
if (!raison) { $overlay.find('.tl-kick-msg').css('color', '#ef4444').text('La raison est obligatoire.'); return; }
const formData = new FormData();
formData.append('message_id', this.id_message);
formData.append('forum_id', TL.currentForumId);
formData.append('ajax_hash', TL.ajaxHash);
formData.append('action', 'submit');
formData.append('motif_kick', motifId);
formData.append('raison_kick', raison);
formData.append('duree_kick', '3');
fetch(kickUrl + '&action=submit', {
method: 'POST',
credentials: 'include',
headers: {
'x-requested-with': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: formData
})
.then(r => r.json())
.then(res => { console.log('[TL DEBUG] réponse kick:', JSON.stringify(res));
if (res.success) {
$overlay.remove();
TL.showToast(res.success[0] || "L'utilisateur a été kické.");
const $kickBtn = this.$message.find('.tl-kick-btn');
$kickBtn
.find('i')
.removeClass('icon-kick')
.addClass('icon-kick-active');
} else {
$overlay.find('.tl-kick-msg')
.css('color', '#ef4444')
.text((res.errors || ['Erreur lors du kick.']).join(' '));
}
})
.catch(() => { $overlay.find('.tl-kick-msg').css('color', '#ef4444').text('Erreur réseau.'); });
});
})
.catch(err => console.error('[TopicLive+] Erreur chargement formulaire kick:', err));
}
initPartialQuote() {
const partialQuoteEvent = async (pointerEvent) => {
await new Promise(resolve => setTimeout(resolve, 50));
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText.length) return;
const messageContentNode = this.trouver(TL.class_contenu)[0];
const selectionContainer = selection.getRangeAt(0).commonAncestorContainer;
if (!messageContentNode.contains(selectionContainer)) return;
TL.$partialQuoteButton[0].onclick = () => this.buildPartialQuote(selectedText);
const rect = selection.getRangeAt(0).getBoundingClientRect();
const top = rect.bottom + window.scrollY + 10;
const left = rect.left + (rect.width / 2) + window.scrollX;
TL.$partialQuoteButton.css({ top: `${top}px`, left: `${left}px` }).addClass('active');
};
this.$message[0].onpointerup = (pe) => partialQuoteEvent(pe);
this.$message[0].oncontextmenu = (pe) => partialQuoteEvent(pe);
}
buildPartialQuote(selection) {
const textarea = TL.formu.obtenirMessage()[0];
if (!textarea) return;
const datePropre = this.date.trim().replace(/\s+/g, ' ');
const pseudoPropre = this.pseudo.trim().replace(/\s+/g, ' ');
const newQuoteHeader = `> Le ${datePropre} ${pseudoPropre} a écrit :`;
const quotedText = selection.replace(/\n/g, '\n> ');
const fullQuote = `${newQuoteHeader}\n> ${quotedText}\n\n`;
const currentContent = textarea.value.length === 0 ? '' : `${textarea.value.trim()}\n\n`;
Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set.call(textarea, `${currentContent}${fullQuote}`);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
TL.$partialQuoteButton.removeClass('active');
}
fixDeroulerCitation() {
this.trouver('.message__blockquote').each(function() {
const $quote = $(this);
// On compte le nombre de blockquotes parents : on veut exactement 1 (= niveau 2)
const nbParentsBlockquote = $quote.parents('.message__blockquote').length;
if (nbParentsBlockquote !== 1) return;
// Évite les doublons si déjà traité
if ($quote.find('> .message__collapsedQuote').length > 0) return;
const $btn = $('<button type="button" class="message__collapsedQuote"></button>');
$quote.prepend($btn);
$btn.on('click', function() {
const isDown = $(document).scrollTop() + $(window).height() >= $(document).height() - 10;
$quote.toggleClass('message__blockquote--visible');
if (isDown) $('html, body').scrollTop($(document).height());
});
});
}
fixImages() {
// Images <img>
this.trouver(TL.class_contenu).find('img').each(function () {
const $img = $(this);
const src = $img.attr('src');
const alt = $img.attr('alt');
if (!src || !src.includes('/minis/') || !alt) return;
const extension = alt.split('.').pop();
const direct = src.replace(/\/minis\/(.*)\.\w+$/, `/fichiers/$1.${extension}`);
$img
.attr('loading', 'eager')
.attr('decoding', 'sync')
.css('object-fit', 'contain');
this.src = direct;
});
// Images Noelshack en background
this.trouver(TL.class_contenu).find('.js-lazy').each(function () {
const url = $(this).attr('data-src-background');
if (!url) return;
$(this).css('background-image', `url("${url}")`);
});
}
trouver(chose) {
return this.$message.find(chose);
}
update(nvMessage) {
if (this.edition == nvMessage.edition) return;
this.edition = nvMessage.edition;
this.trouver(TL.class_contenu).html(nvMessage.trouver(TL.class_contenu).html());
TL.page.Transformation();
this.fixImages();
this.fixDeroulerCitation();
dispatchEvent(new CustomEvent('topiclive:edition', {
'detail': { id: this.id_message, jvcake: TL.jvCake }
}));
}
}
/**
* Gère le formulaire de réponse.
*/
class Formulaire {
constructor() {
this.formSessionData = null;
this.selectedGroup = "1";
this.observerLeBouton('.postMessage');
this.observerLeMenuModeration();
}
observerLeMenuModeration() {
const SELECTEUR_MENU_MODO = '#form_alias_rang';
const setupListener = (selectElement) => {
selectElement.addEventListener('change', () => {
this.selectedGroup = selectElement.value;
});
this.selectedGroup = selectElement.value;
};
const observer = new MutationObserver((mutations, obs) => {
const menuModo = document.querySelector(SELECTEUR_MENU_MODO);
if (menuModo) {
setupListener(menuModo);
obs.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
observerLeBouton(selecteurBouton) {
// Avec la délégation, pas besoin d'attendre que le bouton existe
// On attache directement sur document
$(document).off('click.topiclive', selecteurBouton)
.on('click.topiclive', selecteurBouton, (e) => this.envoyer(e));
}
_getForumPayload() {
try {
return JSON.parse(atob(unsafeWindow.jvc.forumsAppPayload));
} catch (e) {
return null;
}
}
_getTopicId() {
return $('#bloc-formulaire-forum').attr('data-topic-id');
}
_getForumId() {
const match = window.location.pathname.match(/forums\/(?:1|42)-(?<forumid>[0-9]+)-/);
return match ? match.groups.forumid : null;
}
_setTextAreaValue(textarea, value) {
const prototype = Object.getPrototypeOf(textarea);
const nativeSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
nativeSetter.call(textarea, value);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
envoyer(e) {
e.preventDefault();
e.stopImmediatePropagation();
const $boutonEnvoi = $('.postMessage');
const $labelBouton = $boutonEnvoi.find('.postMessage__label');
const $msgTextarea = $('#message_topic');
const message = $msgTextarea.val();
if (message.trim() === '') {
TL.alert('Le message est vide.');
return;
}
let dataObject = {};
if (this.formSessionData) {
dataObject = { ...this.formSessionData };
} else {
const forumPayload = this._getForumPayload();
if (forumPayload && forumPayload.formSession) {
dataObject = { ...forumPayload.formSession };
} else {
this.afficherErreurs("Impossible de récupérer les informations de session initiales.");
return;
}
}
dataObject.text = message;
dataObject.topicId = this._getTopicId();
dataObject.forumId = this._getForumId();
dataObject.group = this.selectedGroup;
dataObject.messageId = "undefined";
dataObject.ajax_hash = $('#ajax_hash_liste_messages').val();
$boutonEnvoi.prop('disabled', true);
$labelBouton.text('Envoi...');
const self = this;
const handleSuccess = (response) => {
// ── NOUVEAU : détection dernière page avec nouvelle pagination ──
const isOnLastPage = TL.estSurDernierePage;
self._setTextAreaValue($msgTextarea[0], '');
if (isOnLastPage) {
if (response.redirectUrl) {
try {
const url = new URL(response.redirectUrl, window.location.origin);
const hash = url.hash;
if (hash && hash.startsWith('#post_')) {
TL.justPostedMessageId = hash.replace('#post_', '');
}
} catch (err) {
console.error("[TopicLive+] Erreur d'analyse de l'URL de redirection.", err);
}
}
setTimeout(() => TL.charger(), 500);
} else {
if (response.redirectUrl) {
localStorage.setItem('tl_force_live_mode', 'true');
window.location.href = response.redirectUrl;
} else {
// ── NOUVEAU : sélecteur pagination dernière page ──
const $bouton = $('.pagination__item--last:not(.pagination__item--disabled)');
let lastPageUrl = '';
if ($bouton.length > 0) {
lastPageUrl = $bouton.attr('href') || TL.jvCake($bouton.attr('class'));
}
if (lastPageUrl) {
localStorage.setItem('tl_force_live_mode', 'true');
window.location.href = lastPageUrl;
} else {
location.reload();
}
}
}
};
$.ajax({
type: 'POST',
url: 'https://www.jeuxvideo.com/forums/message/add',
data: dataObject,
dataType: 'json',
success: (response) => {
if (response.formSession) self.formSessionData = response.formSession;
const hasSessionError = response.errors && response.errors.session;
if (hasSessionError) {
let retryDataObject = {
...self.formSessionData,
text: message,
topicId: self._getTopicId(),
forumId: self._getForumId(),
group: this.selectedGroup,
messageId: "undefined",
ajax_hash: $('#ajax_hash_liste_messages').val()
};
$.ajax({
type: 'POST',
url: 'https://www.jeuxvideo.com/forums/message/add',
data: retryDataObject,
dataType: 'json',
success: (finalResponse) => {
if (finalResponse.errors && Object.keys(finalResponse.errors).length > 0) {
self.afficherErreurs(Object.values(finalResponse.errors).join('\n'));
} else {
handleSuccess(finalResponse);
}
},
error: () => self.afficherErreurs('La relance automatique a échoué (erreur réseau).'),
complete: () => {
$boutonEnvoi.prop('disabled', false);
$labelBouton.text('Poster');
}
});
} else if (response.errors && Object.keys(response.errors).length > 0) {
self.afficherErreurs(Object.values(response.errors).join('\n'));
$boutonEnvoi.prop('disabled', false);
$labelBouton.text('Poster');
} else {
handleSuccess(response);
$boutonEnvoi.prop('disabled', false);
$labelBouton.text('Poster');
}
},
error: () => {
self.afficherErreurs('Une erreur réseau est survenue lors de l\'envoi du message.');
$boutonEnvoi.prop('disabled', false);
$labelBouton.text('Poster');
}
});
}
afficherErreurs(msg) { TL.alert(msg); }
obtenirMessage($form) {
if (typeof $form == 'undefined') $form = this.obtenirFormulaire();
return $form.find('#message_topic, #message_reponse');
}
obtenirFormulaire($page) {
if (typeof $page === 'undefined') $page = $(document);
return $page.find('#forums-post-message-editor');
}
}
/**
* Gère la favicon.
*/
class Favicon {
constructor() {
try {
this.imageLoaded = false;
this.pendingText = '';
this.canv = $('<canvas>').get(0);
this.canv.width = 192;
this.canv.height = 192;
this.context = this.canv.getContext('2d');
this.image = new Image();
this.image.onload = () => {
this.imageLoaded = true;
if (this.pendingText) this.maj(this.pendingText);
};
this.image.src = 'https://www.jeuxvideo.com/favicon.png';
this.maj('');
} catch (err) {
console.error(`[TopicLive+] Erreur init favicon : ${err}`);
}
}
clear() {
this.context.clearRect(0, 0, this.canv.width, this.canv.height);
if (this.imageLoaded) this.context.drawImage(this.image, 0, 0);
}
maj(txt) {
this.pendingText = txt;
if (!this.imageLoaded) return;
this.clear();
if (txt && txt !== '') {
const radius = 70, borderWidth = 8, centerX = radius + borderWidth, centerY = radius + borderWidth;
const font = 'bold 120px Arial Black', verticalTextOffset = 8, shadowOffset = 6;
this.context.beginPath();
this.context.arc(centerX, centerY, radius + borderWidth, 0, 2 * Math.PI);
this.context.fillStyle = 'white';
this.context.fill();
this.context.beginPath();
this.context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
this.context.fillStyle = '#0074ff';
this.context.fill();
this.context.font = font;
this.context.textAlign = 'center';
this.context.textBaseline = 'middle';
this.context.fillStyle = 'black';
this.context.fillText(txt, centerX + shadowOffset, centerY + verticalTextOffset + shadowOffset);
this.context.fillStyle = 'white';
this.context.fillText(txt, centerX, centerY + verticalTextOffset);
}
this.replace();
}
replace() {
$('link[rel*="icon"]').remove();
this.lien = $('<link>', { href: this.canv.toDataURL('image/png'), rel: 'shortcut icon', type: 'image/png' });
$('head').append(this.lien);
}
setCloudflareIcon() {
const cloudflareLogo = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTkuMzUgMTAuMDRDMTguNjcgNi41OSAxNS42NCA0IDEyIDRDOS4xMSA0IDYuNiA1LjY0IDUuMzUgOC4wNEMyLjM0IDguMzYgMCAxMC45MSAwIDE0QzAgMTcuNzEgMi42OSAyMCA2IDIwSDE5QzIxLjc2IDIwIDI0IDE3Ljc2IDI0IDE1QzI0IDEyLjM2IDIxLjk1IDEwLjIyIDE5LjM1IDEwLjA0WiIgZmlsbD0iI0Y0ODAyMiIvPjwvc3ZnPg==';
$('link[rel*="icon"]').remove();
this.lien = $('<link>', { href: cloudflareLogo, rel: 'shortcut icon', type: 'image/svg+xml' });
$('head').append(this.lien);
}
set410Icon() {
const errorIcon16 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAALEUExURUxpcf7ihv/0oeuVNfK1Xv7/wf//6uqPMPS3WvzRff/Vc/nOfv/sh//Pbf3WdEI9Rv7iiP7ce//2rv/Nb/zknv/bevutMv/gkv/wrPzgmfbFcvrXjPzkmv7xq//hf//skP3cgv/mhfGvVP/shF1KOXZNMP/Xff3poP/FYf/AXPmaDf/rhO2ePa+Viv/legAPPf62SvvQd/6NFS0rN/SkPfvZiNp0FoZFD7VUFf7ecf6UGP+fIur//xwYIvOIFuiKH+yIGY6Ki59rPi8rM/+dG1JPXXpwY56hqtyxZ+eQKfvTdURDUEZEUF5cZ/n39pZaHU1JUvnWgv7caIqHjXh5hTk4Q9t7F3dzd0FEVTw6RZViOpaUlpqYm/2QE+aGGEkyJI1pTE9MUtTT02daUyorOX99gPSMGBYVIv+REfCLGP+mDPmLFLFuJWRfYuqHFko3LuKEGEZCSf3ajf71s//4uP/riP/qhv/Zdf/lg//efP/1t//qn/3Sb//ll926b/vGbf/NavfWdP/0o/3iesiaXP/pgv/ddP/XcNSfV/q3VvK4VuvDav7uov/BUv/jeP/fdMSUV//aaLNpGv/rkP3GS/+ySN6QH+OvR//faP/ecP/HNP+yJ+aqMf2jIf66KfS3IvmZJKlnG7FiDfzigP/GKNqIGv6UFumNK2tna/+wJvC9Qv/AJuq0OCMiMP+nIvnOX/6kIpSGaC8wR//mdOyQGZ+WkkpLW4d/eO6dGy8sNsybN7qNPfilII2QnHhoXe3s7NbX24JqULO5x519UKCenyktQ66HSjk1Pv6TFG1rcLSwrj9EXS8qL83MzSEeJ8vKzmpmayMfKGRpfjo/TpWYpyomLY6OltDPzzQxOUFAU3x6fmxwfI6KjRscKZORlWpoctTT1Lu9wpqYm42Lj/+YHnJucp6eojc0PS8rMjU0PtiFJPONGDdGz4cAAABxdFJOUwCN+hdmAgIBAwL97P38aAIDY/392/4V/v7bdLu77P78cf2R/QIDAuX9+hj8agf9HP1ybK/nthuaGv7w/AHz4Ha9/uHY/aL9/LX+2kmW/vz9ytr+/fxveP7+bPz8+P7A/v3++/6I/r79AdwWdeb93X3+CABJWgAAAAlwSFlzAAALEwAACxMBAJqcGAAAARtJREFUGNMBEAHv/gAAABAAFiAcFBkbGgMACAcAAAAFABEdehh7fRdyCwQACQAABgABdBIfIR4VCg0TfyIAJgAADnMCDHV4eXd8gCgpiSwAACongiN2foGGh4iKjTCVNDYAMYwrhYOEi5GUkpabnTugPwA1ky6Oj5CXmp6ipamwRKZAAFGjOZmYnJ+kq7i8rrRJpz4ASrNSr6qsurvGw8G+tU22QgBIsUa30NnU0svCxb3AU61QAEWyVM/W3+Da2M3Hyb9XcUMATLlb197n4+LV3MzEyk6oPQBV0VxiZejm4d3b01hHQXEzAEvOYeXpZ21jX2BaT6E3Ly0APFlkb2zqcOvkXcg6MgAAJAAAAGgAamtmaW5eVjgAJQ8Ah7RsxA/wK1MAAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII=';
$('link[rel*="icon"]').remove();
this.lien = $('<link>', { href: errorIcon16, rel: 'shortcut icon', type: 'image/png' });
$('head').append(this.lien);
}
}
/**
* Classe principale TopicLive.
*/
class TopicLive {
constructor() {
this.isLoading = false;
this.instance = 0;
this.ongletActif = !document.hidden;
this.unreadMessageAnchors = [];
this.isChatModeActive = false;
this.lastScrollTop = 0;
this.isBlocked = false;
this.is410 = false;
this.$partialQuoteButton = null;
this.$tl_forum_button = null;
this.justPostedMessageId = null;
this.forumButtonState = false;
this.$tl_quick_reply_button = null;
this.$tl_settings_button = null;
this.$tl_settings_modal = null;
this.$tl_settings_overlay = null;
this.options = {};
this.tempOptions = {};
this.isForumPage = false;
this.$tl_connected_counter = null;
this.isStandby = false;
this.changelogContent = null;
this.estSurDernierePage = false;
}
showToast(message, success = true) {
const $toast = $(
'<div style="position:fixed;top:60px;left:50%;transform:translateX(-50%);width:800px;max-width:100%;font-size:0.875rem;color:rgb(242,242,242);pointer-events:auto;background-color:' +
(success ? 'rgb(25,135,84)' : 'rgb(220,53,69)') +
';background-clip:padding-box;border:1px solid rgba(255,255,255,0.1);box-shadow:0 16px 48px rgba(0,0,0,0.4);border-radius:12px;z-index:2147483647;display:flex;align-items:center;justify-content:space-between;padding:12px 16px;gap:12px;">' +
'<span>' + message + '</span>' +
'<button style="background:transparent;border:none;color:rgb(242,242,242);font-size:18px;cursor:pointer;line-height:1;padding:0;">×</button>' +
'</div>'
);
$('body').append($toast);
$toast.find('button').on('click', () => $toast.remove());
setTimeout(() => $toast.remove(), 4000);
}
updateSelectionBar() {
const count = $('.messageUser__checkbox:checked').length;
if (!this.$selectionBar || !this.$selectionBar.length) {
this.$selectionBar = $(
'<div class="listActions__fixed">' +
'<button class="simpleButton listActions__button tl-selection-delete" type="button">Supprimer</button>' +
'<button class="simpleButton listActions__button tl-selection-restore" type="button">Restaurer</button>' +
'</div>'
);
$('body').append(this.$selectionBar);
}
if (!this.selectionBarBound) {
this.selectionBarBound = true;
this.$selectionBar.find('.tl-selection-delete')
.off('click')
.on('click', () => {
$('.messageUser__checkbox:checked').each(function () {
const message = $(this).data('message');
if (message) {
message.$message.find('.tl-delete-btn').trigger('click');
} else {
$(this)
.closest('.messageUser')
.find('[title="Supprimer le message"]')
.trigger('click');
}
});
});
this.$selectionBar.find('.tl-selection-restore')
.off('click')
.on('click', () => {
$('.messageUser__checkbox:checked').each(function () {
const message = $(this).data('message');
if (message) {
message.$message.find('.tl-restore-btn').trigger('click');
} else {
$(this)
.closest('.messageUser')
.find('[title="Restaurer le message"]')
.trigger('click');
}
});
});
}
if (count === 0) {
this.$selectionBar.hide();
} else {
this.$selectionBar.show();
}
}
loadSettings() {
const load = (key, defaultValue) => {
const value = localStorage.getItem(key);
return value === null ? defaultValue : (value === 'true');
};
this.options.sound = load('topiclive_sound', false);
this.options.favicon = load('topiclive_favicon', true);
this.options.newMessagesButton = load('topiclive_newMessagesButton', true);
this.options.showCounterOnButton = load('topiclive_showCounterOnButton', true);
this.options.listButton = load('topiclive_listButton', true);
this.options.listButtonSwipe = load('topiclive_listButtonSwipe', true);
this.options.quickReplyButton = load('topiclive_quickReplyButton', true);
this.options.counterOnTopics = load('topiclive_counterOnTopics', true);
this.options.mobileMode = load('topiclive_mobileMode', false);
}
charger() {
if (this.oldInstance != this.instance) return;
TL.GET(data => { new Page(data).scan(); });
}
init() {
const estPageDeForum = document.URL.match(/\/forums\/(?:0|42|1)-/);
if (!estPageDeForum) {
if (this.$tl_button) this.$tl_button.hide();
if (this.$tl_forum_button) this.$tl_forum_button.hide();
if (this.$tl_quick_reply_button) this.$tl_quick_reply_button.hide();
if (this.$tl_connected_counter) this.$tl_connected_counter.hide();
return;
}
if (typeof $ === 'undefined') return;
this.loadSettings();
const forceLive = localStorage.getItem('tl_force_live_mode') === 'true';
if (forceLive) localStorage.removeItem('tl_force_live_mode');
const shouldGoLive = window.location.hash === '#tl-go-live' || window.location.hash.startsWith('#post_') || forceLive;
if (this.$tl_button) this.$tl_button.hide();
if (this.$tl_forum_button) this.$tl_forum_button.hide();
if (this.$tl_quick_reply_button) this.$tl_quick_reply_button.hide();
if (this.$tl_connected_counter) this.$tl_connected_counter.hide();
this.isChatModeActive = false;
this.nvxMessages = 0;
this.unreadMessageAnchors = [];
this.lastScrollTop = 0;
if (document.URL.match(/\/forums\/0-/)) {
this.isForumPage = true;
this.instance++;
this.url = document.URL;
$('#topiclive-connected-counter, .tl-counter-button').hide();
this.updateDesktopButtonPosition();
this.scanForumPageAndUpdate($(document));
this.loopForum();
return;
}
const analysable = document.URL.match(/\/forums\/(?:42|1)-/);
if (!analysable) return;
this.isForumPage = false;
this.instance++;
this.ajaxTs = $('#ajax_timestamp_liste_messages').val();
this.ajaxHash = $('#ajax_hash_liste_messages').val();
this.estMP = false;
this.url = document.URL;
window.clearTimeout(this.idCounterPoll);
this.pollConnectedCounter();
this.messagesActionsMap = {};
this.messagesTextMap = {};
this.isModerator = false;
this.currentForumId = this._getCurrentForumId();
extractPayloadGzip().then(payload => {
if (payload && payload.listMessage) {
for (const msg of payload.listMessage) {
this.messagesActionsMap[msg.id] = msg.actions;
this.messagesTextMap[msg.id] = msg.text;
}
}
this.currentUserPseudo = $('.headerAccount__pseudo').text().trim();
if (payload && payload.forumInfo && payload.forumInfo.data) {
const modBlock = payload.forumInfo.data.find(d => d.type === 'moderator');
if (modBlock && modBlock.body && modBlock.body[0] && modBlock.body[0].label) {
const modList = modBlock.body[0].label.map(l => l.value);
this.isModerator = modList.includes(this.currentUserPseudo);
}
}
});
// ── NOUVEAU : sélecteurs adaptés à la nouvelle interface React JVC ──
this.class_msg = '.messageUser';
this.class_num_page = '.pagination__item--current';
this.class_page_fin = '.pagination__item--last:not(.pagination__item--disabled)';
this.class_date = '.messageUser__date';
this.class_contenu = '.messageUser__main';
this.class_pagination = '.container__pagination';
if ($(this.class_msg).length > 0) {
this.page = new Page($(document));
this.formu = new Formulaire();
this.messages = this.page.obtenirMessages();
this.applySettings();
this.updateDesktopButtonPosition();
this.updateCounters();
if (shouldGoLive) {
this.isChatModeActive = true;
if (window.location.hash.startsWith('#post_')) {
setTimeout(() => {
const targetId = window.location.hash.replace('#post_', '');
const $targetMessage = $(`#message-${targetId}`);
if ($targetMessage.length > 0) {
const targetScrollTop = $targetMessage.offset().top - 100;
$('html, body').stop().animate({ scrollTop: targetScrollTop }, 800);
}
}, 200);
} else if (window.location.hash === '#tl-go-live') {
setTimeout(() => {
const $lastMessage = $(`${TL.class_msg}:last`);
if ($lastMessage.length > 0) {
const targetScrollTop = $lastMessage.offset().top - 100;
$('html, body').animate({ scrollTop: targetScrollTop }, 800);
}
history.replaceState(null, document.title, window.location.pathname + window.location.search);
}, 100);
}
}
this.demarrerSiDernierePage();
}
}
demarrerSiDernierePage() {
const POSTS_PAR_PAGE = 20;
const postsDOM = $(this.class_msg).length;
if (postsDOM < POSTS_PAR_PAGE) {
this.estSurDernierePage = true;
this.page.scan();
this.loop();
return;
}
const testUrl = this.url.split('-');
const pageActuelle = parseInt(testUrl[3], 10) || 1;
testUrl[3] = pageActuelle + 1;
const urlPageSuivante = testUrl.join('-');
$.ajax({
type: 'GET',
url: urlPageSuivante,
timeout: 5000,
success: (data, textStatus, jqXHR) => {
const pageSuivante = $(jqXHR.responseText.substring(jqXHR.responseText.indexOf('<!DOCTYPE html')));
const postsSuivants = pageSuivante.find(this.class_msg).length;
const canonicalMatch = jqXHR.responseText.match(/rel="canonical" href="[^"]*-(\d+)-0-1-0-[^"]*"/);
const pageRetournee = canonicalMatch ? parseInt(canonicalMatch[1], 10) : null;
const pageVoulue = pageActuelle + 1;
const estRedirection = pageRetournee !== null && pageRetournee !== pageVoulue;
if (postsSuivants > 0 && !estRedirection) {
} else {
this.estSurDernierePage = true;
this.page.scan();
this.loop();
}
},
error: () => {
this.estSurDernierePage = true;
this.page.scan();
this.loop();
}
});
}
initOtherScriptObserver() {
window.addEventListener('jvchat:activation', () => {
this.standby();
});
// Si JvChat est déjà ouvert au chargement
if (document.querySelector('#jvchat-main')) {
this.standby();
}
}
standby() {
if (this.isStandby) return;
this.isStandby = true;
window.clearTimeout(this.idanalyse);
window.clearTimeout(this.idForumAnalyse);
}
resume() {
if (!this.isStandby) return;
this.isStandby = false;
this.applySettings();
if (this.isForumPage) { this.loopForum(); } else { this.loop(); }
}
initScrollButton() {
const arrowIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>`;
const buttonCss = `
.messageDeleted {
display: flex;
gap: 12px;
padding: 18px;
border-radius: 8px;
background: #e6f3ff;
border: 1px solid #8fc4ff;
}
.messageDeleted__icon {
font-size: 22px;
color: #1e88e5;
}
.messageDeleted__title {
font-weight: bold;
margin-bottom: 6px;
}
.messageDeleted__reason {
color: #444;
}
.messageDeleted__author {
margin-top: 8px;
font-size: 12px;
color: #777;
}
.message--deleted {
opacity: .95;
}
.topiclive-floating-button { z-index: 1000; font-weight: bold; color: white; background-color: rgba(22, 22, 22, 0.3); border: 1px solid rgba(74, 74, 74, 1); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); cursor: pointer; display: flex; align-items: center; height: 35px; width: 35px; border-radius: 50%; padding: 0; justify-content: center; transform: translateZ(0); transition: width 0.3s ease, padding 0.3s ease, border-radius 0.3s ease, background-color 0.2s ease, transform 0.2s ease; }
.topiclive-floating-button:hover { background-color: rgba(40, 40, 40, 0.9); transform: translateY(-2px); }
.topiclive-floating-button:active { transform: translateY(1px); }
#topiclive-button.has-unread-messages { width: auto; padding: 0 10px 0 8px; border-radius: 50px; }
#topiclive-button .topiclive-counter { font-size: 13px; background-color: #007bff; border-radius: 50%; width: 24px; height: 24px; line-height: 24px; text-align: center; margin-right: 8px; transform: scale(0); transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease, width 0.3s ease; opacity: 0; width: 0; overflow: hidden; display: none; }
#topiclive-button.has-unread-messages .topiclive-counter { display: block; transform: scale(1); opacity: 1; width: 24px; }
#topiclive-button .topiclive-arrow { transition: transform 0.3s ease; display: flex; align-items: center; }
.button-transitioning { transform: scale(0.9); transition: transform 0.15s ease, background-color 0.15s ease; }
#topiclive-connected-counter { pointer-events: none; }
.tl-counter-button {
width: 35px;
height: 35px;
line-height: 35px;
font-size: 15px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.container__messages {
margin-bottom: 0 !important;
}
.pagination__navigation--bottom {
margin-top: 0.625rem !important;
}
`;
$('head').append(`<style>${buttonCss}</style>`);
this.$tl_button = $(`<button id="topiclive-button" class="topiclive-floating-button"><span class="topiclive-counter"></span><span class="topiclive-arrow">${arrowIconSvg}</span></button>`).hide();
this.$tl_button.addClass('jvchat-hide');
this.$tl_button.get(0).TL = this;
$('body').append(this.$tl_button);
this.$tl_button.on('click', () => {
// ── NOUVEAU : détection dernière page ──
const isOnLastPage = TL.estSurDernierePage;
if (isOnLastPage) {
if (this.nvxMessages > 0) {
this.page.performScroll();
} else {
this.isChatModeActive = true;
const $lastMessage = $(`${TL.class_msg}:last`);
if ($lastMessage.length > 0) {
const targetScrollTop = $lastMessage.offset().top - 100;
$('html, body').animate({ scrollTop: targetScrollTop }, 800);
}
this.updateCounters();
}
} else {
const $bouton = $('.pagination__item--last:not(.pagination__item--disabled)');
let lastPageUrl = $bouton.attr('href') || '';
if (lastPageUrl) window.location.href = lastPageUrl + '#tl-go-live';
}
});
$(window).on('scroll', () => {
this.updateDesktopButtonPosition();
const st = $(window).scrollTop();
if (st < this.lastScrollTop && this.isChatModeActive) this.isChatModeActive = false;
this.lastScrollTop = st;
if (this.unreadMessageAnchors.length === 0) { this.updateCounters(); return; }
const viewportBottom = $(window).scrollTop() + $(window).height();
const messagesJustRead = [];
for (const $message of this.unreadMessageAnchors) {
const messageBottom = $message.offset().top + $message.outerHeight();
if (viewportBottom >= messageBottom) messagesJustRead.push($message);
}
if (messagesJustRead.length > 0) {
this.unreadMessageAnchors = this.unreadMessageAnchors.filter($anchor => !messagesJustRead.some($read => $read.is($anchor)));
this.nvxMessages -= messagesJustRead.length;
if (this.nvxMessages <= 0) { this.nvxMessages = 0; this.isChatModeActive = true; }
}
this.updateCounters();
});
$(window).on('resize', () => this.updateDesktopButtonPosition());
}
initForumListButton() {
const listIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="17.2" height="17.2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`;
this.$tl_forum_button = $(`<button id="topiclive-forumlist-button" class="topiclive-floating-button">${listIconSvg}</button>`).hide();
this.$tl_forum_button.addClass('jvchat-hide');
$('body').append(this.$tl_forum_button);
let startX = 0, startY = 0, isDragging = false;
const swipeThreshold = 30;
this.$tl_forum_button.on('mousedown touchstart', (e) => {
if (!this.options.listButtonSwipe) return;
isDragging = false;
const touch = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
startX = touch.clientX; startY = touch.clientY;
});
this.$tl_forum_button.on('mousemove touchmove', (e) => {
if (!this.options.listButtonSwipe || (startX === 0 && startY === 0)) return;
const touch = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
const diffX = Math.abs(touch.clientX - startX), diffY = Math.abs(touch.clientY - startY);
if (diffX > swipeThreshold && diffX > diffY) isDragging = true;
});
this.$tl_forum_button.on('mouseup touchend', (e) => {
if (!this.options.listButtonSwipe) return;
if (isDragging) { e.preventDefault(); e.stopPropagation(); this.toggleForumButtonState(); }
startX = 0; startY = 0;
});
this.$tl_forum_button.on('click', (e) => {
if (isDragging) { isDragging = false; e.preventDefault(); e.stopPropagation(); return; }
if (this.forumButtonState && this.options.listButtonSwipe) { this.scrollToReplyForm(); } else { this.goToForumList(); }
});
}
initQuickReplyButton() {
const replyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="17.5" height="17.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`;
this.$tl_quick_reply_button = $(`<button id="topiclive-quickreply-button" class="topiclive-floating-button">${replyIconSvg}</button>`).hide();
this.$tl_quick_reply_button.addClass('jvchat-hide');
$('body').append(this.$tl_quick_reply_button);
this.$tl_quick_reply_button.on('click', () => { this.scrollToReplyForm(); });
}
initSettingsMenu() {
if ($('#tl-settings-modal').length > 0) return;
const soundOnIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"/><path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.482 5.482 0 0 1 11.025 8a5.482 5.482 0 0 1-1.61 3.89l.706.706z"/><path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06z"/></svg>`;
const faviconIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M4.5 2A1.5 1.5 0 0 0 3 3.5v9A1.5 1.5 0 0 0 4.5 14h7a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 11.5 2h-7zM10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>`;
const replyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`;
const listIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`;
const arrowIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>`;
const githubIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>`;
const videoIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="m15.586 8.086-5.48-4.383A.5.5 0 0 0 9.5 4.06v7.88a.5.5 0 0 0 .606.457l5.48-4.383a.5.5 0 0 0 0-.914zM1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8A1.5 1.5 0 0 0 11 12.5v-9A1.5 1.5 0 0 0 9.5 2h-8z"/></svg>`;
const menuHtml = `
<div id="tl-settings-overlay" style="display: none;"></div>
<div id="tl-settings-modal" style="display: none;">
<div class="tl-smartphone-frame">
<div class="tl-smartphone-screen">
<div class="tl-smartphone-header">
<span class="tl-header-title">TopicLive+</span>
<div class="tl-dark-mode-container">
<span class="tl-dark-mode-label">Menu Sombre</span>
<div class="tl-dark-mode-toggle">
<input type="checkbox" class="input-on-off" id="tl-setting-dark-mode">
<label for="tl-setting-dark-mode" class="btn-on-off"></label>
</div>
</div>
</div>
<ul class="tl-settings-list">
<li class="tl-setting-separator">Onglet</li>
<li><div class="tl-setting-label"><span class="tl-setting-icon">${soundOnIcon}</span><span>Son des notifications</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-sound"><label for="tl-setting-sound" class="btn-on-off"></label></div></li>
<li><div class="tl-setting-label"><span class="tl-setting-icon">${faviconIcon}</span><span>Compteur sur Favicon</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-favicon"><label for="tl-setting-favicon" class="btn-on-off"></label></div></li>
<li class="tl-setting-separator">Affichage des boutons</li>
<li><div class="tl-setting-label"><div id="tl-sim-quick-reply" class="topiclive-floating-button">${replyIconSvg}</div><span>Réponse rapide</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-quick-reply"><label for="tl-setting-quick-reply" class="btn-on-off"></label></div></li>
<li><div class="tl-setting-label"><div id="tl-sim-list" class="topiclive-floating-button">${listIconSvg}</div><span>Liste des sujets</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-list"><label for="tl-setting-list" class="btn-on-off"></label></div></li>
<li id="tl-li-list-swipe">
<div class="tl-setting-label">
<div class="tl-swipe-visual">
<div class="topiclive-floating-button">${listIconSvg}</div>
<span class="tl-swipe-arrow">→</span>
<div class="topiclive-floating-button" style="background-color: rgba(0, 123, 255, 0.3);">${replyIconSvg}</div>
</div>
<span>Activer le Swipe</span>
</div>
<div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-list-swipe"><label for="tl-setting-list-swipe" class="btn-on-off"></label></div>
</li>
<li><div class="tl-setting-label"><div id="tl-sim-new-messages" class="topiclive-floating-button"><span class="topiclive-arrow">${arrowIconSvg}</span></div><span>Accéder au direct</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-new-messages"><label for="tl-setting-new-messages" class="btn-on-off"></label></div></li>
<li><div class="tl-setting-label"><div id="tl-sim-new-messages-counter" class="topiclive-floating-button"><span class="topiclive-counter">1</span><span class="topiclive-arrow">${arrowIconSvg}</span></div><span>Nouveau message</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-show-counter"><label for="tl-setting-show-counter" class="btn-on-off"></label></div></li>
<li class="tl-setting-separator">Compteur de connectés</li>
<li><div class="tl-setting-label"><div id="tl-sim-counter-topics" class="topiclive-floating-button tl-counter-button">14</div><span>Sur les topics</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-counter-topics"><label for="tl-setting-counter-topics" class="btn-on-off"></label></div></li>
<li><div class="tl-setting-label"><span>Mode Mobile (ancien compteur)</span></div><div class="tl-setting-toggle"><input type="checkbox" class="input-on-off" id="tl-setting-mobile-mode"><label for="tl-setting-mobile-mode" class="btn-on-off"></label></div></li>
</ul>
<div class="tl-settings-actions">
<button id="tl-settings-cancel" class="btn">Annuler</button>
<button id="tl-settings-save" class="btn btn-poster-msg">Sauvegarder</button>
</div>
<div class="tl-settings-footer">
<a href="https://github.com/DreamboxMinerva/TopicLive-Reworked" target="_blank" class="tl-footer-link">${githubIconSvg}<span>GitHub</span></a>
</div>
</div>
</div>
</div>
<div id="tl-changelog-overlay" style="display: none;"></div>
<div id="tl-changelog-modal" style="display: none;">
<h2>Changelog TopicLive+</h2>
<pre id="tl-changelog-content">Chargement...</pre>
<button id="tl-changelog-close" class="btn">Fermer</button>
</div>`;
const menuCss = `
:root { --tl-bg-light: #f5f5f5; --tl-text-light: #333; --tl-border-light: #e0e0e0; --tl-header-bg-light: #f5f5f5; --tl-header-text-light: #111; --tl-separator-bg-light: #e0e0e0; --tl-bg-dark: #2d2d2d; --tl-text-dark: #f0f0f0; --tl-border-dark: #444; --tl-header-bg-dark: #1e1e1e; --tl-header-text-dark: #f0f0f0; --tl-separator-bg-dark: #444; }
.topiclive-deleted .messageUser__card { background-color: rgba(128, 128, 128, 0.5) !important; border: 1px solid rgba(128, 128, 128, 0.5); border-radius: 8px; opacity: 0.7; }
.bloc-pre-right { flex-wrap: wrap; row-gap: 0.625rem; column-gap: 0.3125rem; display: flex; }
.tl-settings-button {
text-decoration: none;
}
#tl-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9998; }
#tl-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 9999; display: flex; align-items: center; justify-content: center; padding: 10px; box-sizing: border-box; }
.tl-smartphone-frame { width: 100%; max-width: 340px; max-height: 70vh; background: #111; border: 2px solid #000; border-radius: 20px; padding: 5px; box-shadow: 0 5px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; }
.tl-smartphone-screen { background: var(--tl-bg-light); height: 100%; border-radius: 15px; display: flex; flex-direction: column; overflow: hidden; }
.tl-smartphone-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border-bottom: 1px solid var(--tl-border-light); background-color: var(--tl-header-bg-light); color: var(--tl-header-text-light); flex-shrink: 0; }
.tl-header-title { font-size: 21px; font-weight: 900; background: linear-gradient(90deg, rgba(0, 82, 204, 0.9), rgba(244, 128, 34, 0.9)); -webkit-background-clip: text; background-clip: text; color: transparent; filter: drop-shadow(0px 0px 1px rgba(0,0,0,0.2)); }
.tl-dark-mode-container { display: flex; align-items: center; gap: 8px; }
.tl-dark-mode-label { font-size: 10px; font-weight: normal; }
.tl-settings-list { list-style: none; padding: 15px 5px; margin: 0; color: var(--tl-text-light); overflow-y: auto; flex-grow: 1; }
.tl-settings-list li { display: flex; align-items: center; justify-content: space-between; padding: 10px 15px; border-bottom: 1px solid var(--tl-border-light); }
.tl-settings-list li:last-child { border-bottom: none; }
.tl-setting-label { display: flex; align-items: center; gap: 15px; font-size: 14px; }
.tl-setting-icon { color: #555; }
.tl-setting-separator { display: block; width: 100%; background: var(--tl-separator-bg-light); padding: 2px 15px; font-weight: bold; font-size: 12px; margin-top: 10px; }
#tl-li-list-swipe { background: #f0f0f0; }
.tl-swipe-visual { display: flex; align-items: center; gap: 5px; }
.tl-swipe-visual .topiclive-floating-button { width: 28px; height: 28px; transform: none !important; cursor: default; box-shadow: none; border-width: 1px; }
.tl-swipe-visual .topiclive-floating-button svg { width: 16px; height: 16px; }
.tl-swipe-arrow { font-size: 20px; font-weight: bold; color: #555; }
.tl-sim-disabled { opacity: 0.4; filter: grayscale(80%); }
.tl-settings-actions { padding: 15px; border-top: 1px solid var(--tl-border-light); display: flex; justify-content: space-between; gap: 10px; flex-shrink: 0; }
.tl-settings-actions .btn { flex-grow: 1; transition: transform 0.1s ease, filter 0.2s ease, background-color 0.2s ease; }
.tl-settings-actions .btn:hover { filter: brightness(90%); }
.tl-settings-actions .btn:active { transform: translateY(1px); filter: brightness(80%); }
#tl-settings-cancel { background-color: #6c757d; color: white !important; border: none; }
#tl-settings-cancel:hover { background-color: #5a6268; filter: brightness(100%); }
#tl-sim-new-messages-counter { width: auto; padding: 0 10px 0 8px; border-radius: 50px; }
#tl-sim-new-messages-counter .topiclive-counter { font-size: 13px; background-color: #007bff; border-radius: 50%; width: 24px; height: 24px; line-height: 24px; text-align: center; margin-right: 8px; display: inline-block; }
#tl-sim-new-messages-counter .topiclive-arrow { display: inline-flex; align-items: center; }
#tl-save-toast { position: fixed; top: 100px; left: 50%; transform: translate(-50%, -150%); background-color: rgba(40, 167, 69, 0.5); border: 1px solid #28a745; backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); color: white; padding: 10px 20px; border-radius: 8px; z-index: 10000; font-size: 14px; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.2); opacity: 0; visibility: hidden; transition: transform 0.4s ease, opacity 0.4s ease, visibility 0.4s; text-align: center; }
#tl-save-toast.show { transform: translate(-50%, 0); opacity: 1; visibility: visible; }
#tl-settings-modal.tl-dark-mode .tl-smartphone-screen { background: var(--tl-bg-dark); }
#tl-settings-modal.tl-dark-mode .tl-smartphone-header { background: var(--tl-header-bg-dark); color: var(--tl-header-text-dark); border-bottom-color: var(--tl-border-dark); }
#tl-settings-modal.tl-dark-mode .tl-settings-list { color: var(--tl-text-dark); }
#tl-settings-modal.tl-dark-mode .tl-settings-list li { border-bottom-color: var(--tl-border-dark); }
#tl-settings-modal.tl-dark-mode .tl-setting-separator { background: var(--tl-separator-bg-dark); color: #ccc; }
#tl-settings-modal.tl-dark-mode .tl-setting-icon { color: #ccc; }
#tl-settings-modal.tl-dark-mode .tl-settings-actions { border-top-color: var(--tl-border-dark); }
#tl-settings-modal.tl-dark-mode .tl-footer-link { color: #58a6ff; }
#tl-settings-modal.tl-dark-mode .tl-settings-footer { border-top-color: var(--tl-border-dark); }
#tl-settings-modal.tl-dark-mode #tl-li-list-swipe { background: #333; }
#tl-settings-modal.tl-dark-mode .tl-swipe-arrow { color: #ccc; }
.tl-settings-footer { padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--tl-border-light); flex-shrink: 0; }
.tl-footer-link { color: #007bff; cursor: pointer; font-size: 13px; text-decoration: none; display: flex; align-items: center; gap: 5px; }
.tl-footer-link:hover { text-decoration: underline; color: #ff7505 !important; }
#tl-changelog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; }
#tl-changelog-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 600px; max-height: 80vh; background: #2d2d2d; color: #f0f0f0; border: 1px solid #555; border-radius: 12px; box-shadow: 0 5px 25px rgba(0,0,0,0.5); z-index: 10001; display: flex; flex-direction: column; padding: 20px; }
#tl-changelog-modal h2 { margin-top: 0; color: #fff; border-bottom: 1px solid #555; padding-bottom: 10px; }
#tl-changelog-content { flex-grow: 1; overflow-y: auto; white-space: pre-wrap; font-family: monospace; background: #1e1e1e; padding: 15px; border-radius: 6px; }
#tl-changelog-close { margin-top: 20px; background-color: #6c757d; color: white; border: none; align-self: flex-end; }
.jvc-embed-container { margin: 10px 0; border-radius: 8px; overflow: hidden; background: #000; }
.jvc-embed-container.ratio-16-9 { position: relative; padding-bottom: 56.25%; height: 0; max-width: 640px; }
.jvc-embed-container.ratio-16-9 iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
.jvc-embed-container.tiktok-iframe-embed { max-width: 340px; margin-left: auto; margin-right: auto; }
.jvc-embed-container.tiktok-iframe-embed iframe { width: 100%; height: 760px; border: none; display: block; }
.jvc-embed-container.instagram-native-embed { max-width: 420px; }
.jvc-embed-container.instagram-iframe-embed { max-width: 540px; }`;
$('head').append(`<style>${menuCss}</style>`);
$('body').append(menuHtml);
$('body').append('<div id="tl-save-toast">Paramètres sauvegardés</div>');
this.$tl_settings_modal = $('#tl-settings-modal');
this.$tl_settings_overlay = $('#tl-settings-overlay');
const closeModal = () => { this.$tl_settings_modal.hide(); this.$tl_settings_overlay.hide(); };
this.$tl_settings_modal.on('click', closeModal);
this.$tl_settings_modal.find('.tl-smartphone-frame').on('click', function(e) { e.stopPropagation(); });
$('#tl-settings-cancel').on('click', closeModal);
$('#tl-settings-save').on('click', () => {
this.saveSettings();
closeModal();
const $toast = $('#tl-save-toast');
$toast.addClass('show');
setTimeout(() => { $toast.removeClass('show'); }, 2500);
});
$('#tl-setting-dark-mode').on('change', (e) => {
const isDarkMode = $(e.target).is(':checked');
this.$tl_settings_modal.toggleClass('tl-dark-mode', isDarkMode);
localStorage.setItem('topiclive_dark_mode', isDarkMode);
});
this.attachSettingsListeners();
const $changelogModal = $('#tl-changelog-modal');
const $changelogOverlay = $('#tl-changelog-overlay');
const closeChangelog = () => { $changelogModal.hide(); $changelogOverlay.hide(); };
$('#tl-show-changelog').on('click', () => this.fetchAndShowChangelog());
$('#tl-changelog-close').on('click', closeChangelog);
$changelogOverlay.on('click', closeChangelog);
// ── Bouton settings dans la navbar JVC ──
const attachSettingsButton = () => {
const targetContainers = document.querySelectorAll(
'.buttonsNavbar__sticky .buttonsNavbar'
);
targetContainers.forEach(container => {
// Évite de créer plusieurs boutons
if (container.querySelector('.tl-settings-button')) return;
const button = document.createElement('button');
button.type = 'button';
button.className = 'buttonsNavbar__button tl-settings-button';
button.title = 'Paramètres TopicLive+';
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg"
width="17.5"
height="17.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
class="buttonsNavbar__icon">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
<div class="buttonsNavbar__label">TopicLive</div>
`;
button.onclick = () => {
this.populateSettings();
this.$tl_settings_modal.show();
this.$tl_settings_overlay.show();
};
container.appendChild(button);
});
};
// ── Première tentative immédiate ──
attachSettingsButton();
// ── Surveillance des changements du DOM ──
const observer = new MutationObserver(() => {
attachSettingsButton();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// ── Filets de sécurité ──
setTimeout(attachSettingsButton, 500);
setTimeout(attachSettingsButton, 1500);
setTimeout(attachSettingsButton, 3000);
}
populateSettings() {
this.loadSettings();
this.tempOptions = { ...this.options };
$('#tl-setting-sound').prop('checked', this.tempOptions.sound);
$('#tl-setting-favicon').prop('checked', this.tempOptions.favicon);
$('#tl-setting-new-messages').prop('checked', this.tempOptions.newMessagesButton);
$('#tl-setting-show-counter').prop('checked', this.tempOptions.showCounterOnButton);
$('#tl-setting-quick-reply').prop('checked', this.tempOptions.quickReplyButton);
$('#tl-setting-list').prop('checked', this.tempOptions.listButton);
$('#tl-setting-list-swipe').prop('checked', this.tempOptions.listButtonSwipe);
$('#tl-setting-counter-topics').prop('checked', this.tempOptions.counterOnTopics);
$('#tl-setting-mobile-mode').prop('checked', this.tempOptions.mobileMode);
const isDarkMode = localStorage.getItem('topiclive_dark_mode') === 'true';
$('#tl-setting-dark-mode').prop('checked', isDarkMode);
this.$tl_settings_modal.toggleClass('tl-dark-mode', isDarkMode);
this.updateSimulatedButtons();
}
updateSimulatedButtons() {
$('#tl-sim-new-messages').toggleClass('tl-sim-disabled', !this.tempOptions.newMessagesButton);
$('#tl-sim-new-messages-counter').toggleClass('tl-sim-disabled', !this.tempOptions.showCounterOnButton);
$('#tl-sim-quick-reply').toggleClass('tl-sim-disabled', !this.tempOptions.quickReplyButton);
$('#tl-sim-list').toggleClass('tl-sim-disabled', !this.tempOptions.listButton);
$('#tl-li-list-swipe').toggle(this.tempOptions.listButton);
$('#tl-sim-counter-topics').toggleClass('tl-sim-disabled', !this.tempOptions.counterOnTopics);
}
attachSettingsListeners() {
$('#tl-setting-sound').off('change').on('change', (e) => { this.tempOptions.sound = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-favicon').off('change').on('change', (e) => { this.tempOptions.favicon = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-new-messages').off('change').on('change', (e) => { this.tempOptions.newMessagesButton = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-show-counter').off('change').on('change', (e) => { this.tempOptions.showCounterOnButton = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-quick-reply').off('change').on('change', (e) => { this.tempOptions.quickReplyButton = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-list').off('change').on('change', (e) => { this.tempOptions.listButton = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-list-swipe').off('change').on('change', (e) => { this.tempOptions.listButtonSwipe = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-counter-topics').off('change').on('change', (e) => { this.tempOptions.counterOnTopics = $(e.target).is(':checked'); this.updateSimulatedButtons(); });
$('#tl-setting-mobile-mode').off('change').on('change', (e) => { this.tempOptions.mobileMode = $(e.target).is(':checked'); });
}
saveSettings() {
this.options = { ...this.tempOptions };
Object.keys(this.options).forEach(key => {
localStorage.setItem(`topiclive_${key}`, this.options[key]);
});
this.applySettings();
}
applySettings() {
if (!this.options.favicon) this.favicon.maj('');
this.updateCounters();
this.$tl_quick_reply_button.toggle(this.options.quickReplyButton && !this.isForumPage);
this.$tl_forum_button.toggle(this.options.listButton && !this.isForumPage);
const wantCounter = this.isForumPage ? this.options.counterOnForums : this.options.counterOnTopics;
if (this.options.mobileMode) {
this.$tl_connected_counter.toggle(wantCounter);
if (this.$tl_connected_counter_clone) this.$tl_connected_counter_clone.hide();
} else {
this.$tl_connected_counter.hide();
if (this.$tl_connected_counter_clone) this.$tl_connected_counter_clone.toggle(wantCounter && !this.isForumPage);
}
}
fetchAndShowChangelog() {
const changelogUrl = 'https://raw.githubusercontent.com/moyaona/TopicLivePlus/main/Changelog.md';
const fallbackUrl = 'https://github.com/moyaona/TopicLivePlus/blob/main/Changelog.md';
const $modal = $('#tl-changelog-modal');
const $overlay = $('#tl-changelog-overlay');
const $content = $('#tl-changelog-content');
$overlay.show(); $modal.show();
if (this.changelogContent) { $content.text(this.changelogContent); return; }
$content.text('Chargement en cours...');
GM_xmlhttpRequest({
method: "GET", url: changelogUrl,
onload: (response) => { this.changelogContent = response.responseText; $content.text(this.changelogContent); },
onerror: () => { $content.html(`Le chargement a échoué. <a href="${fallbackUrl}" target="_blank" style="color:#007bff">${fallbackUrl}</a>`); }
});
}
toggleForumButtonState() {
const listIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="17.5" height="17.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`;
const replyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="17.5" height="17.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`;
this.forumButtonState = !this.forumButtonState;
this.$tl_forum_button.addClass('button-transitioning');
setTimeout(() => {
if (this.forumButtonState) {
this.$tl_forum_button.html(replyIconSvg);
this.$tl_forum_button.css('background-color', 'rgba(0, 123, 255, 0.3)');
} else {
this.$tl_forum_button.html(listIconSvg);
this.$tl_forum_button.css('background-color', 'rgba(22, 22, 22, 0.3)');
}
this.$tl_forum_button.removeClass('button-transitioning');
}, 150);
}
scrollToReplyForm() {
const $replyForm = $('#forums-post-message-editor');
if ($replyForm.length > 0) {
const targetScrollTop = $replyForm.offset().top - 100;
$('html, body').animate({ scrollTop: targetScrollTop }, 800, () => {
const $textarea = $('#message_topic');
if ($textarea.length > 0) $textarea.focus();
});
}
}
goToForumList() {
const forumIdMatch = window.location.href.match(/\/forums\/(?:42|1)-(\d+)-/);
if (forumIdMatch && forumIdMatch[1]) {
window.location.href = `https://www.jeuxvideo.com/forums/0-${forumIdMatch[1]}-0-1-0-1-0-0.htm`;
} else {
window.location.href = 'https://www.jeuxvideo.com/forums.htm';
}
}
initConnectedCounter()
{if (this.$tl_connected_counter) this.$tl_connected_counter.remove();
if (this.$tl_connected_counter_clone) this.$tl_connected_counter_clone.remove();
this.$tl_connected_counter = $('<div id="topiclive-connected-counter" class="topiclive-floating-button tl-counter-button"></div>');
this.$tl_connected_counter_clone = $('<div class="topiclive-floating-button tl-counter-button"></div>');
this.$tl_connected_counter.addClass('jvchat-hide');
this.$tl_connected_counter_clone.addClass('jvchat-hide');
$('body').append(this.$tl_connected_counter);
$('body').append(this.$tl_connected_counter_clone);
setTimeout(() => {
const nb = $('.userCount__number').first().text().trim();
if (nb) {
this.$tl_connected_counter.text(nb);
this.$tl_connected_counter_clone.text(nb);
}
}, 200);
}
initPartialQuoteSystem() {
const buttonCSS = `
#tl-partial-quote-button { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEVJREFUeNpiYBgFFANGbIL/gQCuAAhwiYEAE6UuoI0BSE78gE8MpwFA7yZAmRvwiTHg0NwAxOeBuACIFXCJjQIqAoAAAwDEvS2y79EjywAAAABJRU5ErkJggg==) no-repeat; background-color: rgb(3, 94, 191); background-position: -1px -1px; border: 0; border-bottom: solid 2px rgb(2, 63, 128); border-radius: 2px; box-sizing: content-box; cursor: pointer; height: 16px; width: 16px; padding: 0; position: absolute; display: none; z-index: 1001; transform: translateX(-50%); }
#tl-partial-quote-button.active { display: block; animation: tl-quote-pop 0.2s ease-out; }
#tl-partial-quote-button:after { content: ""; position: absolute; width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 8px solid rgb(3, 94, 191); top: -8px; left: 50%; transform: translateX(-50%); }
@keyframes tl-quote-pop { 0% { transform: translateX(-50%) scale(0.8); opacity: 0; } 70% { transform: translateX(-50%) scale(1.1); opacity: 1; } 100% { transform: translateX(-50%) scale(1.0); } }`;
$('head').append(`<style>${buttonCSS}</style>`);
this.$partialQuoteButton = $('<button id="tl-partial-quote-button"></button>').appendTo('body');
$(document).on('pointerdown', (e) => {
if (!$(e.target).is('#tl-partial-quote-button')) this.$partialQuoteButton.removeClass('active');
});
}
updateDesktopButtonPosition() {
if (!this.$tl_button || !this.$tl_forum_button || !this.$tl_quick_reply_button) return;
const scrollButtonBottom = 25;
const listButtonBottom = scrollButtonBottom + 35 + 8;
const replyButtonBottom = listButtonBottom + 35 + 8;
const $container = $('.conteneur-messages-pagi, .conteneur-topic-pagi, .container__main');
if (!$container.length) return;
const basePositionRight = Math.round(
$container.offset().left + $container.outerWidth() + 15
);
if (this.$tl_connected_counter_clone) {
this.$tl_connected_counter_clone.css({
position: 'fixed',
bottom: `${replyButtonBottom + 43}px`,
left: `${basePositionRight}px`,
right: 'auto',
top: 'auto',
transform: 'translateX(0)'
});
}
this.$tl_button.css({
position: 'fixed',
bottom: `${scrollButtonBottom}px`,
left: `${basePositionRight}px`,
right: 'auto',
top: 'auto'
});
this.$tl_forum_button.css({
position: 'fixed',
bottom: `${listButtonBottom}px`,
left: `${basePositionRight}px`,
right: 'auto',
top: 'auto'
});
this.$tl_quick_reply_button.css({
position: 'fixed',
bottom: `${replyButtonBottom}px`,
left: `${basePositionRight}px`,
right: 'auto',
top: 'auto'
});
if (this.$tl_connected_counter) {
this.$tl_connected_counter.css({
position: 'fixed',
top: '60px',
right: '20px',
left: 'auto',
bottom: 'auto'
});
}
}
updateCounters() {
if (this.isStandby || this.isForumPage) return;
if (this.isBlocked) {
const $counter = this.$tl_button.find('.topiclive-counter');
const cloudflareLogo = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTkuMzUgMTAuMDRDMTguNjcgNi45OSAxNS42NCA0IDEyIDRDOS4xMSA0IDYuNiA1LjY0IDUuMzUgOC4wNEMyLjM0IDguMzYgMCAxMC45MSAwIDE0QzAgMTcuMzEgMi42OSAyMCA2IDIwSDE5QzIxLjc2IDIwIDI0IDE3Ljc2IDI0IDE1QzI0IDEyLjM2IDIxLjk1IDEwLjIyIDE5LjM1IDEwLjA0WiIgZmlsbD0iI0Y0ODAyMiIvPjwvc3ZnPg==';
$counter.html('').css({ 'background-color': '#ffffff', 'background-image': `url("${cloudflareLogo}")`, 'background-size': '16px 16px', 'background-repeat': 'no-repeat', 'background-position': 'center' });
this.$tl_button.addClass('has-unread-messages').fadeIn();
return;
}
if (this.is410) {
const $counter = this.$tl_button.find('.topiclive-counter');
const errorIcon16 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAALEUExURUxpcf7ihv/0oeuVNfK1Xv7/wf//6uqPMPS3WvzRff/Vc/nOfv/sh//Pbf3WdEI9Rv7iiP7ce//2rv/Nb/zknv/bevutMv/gkv/wrPzgmfbFcvrXjPzkmv7xq//hf//skP3cgv/mhfGvVP/shF1KOXZNMP/Xff3poP/FYf/AXPmaDf/rhO2ePa+Viv/legAPPf62SvvQd/6NFS0rN/SkPfvZiNp0FoZFD7VUFf7ecf6UGP+fIur//xwYIvOIFuiKH+yIGY6Ki59rPi8rM/+dG1JPXXpwY56hqtyxZ+eQKfvTdURDUEZEUF5cZ/n39pZaHU1JUvnWgv7caIqHjXh5hTk4Q9t7F3dzd0FEVTw6RZViOpaUlpqYm/2QE+aGGEkyJI1pTE9MUtTT02daUyorOX99gPSMGBYVIv+REfCLGP+mDPmLFLFuJWRfYuqHFko3LuKEGEZCSf3ajf71s//4uP/riP/qhv/Zdf/lg//efP/1t//qn/3Sb//ll926b/vGbf/NavfWdP/0o/3iesiaXP/pgv/ddP/XcNSfV/q3VvK4VuvDav7uov/BUv/jeP/fdMSUV//aaLNpGv/rkP3GS/+ySN6QH+OvR//faP/ecP/HNP+yJ+aqMf2jIf66KfS3IvmZJKlnG7FiDfzigP/GKNqIGv6UFumNK2tna/+wJvC9Qv/AJuq0OCMiMP+nIvnOX/6kIpSGaC8wR//mdOyQGZ+WkkpLW4d/eO6dGy8sNsybN7qNPfilII2QnHhoXe3s7NbX24JqULO5x519UKCenyktQ66HSjk1Pv6TFG1rcLSwrj9EXS8qL83MzSEeJ8vKzmpmayMfKGRpfjo/TpWYpyomLY6OltDPzzQxOUFAU3x6fmxwfI6KjRscKZORlWpoctTT1Lu9wpqYm42Lj/+YHnJucp6eojc0PS8rMjU0PtiFJPONGDdGz4cAAABxdFJOUwCN+hdmAgIBAwL97P38aAIDY/392/4V/v7bdLu77P78cf2R/QIDAuX9+hj8agf9HP1ybK/nthuaGv7w/AHz4Ha9/uHY/aL9/LX+2kmW/vz9ytr+/fxveP7+bPz8+P7A/v3++/6I/r79AdwWdeb93X3+CABJWgAAAAlwSFlzAAALEwAACxMBAJqcGAAAARtJREFUGNMBEAHv/gAAABAAFiAcFBkbGgMACAcAAAAFABEdehh7fRdyCwQACQAABgABdBIfIR4VCg0TfyIAJgAADnMCDHV4eXd8gCgpiSwAACongiN2foGGh4iKjTCVNDYAMYwrhYOEi5GUkpabnTugPwA1ky6Oj5CXmp6ipamwRKZAAFGjOZmYnJ+kq7i8rrRJpz4ASrNSr6qsurvGw8G+tU22QgBIsUa30NnU0svCxb3AU61QAEWyVM/W3+Da2M3Hyb9XcUMATLlb197n4+LV3MzEyk6oPQBV0VxiZejm4d3b01hHQXEzAEvOYeXpZ21jX2BaT6E3Ly0APFlkb2zqcOvkXcg6MgAAJAAAAGgAamtmaW5eVjgAJQ8Ah7RsxA/wK1MAAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII=';
$counter.html('').css({ 'background-color': '#FFffff', 'background-image': `url("${errorIcon16}")`, 'background-size': '16px 16px', 'background-repeat': 'no-repeat', 'background-position': 'center' });
this.$tl_button.addClass('has-unread-messages').fadeIn();
return;
}
let countText = '';
if (this.nvxMessages > 0) countText = this.nvxMessages > 99 ? '99+' : `${this.nvxMessages}`;
if (this.options.favicon) this.favicon.maj(countText);
if (this.nvxMessages > 0) {
if (this.options.showCounterOnButton) {
this.$tl_button.find('.topiclive-counter').text(countText).css({ 'background-color': '#007bff', 'background-image': 'none' });
this.$tl_button.addClass('has-unread-messages').fadeIn();
} else {
this.$tl_button.fadeOut();
}
} else if (!this.isChatModeActive) {
if (this.options.newMessagesButton) { this.$tl_button.removeClass('has-unread-messages').fadeIn(); } else { this.$tl_button.fadeOut(); }
} else {
this.$tl_button.fadeOut();
}
}
markAllAsRead() {
this.nvxMessages = 0;
this.unreadMessageAnchors = [];
this.updateCounters();
}
addUnreadAnchor($message) {
this.unreadMessageAnchors.push($message);
}
initStatic() {
this.favicon = new Favicon();
this.son = new Audio('https://github.com/moyaona/TopicLivePlus/raw/refs/heads/main/notification_sound_tl.mp3');
this.suivreOnglets();
this.initScrollButton();
this.initForumListButton();
this.initQuickReplyButton();
this.initConnectedCounter();
this.initPartialQuoteSystem();
if (window.location.pathname.startsWith('/forums/0-')) {
this.initSettingsMenu();
}
this.initOtherScriptObserver();
this.init();
addEventListener('instantclick:newpage', this.init.bind(this));
$("head").append(`<style type='text/css'>.topiclive-loading:after { content: ' ○' }.topiclive-loaded:after { content: ' ●' }</style>`);
}
jvCake(classe) {
const base16 = '0A12B34C56D78E9F';
let lien = '';
const s = classe.split(' ')[1];
for (let i = 0; i < s.length; i += 2) {
lien += String.fromCharCode(base16.indexOf(s.charAt(i)) * 16 + base16.indexOf(s.charAt(i + 1)));
}
return lien;
}
_getCurrentForumId() {
const match = window.location.pathname.match(/forums\/(?:1|42)-(?<forumid>[0-9]+)-/);
return match ? match.groups.forumid : null;
}
alert(message) {
try { modal('erreur', { message }); } catch (err) { alert(message); }
}
loop() {
if (this.isStandby || this.isBlocked || this.is410) return;
if (typeof this.idanalyse !== 'undefined') window.clearTimeout(this.idanalyse);
let duree = this.ongletActif ? 5000 : 10000;
this.oldInstance = this.instance;
this.idanalyse = setTimeout(this.charger.bind(this), duree);
}
majUrl(page) {
if (this.estMP) return;
// La pagination JVC étant injectée côté client, on détecte le débordement
// par comptage : si le fetch renvoie 20 posts et qu'on en connaît déjà 20,
// on passe à la page suivante sans vider les messages.
const POSTS_PAR_PAGE = 20;
const nvMsgsCount = page.trouver(`${TL.class_msg}:not(.msg-pseudo-blacklist)`).length;
const testUrl = this.url.split('-');
const pageActuelle = parseInt(testUrl[3], 10) || 1;
if (nvMsgsCount >= POSTS_PAR_PAGE && this.messages.length >= POSTS_PAR_PAGE) {
const pageNext = pageActuelle + 1;
testUrl[3] = pageNext;
this.url = testUrl.join('-');
return;
}
const $bouton = page.trouver('.pagination__item--last:not(.pagination__item--disabled)');
const numPage = page.trouver(`${this.class_num_page}:first`).text();
if ($bouton.length > 0) {
const nouvelleUrl = $bouton.attr('href') || '';
if (nouvelleUrl && nouvelleUrl !== this.url) {
this.url = nouvelleUrl;
}
} else if (testUrl[3] != numPage) {
testUrl[3] = numPage;
this.url = testUrl.join('-');
}
}
suivreOnglets() {
document.addEventListener('visibilitychange', () => { this.ongletActif = !document.hidden; });
}
handleCloudflareBlock() {
if (this.isBlocked) return;
this.isBlocked = true;
window.clearTimeout(this.idanalyse);
this.showCloudflareBanner();
this.favicon.setCloudflareIcon();
this.updateCounters();
}
showCloudflareBanner() {
const bannerId = 'tl-cloudflare-banner';
if (document.getElementById(bannerId)) return;
const bannerCSS = `#${bannerId} { display: flex; align-items: center; justify-content: center; position: fixed; top: 25%; width: auto; background-color: rgba(22, 22, 22, 0.5); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); color: #FFFFFF; text-align: center; padding: 15px 25px; font-size: 16px; font-weight: bold; z-index: 99999; border-radius: 8px; border: 1px solid #F48022; box-shadow: 0 5px 15px rgba(0,0,0,0.3); opacity: 0; visibility: hidden; transition: opacity 0.4s ease-out; } #${bannerId}.visible { opacity: 1; visibility: visible; } #${bannerId} svg { width: 24px; height: 24px; margin-right: 15px; flex-shrink: 0; }`;
$('head').append(`<style type='text/css'>${bannerCSS}</style>`);
const cloudflareLogoSVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4C9.11 4 6.6 5.64 5.35 8.04C2.34 8.36 0 10.91 0 14C0 17.31 2.69 20 6 20H19C21.76 20 24 17.76 24 15C24 12.36 21.95 10.22 19.35 10.04Z" fill="#F48022"/></svg>`;
const $banner = $(`<div id="${bannerId}">${cloudflareLogoSVG}<span>Cloudflare : Actualisez la page pour effectuer la vérification</span></div>`);
$('body').prepend($banner);
const positionBanner = () => {
if ($(window).width() < 567) {
$banner.css({ 'left': '50%', 'transform': 'translate(-50%, -50%)' });
} else {
const $container = $('.conteneur-messages-pagi, .container__main');
if ($container.length > 0) {
const bannerLeft = $container.offset().left + ($container.outerWidth() / 2) - ($banner.outerWidth() / 2);
$banner.css({ 'left': bannerLeft + 'px', 'transform': 'translateY(-50%)' });
}
}
};
positionBanner();
requestAnimationFrame(() => { $banner.addClass('visible'); });
$(window).off('resize.cfbanner').on('resize.cfbanner', positionBanner);
}
handle410Error() {
if (this.is410) return;
this.is410 = true;
window.clearTimeout(this.idanalyse);
this.show410Banner();
this.favicon.set410Icon();
this.updateCounters();
}
show410Banner() {
try {
const bannerId = 'tl-410-banner';
if (document.getElementById(bannerId)) return;
const bannerCSS = `#${bannerId} { display: flex; align-items: center; justify-content: center; position: fixed; top: 25%; width: auto; background-color: rgba(22, 22, 22, 0.5); backdrop-filter: blur(3px); color: #FFFFFF; text-align: center; padding: 15px 25px; font-size: 16px; font-weight: bold; z-index: 99999; border-radius: 8px; border: 1px solid #FFFFFF; opacity: 0; visibility: hidden; transition: opacity 0.4s ease-out; } #${bannerId}.visible { opacity: 1; visibility: visible; } #${bannerId} .tl-410-icon { width: 24px; height: 24px; margin-right: 15px; flex-shrink: 0; }`;
$('head').append(`<style type='text/css'>${bannerCSS}</style>`);
const errorIcon24 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAACeVBMVEUAAAD8+/siHib7lxv+1mk6OEUmIiv+pyNHR1b+43r+uCksKTT8ixf+3HL+64v/6YT+wS00Mjz+44MYFBz+03P+23v+zGtVVmZCQkz+w1vqiBSsWgv++sv/8JHp6Oj/niD944rX1tf+7JH+3YIwLjf+yWP+xGPGllf+u1QdGSLtkxbleRb9vFr9s0uXlpmIhopVU1n++8T+9MO2tbdoaHPBiUrGxsdAPUnkdgv+1Xp3dXn+87z+87Wnpqn8oR1oZWr97Kv8zHFXWnD/0V1XSUXqpCf98pv65JPInmJLTWL+q0P/xzvoiCTx8O/96rT+/KL+7Jnys1LDjFD97br886L724z/yEXohzX+/qr866L93ZxeYHJxbnFPUmbqm1X9tlP9qjbomijHdhTogg3Pz9H+9smurbL984v0wHugg4SBiZX7zIX1034ZHCpkXF/jtV3Wplqaelr2t1b6r0r2qUJrU0HXlDXajDPlfijHeSPZihoRDRTU0c7++7PDubP85Kv625T904p5eoWKgnn1zHL/xWr2xWmSeGLzq1RSTlPLkE7ntEcvMkCUZDtDOzt4VDq8eznqpTjrlzXLgzS4dCnYfiTNiRrafxe4axPm4t7++r6zqqSlnJf3ypb1woz204jyuoLrsn713H2Ui3z9ynvwzXnprXf203O2oHL1yXGql2/dvGzyr2x6cmvTqmXlu2Lzq2G9l17LoFnXsFaqflHwnE3rlUfrq0WodzOrcC63bSW3ZQ6qoJ2RjI+lmHXatnDNsG2Zg2fywmTzqVuRb1Z0XVGieUvyp0jpikFkT0HaCA+lAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEwAACxMBAJqcGAAABO5pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMS1jMDAxIDc5LjE0NjI4OTksIDIwMjMvMDYvMjUtMjA6MDE6NTUgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCAyNS4zIChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjUtMDktMDJUMDE6NTY6NDUrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDI1LTA5LTAyVDAzOjMzOjQxKzAyOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDI1LTA5LTAyVDAzOjMzOjQxKzAyOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpiMjYyYmUwOC04OWExLTE1NGItOGZhYS1mOTVmNDcxNjE1ZmEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6YjI2MmJlMDgtODlhMS0xNTRiLThmYWEtZjk1ZjQ3MTYxNWZhIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6YjI2MmJlMDgtODlhMS0xNTRiLThmYWEtZjk1ZjQ3MTYxNWZhIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMjYyYmUwOC04OWExLTE1NGItOGZhYS1mOTVmNDcxNjE1ZmEiIHN0RXZ0OndoZW49IjIwMjUtMDktMDJUMDE6NTY6NDUrMDI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4zIChXaW5kb3dzKSIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6AltmBAAAB9klEQVQokV1Sy2vTcBx9n883SX8k7cziZsBOLZQyWzA6x+bJoghDmBuIDBSdlyLozjuJA0X8A7yIJw+ioggDL8p2mAdRROpFq45NqtUdxmraNbi0abN4cCviuzweDx68xwO2Md2789jFhx1Jf2nqZ3mtFwmgPjQxCAAQAID00We5HSW3sva1S7zJv+gY4+W1jVIxUm/1/dr9diKIftmKGi9kliuQIBCgliTHuDUGCKB/w61UQewqQa2RJMdo1/uLYKRzPVgn6YKrNWrdY3DQlt+nAUb74+c6eQ/mH1VdXBvZZ9sl/dOPUQhkexbhQQvsAkYGZgY+AN8TrxyHp7HgeZBi0bimLZQBwJSA1Um6eseF5AKHlgAPetUH2kAkznUXLQCnr8Dz4Ff87UmkeAtwc+G5OQD+ubOzi8V1GVB1ZuDS85OroVBjc5OWn9x7nWg2I749JS6XWn335yMrzTOxcuC/pGAl2upytRRgDsukWnI4mSTdolRqF1mqopqALBPtyasKHVBUnRQ+ZdFTi1VCCvJvNbQEvwk5ZpNud8e/ye0IoChElGeDdGI+bHCO6PxtxQQUNoYOMrNJN5mt/YqqD58AAKhsWMzMiq7K4Zm7mpY5stUxbA4WbgghhBCapmUy1/89A0Yr7wDguLP3Mf7DbDabzU525B+VgKLP+4NhawAAAABJRU5ErkJggg==';
const $banner = $(`<div id="${bannerId}"><img class="tl-410-icon" src="${errorIcon24}" alt="Erreur 410"><span>410 : Topic censuré par la liberté d'expression</span></div>`);
$('body').prepend($banner);
const positionBanner = () => {
if ($(window).width() < 567) {
$banner.css({ 'left': '50%', 'transform': 'translate(-50%, -50%)' });
} else {
const $container = $('.conteneur-messages-pagi, .conteneur-topic-pagi, .container__main');
if ($container.length > 0) {
const bannerLeft = $container.offset().left + ($container.outerWidth() / 2) - ($banner.outerWidth() / 2);
$banner.css({ 'left': bannerLeft + 'px', 'transform': 'translateY(-50%)' });
}
}
};
positionBanner();
requestAnimationFrame(() => { $banner.addClass('visible'); });
$(window).off('resize.410banner').on('resize.410banner', positionBanner);
} catch (err) {
console.error("[TopicLive+] ERREUR DANS show410Banner:", err);
}
}
GET(cb) {
if (this.isLoading) return;
this.isLoading = true;
const blocChargement = $('#bloc-formulaire-forum .titre-bloc');
blocChargement.addClass('topiclive-loading');
window.clearTimeout(this.idanalyse);
$.ajax({
type: 'GET', url: this.url, timeout: 5000,
success: (data, textStatus, jqXHR) => {
const responseText = jqXHR.responseText;
if (responseText.includes('id="cf-challenge-form"') || responseText.includes('<title>Just a moment...</title>')) {
TL.handleCloudflareBlock(); return;
}
if (this.oldInstance != this.instance) return;
blocChargement.removeClass('topiclive-loading').addClass('topiclive-loaded');
cb($(responseText.substring(responseText.indexOf('<!DOCTYPE html>'))));
this.updateActionsMapFromHtml(responseText);
setTimeout(() => { blocChargement.removeClass('topiclive-loaded'); }, 100);
TL.loop();
},
error: (jqXHR) => {
if (jqXHR.status === 403 && jqXHR.responseText.includes('Cloudflare')) { TL.handleCloudflareBlock(); return; }
if (jqXHR.status === 410) { TL.handle410Error(); return; }
TL.loop();
},
complete: () => { this.isLoading = false; }
});
}
async updateActionsMapFromHtml(html) {
try {
const match = html.match(/forumsAppPayload\s*=\s*["']?([^"']+)["']?/);
if (!match || !match[1]) return;
const binaryString = atob(match[1]);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
const decompressed = await new Response(stream).text();
const payload = JSON.parse(decompressed);
TL.isModerator = !!payload.topicActions?.showCheckbox;
if (payload && payload.listMessage) {
if (!this.messagesActionsMap) this.messagesActionsMap = {};
if (!this.messagesTextMap) this.messagesTextMap = {};
for (const msg of payload.listMessage) {
this.messagesActionsMap[msg.id] = msg.actions;
this.messagesTextMap[msg.id] = msg.text;
}
}
if (payload?.forumInfo?.header?.btnVal !== undefined) {
const nb = payload.forumInfo.header.btnVal;
const wantCounter = this.isForumPage ? this.options.counterOnForums : this.options.counterOnTopics;
if (this.options.mobileMode) {
// Mode Mobile : ancien compteur flottant, en haut, sur topic ET liste
if (this.$tl_connected_counter_clone) this.$tl_connected_counter_clone.hide();
if (this.$tl_connected_counter) {
if (wantCounter) { this.$tl_connected_counter.text(nb).show(); } else { this.$tl_connected_counter.hide(); }
}
} else {
// Mode Web : rien sur la liste, compteur en bas sur un topic
if (this.$tl_connected_counter) this.$tl_connected_counter.hide();
if (this.isForumPage) {
if (this.$tl_connected_counter_clone) this.$tl_connected_counter_clone.hide();
} else if (this.$tl_connected_counter_clone) {
if (wantCounter) { this.$tl_connected_counter_clone.text(nb).show(); } else { this.$tl_connected_counter_clone.hide(); }
}
}
$('.userCount__number').text(nb);
}
} catch (e) {
console.error('[TopicLive+] updateActionsMapFromHtml:', e);
}
}
loopForum() {
if (this.isStandby) return;
if (typeof this.idForumAnalyse !== 'undefined') window.clearTimeout(this.idForumAnalyse);
let duree = this.ongletActif ? 15000 : 30000;
this.oldInstance = this.instance;
this.idForumAnalyse = setTimeout(this.chargerForum.bind(this), duree);
}
pollConnectedCounter() {
const instanceAuLancement = this.instance;
const executer = () => {
if (instanceAuLancement !== this.instance) return;
if (this.isStandby) {
this.idCounterPoll = setTimeout(executer, 15000);
return;
}
$.ajax({
type: 'GET',
url: this.url,
timeout: 5000,
success: (data, textStatus, jqXHR) => {
if (instanceAuLancement !== this.instance) return;
this.updateActionsMapFromHtml(jqXHR.responseText);
},
complete: () => {
if (instanceAuLancement !== this.instance) return;
this.idCounterPoll = setTimeout(executer, 15000);
}
});
};
executer();
}
chargerForum() {
if (this.isLoading) return;
if (this.oldInstance != this.instance) return;
this.isLoading = true;
$.ajax({
type: 'GET', url: this.url, timeout: 5000,
success: (data, textStatus, jqXHR) => {
if (this.oldInstance != this.instance) return;
this.updateActionsMapFromHtml(jqXHR.responseText);
this.loopForum();
},
error: () => { if (this.oldInstance == this.instance) this.loopForum(); },
complete: () => { this.isLoading = false; }
});
}
scanForumPageAndUpdate($page) {
const connectesText = $page.find('.nb-connect-fofo').text().trim();
if (connectesText && this.$tl_connected_counter) {
this.$tl_connected_counter.text(connectesText.split(' ')[0]);
this.$tl_connected_counter_clone
.text(connectesText.split(' ')[0])
.show();
}
}
}
var TL = new TopicLive();
TL.initStatic();Editor is loading...
Leave a Comment