From 6486e1b9d9df7fb9165a40c9716ecbf02f48bdb1 Mon Sep 17 00:00:00 2001 From: rgantzos <86856959+rgantzos@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:20:55 -0700 Subject: [PATCH 1/2] New feature: `mutual-following` --- features/features.json | 5 ++ features/mutual-following/data.json | 12 +++ features/mutual-following/script.js | 109 ++++++++++++++++++++++++++++ features/mutual-following/style.css | 50 +++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 features/mutual-following/data.json create mode 100644 features/mutual-following/script.js create mode 100644 features/mutual-following/style.css diff --git a/features/features.json b/features/features.json index 0d2a5768..4a05c737 100644 --- a/features/features.json +++ b/features/features.json @@ -1,4 +1,9 @@ [ + { + "version": 2, + "id": "mutual-following", + "versionAdded": "v5.0.0" + }, { "version": 2, "id": "studio-invite-comments", diff --git a/features/mutual-following/data.json b/features/mutual-following/data.json new file mode 100644 index 00000000..fffe004a --- /dev/null +++ b/features/mutual-following/data.json @@ -0,0 +1,12 @@ +{ + "title": "Mutual Following", + "description": "See your mutual following with other users on their profiles.", + "credits": [ + { "username": "rgantzos", "url": "https://scratch.mit.edu/users/rgantzos/" } + ], + "type": ["Website"], + "tags": ["New", "Featured"], + "dynamic": true, + "scripts": [{ "file": "script.js", "runOn": "/users/*", "module": true }], + "styles": [{ "file": "style.css", "runOn": "/users/*" }] +} diff --git a/features/mutual-following/script.js b/features/mutual-following/script.js new file mode 100644 index 00000000..221c9d3a --- /dev/null +++ b/features/mutual-following/script.js @@ -0,0 +1,109 @@ +export default async function ({ feature, console, className }) { + window.feature = feature + + let auth = await feature.auth.fetch() + if (!auth?.user?.username) return console.log("User not logged in."); + + let user = auth.user.username + let profile = Scratch.INIT_DATA.PROFILE.model.username + + if (user === profile) return; + + async function getFollow(username, type, maxRequests) { + const LIMIT = 40 + + let url = `https://api.scratch.mit.edu/users/${username}/${type}` + let follows = [] + + let keepGoing = true + let offset = 0 + let requests = 0 + while (keepGoing) { + let data = await (await fetch(url + `?offset=${offset}&limit=${LIMIT}`)).json() + follows.push(...data) + + requests += 1 + + if (data.length < 20) { + keepGoing = false + } + + if (requests === maxRequests) { + keepGoing = false + } + + offset += LIMIT + } + + return follows + } + + const profileFollowing = await getFollow(profile, "following", 10) + const profileFollowers = await getFollow(profile, "followers", 10) + const userFollowing = await getFollow(user, "following", 5) + + const mutualFollowing = profileFollowing.filter((pF) => userFollowing.find((uF) => uF.username === pF.username)) + const mutualFollowers = profileFollowers.filter((pF) => userFollowing.find((uF) => uF.username === pF.username)) + + let followingUsernames = [] + let followersUsernames = [] + + for (var i in mutualFollowing) { + followingUsernames.push(mutualFollowing[i].username) + } + + for (var i in mutualFollowers) { + followersUsernames.push(mutualFollowers[i].username) + } + + const followingBox = document.querySelector(`div.box.slider-carousel-container a[href='/users/${profile}/following/']`).closest(".box") + const followersBox = document.querySelector(`div.box.slider-carousel-container a[href='/users/${profile}/followers/']`).closest(".box") + + let followingContainer = Object.assign(document.createElement("div"), { + className: className("mutual following container") + }) + followingContainer.title = followingUsernames.join(", ") + let followersContainer = Object.assign(document.createElement("div"), { + className: className("mutual followers container") + }) + followersContainer.title = followersUsernames.join(", ") + feature.self.hideOnDisable(followingContainer) + feature.self.hideOnDisable(followersContainer) + + followingBox.querySelector(".box-head").insertBefore(followingContainer, followingBox.querySelector(".box-head a")) + followersBox.querySelector(".box-head").insertBefore(followersContainer, followersBox.querySelector(".box-head a")) + + for (var i in mutualFollowing) { + if (Number(i) < 5) { + let mF = mutualFollowing[i] + let image = Object.assign(document.createElement("img"), { + src: mF.profile.images["90x90"] + }) + image.setAttribute("style", "--i:"+i) + followingContainer.appendChild(image) + } + } + + if (mutualFollowing.length > 0) { + let span = Object.assign(document.createElement("span"), { + textContent: `Following ${mutualFollowing[0].username}${mutualFollowing.length > 1 ? ` and ${mutualFollowing.length - 1} ${mutualFollowing.length > 2 ? "others" : "other"}` : ""}` + }) + followingContainer.appendChild(span) + } + + for (var i in mutualFollowers) { + let mF = mutualFollowers[i] + let image = Object.assign(document.createElement("img"), { + src: mF.profile.images["90x90"] + }) + image.setAttribute("style", "--i:"+i) + followersContainer.appendChild(image) + } + + if (mutualFollowers.length > 0) { + let span = Object.assign(document.createElement("span"), { + textContent: `Followed by ${mutualFollowers[0].username}${mutualFollowers.length > 1 ? ` and ${mutualFollowers.length - 1} ${mutualFollowers.length > 2 ? "others" : "other"}` : ""}` + }) + followersContainer.appendChild(span) + } +} \ No newline at end of file diff --git a/features/mutual-following/style.css b/features/mutual-following/style.css new file mode 100644 index 00000000..023d2339 --- /dev/null +++ b/features/mutual-following/style.css @@ -0,0 +1,50 @@ +.ste-mutual-following-container { + margin-left: .5rem; + display: inline-block; +} + +.ste-mutual-following-container img { + height: 1.5rem; + width: 1.5rem; + border-radius: .35rem; + margin-right: -.25rem; + position: relative; + top: .25rem; + position: relative; + z-index: calc(30 - var(--i)); +} + +.ste-mutual-following-container span { + margin-left: .75rem; + opacity: .5; + font-style: italic; + position: relative; + top: -.1rem; + font-weight: 500; +} + + +.ste-mutual-followers-container { + margin-left: .5rem; + display: inline-block; +} + +.ste-mutual-followers-container img { + height: 1.5rem; + width: 1.5rem; + border-radius: .35rem; + margin-right: -.25rem; + position: relative; + top: .25rem; + position: relative; + z-index: calc(30 - var(--i)); +} + +.ste-mutual-followers-container span { + margin-left: .75rem; + opacity: .5; + font-style: italic; + position: relative; + top: -.1rem; + font-weight: 500; +} \ No newline at end of file From 3b5e475cbf49699cc978619c21782607c83a7bdf Mon Sep 17 00:00:00 2001 From: rgantzos <86856959+rgantzos@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:23:35 -0700 Subject: [PATCH 2/2] Update script.js --- features/mutual-following/script.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/features/mutual-following/script.js b/features/mutual-following/script.js index 221c9d3a..f5197007 100644 --- a/features/mutual-following/script.js +++ b/features/mutual-following/script.js @@ -1,6 +1,4 @@ export default async function ({ feature, console, className }) { - window.feature = feature - let auth = await feature.auth.fetch() if (!auth?.user?.username) return console.log("User not logged in.");