From 18879136b4e19d67494f68dc0edacbcb0b7fe0fc Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 16 Apr 2026 14:46:08 -0500 Subject: [PATCH] connections updates --- src/webpage/instances.json | 17 ++-- src/webpage/jsontypes.ts | 19 ++++ src/webpage/localuser.ts | 180 ++++++++++++++++++++++++++++++------- src/webpage/style.css | 37 +++++++- 4 files changed, 215 insertions(+), 38 deletions(-) diff --git a/src/webpage/instances.json b/src/webpage/instances.json index 42e4a93..7628d6d 100644 --- a/src/webpage/instances.json +++ b/src/webpage/instances.json @@ -1,16 +1,23 @@ [ { - "name": "Spacebar", - "description": "The official Spacebar instance.", + "name": "Harmony", + "description": "A staging instance for Harmony", "image": "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png", - "url": "https://spacebar.chat", - "display":false + "url": "https://api.harmony.melodychat.org/", + "display":true }, { "name": "Harmony Staging", "description": "A staging instance for Harmony", "image": "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png", "url": "https://stg-api.harmony.melodychat.org/", - "display":true + "display":false + }, + { + "name": "Spacebar", + "description": "The official Spacebar instance.", + "image": "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png", + "url": "https://spacebar.chat", + "display":false } ] diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index b89cce5..9c4460d 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -988,6 +988,12 @@ type wsjson = s: number; t: "MESSAGE_DELETE"; } + | { + op: 0; + d: ConnectionJson; + s: number; + t: "USER_CONNECTIONS_UPDATE"; + } | { op: 0; t: "THREAD_MEMBERS_UPDATE"; @@ -1380,6 +1386,19 @@ type opRTC12 = { ]; }; }; +export interface ConnectionJson { + id: string; + friend_sync: boolean; + name: string; + revoked: boolean; + show_activity: number; + type: string; //While it is kinda an enum, we must act like it's just a generic "type" + verified: boolean; + visibility: number; + integrations: []; //idk + metadata_visibility: number; + two_way_link: boolean; +} export { readyjson, diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 0e52adf..ac93350 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -13,6 +13,7 @@ import { memberlistupdatejson, messageCreateJson, messagejson, + ConnectionJson, presencejson, readStateEntry, readyjson, @@ -709,6 +710,7 @@ class Localuser { this.handleTrace(e.trace); }); } + conectionChange = () => {}; async handleEvent(temp: wsjson) { if (temp.d._trace) this.handleTrace(temp.d._trace); if (getDeveloperSettings().gatewayLogging) console.debug(temp); @@ -758,6 +760,10 @@ class Localuser { this.messageCreate(temp); } break; + case "USER_CONNECTIONS_UPDATE": { + this.conectionChange(); + break; + } case "MESSAGE_DELETE": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); @@ -2989,40 +2995,152 @@ class Localuser { { const connections = settings.addButton(I18n.localuser.connections()); const connectionContainer = document.createElement("div"); - connectionContainer.id = "connection-container"; + const actConDivCont = document.createElement("div"); - fetch(this.info.api + "/connections", { - headers: this.headers, - }) - .then((r) => r.json() as Promise<{[key: string]: {enabled: boolean}}>) - .then((json) => { - Object.keys(json) - .sort((key) => (json[key].enabled ? -1 : 1)) - .forEach((key) => { - const connection = json[key]; - - const container = document.createElement("div"); - container.textContent = key.charAt(0).toUpperCase() + key.slice(1); - - if (connection.enabled) { - container.addEventListener("click", async () => { - const connectionRes = await fetch( - this.info.api + "/connections/" + key + "/authorize", - { - headers: this.headers, - }, - ); - const connectionJSON = await connectionRes.json(); - window.open(connectionJSON.url, "_blank", "noopener noreferrer"); - }); - } else { - container.classList.add("disabled"); - } - - connectionContainer.appendChild(container); - }); + connectionContainer.classList.add("connection-container"); + this.conectionChange = () => { + if (document.contains(settings.html)) { + remake(); + } else { + this.conectionChange = () => {}; + } + }; + const remake = () => { + connectionContainer.innerHTML = ""; + actConDivCont.innerHTML = ""; + const cons = fetch(this.info.api + "/users/@me/connections", { + headers: this.headers, }); + + fetch(this.info.api + "/connections", { + headers: this.headers, + }) + .then((r) => r.json() as Promise<{[key: string]: {enabled: boolean; icon_url?: string}}>) + .then(async (json) => { + const actCons = (await (await cons).json()) as ConnectionJson[]; + const actConMap = new Map( + actCons.map((_) => [_.type, _] as const), + ); + const serverConnections = Object.keys(json).sort((key) => (json[key].enabled ? -1 : 1)); + + serverConnections + .filter((_) => !actConMap.has(_)) + .forEach((key) => { + const connection = json[key]; + + const container = document.createElement("div"); + if (connection.icon_url) { + const span = document.createElement("span"); + span.classList.add("conImg", "svgicon"); + span.style.setProperty("mask", `url("${connection.icon_url}")`); + //span.alt = key; + container.append(span); + } else { + container.textContent = key.charAt(0).toUpperCase() + key.slice(1); + } + + if (connection.enabled) { + container.addEventListener("click", async () => { + const connectionRes = await fetch( + this.info.api + "/connections/" + key + "/authorize", + { + headers: this.headers, + }, + ); + const connectionJSON = await connectionRes.json(); + window.open(connectionJSON.url, "_blank", "noopener noreferrer"); + }); + } else { + container.classList.add("disabled"); + } + + connectionContainer.appendChild(container); + }); + serverConnections + .filter((_) => actConMap.has(_)) + .forEach((_) => { + const con = actConMap.get(_); + if (!con) return; + const connectionObj = json[_]; + + const actConDiv = document.createElement("div"); + actConDiv.classList.add("flexttb", "actConnectionDiv"); + const topRow = document.createElement("div"); + actConDiv.append(topRow); + topRow.classList.add("flexltr"); + if (connectionObj.icon_url) { + const span = document.createElement("span"); + span.classList.add("conImg", "svgicon"); + span.style.setProperty("mask", `url("${connectionObj.icon_url}")`); + //span.alt = key; + topRow.append(span); + } + + const nameDiv = document.createElement("div"); + nameDiv.classList.add("flexttb"); + + const name = document.createElement("span"); + name.textContent = con.name; + + const serviceName = document.createElement("span"); + serviceName.textContent = _; + + nameDiv.append(name, serviceName); + + topRow.append(nameDiv); + + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = !!con.visibility; + input.onchange = () => { + fetch(this.info.api + "/users/@me/connections/" + con.type + "/" + con.id, { + method: "PATCH", + body: JSON.stringify({ + visibility: input.checked, + }), + headers: this.headers, + }); + }; + + const dispRow = document.createElement("div"); + dispRow.classList.add("flexltr"); + actConDiv.append(dispRow); + + const dispText = document.createElement("span"); + dispText.textContent = "Display on profile"; + dispRow.append(dispText, input); + + const remove = document.createElement("button"); + remove.textContent = "Delete Connection"; + actConDiv.append(remove); + remove.onclick = () => { + const d = new Dialog("Are you sure?"); + d.options.addText("If you remove this connection, you can't undo it"); + const row = d.options.addOptions("", {ltr: true}); + row.addButtonInput("", I18n.yes(), async () => { + await fetch( + this.info.api + "/users/@me/connections/" + con.type + "/" + con.id, + { + method: "DELETE", + headers: this.headers, + }, + ); + d.hide(); + }); + row.addButtonInput("", I18n.no(), () => { + d.hide(); + }); + d.show(); + }; + + actConDivCont.append(actConDiv); + }); + }); + }; + remake(); connections.addHTMLArea(connectionContainer); + connections.addHR(); + connections.addHTMLArea(actConDivCont); } { const devPortal = settings.addButton(I18n.localuser.devPortal()); diff --git a/src/webpage/style.css b/src/webpage/style.css index bd3c48a..f6f92b4 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -221,6 +221,24 @@ body { display: flex; flex-direction: column; } +.actConnectionDiv { + background: #0000005c; + padding: 6px; + border-radius: 4px; + > * { + padding: 2px; + span { + margin-right: 10px; + } + input { + margin-left: auto; + width: 20px; + height: 20px; + } + display: flex; + align-items: center; + } +} .mediaDisp { > * { height: 150px; @@ -4110,8 +4128,13 @@ fieldset input[type="radio"] { .optionElement .fileinputdiv { max-width: 500px; } +.connection-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} #app-list-container div, -#connection-container div { +.connection-container div { max-width: 500px; padding: 8px; margin-top: 12px; @@ -4123,6 +4146,7 @@ fieldset input[type="radio"] { align-items: center; gap: 8px; cursor: pointer; + margin-left: 6px; } #app-list-container div:hover { border: 1.5px solid color-mix(in srgb, var(--secondary-text), transparent); @@ -4135,7 +4159,16 @@ fieldset input[type="radio"] { #app-list-container h2 { font-size: 1rem; } -#connection-container .disabled { +.conImg { + width: 36px; + height: 36px; + background: var(--primary-text); +} + +.disabled .conImg { + opacity: 0.3; +} +.connection-container .disabled { color: var(--button-disabled-text); cursor: not-allowed; }