Another Kiwifarms Userscript: Preview Chat User's Profile - Hover over a chatter to view a preview of their profile! Easily click on members to view their profiles in depth.

  • 🔧 At about Midnight EST I am going to completely fuck up the site trying to fix something.

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.

preview-chat-user.png
preview-chat-user-2.png


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-03-15
// @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"
     
        const previewRect = preview.getBoundingClientRect();
        this.setByMouse(preview, {offsetX: 0, offsetY: previewRect.height + 20})
        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

        const previewRect = preview.getBoundingClientRect();
        this.setByMouse(preview, {offsetX: 0, offsetY: previewRect.height + 20})
    },

    /** Move an item by the mouse, factoring in offsets from the chat frame */
    setByMouse(elem, {offsetX, offsetY}) {
        const { scrollY } = this.frame.contentWindow
        elem.style.position = "absolute"
        elem.style.left = (state.mouseX - offsetX) + "px"
        elem.style.top = (state.mouseY - scrollY - offsetY) + "px"
    }
}

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}

        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")

        const messageCount = header.querySelector(".memberHeader-stats .fauxBlockLink dd a.fauxBlockLink-linkRow").innerText
        const mainContent = header.querySelector(".memberHeader-mainContent")
        // Delete report button
        for (const btn of mainContent.querySelectorAll(".buttonGroup")) {
            const btnText = btn.querySelector(".button-text")
            if (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
        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)
            if (! fromCache) {
                ui.profilePreview.remove()
                const prev = ui.buildPreview()
                prev.innerText = "Loading..."
                prev.append(ui.buildArrow())
                document.body.append(prev)
                ui.setPreviewContent(prev, content, BACKGROUND_COLOR)
            } else {
                ui.setPreviewContent(preview, content, BACKGROUND_COLOR)
            }
        } catch (error) {
            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)
    },

    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)
        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 (because the chat is in an iframe that messes the positions up) */
function trackMouse() {
    document.addEventListener("mousemove", (event) => {
        state.mouseX = event.pageX
        state.mouseY = event.pageY
    })
  
    ui.frame.addEventListener("load", () => {
        ui.document.addEventListener("mousemove", (event) => {
            const frameRect = ui.frame.getBoundingClientRect();
            const pageX = frameRect.left + event.clientX + window.scrollX;
            const pageY = frameRect.top + event.clientY + window.scrollY;

            state.mouseX = pageX;
            state.mouseY = pageY;
        });

        ui.chatMain.addEventListener("mouseleave" , () => {
            // special case because sometimes the mouse leave event on the profile pic
            // is missed
            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
 
Last edited:
it'd be great if you hooked it up to AI and let it parse through the users post history to generate a short summary of said posters posting, think about the possibilities
ChatGPT: It looks like this user is le unheckin transphobic! Reporting to the police ASAP! Thanks for finding the meanie for me, I Hate Women +100 social credit score!
 
// ==UserScript==
// @Name Kiwifarms Chat Profile Preview
// @namespace http://tampermonkey.net/
// @version 2025-03-15
// @description Adds a preview popup for chat users
// @author PoonBrosAreValid
// @match https://kiwifarmsaaf4t2h7gc3dfc5ojhmqruw2nit3uejrpiagrxeuxiyxcyd.onion/chat/*
// @match https://kiwifarmsaaf4t2h7gc3dfc5ojhmqruw2nit3uejrpiagrxeuxiyxcyd.onion/
// @icon https://kiwifarmsaaf4t2h7gc3dfc5ojh....onion/styles/custom/logos/kiwi_square.og.png
// @grant none
// ==/UserScript==

For Tor users
 
lol at the time when there was a similar thread a few months back and Jersh completely disabled the notifications page. Nobody let him see this thread :stress:
 
  • Informative
Reactions: gagabobo1997
lol at the time when there was a similar thread a few months back and Jersh completely disabled the notifications page. Nobody let him see this thread :stress:
Mine uses a cache system to make sure it only makes 1 request per user. I could even save this cache to local storage to make it more efficient. It shouldn't DDOS Null hopefully. I can't imagine enough people will use the script to make a big difference. Please don't get mad nullerino.
 
Back