diff --git a/CSS/account.css b/CSS/account.css index 7c84b5c..396ec81 100644 --- a/CSS/account.css +++ b/CSS/account.css @@ -1,3 +1,7 @@ +html { + background: linear-gradient(to right, #00FFFF, #dcdcdc, #dcdcdc, #dcdcdc, #dcdcdc, #dcdcdc, #00FFFF); +} + .Button { margin-top: 30px; @@ -13,14 +17,46 @@ body { text-align: center; } +section { + display: flex; + flex-wrap: wrap; +} + +div:not(.Posts) { + margin-left: auto; + margin-right: auto; + margin-bottom: auto; + + border-style: solid; + border-width: 2px 0; + + width: 49%; +} + +#stick { + position: sticky; + top: 0; +} + +img { + width: 20vh; + height: 20vh; +} + footer { display: flex; justify-content: center; - height: 5vh; + height: 20vh; } /* Mastodon things */ +.Trees { + display: flex; + justify-content: center; + flex-wrap: wrap; +} + .Entry { border-left: 2px solid; border-right: 1px dashed; diff --git a/CSS/expanded.css b/CSS/expanded.css index 507d9cc..89534d2 100644 --- a/CSS/expanded.css +++ b/CSS/expanded.css @@ -15,7 +15,7 @@ body { .Embed { border-style: solid; - border-radius: 1%; + border-radius: 2%; background-color: yellow; } @@ -40,6 +40,10 @@ body { background-color: #050505; } +.Favorite, .Boost, .Reply { + margin: auto; +} + .Button { margin-top: 30px; diff --git a/CSS/index.css b/CSS/index.css index 625db26..a64f9b5 100644 --- a/CSS/index.css +++ b/CSS/index.css @@ -221,9 +221,9 @@ html { .Username { text-align: center; font-weight: bold; - font-size: 2ch; + font-size: min(2vw, 2ch); - margin-top: -5px; + margin-top: max(-5px, -0.5vw); } .PostContent { @@ -247,6 +247,11 @@ html { .Time { font-size: 4ch; + text-decoration: none; +} + +.Time:hover { + text-decoration: underline; } .MainFooter, .MainBlackedFooter { @@ -260,6 +265,8 @@ html { border-width: 2px; border-color: #00FFFF; + padding: auto; + height: 20vh; } diff --git a/CSS/mail.css b/CSS/mail.css index af328d1..cddedba 100644 --- a/CSS/mail.css +++ b/CSS/mail.css @@ -1,6 +1,6 @@ /* Combination */ header, footer { - style="position: relative; + position: relative; z-index: 1; } diff --git a/CSS/post.css b/CSS/post.css index b5eb3a9..4a3be31 100644 --- a/CSS/post.css +++ b/CSS/post.css @@ -1,4 +1,5 @@ html { + background: linear-gradient(#FFFFFF, #dcdcdc, #2eff51); overflow-x: hidden; overflow-y: hidden; } @@ -14,6 +15,43 @@ body { section { height: 75vh; + + display: flex; + flex-wrap: wrap; + justify-content: center; + text-align: center; +} + +textarea, select, input, label { + resize: none; + + border-style: solid; + border-width: 2px; + border-color: #00FFFF; + border-radius: 2% / 50%; +} + +.Divide { + margin-top: auto; + margin-bottom: auto; + margin-left: 1%; + margin-right: 1%; + + border-style: solid; + border-width: 4px; + border-radius: 10%; + + padding: 1%; + + background-color: white; + filter: drop-shadow(0px 20px 10px #00cf23); + + transition: background-color 0.5s, filter 0.5s; +} + +.Button:hover { + background-color: #dcdcdc; + filter: drop-shadow(0px -20px 10px #2eff51); } .SmallButton { @@ -30,5 +68,5 @@ footer { display: flex; justify-content: center; - height: 5vh; + height: 20vh; } diff --git a/CSS/setting.css b/CSS/setting.css index c8cad4f..cb2d041 100644 --- a/CSS/setting.css +++ b/CSS/setting.css @@ -28,9 +28,8 @@ body { border-width: 6px; border-color: #00FFFF; - padding: 8%; + padding: 8vh 6vw; - display: flex; justify-content: center; background: linear-gradient(#dcdcdc, #b4b4b4); color: black; diff --git a/HTML/account.html b/HTML/account.html index ba539fc..102fe34 100644 --- a/HTML/account.html +++ b/HTML/account.html @@ -15,18 +15,25 @@ <header> <h1>Account</h1> </header> - <div> - <h2 class="Account"></h2> - <img class="AccountImage" /> - <p class="AccountDescription"></p> - <div style="display: flex; justify-content: center;"> - <p class="Button Follow">Follow!</p> - <p class="Button Block">Block!</p> + <section> + <!-- Account information goes into this div. --> + <div id="stick"> + <h2 class="Account"></h2> + <img class="AccountImage"/> + <p class="AccountDescription"></p> + <aside style="display: flex; justify-content: center;"> + <p class="Button Follow">Follow!</p> + <p class="Button Block">Block!</p> + </aside> </div> - <div class="Posts"> - <p>Not implemented. Posts should populate here.</p> + <!-- Either other account info goes here, or posts go here. --> + <div> + <h2 class="Account"></h2> + <img class="AccountImage" hidden=true/> + <p class="AccountDescription"></p> + <div class="Posts"></div> </div> - </div> + </section> <footer> <p class="Button" onclick="history.back();"><b>Back</b></p> </footer> diff --git a/HTML/expanded.html b/HTML/expanded.html index 7e9f761..a4acf62 100644 --- a/HTML/expanded.html +++ b/HTML/expanded.html @@ -35,7 +35,8 @@ </header> <p class="PostText Regular"></p> <div class="Images Regular"></div> - <div "display: flex;"> + <!-- Interactables. --> + <div style="display: flex;"> <p class="Favorite">Favorite!</p> <p class="Boost">Boost!</p> <p class="Reply">Reply!</p> diff --git a/HTML/post.html b/HTML/post.html index b8cf3f1..94e50bf 100644 --- a/HTML/post.html +++ b/HTML/post.html @@ -16,19 +16,28 @@ <h1>Post</h1> </header> <section> - <textarea cols="50" rows="25" class="Text" placeholder="status here..."></textarea> - <div> + <!-- This the typing div :3 --> + <div class="Divide"> + <textarea cols="50" rows="25" class="Text" placeholder="status here..."></textarea> + <div><textarea cols="50" rows="2" class="Warning" maxlength="100" placeholder="Content Warnings"></textarea></div> + <div><textarea cols="50" rows="1" class="Tags" maxlength="50" placeholder="Tags (seperate with ;)"></textarea></div> + </div> + <!-- This the "settings" div :3 --> + <div class="Divide"> <select class="PostVisibility"> <option value="Public">Public Post</option> <option value="Quiet">Quiet Public Post</option> <option value="Friend">Friends Only Post</option> <option value="Private">Private Post</option> </select> - <input type="checkbox" class="PostYoutube" /> - <label>Post to Youtube</label> + <div></div> + <input type="checkbox" class="PostYoutube" /> + <label>Post to Youtube</label> + </div> + </div> + <div class="Divide Button"> + <p>POST!</p> </div> - <textarea cols="50" rows="1" class="Warning" placeholder="Content Warnings"></textarea> - <p class="Button">POST!</p> </section> <footer> <p class="SmallButton" onclick="history.back();"><b>Back</b></p> diff --git a/HTML/setting.html b/HTML/setting.html index 97d020a..d389c1f 100644 --- a/HTML/setting.html +++ b/HTML/setting.html @@ -15,31 +15,29 @@ <header> <h1>Setting</h1> </header> - <div style="display: flex; justify-content: center; height: 75vh;"> + <div style="display: flex; flex-wrap: wrap; justify-content: center; height: 75vh;"> + <!-- Toggling things. --> <div class="Button"> <p class="Remote">Toggle Remote</p> </div> + <!-- Mastodon things. --> <div class="Button"> - <!-- Sorry people! --> - <!-- Mastodon things. --> - <div> - <input type="text" class="WebInput Mastodon" placeholder="Website (mastodon.social)"/> - <p class="Login Mastodon"><em>Login to Mastodon</em></p> - <p class="Logout Mastodon Hidden"><em>Logout of Mastodon</em></p> - </div> - <!-- Bluesky things. --> - <div> - <input type="text" class="WebInput Bluesky" placeholder="Website (bsky.social)"/> - <p class="Login Bluesky"><em>Login to Bluesky</em></p> - <p class="Logout Bluesky Hidden"><em>Logout of Bluesky</em></p> - </div> - <!-- Youtube things. --> - <div> - <input type="text" class="WebInput Youtube" placeholder="Youtube API Key"/> - <input type="text" class="WebInput Youtube" placeholder="Youtube Channel Handle"/> - <p class="Login Youtube"><em>Login to Youtube</em></p> - <p class="Logout Youtube Hidden"><em>Logout of Youtube</em></p> - </div> + <input type="text" class="WebInput Mastodon" placeholder="Website (mastodon.social)"/> + <p class="Login Mastodon"><em>Login to Mastodon</em></p> + <p class="Logout Mastodon Hidden"><em>Logout of Mastodon</em></p> + </div> + <!-- Bluesky things. --> + <div class="Button"> + <input type="text" class="WebInput Bluesky" placeholder="Website (bsky.social)"/> + <p class="Login Bluesky"><em>Login to Bluesky</em></p> + <p class="Logout Bluesky Hidden"><em>Logout of Bluesky</em></p> + </div> + <!-- Youtube things. --> + <div class="Button"> + <input type="text" class="WebInput Youtube" placeholder="Youtube API Key"/> + <input type="text" class="WebInput Youtube" placeholder="Youtube Channel Handle"/> + <p class="Login Youtube"><em>Login to Youtube</em></p> + <p class="Logout Youtube Hidden"><em>Logout of Youtube</em></p> </div> </div> <footer> diff --git a/JS/BlueskyAPI.js b/JS/BlueskyAPI.js index ef519b4..d171abb 100644 --- a/JS/BlueskyAPI.js +++ b/JS/BlueskyAPI.js @@ -65,6 +65,23 @@ export async function GetProfile(DID) { return body; } +export async function GetProfileFeed(DID) { + if (Token == null) { + return ""; + } + let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getAuthorFeed?actor=" + DID; + let DPoP = await ClientDPoPPDS("GET", FetchThing); + let request = fetch(FetchThing, { method: "GET", headers: {"Authorization": "DPoP " + Token, "DPoP": DPoP}}); + let body = await request.then((response) => response.json()); + let status = await request.then((response) => response.status); + let header = await request.then((response) => response.headers.get("dpop-nonce")); + if (status == 401) { + await HandleError(body, header); + body = await GetProfileFeed(DID); + } + return body; +} + // This gets the post. If there are multiple URIs, they must be within an array. export async function GetPosts(URIs) { if (Token == null) { @@ -115,7 +132,7 @@ export async function GetRecord(Repo, Collection, RKey) { // Creators // This creates a post. Requires the DID of the account and the Text for the record. -export async function CreatePost(DID, Text, ContentWarning = undefined) { +export async function CreatePost(DID, Text, ContentWarning = undefined, Tags = [], ReplyID = undefined, RootID = undefined) { if (Token == null) { return ""; } @@ -124,34 +141,18 @@ export async function CreatePost(DID, Text, ContentWarning = undefined) { "text": Text, "createdAt": new Date(Date.now()).toISOString() }; + // Content warning stuff. if (ContentWarning != undefined) { Record.labels = { "$type": "com.atproto.label.defs#selfLabels", "values": [{ "val": ContentWarning }] - } + }; } - let body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined); - if (body.hasOwnProperty("error") && body.error == "InvalidRequest") { - let matches = body.message.match(/(\d+)/); - Record.text = Text.slice(0, matches[0] - 1); - body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined); - await CreateReplyPost(DID, Text.slice(matches[0] - 1, Text.length - 1), body, body); - } - return body; -} - -// Creates a reply post. The RootID is always the first post, the ReplyID is the post you are replying to. -export async function CreateReplyPost(DID, Text, ReplyID, RootID, ContentWarning = undefined) { - if (Token == null) { - return ""; - } - let Record = { - "$type": "app.bsky.feed.post", - "text": Text, - "createdAt": new Date(Date.now()).toISOString(), - "reply": { + // ReplyID and RootID simultaniously. + if (ReplyID != undefined && RootID != undefined) { + Record.reply = { "parent": { "uri": ReplyID.uri, "cid": ReplyID.cid @@ -160,22 +161,22 @@ export async function CreateReplyPost(DID, Text, ReplyID, RootID, ContentWarning "uri": RootID.uri, "cid": RootID.cid } - } - }; - if (ContentWarning != undefined) { - Record.labels = { - "$type": "com.atproto.label.defs#selfLabels", - "values": [{ - "val": ContentWarning - }] - } + }; + } + // Tags + if (Tags.length != 0) { + Record.tags = Tags; } let body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined); if (body.hasOwnProperty("error") && body.error == "InvalidRequest") { let matches = body.message.match(/(\d+)/); Record.text = Text.slice(0, matches[0] - 1); body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined); - await CreateReplyPost(DID, Text.slice(matches[0] - 1, Text.length - 1), body, RootID); + if (ReplyID == undefined && RootID == undefined) { + await CreatePost(DID, Text.slice(matches[0] - 1, Text.length), ContentWarning, Tags, body, body); + } else { + await CreatePost(DID, Text.slice(matches[0] - 1, Text.length), ContentWarning, Tags, body, RootID); + } } return body; } @@ -402,8 +403,7 @@ export async function DeleteRecord(Repo, Collection, RKey) { // Things to get this to work. // Component 1/4 export async function GetPDSWellKnown() { - return await fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"}) - .then((response) => response.json()); + return fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"}); } // Component 2/4 @@ -525,7 +525,7 @@ export async function HandleAuthorization(Website) { // Declare Variables let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]); - let WellKnown = await GetPDSWellKnown(); + let WellKnown = await GetPDSWellKnown().then((response) => response.json()); let State = crypto.randomUUID(); @@ -551,7 +551,7 @@ export async function HandleAuthorization(Website) { } export async function GainTokens() { - let WellKnown = await GetPDSWellKnown(); + let WellKnown = await GetPDSWellKnown().then((response) => response.json()); // Check to see if something's a miss... if ((document.location.href.split("state=").length > 1 && document.location.href.split("iss=").length > 1 && document.location.href.split("code=").length > 1) && localStorage.getItem(Variables.BlueskyPKCEVerifier) != null && localStorage.getItem(Variables.BlueskyAccessToken) == null) { @@ -562,41 +562,41 @@ export async function GainTokens() { let Var = await localStorage.getItem(Variables.BlueskyPKCEVerifier); let Auth = await AuthRequest(WellKnown.token_endpoint, code, DPoP, Var); // Save the tokens and be done - localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token); - localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token); + await localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token); + await localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token); // That long string just gets the payload // aud = PDS server we are communicating with; sub = user DID - localStorage.setItem(Variables.BlueskyPDS, "https://" + KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).aud.split(":")[2]); - localStorage.setItem(Variables.BlueskyDID, KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).sub); + await localStorage.setItem(Variables.BlueskyPDS, "https://" + KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).aud.split(":")[2]); + await localStorage.setItem(Variables.BlueskyDID, KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).sub); } } // Refreshing tokens is an integral part of auth. export async function RefreshTokens() { - let WellKnown = await GetPDSWellKnown(); + let WellKnown = await GetPDSWellKnown().then((response) => response.json()); + // Fake PAR request to get the nonce. + let State = crypto.randomUUID(); + let PKCEverifier = await CreatePKCECodeVerifier(); + let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier); + let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge); + let nonce = await PAR.then((response) => response.headers.get("dpop-nonce")); + // Save nonce + await localStorage.setItem(Variables.BlueskyNonce, nonce); // Create varaibles, be aware of waits because of internet. let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint); // Token refresh let Var = await localStorage.getItem(Variables.BlueskyRefreshToken); - let Auth = ReauthRequest(WellKnown.token_endpoint, Var, DPoP); - let body = await Auth.then((response) => response.json()); - let header = await Auth.then((response) => response.headers.get("dpop-nonce")); - if (body.hasOwnProperty("error_description") && body.error_description.includes("DPoP nonce mismatch")) { - localStorage.setItem(Variables.BlueskyNonce, header); - DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint); - body = await ReauthRequest(WellKnown.token_endpoint, Var, DPoP).then((response) => response.json()); - } + let Auth = await ReauthRequest(WellKnown.token_endpoint, Var, DPoP).then((response) => response.json()); // Save the tokens and be done - localStorage.setItem(Variables.BlueskyAccessToken, body.access_token); - localStorage.setItem(Variables.BlueskyRefreshToken, body.refresh_token); + await localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token); + await localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token); } async function HandleError(body, header) { - if (body.message.includes("DPoP nonce mismatch")) { - await localStorage.setItem(Variables.BlueskyNonce, header); - } - if (body.message.includes("claim timestamp check failed")) { + await localStorage.setItem(Variables.BlueskyNonce, header); + if (body.message != undefined && body.message.includes("claim timestamp check failed")) { await RefreshTokens(); + Token = localStorage.getItem(Variables.BlueskyAccessToken); } } diff --git a/JS/MastodonAPI.js b/JS/MastodonAPI.js index b903602..0680fc4 100644 --- a/JS/MastodonAPI.js +++ b/JS/MastodonAPI.js @@ -93,19 +93,59 @@ export async function GetStatus(ID) { .then((response) => response.json()); } +// Get your own account by verifying your credentials. +export async function GetOwnAccount() { + if (Token == null || TokenType == null) { + return ""; + } + return await fetch(localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/accounts/verify_credentials", {method: "GET", headers: {"Authorization": TokenType + " " + Token}}) + .then((response) => response.json()); +} + +// Get your own account blocks. +export async function GetOwnAccountBlocks() { + if (Token == null || TokenType == null) { + return ""; + } + return await fetch(localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/blocks/", {method: "GET", headers: {"Authorization": TokenType + " " + Token}}) + .then((response) => response.json()); +} + +// Get another account's statuses. +export async function GetAccountStatuses(ID) { + if (Token == null || TokenType == null) { + return ""; + } + return await fetch(localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/accounts/" + ID + "/statuses", {method: "GET", headers: {"Authorization": TokenType + " " + Token}}) + .then((response) => response.json()); +} + +// Get another account. +export async function GetAccount(ID) { + if (Token == null || TokenType == null) { + return ""; + } + return await fetch(localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/accounts/" + ID, {method: "GET", headers: {"Authorization": TokenType + " " + Token}}) + .then((response) => response.json()); +} + // Creators // Make a status -export async function CreateStatus(Text, SpoilerText = undefined, Visibility = "public") { +export async function CreateStatus(Text, Visibility = "public", SpoilerText = undefined, ReplyID = undefined) { if (Token == null || TokenType == null) { return ""; } // Stolen from StackOverflow. Text = Text.replace(/(?:\r|\n|\r\n)/g, '<br>'); // Get the correct fetch body. - let FetchThing = localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/statuses?status=" + Text + "&visibility=" + Visibility; + let FetchThing = localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/statuses?status=" + encodeURIComponent(Text) + "&visibility=" + Visibility; + // Content warning stuff. if (SpoilerText != undefined) { FetchThing += "&spoiler_text=" + SpoilerText; } + if (ReplyID != undefined) { + FetchThing += "&in_reply_to_id=" + ReplyID; + } // Send the request. let request = fetch(FetchThing, {method: "POST", headers: {"Authorization": TokenType + " " + Token}}); let body = await request.then((response) => response.json()); @@ -114,45 +154,16 @@ export async function CreateStatus(Text, SpoilerText = undefined, Visibility = " if (status == 422) { let matches = body.error.match(/(\d+)/); // Get the correct fetch body. - FetchThing = localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/statuses?status=" + Text.slice(0, matches[0] - 1) + "&visibility=" + Visibility + FetchThing = localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/statuses?status=" + encodeURIComponent(Text.slice(0, matches[0] - 1)) + "&visibility=" + Visibility if (SpoilerText != undefined) { FetchThing += "&spoiler_text=" + SpoilerText; } // Send the request. - request = fetch(FetchThing, {method: "POST", headers: {"Authorization": TokenType + " " + Token}}); + request = fetch(enFetchThing, {method: "POST", headers: {"Authorization": TokenType + " " + Token}}); body = await request.then((response) => response.json()); - await CreateReplyStatus(Text.slice(matches[0] - 1, SpoilerText, Text.length - 1), Visibility, body.id); - } - return body; -} - -export async function CreateReplyStatus(Text, SpoilerText = undefined, Visibility = "public", ReplyID) { - if (Token == null || TokenType == null) { - return ""; - } - // Stolen from StackOverflow - Text = Text.replace(/(?:\r|\n|\r\n)/g, '<br>'); - // Get the correct fetch body. - let FetchThing = localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/statuses?status=" + Text + "&visibility=" + Visibility + "&in_reply_to_id=" + ReplyID; - if (SpoilerText != undefined) { - FetchThing += "&spoiler_text=" + SpoilerText; - } - // Send the request. - let request = fetch(FetchThing, {method: "POST", headers: {"Authorization": TokenType + " " + Token}}); - let body = await request.then((response) => response.json()); - let status = await request.then((response) => response.status); - // This is in case you went over the characters. - if (status == 422) { - let matches = body.error.match(/(\d+)/); - // Get the correct fetch body. - FetchThing = localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/statuses?status=" + Text.slice(0, matches[0] - 1) + "&visibility=" + Visibility + "&in_reply_to_id=" + ReplyID; - if (SpoilerText != undefined) { - FetchThing += "&spoiler_text=" + SpoilerText; + if (ReplyID != undefined) { + await CreateReplyStatus(Text.slice(matches[0] - 1, Text.length), Visibility, SpoilerText, body.id); } - // Send the request. - request = fetch(FetchThing, {method: "POST", headers: {"Authorization": TokenType + " " + Token}}); - body = await request.then((response) => response.json()); - await CreateReplyStatus(Text.slice(matches[0] - 1, Text.length - 1), SpoilerText, Visibility, body.id); } return body; } @@ -205,7 +216,7 @@ export async function CreateBlock(ID, IsBlocked) { return ""; } let request = localStorage.getItem(Variables.MastodonWebsite) + "/api/v1/accounts/" + ID; - if (IsReblogged == false) { + if (IsBlocked == false) { request += "/block"; } else { request += "/unblock"; diff --git a/JS/account.js b/JS/account.js index a0da97c..30d9e84 100644 --- a/JS/account.js +++ b/JS/account.js @@ -10,15 +10,24 @@ let post = JSON.parse(localStorage.getItem("post")); let AccountName = document.getElementsByClassName("Account")[0]; let AccountImage = document.getElementsByClassName("AccountImage")[0]; let AccountDescription = document.getElementsByClassName("AccountDescription")[0]; + +let AlternateAccountName = document.getElementsByClassName("Account")[1]; +let AlternateAccountImage = document.getElementsByClassName("AccountImage")[1]; +let AlternateAccountDescription = document.getElementsByClassName("AccountDescription")[1]; + let FollowButton = document.getElementsByClassName("Follow")[0]; let BlockButton = document.getElementsByClassName("Block")[0]; +let AccountPosts = document.getElementsByClassName("Posts")[0]; + // Other vars. let Following = false; let Blocking = false; +// SPAWN GetAccount(); +// The interaction is given to another function for async. FollowButton.onclick = (event) => { FollowBoop(); } @@ -33,6 +42,7 @@ async function FollowBoop() { SetFollow(); } +// Extra thing for the frontend to say "hey I am the follow meister". function SetFollow() { Following = !(Following); if (Following == true) { @@ -42,6 +52,7 @@ function SetFollow() { } } +// The interaction is given to another function for async. BlockButton.onclick = (event) => { BlockBoop(); } @@ -56,6 +67,7 @@ async function BlockBoop() { SetBlock(); } +// Extra thing for the frontend to say "hey I am the block meister". function SetBlock() { Blocking = !(Blocking); if (Blocking == true) { @@ -74,20 +86,24 @@ async function GetAccount() { Blocking = !(Relations[0].blocking); SetFollow(); SetBlock(); - // Check for a reblog. - if (post.reblog != null) { - post = post.reblog; - } AccountName.innerHTML = post.account.username; AccountDescription.innerHTML = post.account.note; AccountImage.setAttribute("src", post.account.avatar); // Build the fields - let Field = "<div style=\"display: flex; justify-content: center;\">"; + let Field = "<aside class=\"Trees\">"; for (let i of post.account.fields) { Field += "<b class=\"Entry\">" + i.name + "</b><em class=\"Exitry\">" + i.value + "</em>"; } - Field += "</div>"; + Field += "</aside>"; AccountDescription.innerHTML += Field; + // Get their recent posts. Determine if they are cewl or not. + let Posts = await MastodonAPI.GetAccountStatuses(post.account.id); + for (let i of Posts) { + if (i.reblog != null) { + i = i.reblog; + } + AccountPosts.innerHTML += i.content + "<br/><br/>"; + } } else if (website == "Bluesky") { // Set the relationship follows let thing = await BlueskyAPI.GetRecord(localStorage.getItem(Variables.BlueskyDID), "app.bsky.graph.follow", post.post.author.did); @@ -108,5 +124,40 @@ async function GetAccount() { AccountName.innerHTML = account.handle; AccountDescription.innerHTML = account.description; AccountImage.setAttribute("src", account.avatar); + // Get their recent posts. Determine if they are cewl or not. + let Posts = await BlueskyAPI.GetProfileFeed(account.did); + console.log(Posts); + for (let i of Posts.feed) { + AccountPosts.innerHTML += i.post.record.text + "<br/><br/>"; + } + } else { + FollowButton.setAttribute("hidden", ""); + BlockButton.setAttribute("hidden", ""); + AlternateAccountImage.setAttribute("hidden", false); + // This is meant for the regular account. A big ol' you :3 + let Token = localStorage.getItem(Variables.MastodonAccessToken); + if (Token != null) { + let MastoProfile = await MastodonAPI.GetOwnAccount(); + AccountName.innerHTML = MastoProfile.username; + AccountDescription.innerHTML = MastoProfile.note; + AccountImage.setAttribute("src", MastoProfile.avatar); + // Build the fields + let Field = "<div style=\"display: flex; justify-content: center;\">"; + for (let i of MastoProfile.fields) { + Field += "<b class=\"Entry\">" + i.name + "</b><em class=\"Exitry\">" + i.value + "</em>"; + } + Field += "</div>"; + AccountDescription.innerHTML += Field; + } + Token = localStorage.getItem(Variables.BlueskyAccessToken); + if (Token != null) { + let BlueProfile = await BlueskyAPI.GetProfile(localStorage.getItem(Variables.BlueskyDID)); + AlternateAccountImage.setAttribute("width", "20%"); + AlternateAccountImage.setAttribute("height", "20%"); + AlternateAccountName.innerHTML = BlueProfile.handle; + AlternateAccountDescription.innerHTML = BlueProfile.description; + AlternateAccountImage.setAttribute("src", BlueProfile.avatar); + } } + } diff --git a/JS/expanded.js b/JS/expanded.js index 2cec846..baf7aa4 100644 --- a/JS/expanded.js +++ b/JS/expanded.js @@ -20,6 +20,9 @@ let GrandParentpost; document.getElementsByClassName("Origin Regular")[0].innerHTML = website; GetPost(); +// Fixes a bug where the interpreter sucks ass. +let EmbedCounter = 0; + // Button stuff Favorite.onclick = (event) => { if (website == "Mastodon") { @@ -83,20 +86,19 @@ async function SetThreadPost(element, i) { // Functions and things. async function GetPost() { if (website == "Mastodon") { + // Check for a reblog. + if (post.reblog != null) { + document.getElementsByClassName("Handle Regular")[0].innerHTML = post.account.username + " ( R: " + post.reblog + " )"; + } else { + document.getElementsByClassName("Handle Regular")[0].innerHTML = post.account.username; + } + document.getElementsByClassName("PostText Regular")[0].innerHTML = post.content; // Set the texts. It's opposite because "setting" causes it to switch. post = await MastodonAPI.GetStatus(post.id); FavoriteFlipper = !(post.favourited); BoostFlipper = !(post.reblogged); SetFavorite(); SetBoost(); - // Check for a reblog. - if (post.reblog != null) { - document.getElementsByClassName("Handle Regular")[0].innerHTML = post.reblog.account.username + " ( R: " + post.account.username + " )"; - post = post.reblog; - } else { - document.getElementsByClassName("Handle Regular")[0].innerHTML = post.account.username; - } - document.getElementsByClassName("PostText Regular")[0].innerHTML = post.content; // Show the image if it exists. if (post.media_attachments.length != 0) { for (let i of post.media_attachments) { @@ -105,10 +107,10 @@ async function GetPost() { } // Now time to see if there are any parents if (post.in_reply_to_id != null) { - let ParentPost = await MastodonReplylFunction("Parent", post.in_reply_to_id); + Parentpost = await MastodonReplylFunction("Parent", post.in_reply_to_id); // Now time to see if there are any grandparents - if (ParentPost == undefined && ParentPost.in_reply_to_id != null) { - MastodonReplylFunction("GrandParent", ParentPost.in_reply_to_id); + if (Parentpost != undefined && Parentpost.in_reply_to_id != null) { + GrandParentpost = await MastodonReplylFunction("GrandParent", Parentpost.in_reply_to_id); } } } else if (website == "Bluesky") { @@ -144,7 +146,7 @@ async function GetPost() { Parentpost = await BlueskyReplyFunction("Parent", post.reply.parent, post.reply.parent.author); // Now time to see if there are any grandparents. if (Parentpost.value.hasOwnProperty("reply")) { - GrandParentpost = await BlueskyReplyFunction("Parent", Parentpost.value.reply.parent, post.reply.grandparentAuthor); + GrandParentpost = await BlueskyReplyFunction("GrandParent", Parentpost.value.reply.parent, post.reply.grandparentAuthor); } } } else { @@ -213,8 +215,8 @@ async function ApplyMedia(Media, Element, Author = undefined) { // Record and media embed; They are from seperate posts. if (Media.$type == "app.bsky.embed.recordWithMedia") { await BlueskyBlobEmbed(Media.media, Element, Author); - var Texty = await BlueskyAPI.GetPosts([Media.record.record.uri]); - var Text = BlueskyAPI.ApplyFacets(Texty.posts[0].record, Texty.posts[0].record.text); + let Texty = await BlueskyAPI.GetPosts([Media.record.record.uri]); + let Text = BlueskyAPI.ApplyFacets(Texty.posts[0].record, Texty.posts[0].record.text); Text = Text.replace(/\r?\n|\r/g, "<br/>"); Element.innerHTML += "<p class='Embed'>" + Text + "</p><br/>"; // To stop confusion: this is for the RECORD EMBED! It gets an image from the record embed and puts it there. @@ -223,8 +225,8 @@ async function ApplyMedia(Media, Element, Author = undefined) { } // Just a record on it's own. } else if (Media.$type == "app.bsky.embed.record") { - var Texty = await BlueskyAPI.GetPosts([Media.record.uri]); - var Text = BlueskyAPI.ApplyFacets(Texty.posts[0].record, Texty.posts[0].record.text); + let Texty = await BlueskyAPI.GetPosts([Media.record.uri]); + let Text = BlueskyAPI.ApplyFacets(Texty.posts[0].record, Texty.posts[0].record.text); Text = Text.replace(/\r?\n|\r/g, "<br/>"); Element.innerHTML += "<p class='Embed'>" + Text + "</p><br/>"; if (Texty.posts[0].record.embed.$type == "app.bsky.embed.images" || Texty.posts[0].record.embed.$type == "app.bsky.embed.video") { @@ -234,6 +236,7 @@ async function ApplyMedia(Media, Element, Author = undefined) { } else if (Media.$type == "app.bsky.embed.images" || Media.$type == "app.bsky.embed.video") { await BlueskyBlobEmbed(Media, Element, Author); } + EmbedCounter = 0; } } @@ -241,15 +244,22 @@ async function ApplyMedia(Media, Element, Author = undefined) { async function BlueskyBlobEmbed(Blob, Element, Author) { if (Blob.$type == "app.bsky.embed.images") { for (let i of Blob.images) { + Element.innerHTML += "<img src=\"\" alt=\"\"/>"; var Blobby = await BlueskyAPI.GetBlob(Author, i.image.ref.$link); var ObjectURL = URL.createObjectURL(Blobby); - Element.innerHTML += "<img src=" + ObjectURL + " alt=\"" + EscapeRegExp(i.alt) + "\"/>"; + let TempElement = Element.getElementsByTagName("img")[EmbedCounter]; + EmbedCounter += 1; + TempElement.setAttribute("src", ObjectURL); + TempElement.setAttribute("alt", EscapeRegExp(i.alt)); } // Video embed. } else if (Blob.$type == "app.bsky.embed.video") { + Element.innerHTML += "<video controls src=\"\">Loading...</video>"; + Element = Element.getElementsByTagName("video")[0]; var Blobby = await BlueskyAPI.GetBlob(Author, Blob.video.ref.$link); var ObjectURL = URL.createObjectURL(Blobby); - Element.innerHTML += "<video controls src=" + ObjectURL + "></video>"; + Element.setAttribute("src", ObjectURL); + Element.innerHTML = ""; } } diff --git a/JS/index.js b/JS/index.js index 7eb3814..ece0eab 100644 --- a/JS/index.js +++ b/JS/index.js @@ -19,6 +19,7 @@ let SettingButton = document.getElementsByClassName("Setting")[0]; let MailButton = document.getElementsByClassName("Mail")[0]; let PostingButton = document.getElementsByClassName("Posting")[0]; let FeedButton = document.getElementsByClassName("Feed")[0]; +let TimeAccountButton = document.getElementsByClassName("Time")[0]; // Sounds const ButtonSound = new Audio("Audio/button-305770.mp3"); @@ -205,9 +206,6 @@ async function PosterContainerUpdate(Direction) { if (Mastodon == false) { countergroup = countergroup + 1; } - if (Bluesky == false) { - countergroup = countergroup + 1; - } // This var is meant to stop "already seen" posts. let BlueskyPostsDup = []; WebsiteAPIType = []; @@ -218,23 +216,26 @@ async function PosterContainerUpdate(Direction) { // Begin by having the website we are using get pushed to the expanded view array and clean out the HTML. WebsiteAPIType.push("Mastodon"); i.getElementsByClassName("PostContent")[0].innerHTML = ""; - if (MastodonLoadedFeed[CurrentThing + counter] == undefined) { + if (MastodonLoadedFeed[CurrentThing + counter + 1] == undefined) { let TempFeed; if (Discover == true) { if (localStorage.getItem("Remote") != null) { - TempFeed = await MastodonAPI.GetPublicTimeline(true, true, localStorage.getItem(Variables.MastodonWebsite), MastodonLoadedFeed[CurrentThing + counter - 1].id); + TempFeed = await MastodonAPI.GetPublicTimeline(true, true, localStorage.getItem(Variables.MastodonWebsite), MastodonLoadedFeed[CurrentThing + counter].id); } else { - TempFeed = await MastodonAPI.GetPublicTimeline(true, false, localStorage.getItem(Variables.MastodonWebsite), MastodonLoadedFeed[CurrentThing + counter - 1].id); + TempFeed = await MastodonAPI.GetPublicTimeline(true, false, localStorage.getItem(Variables.MastodonWebsite), MastodonLoadedFeed[CurrentThing + counter].id); } } else { - TempFeed = await MastodonAPI.GetTimeline(MastodonLoadedFeed[CurrentThing + counter - 1].id); + TempFeed = await MastodonAPI.GetTimeline(MastodonLoadedFeed[CurrentThing + counter].id); } - MastodonLoadedFeed = MastodonLoadedFeed.concat(TempFeed); + MastodonLoadedFeed = await MastodonLoadedFeed.concat(TempFeed); } // put the reblog into the regular post; Make changes as necessary. if (MastodonLoadedFeed[CurrentThing + counter].reblog != null) { i.getElementsByClassName("Username")[0].innerHTML = MastodonLoadedFeed[CurrentThing + counter].reblog.account.username + " ( R: " + MastodonLoadedFeed[CurrentThing + counter].account.username + " )"; + // Fix a reblog issue. + let ReblogFix = MastodonLoadedFeed[CurrentThing + counter].account.username; MastodonLoadedFeed[CurrentThing + counter] = MastodonLoadedFeed[CurrentThing + counter].reblog; + MastodonLoadedFeed[CurrentThing + counter].reblog = ReblogFix; } else { i.getElementsByClassName("Username")[0].innerHTML = MastodonLoadedFeed[CurrentThing + counter].account.username } @@ -263,14 +264,14 @@ async function PosterContainerUpdate(Direction) { // Begin by having the website we are using get pushed to the expanded view array and clean out the HTML. WebsiteAPIType.push("Bluesky"); i.getElementsByClassName("PostContent")[0].innerHTML = ""; - if (BlueskyLoadedFeed[CurrentThing + counter] == undefined) { + if (BlueskyLoadedFeed[CurrentThing + counter + 1] == undefined) { let TempFeed; if (Discover == true) { - BlueskyLoadedFeed = await BlueskyAPI.GetPublicTimeline(BlueskyLoadedFeed[CurrentThing + counter - 1].post.indexedAt).then((response) => response.feed); + BlueskyLoadedFeed = await BlueskyAPI.GetPublicTimeline(BlueskyLoadedFeed[CurrentThing + counter].post.indexedAt).then((response) => response.feed); } else { - TempFeed = await BlueskyAPI.GetTimeline(BlueskyLoadedFeed[CurrentThing + counter - 1].post.indexedAt).then((response) => response.feed); + TempFeed = await BlueskyAPI.GetTimeline(BlueskyLoadedFeed[CurrentThing + counter].post.indexedAt).then((response) => response.feed); } - BlueskyLoadedFeed = BlueskyLoadedFeed.concat(TempFeed); + BlueskyLoadedFeed = await BlueskyLoadedFeed.concat(TempFeed); } // check for "already seen" posts, then put it as a seen post. BlueskyLoadedFeed = CheckForDups(BlueskyLoadedFeed, BlueskyPostsDup, counter); @@ -343,6 +344,12 @@ MailButton.onclick = (event) => { window.location.href = "./HTML/mail.html"; } +// A quick way of opening the account. +// In theory, this should be in another "channel" or the mail area, but those areas are crowded. +TimeAccountButton.onclick = (event) => { + window.location.href = "./HTML/account.html"; +} + // Open the posting area. PostingButton.onclick = (event) => { window.location.href = "./HTML/post.html"; diff --git a/JS/post.js b/JS/post.js index 40243f5..20e9921 100644 --- a/JS/post.js +++ b/JS/post.js @@ -9,6 +9,7 @@ let VisibilityDropdown = document.getElementsByClassName("PostVisibility")[0]; let InputArea = document.getElementsByClassName("Text")[0]; let YoutubePoser = document.getElementsByClassName("PostYoutube")[0]; let WarningInputArea = document.getElementsByClassName("Warning")[0]; +let TagsInputArea = document.getElementsByClassName("Tags")[0]; // Clicking the beeg POST button. PostButton.onclick = (event) => { @@ -32,6 +33,21 @@ async function Post() { // Mastodon posting. if (localStorage.getItem(Variables.MastodonAccessToken) != null) { let TempVisible; + // Adding tags + let TempText = Text; + let Tags = TagsInputArea.value.split(";"); + if (Tags.length > 1) { + TempText += "<br/><br/>"; + } + for (let i of Tags) { + while (i[0] == " ") { + i = i.substring(1, i.length); + } + if (i[0] != "#" && i.length != 0) { + i = "#" + i; + } + TempText += i + " "; + } switch(Visible) { case "Public": TempVisible = "public"; @@ -48,24 +64,38 @@ async function Post() { } if (website == "Mastodon") { if (WarningInputArea.value == "") { - await MastodonAPI.CreateReplyStatus(Text, undefined, TempVisible, JSON.parse(localStorage.getItem("post")).id); + await MastodonAPI.CreateStatus(TempText, TempVisible, undefined, JSON.parse(localStorage.getItem("post")).id); } else { - await MastodonAPI.CreateReplyStatus(Text, WarningInputArea.value, TempVisible, JSON.parse(localStorage.getItem("post")).id); + await MastodonAPI.CreateStatus(TempText, TempVisible, WarningInputArea.value, JSON.parse(localStorage.getItem("post")).id); } InputArea.value = ""; WarningInputArea.value = ""; + TagsInputArea.value = ""; return; } else if (website == "All") { if (WarningInputArea.value == "") { - await MastodonAPI.CreateStatus(Text, undefined, TempVisible); + await MastodonAPI.CreateStatus(TempText, TempVisible, undefined); } else { - await MastodonAPI.CreateStatus(Text, WarningInputArea.value, TempVisible); + await MastodonAPI.CreateStatus(TempText, TempVisible, WarningInputArea.value); } } } // Bluesky posting. if (localStorage.getItem(Variables.BlueskyAccessToken) != null) { let TempVisible; + // Adding tags + let Tags = TagsInputArea.value.split(";"); + let TagsTemp = []; + for (let i of Tags) { + while (i[0] == " ") { + i = i.substring(1, i.length); + } + if (i[0] != "#" && i.length != 0) { + i = "#" + i; + } + TagsTemp.push(i); + } + Tags = TagsTemp; switch(Visible) { case "Public": TempVisible = undefined; @@ -84,28 +114,29 @@ async function Post() { let Post; if (JSON.parse(localStorage.getItem("post")).hasOwnProperty("reply")) { if (WarningInputArea.value == "") { - Post = await BlueskyAPI.CreateReplyPost(localStorage.getItem(Variables.BlueskyDID), Text, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).reply.root); + Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text, undefined, Tags, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).reply.root); } else { - Post = await BlueskyAPI.CreateReplyPost(localStorage.getItem(Variables.BlueskyDID), Text, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).reply.root, WarningInputArea.value); + Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text, WarningInputArea.value, Tags, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).reply.root); } await BlueskyAPI.CreateThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); } else { if (WarningInputArea.value == "") { - Post = await BlueskyAPI.CreateReplyPost(localStorage.getItem(Variables.BlueskyDID), Text, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).post); + Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text, undefined, Tags, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).post); } else { - Post = await BlueskyAPI.CreateReplyPost(localStorage.getItem(Variables.BlueskyDID), Text, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).post, WarningInputArea.value); + Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text, WarningInputArea.value, Tags, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).post); } await BlueskyAPI.CreateThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); } InputArea.value = ""; WarningInputArea.value = ""; + TagsInputArea.value = ""; return; } else if (website == "All") { let Post; if (WarningInputArea.value == "") { - Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text); + Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text, undefined, Tags); } else { - Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text, WarningInputArea.value); + Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text, WarningInputArea.value, Tags); } await BlueskyAPI.CreateThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); } @@ -118,6 +149,7 @@ async function Post() { // The input being cleared means that the posting happened. InputArea.value = ""; WarningInputArea.value = ""; + TagsInputArea.value = ""; } // Check if you can interact with the textbox