PoonBrosAreValid
kiwifarms.net
- Joined
- Sep 15, 2023
I've been working on another userscript! (Thanks @Sicko Hunter for the idea)
This one allows you to hover over chatters avatars and get a live preview of their profile.
Give it a try using Tampermonkey or Greasemonkey (Kiwis have reported bugs with violent monkey on my previous scripts). Report any bugs / improvements here.
If you like this script, you may also like my Infinite Scroll Script that allows you to have a Twitter / Instagram like experience. Also checkout my Thread Summary Script that allows you to see all the thread highlights in a twitter like scroll experience.
Edit: Fix bug that would sometimes not load preview properly
Edit: Better error handling
Edit: More accurate positioning of popup
Edit: Fixed bug of preview not showing when loading speed is slow
Edit: Updated the styling and made it more stable and less buggy
This one allows you to hover over chatters avatars and get a live preview of their profile.
Give it a try using Tampermonkey or Greasemonkey (Kiwis have reported bugs with violent monkey on my previous scripts). Report any bugs / improvements here.
JavaScript:
// ==UserScript==
// @name Kiwifarms Chat Profile Preview
// @namespace http://tampermonkey.net/
// @version 2025-09-01
// @description Adds a preview popup for chat users
// @author PoonBrosAreValid
// @match https://kiwifarms.st/chat/*
// @match https://kiwifarms.st/
// @icon https://kiwifarms.st/styles/custom/logos/kiwi_square.og.png
// @grant none
// ==/UserScript==
const BACKGROUND_COLOR = "#40444B"
const BACKGROUND_COLOR_ERROR = "#660000"
const TEXT_COLOR_MUTED = "#b3b3b3"
const state = {
previewCache: {},
isPreviewLoading: false,
mouseX: 0,
mouseY: 0,
}
const ui = {
/**
* The iframe that contains the chat
* @return {HTMLIFrameElement}
*/
get frame() {
const shim = document.querySelector("#rust-shim")
if (! shim) {
throw new Error("Chat hasn't loaded")
}
return shim
},
/**
* The document element within the chat iframe
* (note: this has its own coordinates that must be calculated for)
* @return {Window.Document}
*/
get document() {
return this.frame.contentWindow.document
},
/**
* The `main` element in the chat frame
* @return {HTMLElement}
*/
get chatMain() {
return this.document.querySelector("#chat")
},
/**
* Get or create an element used as the preview popup
* @return {HTMLDivElement}
*/
get profilePreview() {
const got = document.querySelector(".profile-header-preview")
if (got) {
return got
}
const preview = document.createElement("div")
preview.classList.add("profile-header-preview")
preview.innerText = "Loading..."
return preview
},
/**
* @return {HTMLDivElement}
*/
buildArrow(color = BACKGROUND_COLOR) {
const arrow = document.createElement("div")
arrow.style.position = 'absolute';
arrow.style.bottom = '-10px';
arrow.style.left = '20px';
arrow.style.width = '0';
arrow.style.height = '0';
arrow.style.borderLeft = '10px solid transparent';
arrow.style.borderRight = '10px solid transparent';
arrow.style.borderTop = `10px solid ${color}`
return arrow
},
/**
* @return {HTMLDivElement}
*/
buildPreview() {
const preview = this.profilePreview
preview.innerText = "Loading..."
preview.style.backgroundColor = BACKGROUND_COLOR
preview.style.padding = "10px"
preview.style.borderRadius = "10px"
preview.style.position = "absolute"
preview.style.zIndex = "10000"
preview.style.maxWidth = "400px"
preview.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)"
// Position initially - will be repositioned after content loads
this.positionPreview(preview)
return preview
},
hideProfileHeader({ignoreLoading} = {ignoreLoading: false}) {
if (state.isPreviewLoading && !ignoreLoading) return;
ui.profilePreview.remove()
},
/** Add content to the preview */
setPreviewContent(preview, content, color) {
preview.innerText = ""
preview.append(content, this.buildArrow(color))
preview.style.backgroundColor = color
// Reposition after content is added
this.positionPreview(preview)
},
/** Position preview near mouse with proper bounds checking */
positionPreview(preview) {
// Ensure preview is in DOM to get dimensions
if (!preview.parentNode) {
document.body.appendChild(preview)
}
// Get current dimensions
const previewRect = preview.getBoundingClientRect()
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
const scrollY = window.pageYOffset
const scrollX = window.pageXOffset
let x = state.mouseX + 15 // Small offset from cursor
let y = state.mouseY - previewRect.height - 15 // Above cursor
// Boundary checking - keep preview on screen
if (x + previewRect.width > windowWidth + scrollX) {
x = state.mouseX - previewRect.width - 15 // Move to left of cursor
}
if (x < scrollX) {
x = scrollX + 10 // Keep some margin from edge
}
if (y < scrollY) {
y = state.mouseY + 15 // Below cursor if no room above
}
if (y + previewRect.height > windowHeight + scrollY) {
y = windowHeight + scrollY - previewRect.height - 10
}
preview.style.left = x + "px"
preview.style.top = y + "px"
// Adjust arrow position based on preview position relative to mouse
const arrow = preview.querySelector('div[style*="border"]')
if (arrow) {
const previewLeft = parseInt(preview.style.left)
const arrowLeft = Math.max(10, Math.min(previewRect.width - 30, state.mouseX - previewLeft))
arrow.style.left = arrowLeft + "px"
// Flip arrow if preview is below cursor
if (y > state.mouseY) {
arrow.style.bottom = "auto"
arrow.style.top = "-10px"
arrow.style.borderTop = "none"
arrow.style.borderBottom = `10px solid ${preview.style.backgroundColor}`
}
}
}
}
const chat = {
buildUserUrl(id) {
return `${location.origin}/members/${id}`
},
/** Formatted like "chat-activity-{{USER_ID}}" */
urlFromActivityId(activityId) {
const userId = activityId.replace('chat-activity-', '')
return this.buildUserUrl(userId)
},
/** Formatted like "/blah/blah/blah/{{USER_ID}}.jpg" */
urlFromMessageAvatarSrc(src) {
const path = new URL(location.origin + src).pathname
const pieces = path.split('/')
const userId = pieces[pieces.length - 1].replace('.jpg', '')
return this.buildUserUrl(userId)
},
/** Create and click a fake link to avoid popup warnings */
openProfile(url) {
const fakeLink = document.createElement('a')
fakeLink.href = url
fakeLink.target = '_blank'
fakeLink.click()
},
/** Get a little preview element of a users profile
* This is just a trimmed version of the memebers page
* @param {string} url
*/
async fetchProfileHeader(url) {
const cached = state.previewCache[url]
if (cached) return { fromCache: true, content: cached.cloneNode(true)}
const res = await fetch(url);
if (res.status === 203) {
throw new Error(`Reload to solve KiwiFlare...`);
} else if (res.status !== 200) {
throw new Error(`Page responded error ${res.status}`);
}
const text = await res.text()
const dummyElement = document.createElement("div")
dummyElement.innerHTML = text
const header = dummyElement.querySelector(".memberHeader")
if (!header) {
throw new Error("Could not find member header")
}
const messageCountEl = header.querySelector(".memberHeader-stats .fauxBlockLink dd a.fauxBlockLink-linkRow")
const messageCount = messageCountEl ? messageCountEl.innerText : "N/A"
const mainContent = header.querySelector(".memberHeader-mainContent")
if (!mainContent) {
throw new Error("Could not find member content")
}
// Delete report button
for (const btn of mainContent.querySelectorAll(".buttonGroup")) {
const btnText = btn.querySelector(".button-text")
if (btnText && btnText.innerText.trim() === "Report") {
btn.remove()
}
}
const messagesContainer = document.createElement("span")
const messageLabel = document.createElement("span")
messageLabel.style.color = TEXT_COLOR_MUTED
messageLabel.innerText = "Messages: "
messagesContainer.append(messageLabel, messageCount)
mainContent.append(messagesContainer)
state.previewCache[url] = mainContent.cloneNode(true)
return { fromCache: false, content: mainContent }
},
async displayProfileHeader(url) {
if (state.isPreviewLoading) {
return
}
ui.profilePreview.remove()
state.isPreviewLoading = true
const preview = ui.buildPreview()
preview.innerText = "Loading..."
preview.append(ui.buildArrow())
document.body.append(preview)
try {
const {content, fromCache} = await this.fetchProfileHeader(url)
// Always rebuild preview to ensure proper positioning
ui.profilePreview.remove()
const newPreview = ui.buildPreview()
newPreview.innerText = "Loading..."
newPreview.append(ui.buildArrow())
document.body.append(newPreview)
ui.setPreviewContent(newPreview, content, BACKGROUND_COLOR)
} catch (error) {
const content = document.createElement("div")
content.innerText = error.toString()
ui.setPreviewContent(preview, content, BACKGROUND_COLOR_ERROR)
}
state.isPreviewLoading = false
},
addAvatarClick(avatar, url) {
if (avatar.dataset.hasListener) return
const link = document.createElement('a')
link.href = url
link.target = '_blank'
avatar.classList.add('clickable-avatar')
avatar.parentElement.prepend(link)
link.append(avatar)
avatar.dataset.hasListener = "true"
},
addAvatarHover(avatar, url) {
if (avatar.dataset.hasHoverListener) return;
avatar.addEventListener("mouseenter", () => this.displayProfileHeader(url))
avatar.addEventListener("mouseleave", () => ui.hideProfileHeader())
avatar.dataset.hasHoverListener = "true"
},
observe(selector, callback) {
const observerOptions = {
childList: true,
subtree: true,
}
const elem = ui.document.querySelector(selector)
if (!elem) return
const observer = new MutationObserver(callback, observerOptions)
observer.observe(elem, observerOptions)
},
}
function onMembersChange(event) {
const unfilledLinks = ui.document.querySelectorAll('.chat .activity a:not([href])')
for (const link of unfilledLinks) {
const activityId = link.parentElement.id
const profileUrl = chat.urlFromActivityId(activityId)
link.href = profileUrl
link.style.textDecoration = 'none'
link.style.color = "white"
link.target = '_blank'
const username = link.innerText
link.title = `Go to ${username}'s profile`
const avatar = link.parentElement.querySelector('.avatar')
if (avatar) {
chat.addAvatarClick(avatar, profileUrl)
}
}
}
function onMessagesChange(event) {
const avatars = ui.document.querySelectorAll('.chat-message .avatar:not(.clickable-avatar)')
for (const avatar of avatars) {
const url = chat.urlFromMessageAvatarSrc(avatar.src)
chat.addAvatarClick(avatar, url)
chat.addAvatarHover(avatar, url)
}
}
/** Track the mouse accurately */
function trackMouse() {
// Track mouse on main document
document.addEventListener("mousemove", (event) => {
state.mouseX = event.pageX
state.mouseY = event.pageY
})
// Track mouse in iframe when it loads
ui.frame.addEventListener("load", () => {
ui.document.addEventListener("mousemove", (event) => {
const frameRect = ui.frame.getBoundingClientRect();
state.mouseX = frameRect.left + event.clientX + window.pageXOffset;
state.mouseY = frameRect.top + event.clientY + window.pageYOffset;
});
ui.chatMain.addEventListener("mouseleave", () => {
ui.hideProfileHeader({ ignoreLoading: true })
})
});
}
function init() {
// Try to grab the frame and observe the chat
let timerInit = setInterval(() => {
if (!ui.frame) return
if (ui.document.readyState !== 'complete') return
chat.observe('#chat-activity', onMembersChange)
chat.observe('#chat-messages', onMessagesChange)
clearInterval(timerInit)
}, 500)
trackMouse()
}
init()
If you like this script, you may also like my Infinite Scroll Script that allows you to have a Twitter / Instagram like experience. Also checkout my Thread Summary Script that allows you to see all the thread highlights in a twitter like scroll experience.
Edit: Fix bug that would sometimes not load preview properly
Edit: Better error handling
Edit: More accurate positioning of popup
Edit: Fixed bug of preview not showing when loading speed is slow
Edit: Updated the styling and made it more stable and less buggy
Last edited:
