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.

  • 🐕 I am attempting to get the site runnning as fast as possible. If you are experiencing slow page load times, please report it.

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