From 5a38961f36d88ade0c08abc732b98b8b6dfa7f27 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 04:23:06 +0530 Subject: [PATCH 01/94] feat: implement followed accounts --- actor-profile.css | 74 +++++++++++++++++++++++++++++++ actor-profile.js | 98 ++++++++++++++++++++++++++++++++++++++++++ db.js | 47 ++++++++++++++++++++ followed-accounts.css | 20 +++++++++ followed-accounts.html | 21 +++++++++ followed-accounts.js | 56 ++++++++++++++++++++++++ index.html | 10 +++++ post.css | 2 + post.js | 14 +++++- profile.html | 41 ++++++++++++++++++ timeline.css | 15 +++++++ timeline.js | 35 ++++++++++----- 12 files changed, 422 insertions(+), 11 deletions(-) create mode 100644 actor-profile.css create mode 100644 actor-profile.js create mode 100644 followed-accounts.css create mode 100644 followed-accounts.html create mode 100644 followed-accounts.js create mode 100644 profile.html diff --git a/actor-profile.css b/actor-profile.css new file mode 100644 index 0000000..cd5dabf --- /dev/null +++ b/actor-profile.css @@ -0,0 +1,74 @@ +.actor-profile { + text-align: center; + margin-top: 20px; +} + +.actor-container { + margin-bottom: 10px; +} + +.distributed-post-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.actor-icon { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #000000; + margin-right: 8px; + margin-bottom: 8px; +} + +.actor-details { + display: flex; + flex-direction: column; +} + +.actor-name { + color: var(--rdp-text-color); + font-weight: bold; +} + +#followButton { + appearance: none; + border: 1px solid rgba(27, 31, 35, 0.15); + border-radius: 4px; + box-shadow: rgba(27, 31, 35, 0.1) 0 1px 0; + box-sizing: border-box; + cursor: pointer; + display: inline-block; + font-family: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 14px; + font-weight: 600; + line-height: 20px; + padding: 4px 16px; + position: relative; + text-align: center; + text-decoration: none; + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; + vertical-align: middle; + white-space: nowrap; +} + +#followButton.follow { + background-color: #3b82f6; + color: #fff; +} +#followButton.follow:hover { + background-color: #2563eb; +} + +#followButton.unfollow { + background-color: #ef4444; + color: #fff; +} +#followButton.unfollow:hover { + background-color: #dc2626; +} diff --git a/actor-profile.js b/actor-profile.js new file mode 100644 index 0000000..9e3d164 --- /dev/null +++ b/actor-profile.js @@ -0,0 +1,98 @@ +import { db } from "./dbInstance.js"; +import { fetchActorInfo } from "./post.js"; + +class ActorProfile extends HTMLElement { + static get observedAttributes() { + return ["url"]; + } + + constructor() { + super(); + this.url = ""; + } + + connectedCallback() { + this.url = this.getAttribute("url"); + this.fetchAndRenderActorProfile(this.url); + } + + async fetchAndRenderActorProfile(url) { + const actorInfo = await fetchActorInfo(url); + console.log(actorInfo); + if (actorInfo) { + this.renderActorProfile(actorInfo); + this.updateFollowButtonState(); + // Update distributed-outbox URL based on fetched actorInfo + const distributedOutbox = document.querySelector("distributed-outbox"); + distributedOutbox.setAttribute("url", actorInfo.outbox); + } + } + + renderActorProfile(actorInfo) { + // Clear existing content + this.innerHTML = ""; + + const profileContainer = document.createElement("div"); + profileContainer.classList.add("actor-profile"); + + // Create a container for the actor icon and name, to center them + const actorContainer = document.createElement("div"); + actorContainer.classList.add("actor-container"); + + // Handle both single icon object and array of icons + let iconUrl = null; + if (actorInfo.icon) { + if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) { + iconUrl = actorInfo.icon[0].url; + } else if (actorInfo.icon.url) { + iconUrl = actorInfo.icon.url; + } + + if (iconUrl) { + const img = document.createElement("img"); + img.classList.add("actor-icon"); + img.src = iconUrl; + img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + actorContainer.appendChild(img); // Append to the actor container + } + } + + if (actorInfo.name) { + const pName = document.createElement("div"); + pName.classList.add("actor-name"); + pName.textContent = actorInfo.name; + actorContainer.appendChild(pName); // Append to the actor container + } + + // Append the actor container to the profile container + profileContainer.appendChild(actorContainer); + + // Create and position the follow button + const followButton = document.createElement("button"); + followButton.id = "followButton"; + followButton.textContent = "Follow"; + profileContainer.appendChild(followButton); + + // Append the profile container to the main component + this.appendChild(profileContainer); + } + + async updateFollowButtonState() { + const followButton = this.querySelector("#followButton"); + const followedActors = await db.getFollowedActors(); + const isFollowed = followedActors.some((actor) => actor.url === this.url); + + followButton.textContent = isFollowed ? "Unfollow" : "Follow"; + followButton.className = isFollowed ? "unfollow" : "follow"; + followButton.onclick = async () => { + if (isFollowed) { + await db.unfollowActor(this.url); + } else { + await db.followActor(this.url); + } + this.updateFollowButtonState(); + }; + } +} + +customElements.define("actor-profile", ActorProfile); diff --git a/db.js b/db.js index b1e1db2..4b025fa 100644 --- a/db.js +++ b/db.js @@ -4,6 +4,7 @@ export const DEFAULT_DB = 'default' export const ACTORS_STORE = 'actors' export const NOTES_STORE = 'notes' export const ACTIVITIES_STORE = 'activities' +export const FOLLOWED_ACTORS_STORE = 'followedActors'; export const ID_FIELD = 'id' export const URL_FIELD = 'url' @@ -246,6 +247,46 @@ export class ActivityPubDB { // delete note using the url as the `id` from the notes store this.db.delete(NOTES_STORE, url) } + + // Method to follow an actor + async followActor(url) { + const followedAt = new Date(); + await this.db.put(FOLLOWED_ACTORS_STORE, { url, followedAt }); + console.log(`Followed actor: ${url} at ${followedAt}`); + } + + // Method to unfollow an actor + async unfollowActor(url) { + await this.db.delete(FOLLOWED_ACTORS_STORE, url); + console.log(`Unfollowed actor: ${url}`); + } + + // Method to retrieve all followed actors + async getFollowedActors() { + const tx = this.db.transaction(FOLLOWED_ACTORS_STORE, 'readonly'); + const store = tx.objectStore(FOLLOWED_ACTORS_STORE); + const followedActors = []; + for await (const cursor of store) { + followedActors.push(cursor.value); + } + return followedActors; + } + + // Method to check if an actor is followed + async isActorFollowed(url) { + try { + const record = await this.db.get(FOLLOWED_ACTORS_STORE, url); + return !!record; // Convert the record to a boolean indicating if the actor is followed + } catch (error) { + console.error(`Error checking if actor is followed: ${url}`, error); + return false; // Assume not followed if there's an error + } + } + + async hasFollowedActors() { + const followedActors = await this.getFollowedActors(); + return followedActors.length > 0; + } } function upgrade (db) { @@ -258,6 +299,12 @@ function upgrade (db) { actors.createIndex(UPDATED_FIELD, UPDATED_FIELD) actors.createIndex(URL_FIELD, URL_FIELD) + if (!db.objectStoreNames.contains(FOLLOWED_ACTORS_STORE)) { + db.createObjectStore(FOLLOWED_ACTORS_STORE, { + keyPath: "url", + }); + } + const notes = db.createObjectStore(NOTES_STORE, { keyPath: 'id', autoIncrement: false diff --git a/followed-accounts.css b/followed-accounts.css new file mode 100644 index 0000000..90d207a --- /dev/null +++ b/followed-accounts.css @@ -0,0 +1,20 @@ +html { + background: var(--bg-color); + font-family: var(--rdp-font); +} + +.followed-container { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 20px; +} + +#followedList { + text-align: left; + color: var(--rdp-text-color); + width: 80%; + max-width: fit-content; + margin: 0 auto; + overflow-wrap: break-word; +} diff --git a/followed-accounts.html b/followed-accounts.html new file mode 100644 index 0000000..c07e3d2 --- /dev/null +++ b/followed-accounts.html @@ -0,0 +1,21 @@ + + +Followed Accounts + +
+

+ You're following 0 accounts.
Import and export followed list coming soon.. +

+
+
+ + diff --git a/followed-accounts.js b/followed-accounts.js new file mode 100644 index 0000000..bc1386e --- /dev/null +++ b/followed-accounts.js @@ -0,0 +1,56 @@ +import { db } from "./dbInstance.js"; + +function formatDate(dateString) { + const options = { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + }; + const date = new Date(dateString); + return date.toLocaleDateString("en-US", options); +} + +async function displayFollowedActors() { + const followedListElement = document.getElementById("followedList"); + const followedActors = await db.getFollowedActors(); + console.log(followedActors); + + followedActors.forEach((actor) => { + const actorElement = document.createElement("div"); + const formattedDate = formatDate(actor.followedAt); + actorElement.textContent = `- Followed URL: ${actor.url} - Followed At: ${formattedDate}`; + followedListElement.appendChild(actorElement); + }); +} +displayFollowedActors(); + +export async function updateFollowCount() { + const followCountElement = document.getElementById("followCount"); + const followedActors = await db.getFollowedActors(); + followCountElement.textContent = followedActors.length; +} + +// test following/unfollowing +// (async () => { +// const actorUrl1 = "https://example.com/actor/1"; +// const actorUrl2 = "https://example.com/actor/2"; + +// console.log("Following actors..."); +// await db.followActor(actorUrl1); +// await db.followActor(actorUrl2); + +// console.log("Retrieving followed actors..."); +// let followedActors = await db.getFollowedActors(); +// console.log("Followed Actors:", followedActors); + +// console.log("Unfollowing an actor..."); +// await db.unfollowActor(actorUrl2); + +// console.log("Retrieving followed actors after unfollowing..."); +// followedActors = await db.getFollowedActors(); +// console.log("Followed Actors after unfollowing:", followedActors); +// })(); diff --git a/index.html b/index.html index bb7b9e8..047bf94 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,11 @@
+ + diff --git a/post.css b/post.css index e0d9114..4d32638 100644 --- a/post.css +++ b/post.css @@ -45,6 +45,7 @@ img { border-radius: 50%; background-color: #000000; margin-right: 8px; + cursor: pointer; } .actor-details { @@ -55,6 +56,7 @@ img { .actor-name { color: var(--rdp-text-color); font-weight: bold; + cursor: pointer; } .actor-username { diff --git a/post.js b/post.js index 7daa4ce..fec52ef 100644 --- a/post.js +++ b/post.js @@ -148,7 +148,7 @@ async function loadPostFromHyper(hyperUrl) { } } -async function fetchActorInfo(actorUrl) { +export async function fetchActorInfo(actorUrl) { try { const response = await fetch(actorUrl); if (!response.ok) { @@ -388,12 +388,22 @@ class ActorInfo extends HTMLElement { return ["url"]; } + constructor() { + super(); + this.actorUrl = ''; + } + attributeChangedCallback(name, oldValue, newValue) { if (name === "url" && newValue) { + this.actorUrl = newValue; this.fetchAndRenderActorInfo(newValue); } } + navigateToActorProfile() { + window.location.href = `/profile.html?actor=${encodeURIComponent(this.actorUrl)}`; + } + async fetchAndRenderActorInfo(url) { try { const actorInfo = await fetchActorInfo(url); @@ -421,6 +431,7 @@ class ActorInfo extends HTMLElement { img.classList.add("actor-icon"); img.src = iconUrl; img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + img.addEventListener('click', this.navigateToActorProfile.bind(this)); author.appendChild(img); } } @@ -429,6 +440,7 @@ class ActorInfo extends HTMLElement { const pName = document.createElement("div"); pName.classList.add("actor-name"); pName.textContent = actorInfo.name; + pName.addEventListener('click', this.navigateToActorProfile.bind(this)); authorDetails.appendChild(pName); } diff --git a/profile.html b/profile.html new file mode 100644 index 0000000..fb01839 --- /dev/null +++ b/profile.html @@ -0,0 +1,41 @@ + + + +User Profile + + + +
+ + +
+ + + + + diff --git a/timeline.css b/timeline.css index 69938c4..c7ed68a 100644 --- a/timeline.css +++ b/timeline.css @@ -30,6 +30,21 @@ body { margin-bottom: 0.6em; } +.controls { + margin-bottom: 0.4em; +} + +.controls a { + color: var(--rdp-details-color); + text-decoration: none; + font-size: 0.875rem; + font-weight: bold; + margin-bottom: 0.4em; +} +.controls a:hover { + text-decoration: underline; +} + .sidebar nav { display: flex; flex-direction: column; diff --git a/timeline.js b/timeline.js index cb36235..58f0523 100644 --- a/timeline.js +++ b/timeline.js @@ -3,22 +3,39 @@ import { db } from "./dbInstance.js"; class ReaderTimeline extends HTMLElement { constructor() { super(); - this.actorUrls = [ - "https://staticpub.mauve.moe/about.jsonld", - "https://hypha.coop/about.jsonld", - "https://prueba-cola-de-moderacion-2.sutty.nl/about.jsonld", - ]; - this.processedNotes = new Set(); // To keep track of notes already processed + this.processedNotes = new Set(); // To keep track of already processed notes } connectedCallback() { - this.initTimeline(); + this.initializeDefaultFollowedActors().then(() => this.initTimeline()); + } + + async initializeDefaultFollowedActors() { + const defaultActors = [ + "https://social.dp.chanterelle.xyz/v1/@announcements@social.dp.chanterelle.xyz/", + "https://hypha.coop/about.jsonld", + "https://sutty.nl/about.jsonld", + // "https://akhilesh.sutty.nl/about.jsonld", + // "https://staticpub.mauve.moe/about.jsonld", + ]; + + // Check if followed actors have already been initialized + const hasFollowedActors = await db.hasFollowedActors(); + if (!hasFollowedActors) { + for (const actorUrl of defaultActors) { + await db.followActor(actorUrl); + } + } } async initTimeline() { this.innerHTML = ""; // Clear existing content - for (const actorUrl of this.actorUrls) { + // Dynamically load followed actors + const followedActors = await db.getFollowedActors(); + const actorUrls = followedActors.map(actor => actor.url); + + for (const actorUrl of actorUrls) { try { console.log("Loading actor:", actorUrl); await db.ingestActor(actorUrl); @@ -30,10 +47,8 @@ class ReaderTimeline extends HTMLElement { // After ingesting all actors, search for all notes once try { const allNotes = await db.searchNotes({}); - // Sort all notes by published date in descending order allNotes.sort((a, b) => new Date(b.published) - new Date(a.published)); - // Create and append elements for each note allNotes.forEach((note) => { if (!this.processedNotes.has(note.id)) { const activityElement = document.createElement("distributed-post"); From f7bb914894033838ea07c93f7c6638c5cc58cf17 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 04:54:15 +0530 Subject: [PATCH 02/94] feat: add default profile image for actors without a profile icon --- actor-profile.js | 16 +++++++--------- assets/profile.png | Bin 0 -> 27663 bytes post.js | 18 ++++++++---------- 3 files changed, 15 insertions(+), 19 deletions(-) create mode 100644 assets/profile.png diff --git a/actor-profile.js b/actor-profile.js index 9e3d164..6491cc0 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -40,22 +40,20 @@ class ActorProfile extends HTMLElement { actorContainer.classList.add("actor-container"); // Handle both single icon object and array of icons - let iconUrl = null; + let iconUrl = './assets/profile.png'; // Default profile image path if (actorInfo.icon) { if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) { iconUrl = actorInfo.icon[0].url; } else if (actorInfo.icon.url) { iconUrl = actorInfo.icon.url; } - - if (iconUrl) { - const img = document.createElement("img"); - img.classList.add("actor-icon"); - img.src = iconUrl; - img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; - actorContainer.appendChild(img); // Append to the actor container - } } + + const img = document.createElement("img"); + img.classList.add("actor-icon"); + img.src = iconUrl; + img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + actorContainer.appendChild(img); // Append to the actor container if (actorInfo.name) { const pName = document.createElement("div"); diff --git a/assets/profile.png b/assets/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..6dfa7b623eb3a4d0499abb23fcb890414ab5a8af GIT binary patch literal 27663 zcmbTd1z40%*EkHziYvKvNG#p3v>>r`gVK$pAPq~mG}4mNN=kQ2N;gs}pmfJl0s{ZV z-_!5&zTb6y_g?pP&z_kRbLLE)nG^X;RSp}290LUf1zSO0S{(%iHU9nup(7!I9RefB z52~xWoFq!c7{xa7&w-_`f|ZI23M(=VLP1BxM?t$Ug8ZSNlA}EMlSV<&Mt%H`v@t3- z3IGYer)Y)x^iLWWnYIhJK>_})yf5;5|GYrHNJ@WwrA!^%O;IG27mBwqWv?CEQcbYA*G;z{Cc_wTL?=_$eQq4@;T`$ z$O!YZzY-P{OI6Zhd9bH~>aSI6vaY1>wcz8IFB{*EY9N?y&91gB@e-ZgR9cfEfa~B&Y zxQ(L&C+dY|(b+Mg!)OEV(Kb`+L!u{4D{x@bB&+KK(KOcAdC1piZ)A{OSt z=6@ml3mqi2KW(aE>HP1|zh~Ili2b{{i*Vhy6%zbU()VTmK>wYJ2-kmQc`pdu25x8h z9~AD%O4z~w9r{l(?oCeE&eY+hD6J=lg{76Ln;o21Ov~X9DGP{&rK`1)U3E)WN4q~wCnh8y!u3CZ|E&FA2zvhw;a`FOiJ<0UgS0}^d*cv8 z%KV4Hzry~d{9l$OEMejL@5=fAYPmnX%E{El)l$>T$x@W|UPLz+bIX5pL!@^9@reA5 zi>9NSx%GW{F&+`F{{j1_RR068#xc?9@{ zxcRsRh4`O9|AGA{^FNXQFWdP~q984y z>500(_%L;^p_=1|xzV-p0*3ZjVjM-q-Cd=Z5)MX+-2*nI6o$pqvHn%jp6%^yReI8v zD&h<~0R@y+jQZyfz&CFjm%MlH4zyT-R$-lYH_IyZzX#R)UB(&D;_fa2?k=+T18z41 z4x0O&&+pDm?%WPL?yfs38+<+N4_x<+48%LuTAKsb666jRZuggO_m|WHuBLs&uNTH! z+E;xqTCZKo&F8K;3$QZ!7vpfn3{6VFX^#QoSq4ELMN%VC(B2VB+2&y5g`iN-p)Lpf zKJNDs|9#XmBNp=0%kM+R>fT$O^VYHJcE|6oejRN>skYbG6h8g~Ys*MU4;OA%#cw}+ zljO?)Ci!0bjJKAS`B!R!>LhbP`9xF6On2 zR;=3%w{U;8X2Htw%f0IkwqS3+yPFFg<3!hp;JK+~aP*-(qs6L^`p#RwvAIuR>&VXi zlv$pOA=@)hZzp&0LgsK^?(Ip=-Cp#oXj_r!Jt@KiT)q6crH4>2%jLWBv*QY$e4LDR zJasAdWxtC>B7q17gEo>7#MXy6_g(}Nv5hYE$17^Xw;is@7(vxwW&j!H0;Zn>R_YG# zIaaPK4VrPU`0a6=k4eDIS-WG$&H1an_m01=(7DIAdeUvLH&fu>`-@E+-0B$$vZ1S? zmrMP9+-6=#1s;lCuD!jW^4Y6CXmxeE@agkLC~vY70i%D1as@UZFyiYqO;l>^Twp2BwHTCXuTK zJm!oKQL!G{@Qq*)wUaHIasu_%f)wWIaOq!b`X9eBfm12A5q2msg#WZ~id6duo@G$H z2@x)uBkztt<#9M36s3ki>0B$#iUQm4*yCL2zZzkSDJ#{d_$7!sQFgY8EC+z|x?OiQfznVYt3j5#Gh)^QG|F2cex*KuN4_ok?zu*CyWE1*I2Ch9BtY z)~`h4%5zBW^#url%j5BX2ZyI?k`W5d20=egQoS}a;w1!};+cYMMDKo|7(`_1(Lb^0C=L#YiL`VHF;H#$8hn);MDLe zKABhtZTwGe%9}`XV|gjGv+iZ49N#JIcuWH#=V;E?Lq9-BlQhc&2DKqgZP7DwS&}i9 zjRO;D3l9d_db>)YXnoNiam9aE`D|C-WvnWOc76wg5~OuFG7yMiE)OoIozv6)XB*n* ztmE#$C|JWPPirlCmMkMh=jzc^j#IL1y2SAUQ45A9=z9=Kcl^j#9@TE%ym5Y6mxtE8 z*CmC=W6F?kQ6HW!MBDpbZ5J&XukvCyw#C>-%rrAV;s+_aE8*XM$rqwb?@sMsDpi9l z!g!{fB*|mjk1i(VsNJ&VmPok}X?Z_B%FEHgI@;CD99x8F@xJmrcv9Q&+iIGTbrdrQ z!*o=M%18?DbPlh6gO#LjARe+$)s@3;JJ zzA-3^jeRLW=Pz^o&R?~c@3QS!zd|adFr~8tA%{;M&5cvhmWo>YWk5OV$v3l8?Ale| zapNx2!P;F%=*wUBH>;VLENkgEe%?c4LX1y;%3Rx_vX2kC3XX63>^st;RKBon=gzIy|BBF z845n(y&$aVf*7Omyif*94*;J3Iv^i(y&B)4KJ13iT^RrmDs{w=E5dKo$Bn_qhmvD@o>xrr9RAty+gi1i;l{H@3{(CT zfXvH1(ub=Q1Hs&n^7uH%@)rua!rAw-zR^FV#~Xj$wZ-JAxNs4Wf$+?cP+F{2(@T(F z)_Bw$Vl5&Vf}fRcu&9yBeeo45%kykTr}Y@meyCYRq@P_%^jac(zr1m&rL5whSAA}) zTrc#=9*bB(2O(X&_t(vNVFltvUV8$GBObd5I?-{%IE%zt3-;F!Zk|=zwc)Mh^_Z?! zzsq{*%D(=U!h=cYHg9a(lavH+wkFBfL|hxEi~S=~Yc-^#=tE;rC`v@|W*4;gNvvUr z0#)T$;#JY}r{T~9VE4q2gohNFja@7j34W~cFE_6BFLtu>$8NAwyuS(vhsZ}`>&}KX zuJ|6{l6h&5tvA~)^q}JiKQ8=DZhg2rQLr_liC_ni;d#Xf42^vAM6MfJjp+);F7av| zZ@2hnPqAi9dK!*7f1775@PVXU+DF~EofbAP4!WD zx;W!DMyuPA+x#-@!d%&L>pds*TyXQo*oFdW+I?O1J|AUEX?${eT(EEY#LXH;smqPY zE0-$Khf86kZCDwy9Q5mgHumUcLO-7JMCW}GlAvhAe~cSSMQ(6xFklhml54)RwU|I+ z?3uUQHZlC77Oh3tj}{;UVFENB3MlSn;b^$>8>7QxumyVa{8Ms&SVEBF{4b*q4VAKO zYPMmQqPo)(X)pJB(;lhKD>ItrfCr(?W4kOn{onld>*s{Cls7Gn3k>~jqG0Grd10l` zt;&Ko^H6OE{0&^9jt}1kc^^aD7$~^tq|@pVdn{)f z7YvWn7=~XK*Ssx{c|LO~LElDT&NM|XEU}a4-?l2a3*e95K9Rvs9l_+9X6Zqm_k=kw^ec(0AcPAH$9 zI)4*R*o@zVZ6waTXU^$6os~rfnhLRnwW|UY3CCnIn=SCoj!wspi0!6&Wx*Fq&UJQx zkUBo0%Bq=iJK>i2Aj9VGhJ3=6iQH&QkcJo1*)`;NDSC!zpR$C2gUQ$(m4 z6yfY!(x>Ju1I(^%tTY^11zX{$OQ8vkXSBo=lmChAVr z?4kG@?_EQ}IiS$+>C1s6!7SEQT461Ddi#Je%bXTF8Hk5`Ab-8}S zH;262h96JiDOf>R0}8%@2!p=&<@0_b?Dq`sa;aWopR8U-EM!GJt-y2YKRM?>6o)~d zI+uTh33GZMpN_QB%S&(RytGoFgs#uHA1rq)$Gm|gzk1rJRS#MdU^GI@{WU-rRcSiH z|J-g}3LFu4ZSTiQ6|ULKtut8OXoy@E&G*y&OosN-q%}9PP;wI}$48Jw$_&>Gsf<&!)y+?4c7~{$V;i-23YYf(UfTH%w!eEN#@c3CPjr=kJnDrGPn1dtSS_m z6T-H7ZXVLp5|({Wu0XCqy#gOF$Py+6zczU7cW@xEu5FxQ`6aY;uRus}s3P#pIRP}G z*D}~Tr$9iv!M*vLLPFuGh5&C}8?XBQuD&|%9r?l4^=HcG=oY5vkt{7M^-10bk#k+s zRhsqL3CJxR;U{$(s2tGDQ)R0PT2#^@Z1;#@yne(+L-Olhh+Ex;vNwlwn>8-|8jY-y zP@Cx+k@s61530E;nc3seer`WnXajF$^2kkIh?e?u0Sl!AE37Db**T_HZ!Yz}u2OuX zZmZ+U;saVgB5aWS*1G2IQ4PemBue$W-fwIT5)1Q+9a55z!uNLYhM}M3E9v~e3Axgx z0DL8Dk3FtOR8CZ%M;LAhpBJ}@FdE<8JY1z^{Ah^yarN7rpNyhwP3>&r+Fl{Fxw9^_S?Ko}9i`x5CsZZ7p?jJjevQET#Zk#su7=KcL zD_NRz3@s030O0j|TP@(-SyNEW*QSxejSaupNqslbCU6=E)jL)b9{W2lS*rrO;hL%@ zz1Z5IR*Uf`d;Uen^6w2Zg|+t}`n4{j4X-W!Q=bz9{(@tLsFF-?L2m)=NA(jKF2=98 zimE}Zt&i~J9~bPRFND@(8HUUM7E8@9XDxES`nG)Au=~SE|8YAhAkxf5G~pqds2t)M zr$X_N?n93Sq4gYdH9O9j=AmjT;QQUvMah`iJSoOc($BBeRQV9Znu zd@raTWNEyee!|0_I!D7;mYLiq_d_%D%|w4HcJyGs!Cr*)K6TO*p|tHxO-$r1qTE0Jj&UXQW|K0ZR~;RM`3Yw^vHyrt$DPfYNfX_ZKnwaNAq zAg#{4<*?8-1>MV1cFed=lb-QU0ehL!Vj}Z<*Zo*wce;4p5zm#D4%{-^?sxgIQYh|6 zf=`Ei=v;rZ*|=aw{yy;NXa%WR)C2KDc{fZOl9T#;Gk0QL>uaDE=mQDi3lvFe&BL7( zVP04@rDSeR4|L{fQRsu%B}vcyT^;XkZ>u-${g;yz?tBn3kO*n!96h%)C)VBkc9fU$g)HjGkF1q7kJB%yu^i>de!Px z#T~_q7n!AS3II1_l@#FuDbY_HMT)JUy*c$)k~TL{tl)ZVZbCQ%i9Z)+h6_S?qS}X*cw9M zaKy3jNS*qUVn0o%XD6P7MJ+{4%<~}LXrt6G6)(Z&9gO2T4o6P<$L0+fV?MI;Y&;i@W-?_lFuF1P1z5`J$+^%j~{4ke5zh3 zh)(qs3k$?zSSB<3iP^5YhN@uzqSM`2YW^|HMbi8&X@Sq)!VD1VROs3~%dh@vynxe{ zMeZxk4OAga&@37l<{B^FJgH(&840pY*nYRxOb*C{687s0_lY17lN5vs!REgZxG%l) zxN%%eHQc3K8vuonPV-%IWgyPwsss#oGK?R~msBe$L9XjDS z?ckwc<|M9FO4s8E_pP3_B08l9KTSb0v^d5E!0CE&bvHHKnY2f=lRoS9iV+s&ZilR% z8xQT^oSw8IiE!AM@7_XVkzw12;IeP+o2bm2iFA<{#&1j1P-H%sCwWmEww;Xa4M;7p z-`GVtEWeqOD+cQ3wOgF=00|(^Tj?TwxU#QOfZclr>r{F(`=j z`t9u#1TQCGoQ1%Pg+zUo?9%*WeZGtj1SloMjDrA^3Ad-Bt)O{=@JI=1qytFfQ?LSbPxXsj5mQyIm(LAyZLm8w`Ju#3 zK0Ty%q^+*Ji;?{uY|6N)%$U6!;ra|8d#STHh*xv~LsVLQ&c{XZ1Mx>miatR;R_uNC z$W`*ZaJj1ZANV8D1(t4B9S>{6J-)mI=SsCbVS6@W=_E?;a@QqI&SThOuge$o3|#6) z8{Zr+hd$CPd`6yH0kMgeI`kr;!4dsT+Mjo#w|XK;dSiLJt%sbPtMIyWwcqDDx<))> zX7HQq;^XYL%KzZhihd$iD-S>wLtBe`x#S1krEQ-n$(xP1*(SIk%p>ypo&wUdfy(#X zjTb#%{!}xzCxsG=KEF*N)%%1tRdEi~tF0RZOZ&F0T}!D^lr;P%OvzZ#))@%!#Aetv zP$>ruQtNpN9j&LDd?HWPdsRv6M*d38ipFb|A|)3(hDwdxKWuvfe~^p^<4rja{*rG? zTFq(s^wg{@v^qL+#57FX;$@5blD5hF5n>t0^N?GE4GnULVy-gK(JC}M_fVii%X{nN z?(sIq32BTLxAz+HZ;Ya8JWv=MJXYC;P;BfDQq}hAoNIdZ7LKDTxdeCKaUx!@AsJZ4 z`1GDwi7sKco~oicdmrxs^r;*d;7B9!VZ0>O1vOSgs7NLkBCxxsxA<6~46E}*Uh2Gb z73m-VV@(s2%IWs_;^e(Mc_sLL@hkl*7Sr^O490vs3&8>nw<*8NiJwh_`SZgd6w(P- zy&w%QtQ3RkC>QM(S(sE{T=TCsoS(rZKE87W28YkF5~~@OB4Sl6s+aVswQGL_N{3|l z73FB)mkV^CfVFvDr|(cX22x3BA7CwIw8Lp{}@r3Xp$# zZOR;v!udy5W8!5*lBy9#Q?BN@Ct~Pjg~}FTz47TJ)~WQR3qBB35@$~xl!-BwgsI>J zK?L-KVd#mB*G3xCWttDy$beJmlfuLW3U2aN<*LF`tOO6+5g3o`X3Y82Tn3GmF?rc< zBhI!$<+ZR=FIx1uK~~CHmSH!YrDE13qnbDJm!;+~95oF>PvhErU;O&gmCBDB>QsXu82c0E=+xf#|(~D)}`=7fHQBaUV4c)@wQw`Y`Dg zjwSYWkx!#hFlG-)TJ+JAnjf0a@N!dQuectmYNa%<8LV-WC}bMP(k3ji*Sr(AH?x4@F-8GVhB@z+nqXy07# zx-bY*#x6t~Y_5$~GgW+Qx3&zIanp%{j>JX`#=qrhkTOQSpEE77JDyO)#f#v1lfh)i zPt=vF9ES9?)1uFALZ_Y6-ep}Rso`&`(8fX`N{&HRyTmyZVl z?|ehce+TDI%zS?d+pC;((&>D!Mz!vR6~3ROh7qY7S0et@c8F(`#<3xA^QX99+r|2_ zOwp0_0F(WAw5Da6#yjjFl%d?_JAs_nFiEN<6n-O*NH+7>Jl!?GW8?G-5a~PGIPUoD zgLImNG6-}XD}Vc0QpR2aO*O1bLi9{u&+Bg8Zar7TZzd-24=~mXp2N!q=6-Q0 zZc^E(*U}&HM0*YCUdJrO9hoKc|7v+6gF}O7?-VP#$O27xDc_s8dqEu?shkcDKk~mN zd^1w0`W9b4Z6?Wx7aKGzpta)lODdZ-E(8Au8SO9Rx2bI3K)zSW&cb+`2Cyxy7u4PR zu4FIRC2dMcApPr8{zzp-hzM?X=SK~|`*8aa^C2=4SG%&cRT))BE)uVi=aN0CEcB8a z7HSV!MWs;$jCoE~)8ZX=#~dtr&}bf)xRFuOI^PI2yGQIU6hfbM%DpJunT0Rf7UULV zg_nHkCUcEyHcqXH?UM}rJ(#(-jbh|ba=6>*~BD1Zci=!%p@OhKs{7_jo3lN5y6a+}yG>TdM3R9PTvpW-zOmR;7thNV)Q; zMi|mubK_S&YJ?Ig;hac8>{C=Hu=g9Mq=54{=0#lXr|N=_iuohsLqTIn4Rs{~z!#$L zLge&o3V(-IjO15oDkSu)XuyckRS8Z$UE2D!Y63}Fg`XdWfB%BA|G5v7JLRkQGovLD z+`|LUM@gBPw(0K(Kq~{(xb+q6h@*~)4VT8T)`DkoqnN}= zSsyoY6thi?ekDW4m{O}ET9{xPH3I3-P$6~|uirCYd`NIU;=&|?w_jqp(`d4)S^mwD z0PtL)oj7>In>*H^5VX+rP@<98Ww?bZgT&6$YJiT*mFhaW<(3CL;c({S2G~}%HRsn! zGB;@}aZt91dX}OfF|Ziut<=m{y~f)6q~yhJQrn}Hz@j66{khAD-&eh2GsBtTDfJZ@ zRH#=|ITCt#W#4#?Nx*6%$G!BxH%<65)-wuJSunHwUe(bRYax3QuPqk6<){~ajt}2Jh10@b7DhBv>wjXYm=>zvYlU7CPQR*u`J4)EryN0@3?sEAlAu7h zHn`9QX=Y36ko{nl@~X9o%uwZ~JFsHe`KAt*&nV=zy3=ftg-~i&KHREmh;<*xA?coN zz~GKmgs=joF$2;oN75RVlNOO3wo<>J!C>|zscj%kx$qsHhhq9}wihJVh+#wQvm*Wp z%+!a)>?CBQ-FiMznwG>kA|Cz*=en4fGR>QnbP|i1$SxVAx;3OE7Momvw5I`7s_v-1 zLS#jXATWW9S=#s$o1@4KH5%XZ{!5p!mq6(|H?0KxD#- zqNUJQHCy7Q#w$@ePI}xB2y&eo_&U-O5d{#?aHxqS|dx#wnf< zs%tg`uX}_u;ci!ixe<|ddg7sZoak(5g>R+tWIgb`ZX50zP7d;5M+h`Q)1qlSU{B~~ z9D0CV-1v?tSv~$uEZ^hkt^S(eV1kI(043bUvlUweIz{6oYb#J!mWTDs2{+F1)8)o1 z#WUR~H{Q9c23g?LY`tqj*ehGX0xVogo8ax+&sA>BO1F~H)jj5QXA+O3QL8B%BMo+a zlnNwCiTQmFnm40F9ClnTV(dm1$Gv3@I7>7_J10RCY?QL1#{Ed2OvDXudViYyaJ;u& zwO$oVzuRD#5V_%3j2)R27UK0)PJQO;DF@)GtS6-0XlIe(qe)6MtPM;BTKouqjG9kF zOPjL@e3w|>RtuZ{xJW_S*b;b-d zj}!9p)_Fid%t`S?B<@%#tsGI|FPS+cPK|o<$lP_7l!S2!c#xM}>Hr*%!dZ8lK&NhH zUw_({O`o?tel4%>zQM!mzO}yekOn+NEAl=Z*>?r7GBbWcL+7SzDvCB_Y)s9;TJu^=7}bqn9h8SS%T}kANuiqrT=`#Vm%U z^vFACGgsH;J6;ZA7G^2R936g~iEejBpb1{#S?{yR)*-&e<#>?Pm_xlWi_}s&hah2O zru{xq)CJACP#^Oh=K~m3Vc(2dWGmR)GuxN%ReQH$C=3(0rH@^BC@Y-hP)Sj@=QOND zDETs=*ms;Z9_42{;MncM%~(tpsf>lCY%o~6U>_9!Kn716${SlWFDdp-N#o^QENl}$qZSVnM1Hiwa6)!N=SS}{udyK}7 zFczS|vHvIYYd|eA3vgVDaEP3Cx%N?rDUatEf%Qyla@>sj`a)EbKFD)Rgc5ZKok7t5Mi#Fu5TP}^Yj#P($X6GKZx*;r!JZW zPU!)WG@E`Xxmtj#5=Uc-BuC6DgBkm1-+{p|JIH~65JajhJ_l2Ie!zlIE^5+}&)#1P znjfqFz=Ripr7~T6t};x&+{QQ0vs|!Ia2zGmQ%;UhFKB9WndTSP0oItfJ575GvmQjjiD{p*`BUqGK;`FOcw{sjs$DFH z`xG(|9^QQ=83R(r7h+di9~Ms=XyaCi5&+t)NKY~Bu>)RgdQGg<#%Kk`3|+j2q+(H$ zjF_ZugZcDxU^ir)i!E++>&tFvWf@5@jFmDB<@H=fahMOOvD?fOP`;d>;<$Sz{^(sv z*T_?v$d+iMSNWmRPagIQqdz2wz*4?=Ws|gKb7WY6VqhZk|R-MFzRjmZ`{F)+^Lcfmb~%}cs~k4j6z443N_m- z<}qcQ!6G-(ODIj`FGi&ST>0Fc!sa+Y5Jd6z96utQ7JLvxM#Mpy(0wf82}zEXgZ0ep zmWKI=xV!aNfvTuEdX{=p{I81iEbziOgr^Wc=-0C1rCxQU!bFk)o?cZlaRPnghStuo zA6iI$JZyWh{pAOw#Ha$noZ7=YQ3iC zTBRN;ss#IOhp#`xsB0qI1QvKO*+tyv7vjWazWk?G~nXO+jQ0Mz(! ztKa5(N6Bp{>&xinK5o_weZ*^1**RgYbuKzym_d?11OT(Br()mdrK9)Uz6xPxqMt7{ zw@zgNS5OPx$aKjvLfI)S>3lgx+;!Z;T&mlH`{rtDg~?Hz;xBRF}{rX z_fn%{2KMwy6MJmOS-|&zSp^9QN4Ekg@$@^d2|$_Ok1vOzFd8Q+z)XS=uDA#94#(3O zg!u`s7t@+5fX0^&!Li~Pw6MoUuim~1QNNbV1vhB|S3*e%1j-w=NQYsT6* z@>x7Fx{;Xi&xh*GZJYdLzlg#%O?5qT;$Yi9FG*|K2R_W$Vq5fGL(kjC50(`mSk0)( z#9{Ihyw|Tx^2SL9TXIl3hRL;GCMVl^uOnQTIq_#QV z-SHzN>Eg71vOO5ZNSf0s`JjOZ5jQ~gT~=V7jqrT;mWo!t_GR~!U77Y_UqS|S_EY?V zHsHCgr1%=D6iRUw!minK9=x7?!mI2Js&M6enQXs^JnX_^G7(6Rv0&?0U1N;46i8V$ zvSABq>SAZ+N~&XKPvP}EC?8hrfq(&I8#ZT2l0W!>^Y|^g+rGdTWPs-3;+&l69;T|B zOpw?9N{|6?MN%-$&l2Ns{-CHWWDrH;&>xG=n}=TT8)p~pTMf|y@O%tXEe>|7H=nVz z4ny}aIkK*P-})h-(at(8G7!#L^B;bYcw%KHabB#w;U9|(6+HpLmY55a#_wAY_;?eM zliX!+F@PN%dkj${X$Ip{TXRe2&-M+M#6eKuZXGidQfpnQEO)500!~7ZVi_H`dg$oUVd17uZg;%?*Y>U$gC43YBJH`*3d*bfjRE@U6N> zcR&$ji$r__Ymy;zqi*=xm?PVz58d9+obzMM@0uAsZm~72&!7Xqxencvsc`6Mp3q`H z&macA{{nhLLc6RY$C|{Lf8Aaq&`(jhao+d>k93!^1zw895$&BOJb5Hf0Ub;;kO*ugP{20i4Dm z$Bb9Y{i_~xl*f|rPh=O|1Pf=6ENVXl(0P#foJ?SwV=kT;aBrodj9)HKLhjdn4=LW*2SD9|VVaGfQG|O6r?gYQThuwzbhH&|)cl!|r#>lN z^hssi@b!$+=W7p6^fBrorQ_x#sh7u~_0O+A_`{sqD33i`~%GF11WrO2p4-JFd#9gXgA1f&iXuRRJf3ONXK>=sV=1Y>L6%97j*>Z)#uETy@LHm6n!$Ysf?G~ zI~l5&S*7Qq6%<#xjSOZw1A? zsjp$DF%^@|Q;p6H)Brrrkh6IO|M!}S=G%A=fy*loBrgXl#jAD=3y#6Lx}etN#2B5o z-Uxhft|L5VtRDJGsncSQ=Uv%^nTQZ@s&T;x^rm#-HS$h%30S_=&9IMn2|ruyN8$BM zStEnIZL_7ti!EK1WqT&6w8yql{eo6wdl523*9jkEVTjv7Q^wHmQ-VrBX`xloP{jgVa|i50S!~bGuVXogSu= z7v*uhd>^lK9(rIf);%i$lM)2MHMfxB(>m=uyG7e;TQl|G9=<9swH&1^f8U*p`U z*wrU}wNX25;ECMe$)+BUX6}Atpy~7%x!$Y&a1p}wXxh)sQqI6_XaSFEKo#()bdT+{ zq{9(^VE!^oe~f?Wp^pZet@CGoMUN}tNiMWa_=t0XJE-#;sr;J@hY%LW=pg9I_eA!e zVueSdj}2zAV%&#j##uzA6`!pw$`!_rO9XOGmTcw(d=mNY8YpsX?5+hWeGnAS4|rq( zrzsN)%G3=Hc6cj7v8Xf0724^KoLCxyRV^7OZd-SvXz=7) zvwP7Jgv@yaTv4NIWCkg8UcEsf!!#GYIi1#Vu{L-;WN#0eV6lIvTEB%_iQPf7^VWS7 zlY)0Jd@=Un$}hZXr4Kvidz{K_;bQm*T7RPpwzYpk@M$1|D)3D$i z6f)4?w|sK166V6A*evIHB~ZZ>9F*T^rdUaZd8xw+RFw!k*aA;6oCiB-y~9CmaI<@Q zaCr>-Qg;H=2S`!C=%XO=~;Z;Rl9WGQAMmpbHa2S@cJZQ>q8|apxEH!A zr|(J4s!27FW+r;Gux5rTa%Z}zNTSMRzbM22*!G%AW`Fm@(u?&l0~8;Zp3IF944^zP z5XPenBE7Rt9_dWn=|orV3^kL zxDD0K3`d!xH5G2Y#q>B$f0@0FE@R7Xp)y6;zzOf^6f!D_->jO)IjhsIR4@#!qL4Rl z5zRJQ>VL+{^V>BZM0PBx3@6nf3FGn;t$qN2zjm0w>xUOkwSgf7RPXr;eMK@>^Q0zY zf}*j}LNSAqoXH>WPiL!fqRW{0?ANPu_i1)cE9!cvgrXSbQr}cO`x%2|3J94**y0F`s50EMVn7A%PX5%v*Ze7U2E6-hU?S(wruE=>#dQ) zCN0n#<0DDg3)qxTz zb&4t=X-j5G+lIiF!+u5nWk)uc7H^|4bfOb6*-1t=$l`!$?Dbth%Zln`&J zuL>*g-Y;V4`PT6zU5FO;h$TEAz1A=kD*HyYkD|J%mnVTg{jDfvE+)g>rLE+iU8%g9 zV1!tJ?#B=fnoes{+~aTw8Cng6Jo`^HxI9ePw6P7pa1$N_Z1A?XLHRPWQi1B@)^UN^ zMb)L(7l$6D6~vKssS?8!QdHXW46ZdrXvrzTC}a%?*j69ia8RM*S8~)Lc?I@~bL0ix zs~u{rQk}2*_^X>Fh@+Yxj{YYZr9|(PW>?L{EpXOmUc=sVsr13ZfW_G}$kk5N-E%qG zB!ZG8QyaE$ZdW=bo^|JVZruE~QEoiyi-kv~F^jUpXkhc)JnS$wTP1T zzy7xDJ}ve}dV_;&KR#M{=4Slv_c%gigQMcD(%jJOR1qT(@F@9}R`uJ2%ExiC5VYNn zyPLy9(c@l%MPYq8@q7pC*MaJm3%8`ge2ASHN<{L{xvn#PV>QFM-$-a-erHV0H zxA#-Q7EZ7Q(Zv1b1%s>WU|g{+^pp1{t%nsReUgy*-2{EDxs`~7WWNG88gP0;RsGLs zMjCda550qMBeT-4u)ydKP4`FFa`aS_Gr7dP`?a)YSDMUW?uNM$=YsZ$AJjGDMzH_d1b3e(m{oDl|qDMQ`8)d;mq9y zY)-311o)oXZht-!yPr};1WYu|_E8fA3*>})rbtLI<^Wj~w>)$pRkt!_CT-;$&V=04s6^@bCkle0;+7oKMJ4SmRN#R6WnY@~5Z zGa};!7TVRsZ%&tx<}f>W;lCO%oyZ8x;j*0!I(LDj~zXH5Hllz?` z$(=p2w%sv7{?)<=xi7b~rEvjet8r*U~BJn5qgNWj<)clQBcJ2n@O3bguP z&VOC<*WalWJD!@EE~_Uo@vZIwaQcTQ?7EKc7tN$A$wICs0l~4b_h}qF>;Rh}gv-eb zLUU=BA}8#)5GJ!sY3to^YX7b1;v-@@HjZR@NkH49-aOSfu`BBf&hVBK5%qpgisqNi z>J^D`kubK$rK3M%AMV3{2h^*eZ5pq8DuZ_yhGikr?}@RBgWRRN3AkM`^X5NYAGE6G zq)0Q;poAnWy(hB~2r$*`6slmzM@#&BE#IYfLh!uN-3= zfhlJ;hSdI*-NB)wNAi$p>y%MTMqAC{X8)qjh9{Y%R@+@&J6P#pAvqw63+PS*JA_Xi zPE8siL*s6vU>33oWEa)EoVaVc+%snxAZa-_PrFaNW4DFL&MBF4%y-BOYr;Gd%-u!b zeZszqU9^ag!yRL5JybDDnz&e`O><(?8Z0nPL-;(^j$`*+D{JI4E*av!k^uT46Wx7N zMpr5?TR#)>i(zv{`6&m*H49*q_#n*-8A9jiWFCDY;+@GInmJtsj%>%yo2Lu0%dYj> z^B=DrtnKhnPL+DGK!$yS65v=tx+pUWKr3BdB+A1H)4H)Q7=GNjFE#@Hh z^27&G>PItyfhs)zDlxIa1?f87SZcHe@@xdj#~Q3HC9j z5F63EM7}b1;{;*^<>9?his>OkBl}jd0=nN*7$6s(5032CCReiF=7nmGvmpyfzHk6R zxAs2HLHiEUhUNSVkBq$_s&xn$A&N#AY|W?y$qZTQ2OK!Bh2-caVr5pl8ua8qQdjjw zay53#Q1U~bLeJfWdvBc$CP+Q*wH~9cC0Ke<#HBl-#dZ>TIaXnsUUt2qAtNQL z1(RY0)unMmtweY3xjnmg8nsdaxY4^CV$;{c>A(@l{qBP!hGC`6aBhs11q81-N+kUu zH{#<OLp>%$ry6n)|4vSS zyXRmuZdN0-ZNKMVoRLxz0>bG!wQKY2w&z!=6=#^@k9w0IPDR1+pPo26b8K)>bH=vR z{L#-kq%-2pN>u?U#GaCOkZYBCa5D;)X}um)Y*#ACq%n!?QTNpl0M1Vm^Xg9@FOzoz zK4N0Uo<5ZCL%;s!*g=`a-(Mm1H*K?kfB049Jz|P6LSO3NXnidx0)B8G6DudUW6E1G zH}5bvpF4Yk^y%h;(=dXN8!Y!PC(b=crweU-Kb21z?@k5@4tD%zz{#kf3`7de;E%Pu zfpwDBsCr@=#K4?-tc)8$`JoRbTQF$d=U!oam9f^;LRbL#;>{c1@|Hfv4C2LmA?`&j zZ!uk*bGcRCIJ`>I@#}MW8`Uk>&kl#lKt!QwX;3 zV({=%eeCt}VN1ZtGjFLL_vwxY%VWux zjyEhp^Fc()q@~D1ZcYHXmOR8~JbH3$$<5myKE82nWRRWk(&2#Lcyo`x%3ti@Z12`M zTcKHC8QF;`qEOmjafd0()stjarNRi8n4jvbBjZW0^W22~UrSdV*JSthVNM)U8zLQJ zARW>TqZ{dvW^{;j3!}S45D5VZk#3X}5Re9u4uMICbV&+PP>1 zAHnbh)Aju?8m}xCeYGHC@5wC#dS_D!%_<9yXT?T*riEQye?aS&z46DA!^InE#K=J3 z&BXq=QoiRbljt90vRMu)O_KRV2;O{1^XQ^vJF!n=j!+|SH1WqeCSdTM8_cIj{ws!F zH&R(0_9OW+f{N;`|BW~-TDk~tHf{3!rufKGnVJTgwMQb3fm&_uNtE0UwYTqxo(`q4 z{;tk&m;CLXXcnL!yf;HioWD0K!^WC%-MDc&$oyUfaF$D0@fnu~-?tQ1HE7yeCIGqd zd~2u|E20gcmP7L<-&`Gy`XrcBu$hvAc+wqdWL1U8Z+v0>(p7ktGcu&RF$ax4<|GmU zqeC^@dn(tp$5n&ZfZc80J9}fMEDSTVPI0Y%&aX)$6i{bcFP$TjFfDo3#ke(Ai@BJq zdVuYMHXDERg-5@@)zjTTQV0?*^99?lS(%}z6Gf_-%eh!n6H>WBn}m#Ky+?sk+BhzE zKJHR`{mOOF>vLnr=s~46eo=RcIJ3=QySc(B3Np#}V=m_h4lHJd&q}#u#B4&t)NcNC z;T5e{>xE-VEfyBz)4MDHTCk^}@BCb(H=k{Rf#>U+aYfOlQYRsZoFxn6lgCFFKqcG9 zVV*$-<)>{^sjCr0Ie#F{qm`rpk!Ydv((sv6$DR_W)@E!*ZI>s*$448A_*E{cogU1+ z|IRV8H8#s{Yux|jZ8-k4X#Tul&0Db2B(HX=V( z&8|m~!U=z9wEJr}l2MT?oKEB%17W_Jg3gEPq7s6Hg*sYm?`g)jpF?Y^mG{Rr+5vZL zr71$UO~z%tv?TY-yAO09Ci*yLe26|JeM2Al>o#>Gj#HM`ks7$CCI#=zhUADI@ zlw}s7lacPE3p&4*n?K$l zMTZCpxo197A;hWv2n>8ttUSvvYeRfQChC+4qe=3BcJ|1Zb`&nze`)1#q7 zAus6}ot**W)!9lEop(N$8Sb8(VlwPyl{XEAsZ>K^dMH-rkxWFOF%)95N%+ji_}{>u zF>VhL@qO!=K8+A9WGBnZqTlq$NKsIH+EctXX-bHk3|mFoE5FSgbE|)LeAw72Gsm=k zOhAcj-NekmIKlkk$fJt69*LY^4XRf;j@e!KxizB#f8qJ4$V1#amF(UTU^!pr%W0Zu zg+(7&#xpLT#P=tKX3=+L9R~#Oi=7R(cn@;X#5c_T%FT2(!=Dm)#6-G&7%(T;&G`GW z7G~(W^<961BPWE%iH-aGS4hj;4nXKU719!3gU5Hz4E#R#FFPa_Z7CZ=fec2M5|GxR ze`y$Sabn$8T`&Z>!?Jr_0{;EP%WckRjw72i2v7MVKXcn?Ta$^>^W5UDkVZZ{sY zinUIbaXQcW+-7e#wMWj*#Bh7$9}Y*H|65B^WQLw=S+van_WTAh+N^qv(biKi3rv=g zo`9ZBM~&i1xU~MPiATo7d7CyB7!lR(93BT0{yzfV11$056o>&Id8MM@&(pd7jbxLF z(-{Rp?Eosi6bTxO%d?_3u+>uy4uQ6jAR_S%A0#fi_3Ci2sB;5LWWS9hcH)q?c}(yg z#^X|JehiU8hCMSo&~tOrw1nzXS+lW1@QLlo?)Cf9YF1f7t#Sxd6|$ zCZT0W$icD^#EmHK|M`fL>R@NoPIzK4Uz{X^>>U&p-MwgI-H2)qud`N67o-8@J{kRg z%eYxq+NN}3hGtVkT1vc{(ys}uJJ<=?VN zQtU%pM)I^FA+mKO^*N{LuYjW83{a8O&vLu*kGXXW@lrNpc&+eNk1fsx%ryvpRqAja zzdaSw-)aaxw=R%BMj5;czztuurna7zjTBFkJN7+^^p*N~L_xf`I9k9G52+fRiL5T# zkMu}mYa0!E*dP{lVKV}es|+$?ZY{d*03gF0n&?D1x^JRI(-NlQUW~X^z#f9=d{G(x!nRX= zXm%RPUs{^ddMa#NIg+q z_*U={sV{Y-nxQ@Y+KQvjGe*orUJ_2VX#XHVh`BiJS z@7t(JYi_hAM1_G+MeA7qhld1Xm`-I1^J#Z0aB4nb_BTi+qb(JuaI1j~?x@LTncU(HfjH^@^OI ziw{u>aFc18_Y?vR=bqG0BEJ7fGs9}%@4_>^;XE|ep{2JnJft2jM@SWBV;_=AWC>V4 z_xcPpszr4rIh22Y$LuTd{@Q+*jY^XX zcg>vl3R?nIuJ(#A>)cpn?zos}@4q`JYFBWNJ8g-E>w$2s*W8P^1KTGk_L&DXC;flM zi0Ocag}B%8YjmmMTXF}zxg=z3BH&Q1Vjwy?^Gg6?Tl68-MB+pW`*()+v#$^6Jab0= zfOj*n{#qS9kElu}rLruTtZBSv0i~pze_a~2*ZN#tvAsB=yysEEc{31(d zxomu?=Tm!wbchB5wpA5;?KGVUQ_oA%_H4Y=rlSEpMH3m@$$+kUdt(wT1`~p;MNfHF zg=3Mv*lcgm=_p>&ljmG-&K@U2o+jD!>l~R51U*HkNKE|ern?!7xSsIs)9)<$rTg1C z3F(=nHCb~We7kq+g?gt#oa18t>3qGvG4q#IY)~AbKOHWhaS8V#K{Lw!@N?npm0_Vj z8~8@oj5x*rv8RQM=_riP^qb!tW{hZxlBH%UvX%gBs_jB| zPrgq4c})?KW#*TFPZClAOE?oJ}o&#b2KJZhq9ne z2;D~Vae6|9G$TIf=XD@>lhah-b2q-Ubyx1pm3OJ~bwbc4G!|a3giuV~n!mrAuGT5B&jDPb0(IAFV$uzK$TDH(N)fdx$ zKk*FdYths)J{V`D=QwBUuGz8&Dfhh^V*P!ib$nCU8jzt%xUR9^Ppn zlmQPbP8l zgsZe_NB%b(gfWWRO(CluW;GX!ImYeMX0)w_tTiz1$o|%z2IR8{tb@q)LgeTX`yLPL_Li^`#j&^=`hCnl_x_Xc zdYr{CVNh;pI<4^zWnna$Ed?6)2|yv1%zRpHgOcc*{`i)i+;;$(@cF)~w9S@>KeW0a zBX+nSB7UNOtbJ@ar35cU`CH>mPl|L&8Ekq5-&u4w+rs&w5QL?Hr{>W-^ku(9@t9rMKc(N9kVs%1344Urc06CnD%E4R*yv; zNPI$wU|!UV*s?^ubSWdpdCeCti)n#NdhB~)*_fRo>9Xja-fZ@cI$Xy4_XdterKHH~ zXta5LZR{NDuhhv7c>NVnnxQ&fZJW?+3quWRO_PB<dIOekz~kh^0c+lV!N7N2t&qn0ze z$li1&J}FJ9-Mo=rvC9o!4-aWf@$NF7jx#5YfkW3s<^oj@lezzz0U17D#Xg1+Lu|U; zH#kPT%DWUwhe}Rg^lKC6vsFFz5(ou^6ZF+|XwT|;I^GJzVl%ZM`fz}u*%!8q}Cj#9SyY}nWc0JHA|kQ(OH zF?UHi9CiK0ZD{HeXdM4{OioUq0td&+4fWSH(9Wa_osGS!WW^lpw(OaGUi08|dQ%2{ z%Lm$Gd1yE6XJMLkrXVq&#@p1XXXadeCMg#&^DX~~E)5wHdf-cM!qj=b#P)qO%`8l} z;%LdB?^q^{6T?&g^z2PO;4hyF4-qUPm}P*u1jn15io7j~XBvRi)u~nxcUi>w(V=F$ zzdFh0Sq0i!N1QyzW^&ESz}slN!v02D_1m5OgWP988XwWQ1?fTZr7Kx$?8UHTc~_~) zOjUeWzv(TE3fx2KXoo#g;_dWB#b0ck&!1ww=04na4MH_L&mXpJn+z~&%rhso?3OOy zAzs2@&w+3!wS$vh|3M>-7L@@~*4OBxsg*#R)LZtd)(J1&9}bIpQ#zvn{}bON(l;~e zR@P(TF&`d#{13gkK%(64PlI!s619iIb&tK!%Z=#FhK@}Q3j>B~E-{;VBCZ1Zisf?) zlpGqL!H~vU?NW#mjrA1&qYNhjb0@|nmczB@0QizaMOvlFGoA<$U}uiXSa3n?jYXa3 zvVF>mH&40|rw`oV$*eL4Q?10ws3fl+{wMg)Q=?lkF>=McbN<*QB8-B1 zBE9+r04xbC+IpT9_PW|3|96Nj77d$#8Ht`4agKS(#$k#aWQYT4$Ur^q>trGMcdH3{ zp(K~}GpKFaev*XZLk{qIC{jsoZ@I;ibKV?Z@NnSI*NT_8e&nLi_EhcMnt`Wm0B~y7 zENo)NMcpD3l!tNM#`E}lB|+zw*=YBJ3g9zHKszubwWwRKj4}Qvr^`eB9N$ky2F^+M zA6?H|+_@jXGB)HdAFo6}>_lF?u^OsK`Dw)@N75f(#-X)p0z^+i?US(PKV0K7WcBkl zGI#mb3FyB~uDUX2YW!JP1cV_qZZ|je2YZ6B;ZKL-YsakOR8At8T-%5gaukvMs3)VR zwa|z+&!@f0{eODkp`Re~cKSX#yOG_?fMBd(U=g<~p~7>R-Om$+&~`?t~m3Fer*X{wkS9 zX>_DHDkFoYNkep_S3I7tNkbhMJku-PY@{-wp~JbJRaO461z^-$;h zF-TPozN1SLD#Y94tCY*Snui*|BkwAv4y(GF;id5iUf*c8anyjb*`*@~Z}D;Sz%Rt* zo>@m9B(o&HW!6r8&aU}K|33*O6}AiCp54s*?U=%fXOI1oI})3{9uQ;`{bX&Y`j*up z_yqb`UwN058)JgKR6bz2OdixmB$47Vy>9U81ZF(kLGSk@dl>j4xFj z8iHxGbi5<_pEm_HDz-asL1}*~lV`jC5IGAMAov(1FzUBddPdxc?Yb1ko5L2!&F*JZ z{Z0HxXZ!it-FB7*W=A1oMaru}d=yD5n%A0C8sFsZySC7IPO`!mEqH<~=&=kMau`n6 zN$|<_8OQ}in_d{Jy~x{(N@sYOmOG^T&{L&tJ6Gn{1Z?cpEIj2NjndB9SX$$rq)r%7 z14WeN?bx<2oe|T3oAaOiGa|1=MsUG?2-$!?P-rv7EPj8#`c~kvys#WZ3r=6QAe8p> zkH{VTi-Ya5C=4MU50GLk+Ohj`bG973>?Kb0*=N%!?e=8aTq^b|k}GUOT>;L^47KE- zvZS@@Tolqx?HGGap=(tTh1sJ#%dGWr-n04WKJ9UL-~@u@Xu{tUnR2tcnD=QNTgFy5 z@WP+wvpbp9$m=3x!y^4Zb=lGj~a^nLT5PpMCsagLh(Q zWO2oTo+wAY(E@;Yepw_~hRW`N6&^=W4=Mh^`bFtuEz+DRAhif}Xk?J(^_Bq5LT=me zyy_si)TEvzkwV{}rdJ&DY%}&j7kla5(X}lb;$x|=zr^uOQCqiwF2UZ&V+q<~KT!GA zqt8R74gu{ouKmR5GuZA-@{D_RMNOzmJwmOM@AD_FmW z8CYj}2WyFyjOmTtd^dJ&6WEbkO~aU9`eLmn5)BAa#2m)~R%t}tE0I6Vz~jA&a6$V& zOYK()xVJZF9eetJ-!ha_NX^riMy2a=`qB|ENw#sqkHv92E-bqnjnD`-lVrpOA@kmb?poh@*Gtsa; zdAOYtNr(~#0Q=?1@XC!e?Lv&200ySB9A>e7)fPcmcgtbaHM3nV0i94lh!YKFzuX$Y z%k-l9*mbwxDInzh8)%MiBg47Kenf7xd=FGPAu2mR-^&Sji9-*uPZQLCNat=}(zak3 zgVBZiA-!WaF;1-bIkf;cct=*LgJ<{$3pdHP;dsWJtemmP5GRd#?Bb#xNJb&P-qhU{ z5`}d*279#{!z`NB*u%3V4^zSt61unvLo+RJNUG-Ni0W(Hl~#=4i?DT%S4R6>YGm?o zwov{ayowm^><0dfJub^hSKuQrMIigap5Yn%_U)`W-(b-X;jkXdad}s#sl1C~!|i~( zAyZ(Vs&e`0W@*IK_68xBNV@MQtnK!@^HvJv{DuX$s@rqX z^VNp=VT)?BJxEI+6gI#cw*sV${=Fg1h%@{jlP3a)jxsz7#?P&KQc=JJ4(D&R_y;82 z6dOOo=@tVLY40Ym6bkV()VjI}qc9+i`nh7N`usi`s-PrCju#`RDUWc{`uRzbz8XL- z1(iLmOZWU2A}$;ua!(kZIw#4+CzQwR>=9dE>jV4cNCv6fV?J?nG2O9y+Fn&5+XCd2 zNYcYx%NStG)4Ub}RqI+stiUgtLaIf?2?hI?waQb(Cxg7e1={o}CAt2eP{A984K=sj z&wtw8P33*Gr(wicO5YsWE7K=v&En+fGufp;XfVmZC-li@{yH{bT$11!YHx+jtEP@? zmnNmv=MqUudnF=)AD&`jw_!CT=gD_+a)m#Fq z;N5y4d-9H9#7ARMt#LE?(q_oezHEVjw6xl=Z%-c+!$zL3UR?gszMHOfG*&=Yqjl#( zaDYkZ2JDzS5{a-!A)?ntgKs>@syK*a*e37RWui1(@SE-R!4>i+5Ev5)C1`G;Gj&71qiN-#?u5EeW{4I~~A^d!FJAEk=9Q+(ziK8vqZ(lkwN0mC&U z2hDjR@LLHj%gS}Nc7mFovWGWNWtDyP+O}B?ei~gYbU58{)4)JH@HnDn4R-S1GNZ-@ zf88EyPl?I^N7OXzA(C1m48ZO}k_HduGVv>ql$HYa+5!Z)Y9BOTe1;lgf#``W9dqP% z#P}*=#ZU37A=Xj~1_o~@HeB5~gRCN_0I*+gFU}-i=Rrj0A#|)b!Kd}+V%yZXeNv!8 z;Abe9=w$_Me#=@9!CnekpR4s%SACHo)ID9tM~Amb?(nb=*{A{x6HS46;a zCu9lU16teiZC5lo<|O{8-rg-p zZiThI1GG4sOLw`byMe42ioB_ch|W&P{?k_Sq3vq>Pr7D(=kSaN?o5%hZH=C2G++65&-Y)y7ZWuCp&OnVDaxyKeQykx4(6ZW?U&7mAY-kbHa6vP?F)l z4*2g%Pn1QLaM0pZ(fI+fP-}~_*;N|zZoaf0UViYtJTFXxmhs{zH1%JX7Db=@(_^@L zB6p~9KouP&kzYv(LLVX{kq7;D$GB^BxBnD{bOw$80?0l!uC3`S7;q2PQ_)5WT7{T2 z4}9?)Ip?Ikk;meFtwM@cV~l1Aq*Um7ey8M{M)DI+0~a3`4Yo^o-`BzFzh64^K1v_@ z*~>>%Go{MNMz#v z8$p@}v1SV07uP_OT|cp4Ajj4O=58Q1yF-~y+A%!p?X>$kNEC} zcnt(YeXQ`s;NFC$3~BM@^R2Ti*Y+)c3#scZ*}icv00A!otVA28zV8{z4VcrHQ>gH_l|#dm)5)vR-l_#}YkmvKr8?OfCa`JKSCe6Xd9lc*T9IY+ZF^r( zq-}TGd#-2c!PI?qARN%);h96G+u{?~y6qfX2MPvh#>~@zx!2p3G8Up>;#6MY7xDMp z;IaPiQKFgDSst+dnrL<3Hfj0r6bfS*<6fn!v*@Mg{e{aOS=G&Nyy#y>AjKht(o#Vs zef_c@*|vVO&-+A1Jl@8@Y%GQpF@J5U?fc;pEDPd)RWipGGr{>DNgAH%Fu2O z0nL%87QDAAo=~n!Bf(kqp~56`paN$+O2)lwV_%$KBcHSEhLZgp2&QgncBkA7|5jGc zO@jSJkvk)@m|szbw+&SM6xf&$ z;p7Ok8_FkBPj9KW|Cdl;U_>=#sD!t^!&@1!K9fM%w~2%3aK!?x?Z*g`QvVQ?Duy@r zSqb9>^SQ=tyz7{}jz`hW-eSy!p*ZL_n!_x-i5*&?)GhdLgh84KXCVcbm?shOIAx)? zdgQNSUX^)bg6qEU;j++mqKc>ctEu%w@mVSZ}gP_NtVa#w>0;MFPh8rPr z?-$+|fiDFp86P!FwVyfb?RTv&o)izY=*tI_kM#Z_IiAZl88U!)HeZlt?1NG*se#M3 z1&YKs`k)e>CP`0V$#!r36+6vb#L~qG8M^#gv9VK{LXL3#%dlH*B-3*6ZIB{ke@~v6 zEc^tw$Un{4(sbwhCZ1{Z->rMKPgzFRkd)$3(Z{R(L1p)g*}U%odDDv_YwqpG384z3 znlt51Hm%`4huuU{o{{Jj3-Q7Q9}y)}eRWwZ2)aBaJQ1hsDZ#Ybr*J;HTMy{j z&}p;_$PgY2c(P)za&m0xSM9b!-+PsMfwA;*&AUhAYQbN z-emdSM42@|H5E5c0A4;hw=|bhD%h+_wDe)Ex!JJ?$I#Y0sN(l8VWpD( zz%Lzle(cVI^mM}UvX)ADh*3=aJ@H)D#P`;Ap{luQc9?*jc?9&sGs4+4IIlqQ*Y0W~ z*fgmtLr@CX za-$kdqsl-lPA&4Wq0&90POA3)t~{Zx>J68fRm(K$$Twe~NLD~F#6^-aITRcU9E|y7 z?33SQr5SC#A`E4OXOlFub~+Zt|Yw7wGwp4t&!6wyQi(^$q(F|3SpzNB7inTJ`5UHu+RMQUfcp^TgQtHz z=;B`s9`&CCP*Dk76YT!b(Q8y1(u4Iy7-zKIbtb&cmu)ssk>7uCmM>&iR^fWXnY7^j z^17?XMI~XD@~qHzemM(QS<>D-Ucx6FzfI^~TTpQ{{%*+h3E~E8$mijVKa;QAosa7* zK8ST2On^uSE|D^9Qd)A$S$Q}Q{t65p{S~SEw@94X&zdgm-5zRBXq&5JQc5mG!j&Fl z@qU;*5~_Pzsj3omaY@}`Ykc#$=`^BK6Jp|o(R^0EK>|{QLqGJrRl%o|{cit`71QEG z42rW$eQBuFD~VYZI_rtHeZS#D;SzXUHT%zDdpa%Ps4shYfkF7B*2V 0) { iconUrl = actorInfo.icon[0].url; } else if (actorInfo.icon.url) { iconUrl = actorInfo.icon.url; } - - if (iconUrl) { - const img = document.createElement("img"); - img.classList.add("actor-icon"); - img.src = iconUrl; - img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; - img.addEventListener('click', this.navigateToActorProfile.bind(this)); - author.appendChild(img); - } } + + const img = document.createElement("img"); + img.classList.add("actor-icon"); + img.src = iconUrl; + img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + img.addEventListener('click', this.navigateToActorProfile.bind(this)); + author.appendChild(img); if (actorInfo.name) { const pName = document.createElement("div"); From e06f2503f4a7593c7296cbdc742c89fdde97dce4 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Tue, 19 Mar 2024 11:42:27 -0400 Subject: [PATCH 03/94] Use db to load actors --- actor-profile.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actor-profile.js b/actor-profile.js index 6491cc0..cea8a89 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -1,5 +1,4 @@ import { db } from "./dbInstance.js"; -import { fetchActorInfo } from "./post.js"; class ActorProfile extends HTMLElement { static get observedAttributes() { @@ -17,7 +16,7 @@ class ActorProfile extends HTMLElement { } async fetchAndRenderActorProfile(url) { - const actorInfo = await fetchActorInfo(url); + const actorInfo = await db.getActor(url); console.log(actorInfo); if (actorInfo) { this.renderActorProfile(actorInfo); From 1f8fcd9311e69054686ba8c5206cffc3f08cf2ce Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 22:30:19 +0530 Subject: [PATCH 04/94] refactor: actor-profile to include distributed-outbox as a child component and rename classes --- actor-profile.css | 10 +++++----- actor-profile.js | 25 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/actor-profile.css b/actor-profile.css index cd5dabf..bf81dd5 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -1,9 +1,9 @@ -.actor-profile { +.profile { text-align: center; margin-top: 20px; } -.actor-container { +.profile-container { margin-bottom: 10px; } @@ -14,7 +14,7 @@ margin-bottom: 16px; } -.actor-icon { +.profile-icon { width: 50px; height: 50px; border-radius: 50%; @@ -23,12 +23,12 @@ margin-bottom: 8px; } -.actor-details { +.profile-details { display: flex; flex-direction: column; } -.actor-name { +.profile-name { color: var(--rdp-text-color); font-weight: bold; } diff --git a/actor-profile.js b/actor-profile.js index cea8a89..2a1e981 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -21,9 +21,6 @@ class ActorProfile extends HTMLElement { if (actorInfo) { this.renderActorProfile(actorInfo); this.updateFollowButtonState(); - // Update distributed-outbox URL based on fetched actorInfo - const distributedOutbox = document.querySelector("distributed-outbox"); - distributedOutbox.setAttribute("url", actorInfo.outbox); } } @@ -32,14 +29,14 @@ class ActorProfile extends HTMLElement { this.innerHTML = ""; const profileContainer = document.createElement("div"); - profileContainer.classList.add("actor-profile"); + profileContainer.classList.add("profile"); // Create a container for the actor icon and name, to center them const actorContainer = document.createElement("div"); - actorContainer.classList.add("actor-container"); + actorContainer.classList.add("profile-container"); // Handle both single icon object and array of icons - let iconUrl = './assets/profile.png'; // Default profile image path + let iconUrl = "./assets/profile.png"; // Default profile image path if (actorInfo.icon) { if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) { iconUrl = actorInfo.icon[0].url; @@ -47,16 +44,16 @@ class ActorProfile extends HTMLElement { iconUrl = actorInfo.icon.url; } } - + const img = document.createElement("img"); - img.classList.add("actor-icon"); + img.classList.add("profile-icon"); img.src = iconUrl; img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; actorContainer.appendChild(img); // Append to the actor container if (actorInfo.name) { const pName = document.createElement("div"); - pName.classList.add("actor-name"); + pName.classList.add("profile-name"); pName.textContent = actorInfo.name; actorContainer.appendChild(pName); // Append to the actor container } @@ -70,8 +67,18 @@ class ActorProfile extends HTMLElement { followButton.textContent = "Follow"; profileContainer.appendChild(followButton); + // Create the distributed-outbox component and append it to the profile container + const distributedOutbox = document.createElement("distributed-outbox"); + profileContainer.appendChild(distributedOutbox); + // Append the profile container to the main component this.appendChild(profileContainer); + + // Update distributed-outbox URL based on fetched actorInfo + this.querySelector("distributed-outbox").setAttribute( + "url", + actorInfo.outbox + ); } async updateFollowButtonState() { From 7b860714796c2ac929d083ee8c5c003c7c69fe39 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 23:29:20 +0530 Subject: [PATCH 05/94] refactor: encapsulate follow button logic in custom web component --- actor-profile.css | 10 +++---- actor-profile.js | 70 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/actor-profile.css b/actor-profile.css index bf81dd5..20d4701 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -33,7 +33,7 @@ font-weight: bold; } -#followButton { +follow-button { appearance: none; border: 1px solid rgba(27, 31, 35, 0.15); border-radius: 4px; @@ -57,18 +57,18 @@ white-space: nowrap; } -#followButton.follow { +follow-button[state="follow"] { background-color: #3b82f6; color: #fff; } -#followButton.follow:hover { +follow-button[state="follow"]:hover { background-color: #2563eb; } -#followButton.unfollow { +follow-button[state="unfollow"] { background-color: #ef4444; color: #fff; } -#followButton.unfollow:hover { +follow-button[state="unfollow"]:hover { background-color: #dc2626; } diff --git a/actor-profile.js b/actor-profile.js index 2a1e981..afd57a0 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -20,7 +20,6 @@ class ActorProfile extends HTMLElement { console.log(actorInfo); if (actorInfo) { this.renderActorProfile(actorInfo); - this.updateFollowButtonState(); } } @@ -61,10 +60,9 @@ class ActorProfile extends HTMLElement { // Append the actor container to the profile container profileContainer.appendChild(actorContainer); - // Create and position the follow button - const followButton = document.createElement("button"); - followButton.id = "followButton"; - followButton.textContent = "Follow"; + // Instead of creating a button, create a FollowButton component + const followButton = document.createElement("follow-button"); + followButton.setAttribute("url", this.url); profileContainer.appendChild(followButton); // Create the distributed-outbox component and append it to the profile container @@ -80,23 +78,53 @@ class ActorProfile extends HTMLElement { actorInfo.outbox ); } +} - async updateFollowButtonState() { - const followButton = this.querySelector("#followButton"); - const followedActors = await db.getFollowedActors(); - const isFollowed = followedActors.some((actor) => actor.url === this.url); - - followButton.textContent = isFollowed ? "Unfollow" : "Follow"; - followButton.className = isFollowed ? "unfollow" : "follow"; - followButton.onclick = async () => { - if (isFollowed) { - await db.unfollowActor(this.url); - } else { - await db.followActor(this.url); - } - this.updateFollowButtonState(); - }; +customElements.define("actor-profile", ActorProfile); + +class FollowButton extends HTMLElement { + static get observedAttributes() { + return ["url"]; + } + + constructor() { + super(); + this.url = this.getAttribute("url") || ""; + this.state = "unknown"; + } + + connectedCallback() { + this.updateState(); + this.render(); + this.addEventListener("click", this.toggleFollowState.bind(this)); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === "url" && newValue !== oldValue) { + this.url = newValue; + this.updateState(); + } + } + + async updateState() { + const isFollowed = await db.isActorFollowed(this.url); + this.state = isFollowed ? "unfollow" : "follow"; + this.render(); + } + + async toggleFollowState() { + if (this.state === "follow") { + await db.followActor(this.url); + } else if (this.state === "unfollow") { + await db.unfollowActor(this.url); + } + this.updateState(); + } + + render() { + this.textContent = this.state === "follow" ? "Follow" : "Unfollow"; + this.setAttribute("state", this.state); } } -customElements.define("actor-profile", ActorProfile); +customElements.define("follow-button", FollowButton); From 6dbbced2cb60879244f87588bc4d0965ed738f41 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 23:31:47 +0530 Subject: [PATCH 06/94] chore: remove objectStoreNames check in db --- db.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/db.js b/db.js index 4b025fa..0408c89 100644 --- a/db.js +++ b/db.js @@ -299,11 +299,9 @@ function upgrade (db) { actors.createIndex(UPDATED_FIELD, UPDATED_FIELD) actors.createIndex(URL_FIELD, URL_FIELD) - if (!db.objectStoreNames.contains(FOLLOWED_ACTORS_STORE)) { - db.createObjectStore(FOLLOWED_ACTORS_STORE, { - keyPath: "url", - }); - } + db.createObjectStore(FOLLOWED_ACTORS_STORE, { + keyPath: "url", + }); const notes = db.createObjectStore(NOTES_STORE, { keyPath: 'id', From bd2120d01bcc25159410e4956eec5bb619052861 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 23:44:41 +0530 Subject: [PATCH 07/94] refactor: display followed actors into a reusable web component --- followed-accounts.html | 2 +- followed-accounts.js | 56 +++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/followed-accounts.html b/followed-accounts.html index c07e3d2..dba2714 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -12,7 +12,7 @@ >Import and export followed list coming soon..

-
+ diff --git a/followed-accounts.js b/followed-accounts.js index ee78625..b4b095a 100644 --- a/followed-accounts.js +++ b/followed-accounts.js @@ -11,10 +11,12 @@ class FollowedActorsList extends HTMLElement { async renderFollowedActors() { const followedActors = await db.getFollowedActors(); - this.innerHTML = followedActors.map(actor => { - const formattedDate = this.formatDate(actor.followedAt); - return `
- Followed URL: ${actor.url} - Followed At: ${formattedDate}
`; - }).join(''); + this.innerHTML = followedActors + .map((actor) => { + const formattedDate = this.formatDate(actor.followedAt); + return `
- Followed URL: ${actor.url} - Followed At: ${formattedDate}
`; + }) + .join(""); } formatDate(dateString) { @@ -34,12 +36,23 @@ class FollowedActorsList extends HTMLElement { customElements.define("followed-actors-list", FollowedActorsList); -export async function updateFollowCount() { - const followCountElement = document.getElementById("followCount"); - const followedActors = await db.getFollowedActors(); - followCountElement.textContent = followedActors.length; +class FollowedCount extends HTMLElement { + connectedCallback() { + this.updateCountOnLoad(); + } + + async updateCountOnLoad() { + setTimeout(() => this.updateCount(), 100); + } + + async updateCount() { + const followedActors = await db.getFollowedActors(); + this.textContent = followedActors.length; + } } +customElements.define("followed-count", FollowedCount); + // test following/unfollowing // (async () => { // const actorUrl1 = "https://example.com/actor/1"; diff --git a/index.html b/index.html index 047bf94..7443371 100644 --- a/index.html +++ b/index.html @@ -13,8 +13,8 @@

Social Reader

Following · 0 + >Following ·