Wanted to do this for a while. Did it today instead of sleeping.

Screenshot:

You can install it from here: https://greasyfork.org/en/scripts/468948-user-details-on-hover

Link to GitHub repo: https://github.com/lemmygod/lemmy-hovercards/tree/main

Or you can copy-paste the following code:

click here to view code.
// ==UserScript==
// @name         User Details on Hover
// @namespace    http://tampermonkey.net/
// @version      0.12
// @description  Show user details on hover
// @author       You
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
  "use strict";
  const isLemmy =
    document.head.querySelector("[name~=Description][content]").content ===
    "Lemmy";
  if (!isLemmy) return;
  // Inject styles for the user card
  function main() {
    const style = document.createElement("style");
    style.innerHTML = `
  .user-card {
    position: absolute;
    display: none;
    width: 350px;
    background-color: #242424;
    color: white;
    padding: 15px;
    border-radius: 10px;
    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
    z-index: 1000;
    grid-gap: 10px;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.4;
  }

  .user-card .header {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
  }

  .user-card img {
    width: 80px;
    height: 80px;
    object-fit: cover;
    border-radius: 50%;
    margin-right: 15px;
  }

  .user-card .username {
    font-size: 1.3em;
    font-weight: bold;
  }

  .user-card .instance {
    font-size: 0.8em;
    color: #888;
  }

  .user-card .body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-gap: 10px;
  }

  .user-card .key {
    font-weight: bold;
  }

  .user-card .value {
    color: #ddd;
    margin-top: 10px;
  }

  .user-card .bio {
    grid-column: 1 / -1;
    font-style: italic;
  }`;
    document.head.appendChild(style);

    // Create the user card
    const userCard = document.createElement("div");
    userCard.classList.add("user-card");
    userCard.id = "user-card";
    document.body.appendChild(userCard);

    let timer;
    // Find all user links
    const userLinks = document.querySelectorAll('a.text-info[href*="/u/"]');
    userLinks.forEach((userLink) => {
      userLink.setAttribute("title", "");
      // When mouse enters, show the user card
      userLink.addEventListener("mouseenter", async (event) => {
        const username = userLink.href.split("/u/")[1];

        // Fetch user details
        const userInfo = await getUserInfo(username);

        // Format the date
        const date = new Date(userInfo.creationDate);
        const formattedDate = `${date.getFullYear()}/${String(
          date.getMonth() + 1
        ).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`;

        // Update the user card
        userCard.innerHTML = `
              <div class="header">
                  <img src="${
                    userInfo.profilePicture ||
                    `https://api.dicebear.com/6.x/identicon/svg?seed=${username}`
                  }" alt="User avatar">
                  <div>
                      <div class="username">${
                        userInfo.name || username.split("@")[0]
                      }</div>
                      <a href="https://${
                        userInfo.instance
                      }/u/${username}" class="instance">${username}${
          username.indexOf("@") === -1 ? "@" + userInfo.instance : ""
        }
                      </a>
                  </div>
              </div>
              <div class="body">
                  <div><span class="key">ID:</span> <span class="value">${
                    userInfo.id
                  }</span></div>
                  <div style="display:flex; flex-direction: column; gap: 3px"><span class="key">
                    <svg class="icon"><use xlink:href="/static/assets/symbols.svg#icon-cake"></use><div class="sr-only"><title>cake</title></div></svg>
                    Cake Day:</span> <span class="value">${formattedDate}</span></div>
                  <div><span class="key">Posts:</span> <span class="value">${
                    userInfo.post_count
                  }</span></div>
                  <div><span class="key">Comments:</span> <span class="value">${
                    userInfo.comment_count
                  }</span></div>
                  <div><span class="key">Post Score:</span> <span class="value">${
                    userInfo.post_score
                  }</span></div>
                  <div><span class="key">Comment Score:</span> <span class="value">${
                    userInfo.comment_score
                  }</span></div>
                  ${
                    userInfo.bio ? `<div class="bio">${userInfo.bio}</div>` : ""
                  }
              </div>`;

        // Show the user card at the cursor
        const rect = userLink.getBoundingClientRect();
        userCard.style.left = `${window.pageXOffset + rect.left}px`;
        userCard.style.top = `${window.pageYOffset + rect.bottom + 5}px`;
        // setTimeout(() => {
        if (userLink.querySelector(":hover")) {
          userCard.style.display = "block";
        }
        // }, 250);
        timer = setTimeout(() => {
          // check if username is not being hovered anymore after 150ms, after which point we must change display to none
          if (!userLink.querySelector(":hover")) {
            userCard.style.display = "none";
          }
        }, 150);
      });

      // When mouse leaves, hide the user card after a slight delay
      userLink.addEventListener("mouseleave", () => {
        // after a slight delay, delete the node
        timer = setTimeout(() => {
          // delete the node
          // userCard.parentElement.removeChild(userCard);
          userCard.style.display = "none";
        }, 250);
        setTimeout(() => {
          // check if both are unhovered after 260ms, and if that's the case, removeChild anyway
          if (!userCard.parentElement) return;
          if (!userCard.querySelector(":hover")) {
            // userCard.parentElement.removeChild(userCard);
            userCard.style.display = "none";
          }
        }, 250);

        // timer = setTimeout(() => {
        //   userCard.style.display = "none";
        // }, 250);
      });
    });

    userCard.addEventListener("mouseenter", () => {
      clearTimeout(timer);
    });

    userCard.addEventListener("mouseleave", () => {
      userCard.style.display = "none";
      // userCard.parentElement.removeChild(userCard);
    });

    // Fetch user info from the API
    async function getUserInfo(userName) {
      const instanceName = location.href.split("/")[2];
      const response = await fetch(
        `https://${instanceName}/api/v3/user?username=${userName}`,
        {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
          },
        }
      );
      const user = await response.json();
      const {
        published: creationDate,
        avatar: profilePicture,
        bio,
        display_name: name,
        name: username,
        id,
        banner,
      } = user.person_view.person;
      const { comment_count, comment_score, post_count, post_score } =
        user.person_view.counts;

      return {
        creationDate,
        profilePicture,
        bio,
        name,
        username,
        id,
        banner,
        instance: instanceName,
        comment_count,
        comment_score,
        post_count,
        post_score,
      };
    }
  }

  // detect react changed url but didn't reload the page by checking for url change
  var oldHref = document.location.href;
  setInterval(function () {
    if (document.location.href !== oldHref) {
      oldHref = document.location.href;
      // Wait for the page to load
      setTimeout(main, 1000);
      console.log("url changed!");
    }
  }, 500);

  // run on page load
  main();
})();
  • @Limeey
    link
    11 year ago

    Awesome. I too am a bit busy, but I’ll take a look and start playing with it. We can def knock this outta the park, and then honestly we’re so close to that RES experience I’ve been missing lol

    • GodOPM
      link
      fedilink
      11 year ago

      btw if you’re playing w it and have a few extra mins, im planning on adding buttons to send msg, block & perhaps even open in local instance.

      something i just realized a couple of days ago is that lemmy ui uses bootstrap and i didn’t leverage this in the component i made, not that i know much bootstrap but some consistency and contrast would not suck. i’ll stop being so busy in a couple of weeks tho so meanwhile that’s that.

      • @Limeey
        link
        11 year ago

        I don’t think I’ll worry too much about revising your styling toward bootstrap and instead focus on logic improvements and feature addition. We can refactor styling later - if what is there works then I see no need to spend time on that. I did notice you were appending styling to the head instead of using the GM functions, but tbh I’m not positive those are available across all implementations like violentMonkey and the rest.