├── LICENSE ├── README.md ├── unity-forum-fixer.js └── unity-old-post-wayback.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mika 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityForumFixer 2 | Firefox [GreaseMonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/)-plugin to improve Unity forums.
3 | Supports both Light & Dark themes! 4 | 5 | ### Forum link 6 | - https://discussions.unity.com/t/browser-greasemonkey-script-to-fix-improve-new-forums-ui-ux/1507183 7 | 8 | ### Fixes 9 | - Add Asset Store link at the top 10 | - hide welcome banner 11 | - remove search bar padding 12 | - hide big bg image 13 | - remove ALL rounded edges, note user icons are now boxes too - might change it later 14 | - hide views icon 15 | - hide replies icon 16 | - hide cookie button from premium location 17 | - move search dropdown, so that it doesnt block search bar bottom 18 | - recent searches font size 19 | - recent searches hover color to something reasonable 20 | - display proper activity times in minutes, not meters : D ("1m" is now "1 minute ago") 21 | - move "resolved" label to right of post title 22 | - hide "unresolved" (since most topics are really unresolved..unless marked solved?) 23 | - hide category icon (most of them seemed to be just unity logo.. might adjust later) 24 | - adjust post title font & size 25 | - add hover color and underline to post titles 26 | - post topic rows, half the padding 27 | - tags below post titles smaller font 28 | - post activity smaller font 29 | - hide "... or filter the topics via" 30 | - navbar smaller padding 31 | - areas, categories tags width to auto 32 | - move new topic button right 33 | - new topic drowdown description texts with different color and font size, easier to read compared to title & description in same color) 34 | - searchbar to 100% width (still need to adjust later for better location) 35 | - sidebar topic headers to slightly left (instead of same intendation with items) 36 | - sidebar row heights smaller 37 | - sidebar icons smaller 38 | - show original poster name and topic creation date 39 | - show last activity username 40 | - post view: show username above user icon (instead of separately in the message area) 41 | - post view: add gray bg for user icon area 42 | - smaller new topics alert panel 43 | - hide suggested topics at bottom 44 | - make unity footer smaller and centered 45 | - latest posts view: make main area less wide 46 | - hot topics: disable orange color for view/reply counts 47 | - Combine Views and Replies into one field 48 | - Optional: Hide all "question" tags 49 | - post view: Display registration date for OP and few other users *Note: requires JSON fetch, you can disable it by removing PostViewFetchOPDetails() call inside update. 50 | - and much more! 51 | 52 | ### TODO 53 | - Main list here https://github.com/unitycoder/UnityForumFixer/issues/1 54 | 55 | ### Before 56 | ![image](https://github.com/user-attachments/assets/a2f0c084-303c-43cf-b876-0440c32e802d) 57 | 58 | ### After *current WIP 59 | ![image](https://github.com/user-attachments/assets/054e24b1-7245-4177-b9a0-f90326802606) 60 | 61 | 62 | ### Related repos (to improve Unity dev user experience) 63 | - Unity Hub Improvements https://github.com/unitycoder/UnityHubModding 64 | - Unity Launcher Pro https://github.com/unitycoder/UnityLauncherPro 65 | -------------------------------------------------------------------------------- /unity-forum-fixer.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name UnityForumFixer 3 | // @namespace https://unitycoder.com/ 4 | // @version 0.87 (26.05.2025) 5 | // @description Fixes For Unity Forums - https://github.com/unitycoder/UnityForumFixer 6 | // @author unitycoder.com 7 | // @match https://discussions.unity.com/latest 8 | // @match https://discussions.unity.com/t/* 9 | // ==/UserScript== 10 | 11 | 12 | (function() { 13 | 'use strict'; 14 | 15 | // Run the script after the DOM is fully loaded 16 | window.addEventListener('DOMContentLoaded', function() 17 | { 18 | 19 | AppendCustomCSS(); 20 | AddAssetStoreLink(); 21 | NavBar(); 22 | TopicsViewShowOriginalPosterInfo(); // TODO needs some css adjustments for name location 23 | FixPostActivityTime(); 24 | PostViewShowOriginalPosterInfo(); 25 | TopicsViewCombineViewAndReplyCounts(); 26 | OnMouseOverPostPreview(); 27 | AddOnHoverOpenNotificationPanel(); 28 | 29 | 30 | // update notification panel icons 31 | const currentUserButton = document.getElementById('toggle-current-user'); 32 | if (currentUserButton) { 33 | currentUserButton.addEventListener('click', () => { 34 | // Add a slight delay to ensure the dropdown content is fully rendered 35 | setTimeout(replaceNotificationIcons, 1000); 36 | }); 37 | } else { 38 | console.warn('Current user button not found.'); 39 | } 40 | 41 | 42 | setTimeout(OnUpdate, 1000); // run loop to update certain itesm (since something is updating them, without page refresh..) 43 | }); 44 | 45 | 46 | })(); 47 | 48 | // runs every second to update things (if you scroll the page, need to update new data) 49 | // TODO could be better to catch page change/update some other way (xhrevent, mutation..), since it doesnt update now if click some item (like open post) 50 | function OnUpdate() 51 | { 52 | FixPostActivityTime(); 53 | TopicsViewShowOriginalPosterInfo(); 54 | PostViewShowOriginalPosterInfo(); 55 | PostViewFetchOPDetails(); 56 | 57 | // TODO only refresh these when notification panel is opened 58 | //replaceNotificationIcons(); 59 | 60 | setTimeout(OnUpdate, 1000); 61 | } 62 | 63 | 64 | 65 | function AppendCustomCSS() 66 | { 67 | 68 | var style = document.createElement('style'); 69 | style.textContent = 70 | ` 71 | /* top banner */ 72 | .before-header-panel-outlet { text-align: center; } 73 | .show-more.has-topics { width: 35% !important; } 74 | /* latest posts view */ 75 | .show-more.has-topics { width: 35%;!important;} 76 | .alert.alert-info.clickable {width: 35%; padding:3px !important;} 77 | 78 | #main-outlet {width:auto !important;} /* smaller main forum width */ 79 | 80 | /* replies/views: heatmap colors */ 81 | html .heatmap-med,html .heatmap-med a,html .heatmap-med .d-icon,html .heatmap-med {color: inherit !important;} 82 | html .heatmap-high,html .heatmap-high a,html .heatmap-high .d-icon,html .heatmap-high {color: inherit !important; font-weight:inherit !important;} 83 | 84 | /* post titles */ 85 | .title.raw-link.raw-topic-link:link {font: bold 11pt 'Inter', sans-serif;} 86 | .title.raw-link.raw-topic-link:hover {color: rgb(82,132,189) !important; text-decoration: underline !important;} 87 | body .main-link .title.raw-link.raw-topic-link:visited { font:normal !important; color: var(--primary) !important} 88 | 89 | .wrap.custom-search-banner-wrap h1 {display: none;} /* hide welcome banner */ 90 | .wrap.custom-search-banner-wrap {padding:0px;} /* remove search bar padding */ 91 | :root {--d-background-image: none !important;} /* hide big bg image */ 92 | * { border-radius: 0 !important; } /* remove ALL rounded edges, note user icons are now boxes too - might change it later */ 93 | .fa.d-icon.d-icon-far-eye.svg-icon.svg-string {display: none !important;} /* hide views icon */ 94 | .fa.d-icon.d-icon-comments.svg-icon.svg-string {display: none !important;} /* hide replies icon */ 95 | .btn.no-text.btn-icon.cookie-settings.btn-flat.onetrust-cookie-settings-toggle {display: none !important;} /* hide cookie button from premium location */ 96 | .results {margin-top:1px; } /* move search dropdown, so that it doesnt block search bar bottom */ 97 | .search-menu-recent {font-size:0.8em !important;} /* recent searches font size */ 98 | .search-menu .search-link:hover,.search-menu .search-link:focus,.search-menu-container.search-link:hover,.search-menu-container.search-link:focus {background-color: #5693b0 !important} /* recent searches hover color to something reasonable */ 99 | .is-solved-label.solved {font-weight: normal !important; font-size: 12px !important; padding: 1px !important; user-select: text !important; float: right !important; } /* resolved tag, initial fixes */ 100 | .badge-category__icon {display: none !important;} /* hide badge category icon for now, since most of them seem to be unity logos */ 101 | .is-solved-label {display: none !important;} /* hide unresolved span, since all are unresolved, unless marked solved? */ 102 | .topic-list .topic-list-data:first-of-type {padding-left: 8px !important;} /* post topic rows, half the padding */ 103 | .discourse-tags {font-size: 0.8em !important;} /* tags below post title, smaller */ 104 | .discourse-tag.simple {border: 1px solid rgba(var(--primary-rgb), 0.05) !important;} 105 | 106 | /* if want to hide all question tags */ 107 | /* a[data-tag-name="question"] { display: none !important; }*/ 108 | 109 | .relative-date {font-size: 0.9em !important; color: rgb(150, 150, 150) !important;} 110 | .ember-view.bread-crumbs-left-outlet.breadcrumb-label {display: none !important;} /* "… or filter the topics via" */ 111 | .navigation-container {--nav-space: 0 !important; padding-bottom: 6px;} /* navbar adjustments */ 112 | .category-breadcrumb.ember-view {width:auto !important;} /* areas,categories,tags not 100% width */ 113 | .navigation-controls { display: flex; justify-content: flex-end; width: 100%; } /* move new topic button to right, still would be nice to have in same row as other nav */ 114 | .select-kit-row .desc { font-size: 0.92em !important; margin-top:1px; color: #777777 !important; } /* new topic dropdown descriptions */ 115 | 116 | .custom-search-banner-wrap > div {max-width:100% !important;} /* search bar maxwidth, need to find better location later */ 117 | .sidebar-wrapper {font-size: 0.99em !important;} /* sidebar */ 118 | .sidebar-section-header-wrapper.sidebar-row {padding:4px !important;} /* sidebar headers bit to the left */ 119 | .ember-view.sidebar-section-link.sidebar-row {height:25px !important;} /* sidebar row heights */ 120 | .sidebar-section-link-prefix .svg-icon {height: 12px !important; width: 12px !important;} /* sidebar icons smaller */ 121 | 122 | /* post view, username */ 123 | .user-name { margin-bottom: 5px; font-weight: bold; text-align: center; font-size: 0.9em; color: var(--primary); text-decoration: none; display: block; word-wrap: break-word; white-space: normal; width: 100%; } 124 | .user-name:hover { color: rgb(82,132,189); text-decoration: underline; } 125 | .names.trigger-user-card {visibility: hidden !important;} 126 | /* .row { display: flex; } */ 127 | .topic-avatar { flex-basis: 10%; margin:0 !important; max-width: 45px;} 128 | .topic-body { flex-basis: 90%; } /* Ensure the main content adjusts accordingly */ 129 | .topic-avatar {background-color: #d1d1d132;} 130 | .post-avatar { display: flex; flex-direction: column; align-items: center; } 131 | /*.avatar { margin: 4px; } bug in topic view*/ 132 | .topic-body {padding: 0 !important;} 133 | .topic-map.--op {display: none !important;} /* hide view count under op post, could move it somewhere else later */ 134 | .container.posts {gap:unset !important;} /* post view thinner */ 135 | 136 | .user-signature {max-height:32px; overflow:hidden;padding: 8px 8px 4px 24px !important;} /* max size for signature */ 137 | .avatar-flair {top:55px; right: -2px; bottom:unset !important;} 138 | 139 | 140 | .more-topics__container {display:none !important;} /* hide suggested topics at bottom */ 141 | /* unity footer & content - could hide it.. but then unity is sad*/ 142 | .unity-footer {font-size:0.7em !important; line-height: none !important; padding:0 !important; text-align:center !important;} 143 | .footer.unity-footer .unity-footer-content {padding-left:10px !important; line-height: 12px !important;} 144 | .unity-footer-content { display: flex; flex-direction: column; align-items: center; text-align: center; } 145 | .unity-footer-menu.unity-footer-menu-legal.processed { list-style: none; padding: 0; margin: 0; display: flex; justify-content: center; } 146 | .unity-footer-menu.unity-footer-menu-legal.processed li { margin: 0 10px; } 147 | 148 | 149 | /* added custom fields */ 150 | .original-poster-span {font: 13px 'Inter', sans-serif !important; color: rgb(150, 150, 150); } /* original poster below post title */ 151 | .latest-poster-span { display: block; word-break: break-all; max-width: 100%; font: 14px 'Inter', sans-serif !important;} /* activity, latest poster */ 152 | 153 | .combined-replies-container {display: flex;justify-content: space-between;width: 100%;white-space: nowrap; font-size:14px; margin-bottom:2px;} 154 | .combined-views-container {display: flex;justify-content: space-between;width: 100%;white-space: nowrap; font-size:13px;} 155 | 156 | .combined-replies-label {color: rgb(150, 150, 150); text-align: left;} 157 | .combined-replies-number {color: var(--primary); margin-left: auto;text-align: right; font-size:15px !important;} 158 | .combined-views-label {color: rgb(150, 150, 150); text-align: left;} 159 | .combined-views-number {color: rgb(150, 150, 150); margin-left: auto;text-align: right;} 160 | 161 | .custom-post-username {margin-bottom:3px;color: var(--primary);} 162 | .custom-user-creation-date {width:45px;margin-top:6px;font: 13px 'Inter', sans-serif !important; color: rgb(150, 150, 150);} 163 | .custom-post-preview { position: absolute; max-width: 450px; max-height: 200px; background-color: var(--primary-low); border: 1px solid black; padding: 5px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 1000; } 164 | 165 | code.lang-auto, code.language-csharp { font-family: 'Fira Code', monospace; } 166 | .select-kit .select-kit-row {padding: 0px 0px 0px 6px !important;} /* project area dropdown */ 167 | 168 | /* notification dismiss button */ 169 | .quick-access-panel .panel-body-bottom {position:absolute;top:0;left:0;right:0;margin:8px 0;display:flex;justify-content:flex-end;padding:0 8px;background:transparent;border:none;box-shadow:none;} 170 | .quick-access-panel .btn.no-text.btn-default.show-all { height:20px; } 171 | .quick-access-panel .notifications-dismiss {font-size:0.8em;padding:4px 8px;height:20px; min-width:50%;} 172 | .panel-body-contents {margin-top:40px !important;} 173 | 174 | img.avatar[title] { pointer-events: none; } 175 | 176 | `; 177 | document.head.appendChild(style); 178 | } 179 | 180 | // HEADER 181 | 182 | function AddAssetStoreLink() 183 | { 184 | // Create the new list item 185 | var newListItem = document.createElement('li'); 186 | var newLink = document.createElement('a'); 187 | newLink.href = 'https://assetstore.unity.com/'; 188 | newLink.className = ''; 189 | newLink.textContent = 'Asset Store'; 190 | newLink.target = '_blank'; 191 | 192 | // Append the link to the new list item 193 | newListItem.appendChild(newLink); 194 | 195 | // Find the correct
    element by its class name and append the new list item 196 | var navMenu = document.querySelector('ol.unity-header-main-links'); 197 | if (navMenu) { 198 | navMenu.appendChild(newListItem); 199 | } 200 | } 201 | 202 | function NavBar() 203 | { 204 | // remove "try the" text 205 | document.querySelectorAll('div[title="Try the Product Areas"] .name').forEach(el => { 206 | if (el.textContent.trim() === "Try the Product Areas") { 207 | el.textContent = "Product Areas"; 208 | } 209 | }); 210 | } 211 | 212 | 213 | // FORUM VIEW 214 | 215 | function TopicsViewShowOriginalPosterInfo() { 216 | const topicRows = document.querySelectorAll('tr.topic-list-item'); 217 | 218 | topicRows.forEach(row => { 219 | // Always take the first username in the posters column as the original poster 220 | const postersColumn = row.querySelector('td.posters.topic-list-data'); 221 | const firstPosterAvatar = postersColumn ? postersColumn.querySelector('a[data-user-card]') : null; 222 | 223 | if (firstPosterAvatar) { 224 | const originalPosterUsername = firstPosterAvatar.getAttribute('data-user-card'); 225 | 226 | // Extract creation date from the activity cell 227 | const activityCell = row.querySelector('td.activity'); 228 | const titleText = activityCell ? activityCell.getAttribute('title') : ''; 229 | const creationDateMatch = titleText.match(/Created: (.+?)(?:\n|$)/); 230 | 231 | let creationDateFormatted = 'Unknown'; 232 | if (creationDateMatch) { 233 | const creationDateStr = creationDateMatch[1]; 234 | const creationDate = new Date(creationDateStr); 235 | creationDateFormatted = formatDateString(creationDate); 236 | } 237 | 238 | // Insert the original poster info into the main-link cell 239 | const linkBottomLine = row.querySelector('td.main-link .link-bottom-line'); 240 | if (linkBottomLine && !row.querySelector('.original-poster-span')) { 241 | const originalPosterSpan = document.createElement('span'); 242 | originalPosterSpan.className = 'original-poster-span'; 243 | originalPosterSpan.style.display = 'block'; 244 | originalPosterSpan.textContent = `${originalPosterUsername}, ${creationDateFormatted}`; 245 | 246 | // Insert the span before the link-bottom-line 247 | linkBottomLine.parentNode.insertBefore(originalPosterSpan, linkBottomLine); 248 | } 249 | } 250 | 251 | // Handle the latest poster's info 252 | const latestPosterLink = row.querySelector('td.posters.topic-list-data a.latest'); 253 | if (latestPosterLink) { 254 | const latestPosterUsername = latestPosterLink.getAttribute('data-user-card'); 255 | const postActivity = row.querySelector('td.activity .post-activity'); 256 | 257 | if (postActivity && !row.querySelector('.latest-poster-span')) { 258 | const latestPosterSpan = document.createElement('span'); 259 | latestPosterSpan.className = 'latest-poster-span'; 260 | latestPosterSpan.style.display = 'block'; 261 | latestPosterSpan.textContent = latestPosterUsername; 262 | 263 | // Insert the latest poster span before the activity link 264 | postActivity.parentNode.insertBefore(latestPosterSpan, postActivity); 265 | } 266 | } 267 | }); 268 | } 269 | 270 | // Utility function to format dates 271 | function formatDateString(date) { 272 | const options = { year: 'numeric', month: 'short', day: 'numeric' }; 273 | return date.toLocaleDateString('en-GB', options); 274 | } 275 | 276 | 277 | 278 | function TopicsViewCombineViewAndReplyCounts() 279 | { 280 | // Select all rows in the topic list 281 | const rows = document.querySelectorAll('tr.topic-list-item'); 282 | 283 | // Iterate through each row 284 | rows.forEach(row => { 285 | // Get the "Replies" and "Views" cells 286 | const repliesCell = row.querySelector('td.posts'); 287 | const viewsCell = row.querySelector('td.views'); 288 | 289 | // Check if both cells are present 290 | if (repliesCell && viewsCell) { 291 | // Create a new cell to combine the information 292 | const combinedCell = document.createElement('td'); 293 | combinedCell.className = 'num topic-list-data combined-views'; // Add class for styling if needed 294 | 295 | combinedCell.innerHTML = ` 296 |
    297 | Replies: 298 | ${repliesCell.innerText} 299 |
    300 |
    301 | Views: 302 | ${viewsCell.innerText} 303 |
    304 | `; 305 | 306 | // Insert the combined cell after the Replies cell 307 | repliesCell.parentNode.insertBefore(combinedCell, repliesCell); 308 | 309 | // Remove the original "Replies" and "Views" cells 310 | repliesCell.remove(); 311 | viewsCell.remove(); 312 | } 313 | }); 314 | 315 | // Modify the header to have a single "Views" column 316 | const repliesHeader = document.querySelector('th.posts'); 317 | const viewsHeader = document.querySelector('th.views'); 318 | if (repliesHeader && viewsHeader) { 319 | repliesHeader.textContent = 'Views'; // Set the new header title 320 | viewsHeader.remove(); // Remove the "Views" header 321 | } 322 | } 323 | 324 | function FixPostActivityTime() 325 | { 326 | document.querySelectorAll('.relative-date').forEach(function (el) { 327 | const dataTime = parseInt(el.getAttribute('data-time'), 10); 328 | if (!dataTime) return; 329 | 330 | const date = new Date(dataTime); 331 | const now = new Date(); 332 | const diffInMinutes = Math.floor((now - date) / (1000 * 60)); 333 | const diffInHours = Math.floor(diffInMinutes / 60); 334 | const diffInDays = Math.floor(diffInHours / 24); 335 | 336 | let timeString; 337 | 338 | if (diffInMinutes < 60) { // Less than 60 minutes 339 | if (diffInMinutes === 0) { 340 | timeString = `just now`; 341 | } else { 342 | timeString = `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`; 343 | } 344 | } else if (diffInHours < 24) { // Less than 24 hours 345 | timeString = `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`; 346 | } else if (diffInDays < 7) { // Within the last 7 days 347 | const dayName = date.toLocaleDateString('en-GB', { weekday: 'long' }); // Get day name like 'Monday' 348 | const formattedTime = date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false }); // Format as "HH:MM" 349 | timeString = `${dayName} at ${formattedTime}`; 350 | } else { // Older than 7 days 351 | timeString = date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); // Format as "20 Sep 2024" 352 | } 353 | 354 | el.textContent = timeString; 355 | }); 356 | } 357 | 358 | let prevTopicId = ''; // Global variable to store the previously fetched topicId 359 | let currentTooltip = null; // Global variable to store the currently visible tooltip 360 | let tooltipTarget = null; // Track the current element triggering the tooltip 361 | let hoverTimeout = null; // Timeout for delaying tooltip display 362 | 363 | // Initialize the mouseover event handler 364 | function OnMouseOverPostPreview() { 365 | document.querySelectorAll('a.title.raw-link.raw-topic-link[data-topic-id]').forEach(function (element) { 366 | const topicId = element.getAttribute('data-topic-id'); 367 | 368 | // Add mouseover event listener to the elements only 369 | element.addEventListener('mouseover', function (event) { 370 | tooltipTarget = element; // Set the current tooltip target 371 | hoverTimeout = setTimeout(() => { 372 | if (tooltipTarget === element && topicId !== prevTopicId) { // Ensure the mouse is still over the same element 373 | fetchPostDataAndShowTooltip(event, topicId, element); 374 | } 375 | }, 250); // Delay tooltip display by 250ms 376 | }); 377 | 378 | // Add mouseout event listener to clear timeout and hide tooltip 379 | element.addEventListener('mouseout', function () { 380 | tooltipTarget = null; // Clear the current tooltip target 381 | clearTimeout(hoverTimeout); // Cancel the tooltip display timeout 382 | hideTooltip(); 383 | }); 384 | }); 385 | 386 | // Add a global mousemove listener to hide the tooltip if the mouse moves outside 387 | document.addEventListener('mousemove', function (event) { 388 | if (currentTooltip && (!tooltipTarget || !tooltipTarget.contains(event.target))) { 389 | hideTooltip(); 390 | } 391 | }); 392 | } 393 | 394 | // Function to fetch data and display tooltip 395 | function fetchPostDataAndShowTooltip(event, topicId, element) { 396 | const url = `https://discussions.unity.com/t/${topicId}.json`; 397 | 398 | fetch(url) 399 | .then(response => response.json()) 400 | .then(data => { 401 | // Extract necessary data from JSON (limit to 250 characters) 402 | const rawPostContent = data['post_stream']['posts'][0]['cooked']; 403 | const postContent = rawPostContent.length > 250 ? rawPostContent.substring(0, 250) + "..." : rawPostContent; 404 | const plainText = stripHtmlTags(postContent); 405 | 406 | // Update the global variable to store the fetched topicId 407 | prevTopicId = topicId; 408 | 409 | // Create and position the tooltip based on the element's position 410 | showTooltip(element, plainText); 411 | }) 412 | .catch(error => { 413 | console.error('Error fetching post data:', error); 414 | }); 415 | } 416 | 417 | // Function to create and show the tooltip 418 | function showTooltip(element, content) { 419 | hideTooltip(); // Ensure any existing tooltip is removed first 420 | 421 | // Create a new tooltip element 422 | currentTooltip = createTooltip(content); 423 | 424 | // Get the bounding rectangle of the element 425 | const rect = element.getBoundingClientRect(); 426 | 427 | // Position the tooltip relative to the element 428 | currentTooltip.style.top = `${window.scrollY + rect.top - currentTooltip.offsetHeight - 10}px`; // 10px above the element 429 | currentTooltip.style.left = `${window.scrollX + rect.left}px`; 430 | } 431 | 432 | // Function to hide the tooltip 433 | function hideTooltip() { 434 | if (currentTooltip) { 435 | currentTooltip.remove(); 436 | currentTooltip = null; 437 | } 438 | } 439 | 440 | // Function to create a tooltip element 441 | function createTooltip(content) { 442 | const tooltip = document.createElement('div'); 443 | tooltip.className = 'custom-post-preview'; // Assign the CSS class 444 | tooltip.textContent = content; 445 | document.body.appendChild(tooltip); 446 | return tooltip; 447 | } 448 | 449 | // Function to strip HTML tags from a string 450 | function stripHtmlTags(html) { 451 | const tempDiv = document.createElement("div"); // Create a temporary
    element 452 | tempDiv.innerHTML = html; // Set its inner HTML to the input HTML string 453 | return tempDiv.textContent || tempDiv.innerText || ""; // Return the text content without HTML tags 454 | } 455 | 456 | 457 | 458 | function stripHtmlTags(html) 459 | { 460 | const tempDiv = document.createElement("div"); // Create a temporary
    element 461 | tempDiv.innerHTML = html; // Set its inner HTML to the input HTML string 462 | return tempDiv.textContent || tempDiv.innerText || ""; // Return the text content without HTML tags 463 | } 464 | 465 | 466 | // POST VIEW 467 | 468 | function PostViewShowOriginalPosterInfo() 469 | { 470 | // Select all elements that contain the avatar with a data-user-card attribute 471 | document.querySelectorAll('.trigger-user-card.main-avatar').forEach(function(avatar) { 472 | // Check if the user link has already been added 473 | if (avatar.parentNode.querySelector('.custom-post-username')) { 474 | return; // Skip to the next avatar if the user link already exists 475 | } 476 | 477 | // Get the user name from the data-user-card attribute 478 | var userName = avatar.getAttribute('data-user-card'); 479 | 480 | // Create a new anchor element to wrap the user name and link to the profile 481 | var userLink = document.createElement('a'); 482 | userLink.className = 'custom-post-username'; 483 | userLink.href = 'https://discussions.unity.com/u/' + userName; 484 | userLink.textContent = userName; 485 | 486 | // Insert the user name link before the avatar image 487 | avatar.parentNode.insertBefore(userLink, avatar); 488 | }); 489 | 490 | } 491 | 492 | let prevPageURL = ''; 493 | function PostViewFetchOPDetails() 494 | { 495 | // Get the current page URL 496 | const currentPageURL = window.location.href; 497 | 498 | // Check if the current page URL has already been processed 499 | if (currentPageURL === prevPageURL) { 500 | //console.log(`Skipping fetch for already processed page URL: ${currentPageURL}`); 501 | return; // Skip execution if the URL has already been processed 502 | } 503 | 504 | // Update the previous page URL to the current one 505 | prevPageURL = currentPageURL; 506 | 507 | // Select all elements with the specified classes to get usernames 508 | const usernames = new Set(); // Using a Set to avoid duplicates 509 | 510 | // Find usernames from elements with class 'trigger-user-card main-avatar' 511 | document.querySelectorAll('.trigger-user-card.main-avatar').forEach(function(avatar) { 512 | const userName = avatar.getAttribute('data-user-card'); 513 | if (userName) { 514 | usernames.add(userName); // Add to the Set 515 | } 516 | }); 517 | 518 | // Convert the Set to an Array and limit to the first 3 users 519 | const userArray = Array.from(usernames).slice(0, 3); 520 | 521 | // Iterate through each of the first three unique usernames and fetch the JSON data 522 | userArray.forEach(function(userName) { 523 | const url = `https://discussions.unity.com/u/${userName}/card.json`; 524 | 525 | console.log(`Fetching data from: ${url}`); 526 | 527 | // Use fetch to make a cross-origin request 528 | fetch(url, { 529 | method: 'GET', 530 | headers: { 531 | 'User-Agent': navigator.userAgent, // Mimic the default browser's User-Agent 532 | 'Accept': 'application/json, text/javascript, */*; q=0.01' // Accept JSON 533 | } 534 | }) 535 | .then(response => { 536 | if (!response.ok) { 537 | throw new Error(`HTTP error! status: ${response.status}`); 538 | } 539 | return response.json(); // Parse the JSON data 540 | }) 541 | .then(data => { 542 | console.log(`Data for ${userName}:`, data); // Print the JSON data to console 543 | 544 | // Get the user creation date 545 | const createdAt = data.user.created_at; 546 | if (createdAt) { 547 | // Format the creation date (optional) 548 | const formattedDate = new Date(createdAt).toLocaleDateString('en-US', { 549 | year: 'numeric', 550 | month: 'long', 551 | day: 'numeric' 552 | }); 553 | 554 | // Create a new element to display the creation date 555 | const creationDateElement = document.createElement('span'); 556 | creationDateElement.className = 'custom-user-creation-date'; 557 | creationDateElement.textContent = `Joined: ${formattedDate}`; 558 | 559 | // Find all post-avatar divs associated with this user 560 | document.querySelectorAll('.trigger-user-card.main-avatar').forEach(function(avatarElement) { 561 | if (avatarElement.getAttribute('data-user-card') === userName) { 562 | const postAvatarDiv = avatarElement.closest('.post-avatar'); 563 | if (postAvatarDiv && !postAvatarDiv.querySelector('.custom-user-creation-date')) { 564 | postAvatarDiv.appendChild(creationDateElement.cloneNode(true)); // Append the new date element to all relevant divs 565 | } 566 | } 567 | }); 568 | } 569 | }) 570 | .catch(error => { 571 | console.error(`Failed to fetch or parse JSON for user ${userName}:`, error); 572 | }); 573 | }); 574 | } 575 | 576 | // TODO: if page uses AJAX navigation, can run this function again when the URL changes without a full reload. 577 | /* 578 | window.addEventListener('popstate', function() {PostViewFetchOPDetails();}); 579 | window.addEventListener('pushstate', function() {PostViewFetchOPDetails();}); 580 | window.addEventListener('replacestate', function() {PostViewFetchOPDetails();}); 581 | */ 582 | 583 | // new post icon in followed topic 584 | function replaceNotificationIcons() { 585 | // Define configuration for each notification type 586 | const notificationConfig = [ 587 | { 588 | selector: '.notification.read.posted', 589 | textContent: '1', 590 | backgroundColor: 'rgba(255, 0, 0, 0.6)', 591 | color: 'white', 592 | borderRadius: '50%', 593 | fontSize: '12px' 594 | }, 595 | { 596 | selector: '.notification.read.reaction', 597 | textContent: '👍', 598 | backgroundColor: 'none', 599 | color: 'white', 600 | borderRadius: '4px', 601 | fontSize: '1.25em' 602 | }, 603 | { 604 | selector: '.notification.read.mentioned', 605 | textContent: '@', 606 | backgroundColor: 'none', 607 | color: 'var(--success)', 608 | borderRadius: '4px', 609 | fontSize: '1.25em' 610 | }, 611 | { 612 | selector: '.notification.read.replied', 613 | textContent: '↩️', 614 | backgroundColor: 'none', 615 | color: 'white', 616 | borderRadius: '4px', 617 | fontSize: '1.25em' 618 | }, 619 | { 620 | selector: '.notification.read.granted-badge', 621 | textContent: '🏆', 622 | backgroundColor: 'none', 623 | color: 'white', 624 | borderRadius: '4px', 625 | fontSize: '1.25em' 626 | }, 627 | ]; 628 | 629 | // Process each notification type 630 | notificationConfig.forEach(({ selector, textContent, backgroundColor, color, borderRadius, fontSize }) => { 631 | const notificationItems = document.querySelectorAll(selector); 632 | 633 | if (notificationItems.length > 0) { 634 | notificationItems.forEach((item) => { 635 | const icon = item.querySelector('svg.fa.d-icon'); 636 | if (icon) { 637 | // Create a replacement element 638 | const newIcon = document.createElement('div'); 639 | newIcon.style.cssText = ` 640 | display: inline-flex !important; 641 | justify-content: center !important; 642 | align-items: center !important; 643 | background-color: ${backgroundColor}; 644 | color: ${color}; 645 | font-size: ${fontSize}; 646 | font-weight: bold; 647 | padding: 5px !important; 648 | box-sizing: border-box !important; 649 | width: 20px; 650 | height: 20px; 651 | min-width: 20px; 652 | min-height: 20px; 653 | line-height: 1; 654 | margin-right: 8px; 655 | border-radius: ${borderRadius} !important; 656 | text-align: center; 657 | `; 658 | newIcon.textContent = textContent; 659 | 660 | // Replace the original SVG icon 661 | icon.replaceWith(newIcon); 662 | } 663 | }); 664 | } 665 | }); 666 | } 667 | 668 | function AddOnHoverOpenNotificationPanel() { 669 | const currentUserButton = document.getElementById('toggle-current-user'); 670 | const headerArea = document.getElementById('main-outlet'); 671 | 672 | if (currentUserButton && headerArea) { 673 | // Open the panel on hover over the user button 674 | currentUserButton.addEventListener('mouseenter', () => { 675 | const dropdown = document.querySelector('.user-menu.revamped.menu-panel.drop-down'); 676 | if (!dropdown) { 677 | currentUserButton.click(); 678 | } 679 | }); 680 | 681 | // Close the panel when mouse moves over the header area 682 | headerArea.addEventListener('mouseenter', () => { 683 | const dropdown = document.querySelector('.user-menu.revamped.menu-panel.drop-down'); 684 | if (dropdown) { 685 | currentUserButton.click(); // Simulate click outside to close 686 | } 687 | }); 688 | 689 | } else { 690 | console.warn('Current user button or header area not found.'); 691 | } 692 | } 693 | 694 | 695 | 696 | 697 | 698 | // HELPER METHODS 699 | 700 | function formatDate(date) 701 | { 702 | const options = { hour: '2-digit', minute: '2-digit', hour12: false }; 703 | return date.toLocaleTimeString('en-GB', options); // Format as "HH:MM" 704 | } 705 | 706 | function formatDateString(date) 707 | { 708 | const today = new Date(); 709 | const yesterday = new Date(today); 710 | yesterday.setDate(today.getDate() - 1); 711 | const oneWeekAgo = new Date(today); 712 | oneWeekAgo.setDate(today.getDate() - 7); 713 | 714 | if (date >= today.setHours(0, 0, 0, 0)) { // Today 715 | return `Today at ${formatDate(date)}`; 716 | } else if (date >= yesterday.setHours(0, 0, 0, 0)) { // Yesterday 717 | return `Yesterday at ${formatDate(date)}`; 718 | } else if (date >= oneWeekAgo) { // Within the past week 719 | const dayName = date.toLocaleDateString('en-GB', { weekday: 'long' }); 720 | return `${dayName} at ${formatDate(date)}`; 721 | } else { // Older than one week 722 | return date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); 723 | } 724 | } 725 | 726 | -------------------------------------------------------------------------------- /unity-old-post-wayback.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Adds Wayback Machine Link to Missing Old Unity Forums Post Page (404) 3 | // @namespace http://unitycoder.com 4 | // @version 0.1 5 | // @description Unity wanted to save few dollars by not importing all forum posts, so just have to hope that its archived in wayback machine.. and btw. they are NOT allowing bots to archive new forums completely : o 6 | // @author unitycoder.com 7 | // @match https://discussions.unity.com/threads/* 8 | // @grant none 9 | // ==/UserScript== 10 | 11 | (function() { 12 | 'use strict'; 13 | 14 | function createWaybackLink() 15 | { 16 | // Get the current URL 17 | const currentUrl = window.location.href; 18 | // Replace 'discussions.' with 'forum.' in the URL 19 | const forumUrl = currentUrl.replace('discussions.unity.com', 'forum.unity.com'); 20 | // Construct the Wayback Machine URL with the updated forum URL 21 | const waybackUrl = `https://web.archive.org/web/*/${forumUrl}`; 22 | 23 | // Use the specific provided target URL instead 24 | const specificWaybackUrl = "https://web.archive.org/web/*/https://forum.unity.com/threads/keywordenum-and-defines.697070/*"; 25 | 26 | // Create the link element 27 | const waybackLink = document.createElement('a'); 28 | waybackLink.href = specificWaybackUrl; 29 | waybackLink.target = '_blank'; 30 | waybackLink.textContent = 'View Archived Version on Wayback Machine'; 31 | 32 | // Create a paragraph to contain the link 33 | const para = document.createElement('p'); 34 | para.style.fontWeight = 'bold'; // Make the text bold 35 | para.style.border = '1px dotted red'; // Add a 1px dotted red border 36 | para.style.padding = '5px'; // Add some padding for better appearance 37 | para.title = specificWaybackUrl; // Add tooltip with the target URL 38 | para.appendChild(waybackLink); 39 | 40 | // Find the parent
    with class "page-not-found" 41 | const pageNotFoundDiv = document.querySelector('.page-not-found'); 42 | 43 | // Check if the element exists and contains the specified

    44 | if (pageNotFoundDiv) { 45 | const titleElement = pageNotFoundDiv.querySelector('h1.title'); 46 | if (titleElement) { 47 | // Insert the styled paragraph with the link after the

    element 48 | titleElement.insertAdjacentElement('afterend', para); 49 | } 50 | } 51 | } 52 | 53 | // Run the function when the page loads 54 | window.addEventListener('load', createWaybackLink); 55 | })(); 56 | --------------------------------------------------------------------------------