Last active
December 13, 2025 09:28
-
-
Save xPaw/34eddc8846868be7a6d176e10cc77136 to your computer and use it in GitHub Desktop.
Display "friend since" dates on Steam Community friend pages
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Steam Friends Since Display | |
| // @namespace xpaw-steam-friends-since-display | |
| // @version 1.0.0 | |
| // @description Display "friend since" dates on Steam Community friend pages | |
| // @author Claude | |
| // @match https://steamcommunity.com/id/*/friends* | |
| // @match https://steamcommunity.com/profiles/*/friends* | |
| // @icon https://steamcommunity.com/favicon.ico | |
| // @grant none | |
| // @run-at document-end | |
| // ==/UserScript== | |
| 'use strict'; | |
| // Inject CSS styles | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| #friends_list { | |
| /* Remove fixed height from friend blocks to accommodate new content */ | |
| .friend_block_v2 { | |
| height: auto !important; | |
| .friend_block_content { | |
| margin-bottom: 0; | |
| } | |
| .player_avatar { | |
| display: flex; | |
| margin-bottom: 0; | |
| } | |
| /* Set fixed size for friend avatars */ | |
| .player_avatar img { | |
| width: 64px; | |
| height: 64px; | |
| border-radius: 0; | |
| } | |
| /* Style for friend since date */ | |
| .friend_since_date { | |
| font-weight: normal; | |
| font-size: 11px; | |
| color: #ababab; | |
| } | |
| } | |
| } | |
| `; | |
| // Create global date formatters | |
| const dateFormatter = new Intl.DateTimeFormat('en-US', { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric' | |
| }); | |
| /** | |
| * Format Unix timestamp to readable date string with relative time | |
| * @param {number} timestamp - Unix timestamp | |
| * @returns {string} Formatted date string with relative time | |
| */ | |
| function formatDate(timestamp) { | |
| const date = new Date(timestamp * 1000); | |
| const absoluteDate = dateFormatter.format(date); | |
| // Calculate difference for relative time | |
| const now = Date.now(); | |
| const diffMs = now - (timestamp * 1000); | |
| const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); | |
| const diffMonths = Math.floor(diffDays / 30.44); | |
| const diffYears = Math.floor(diffDays / 365.25); | |
| let relativeTime; | |
| if (diffYears >= 1) { | |
| relativeTime = `${diffYears} year${diffYears !== 1 ? 's' : ''}`; | |
| } else if (diffMonths >= 1) { | |
| relativeTime = `${diffMonths} month${diffMonths !== 1 ? 's' : ''}`; | |
| } else { | |
| relativeTime = `${diffDays} day${diffDays !== 1 ? 's' : ''}`; | |
| } | |
| return `${absoluteDate} (${relativeTime})`; | |
| } | |
| /** | |
| * Main function to fetch and display friend since dates | |
| */ | |
| async function displayFriendSinceDates() { | |
| try { | |
| // Parse application config to get access token and API base URL | |
| const applicationConfigElement = document.getElementById('application_config'); | |
| if (!applicationConfigElement) { | |
| console.log('[Steam Friends Since] No application config found, user may not be logged in'); | |
| return; | |
| } | |
| const applicationConfig = JSON.parse(applicationConfigElement.dataset.config); | |
| const accessToken = JSON.parse(applicationConfigElement.dataset.loyalty_webapi_token); | |
| if (!accessToken) { | |
| console.log('[Steam Friends Since] No access token found, user may not be logged in'); | |
| return; | |
| } | |
| const apiBaseUrl = applicationConfig.WEBAPI_BASE_URL || 'https://api.steampowered.com/'; | |
| // Build API URL | |
| const params = new URLSearchParams(); | |
| params.set('access_token', accessToken); | |
| const apiUrl = `${apiBaseUrl}ISteamUserOAuth/GetFriendList/v1/?${params.toString()}`; | |
| // Fetch friend list | |
| const response = await fetch(apiUrl); | |
| if (!response.ok) { | |
| console.error('[Steam Friends Since] API request failed:', response.status); | |
| return; | |
| } | |
| const data = await response.json(); | |
| if (!data.friends || !Array.isArray(data.friends)) { | |
| console.error('[Steam Friends Since] Invalid API response format'); | |
| return; | |
| } | |
| // Build map of steamid -> friend_since | |
| const friendMap = new Map(); | |
| for (const friend of data.friends) { | |
| if (friend.steamid && friend.friend_since) { | |
| friendMap.set(friend.steamid, friend.friend_since); | |
| } | |
| } | |
| document.head.appendChild(style); | |
| // Process each friend block | |
| const friendBlocks = document.querySelectorAll('#friends_list .friend_block_v2'); | |
| for (const block of friendBlocks) { | |
| const steamId = block.dataset.steamid; | |
| if (!steamId) continue; | |
| const friendSince = friendMap.get(steamId); | |
| if (!friendSince) continue; | |
| // Store friend_since as data attribute for sorting | |
| block.dataset.friendSince = friendSince; | |
| // Check if date already added | |
| const blockContent = block.querySelector('.friend_block_content'); | |
| if (!blockContent || blockContent.querySelector('.friend_since_date')) continue; | |
| // Create and append date element | |
| const dateDiv = document.createElement('div'); | |
| dateDiv.className = 'friend_since_date'; | |
| dateDiv.textContent = `Since: ${formatDate(friendSince)}`; | |
| blockContent.appendChild(dateDiv); | |
| } | |
| // Add sort button | |
| addSortButton(); | |
| console.log(`[Steam Friends Since] Successfully added dates to ${friendBlocks.length} friend blocks`); | |
| } catch (error) { | |
| console.error('[Steam Friends Since] Error:', error); | |
| } | |
| } | |
| /** | |
| * Add sort button to the page | |
| */ | |
| function addSortButton() { | |
| const searchResults = document.querySelector('.searchBarContainer'); | |
| if (!searchResults || document.getElementById('sort_friends_button')) return; | |
| const button = document.createElement('button'); | |
| button.id = 'sort_friends_button'; | |
| button.className = 'profile_friends manage_link btnv6_blue_hoverfade btn_medium'; | |
| const span = document.createElement('span'); | |
| span.textContent = 'Sort by oldest friend'; | |
| button.appendChild(span); | |
| button.addEventListener('click', sortAndRemoveButton); | |
| searchResults.append(button); | |
| } | |
| /** | |
| * Sort friends and remove the button | |
| */ | |
| function sortAndRemoveButton() { | |
| const button = document.getElementById('sort_friends_button'); | |
| const searchResults = document.getElementById('search_results'); | |
| if (!searchResults) return; | |
| // Sort by friend since | |
| sortByFriendSince(searchResults); | |
| // Remove the button | |
| button.remove(); | |
| } | |
| /** | |
| * Sort friend blocks by friend_since within each group | |
| */ | |
| function sortByFriendSince(container) { | |
| // Get all state blocks and friend blocks | |
| const stateBlocks = Array.from(container.querySelectorAll('.state_block')); | |
| const friendBlocks = Array.from(container.querySelectorAll('.friend_block_v2')); | |
| // Group friends by their state | |
| const groups = {}; | |
| for (const block of friendBlocks) { | |
| // Extract group from classes (e.g., "in-game", "online", "offline") | |
| const classList = Array.from(block.classList); | |
| let group = null; | |
| for (const stateBlock of stateBlocks) { | |
| const dataGroup = stateBlock.dataset.group; | |
| if (classList.includes(dataGroup)) { | |
| group = dataGroup; | |
| break; | |
| } | |
| } | |
| if (!group) group = 'ungrouped'; | |
| if (!groups[group]) { | |
| groups[group] = []; | |
| } | |
| groups[group].push(block); | |
| } | |
| // Sort each group by friend_since (oldest first) | |
| for (const group in groups) { | |
| groups[group].sort((a, b) => { | |
| const aTime = parseInt(a.dataset.friendSince) || 0; | |
| const bTime = parseInt(b.dataset.friendSince) || 0; | |
| return aTime - bTime; // Oldest first | |
| }); | |
| } | |
| // Rebuild the container with sorted groups | |
| container.innerHTML = ''; | |
| for (const stateBlock of stateBlocks) { | |
| const dataGroup = stateBlock.dataset.group; | |
| const groupFriends = groups[dataGroup]; | |
| if (groupFriends && groupFriends.length > 0) { | |
| container.appendChild(stateBlock); | |
| for (const friend of groupFriends) { | |
| container.appendChild(friend); | |
| } | |
| } | |
| } | |
| // Add any ungrouped friends at the end | |
| if (groups['ungrouped']) { | |
| for (const friend of groups['ungrouped']) { | |
| container.appendChild(friend); | |
| } | |
| } | |
| } | |
| // Run the main function | |
| displayFriendSinceDates(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment