From 8e7aea193ccc8f8ead7122813ce65e76b12c9eca Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 11 Dec 2024 21:07:59 -0600 Subject: [PATCH] per guild profiles --- src/webpage/channel.ts | 2 +- src/webpage/guild.ts | 7 +- src/webpage/jsontypes.ts | 4 +- src/webpage/localuser.ts | 15 ++- src/webpage/member.ts | 260 ++++++++++++++++++++++++++++++++++++++- src/webpage/user.ts | 165 ++++++++++++++++++++----- translations/en.json | 6 +- 7 files changed, 414 insertions(+), 45 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 3031b08..a9baa87 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -1440,7 +1440,7 @@ class Channel extends SnowFlake{ } const notification = new Notification(this.notititle(message), { body: noticontent, - icon: message.author.getpfpsrc(), + icon: message.author.getpfpsrc(this.guild), image: imgurl, }); notification.addEventListener("click", _=>{ diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 702ef0a..dac23be 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -342,6 +342,7 @@ class Guild extends SnowFlake{ } }); } + this.perminfo ??= { channels: {} }; for(const thing of json.channels){ const temp = new Channel(thing, this); @@ -436,9 +437,9 @@ class Guild extends SnowFlake{ calculateReorder(){ let position = -1; const build: { - id: string; - position: number | undefined; - parent_id: string | undefined; + id: string; + position: number | undefined; + parent_id: string | undefined; }[] = []; for(const thing of this.headchannels){ const thisthing: { diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 767e91b..45e10d8 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -141,7 +141,7 @@ type userjson = { premium_since: string; premium_type: number; theme_colors: string; - pronouns: string; + pronouns?: string; badge_ids: string[]; }; type memberjson = { @@ -149,6 +149,8 @@ type memberjson = { id: string; user: userjson | null; guild_id: string; + avatar?:string; + banner?:string; guild: { id: string; } | null; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 50dcd89..a59fd06 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -90,6 +90,7 @@ class Localuser{ this.userinfo.username = this.user.username; this.userinfo.id = this.user.id; this.userinfo.pfpsrc = this.user.getpfpsrc(); + this.status = this.ready.d.user_settings.status; this.channelfocus = undefined; this.lookingguild = undefined; @@ -477,8 +478,10 @@ class Localuser{ temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); if(!channel)break; + const message = channel.messages.get(temp.d.message_id); if(!message)break; + message.reactionRemove(temp.d.emoji, temp.d.user_id); } break; @@ -739,7 +742,7 @@ class Localuser{ for(const member of list){ const memberdiv=document.createElement("div"); - const pfp=await member.user.buildstatuspfp(); + const pfp=await member.user.buildstatuspfp(member); const username=document.createElement("span"); username.classList.add("ellipsis"); username.textContent=member.name; @@ -1094,10 +1097,10 @@ class Localuser{ } } updateProfile(json: { - bio?: string; - pronouns?: string; - accent_color?: number; - }){ + bio?: string; + pronouns?: string; + accent_color?: number; + }){ fetch(this.info.api + "/users/@me/profile", { method: "PATCH", headers: this.headers, @@ -1180,7 +1183,7 @@ class Localuser{ const pronounbox = settingsLeft.addTextInput( I18n.getTranslation("pronouns"), _=>{ - if(newpronouns || newbio || changed){ + if(newpronouns!==undefined||newbio!==undefined||changed!==undefined){ this.updateProfile({ pronouns: newpronouns, bio: newbio, diff --git a/src/webpage/member.ts b/src/webpage/member.ts index 2afb788..aa4fbaf 100644 --- a/src/webpage/member.ts +++ b/src/webpage/member.ts @@ -4,7 +4,7 @@ import{ Guild }from"./guild.js"; import{ SnowFlake }from"./snowflake.js"; import{ memberjson, presencejson }from"./jsontypes.js"; import { I18n } from "./i18n.js"; -import { Dialog } from "./settings.js"; +import { Dialog, Options } from "./settings.js"; class Member extends SnowFlake{ static already = {}; @@ -12,8 +12,12 @@ class Member extends SnowFlake{ user: User; roles: Role[] = []; nick!: string; - + avatar:void|string=undefined; + banner:void|string=undefined; private constructor(memberjson: memberjson, owner: Guild){ + if(memberjson.id==="1086860370880362328"&&owner.id==="1006649183970562092"){ + console.trace(memberjson); + } super(memberjson.id); this.owner = owner; if(this.localuser.userMap.has(memberjson.id)){ @@ -59,6 +63,251 @@ class Member extends SnowFlake{ this.user.members.delete(this.guild); this.guild.members.delete(this); } + getpfpsrc():string{ + if(this.hypotheticalpfp&&this.avatar){ + return this.avatar; + } + if(this.avatar !== undefined&&this.avatar!==null){ + return`${this.info.cdn}/guilds/${this.guild.id}/users/${this.id}/avatars${ + this.avatar + }.${this.avatar.startsWith("a_")?"gif":"png"}`; + } + return this.user.getpfpsrc(); + } + getBannerUrl():string|undefined{ + if(this.hypotheticalbanner&&this.banner){ + return this.banner; + } + if(this.banner){ + return `${this.info.cdn}/banners/${this.guild.id}/${ + this.banner + }.${this.banner.startsWith("a_")?"gif":"png"}`;; + }else{ + return undefined; + } + } + joined_at!:string; + premium_since!:string; + deaf!:boolean; + mute!:boolean; + pending!:boolean + clone(){ + return new Member({ + id:this.id+"#clone", + user:this.user.tojson(), + guild_id:this.guild.id, + guild:{id:this.guild.id}, + avatar:this.avatar as (string|undefined), + banner:this.banner as (string|undefined), + //TODO presence + nick:this.nick, + roles:this.roles.map(_=>_.id), + joined_at:this.joined_at, + premium_since:this.premium_since, + deaf:this.deaf, + mute:this.mute, + pending:this.pending + + },this.owner) + } + pronouns?:string; + bio?:string; + hypotheticalpfp=false; + hypotheticalbanner=false; + accent_color?:number; + get headers(){ + return this.owner.headers; + } + + updatepfp(file: Blob): void{ + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = ()=>{ + fetch(this.info.api + `/guilds/${this.guild.id}/members/${this.id}/`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + avatar: reader.result, + }), + }); + }; + } + updatebanner(file: Blob | null): void{ + if(file){ + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = ()=>{ + fetch(this.info.api + `/guilds/${this.guild.id}/profile/${this.id}`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + banner: reader.result, + }), + }); + }; + }else{ + fetch(this.info.api + `/guilds/${this.guild.id}/profile/${this.id}`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + banner: null, + }), + }); + } + } + + updateProfile(json: { + bio?: string|null; + pronouns?: string|null; + nick?:string|null; + }){ + console.log(JSON.stringify(json)); + /* + if(json.bio===""){ + json.bio=null; + } + if(json.pronouns===""){ + json.pronouns=null; + } + if(json.nick===""){ + json.nick=null; + } + */ + fetch(this.info.api + `/guilds/${this.guild.id}/profile/${this.id}`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(json), + }); + } + editProfile(options:Options){ + if(this.hasPermission("CHANGE_NICKNAME")){ + const hypotheticalProfile = document.createElement("div"); + let file: undefined | File | null; + let newpronouns: string | undefined; + let newbio: string | undefined; + let nick:string|undefined; + const hypomember = this.clone(); + + let color: string; + async function regen(){ + hypotheticalProfile.textContent = ""; + const hypoprofile = await hypomember.user.buildprofile(-1, -1,hypomember); + + hypotheticalProfile.appendChild(hypoprofile); + } + regen(); + const settingsLeft = options.addOptions(""); + const settingsRight = options.addOptions(""); + settingsRight.addHTMLArea(hypotheticalProfile); + + const nicky=settingsLeft.addTextInput(I18n.getTranslation("member.nick:"),()=>{},{ + initText:this.nick||"" + }); + nicky.watchForChange(_=>{ + hypomember.nick=_; + nick=_; + regen(); + }) + + const finput = settingsLeft.addFileInput( + I18n.getTranslation("uploadPfp"), + _=>{ + if(file){ + this.updatepfp(file); + } + }, + { clear: true } + ); + finput.watchForChange(_=>{ + if(!_){ + file = null; + hypomember.avatar = undefined; + hypomember.hypotheticalpfp = true; + regen(); + return; + } + if(_.length){ + file = _[0]; + const blob = URL.createObjectURL(file); + hypomember.avatar = blob; + hypomember.hypotheticalpfp = true; + regen(); + } + }); + let bfile: undefined | File | null; + const binput = settingsLeft.addFileInput( + I18n.getTranslation("uploadBanner"), + _=>{ + if(bfile !== undefined){ + this.updatebanner(bfile); + } + }, + { clear: true } + ); + binput.watchForChange(_=>{ + if(!_){ + bfile = null; + hypomember.banner = undefined; + hypomember.hypotheticalbanner = true; + regen(); + return; + } + if(_.length){ + bfile = _[0]; + const blob = URL.createObjectURL(bfile); + hypomember.banner = blob; + hypomember.hypotheticalbanner = true; + regen(); + } + }); + let changed = false; + const pronounbox = settingsLeft.addTextInput( + I18n.getTranslation("pronouns"), + _=>{ + if(newpronouns!==undefined||newbio!==undefined||changed!==undefined){ + this.updateProfile({ + pronouns: newpronouns, + bio: newbio, + //accent_color: Number.parseInt("0x" + color.substr(1), 16), + nick + }); + } + }, + { initText: this.pronouns } + ); + pronounbox.watchForChange(_=>{ + hypomember.pronouns = _; + newpronouns = _; + regen(); + }); + const bioBox = settingsLeft.addMDInput(I18n.getTranslation("bio"), _=>{}, { + initText: this.bio, + }); + bioBox.watchForChange(_=>{ + newbio = _; + hypomember.bio = _; + regen(); + }); + return;//Returns early to stop errors + if(this.accent_color){ + color = "#" + this.accent_color.toString(16); + }else{ + color = "transparent"; + } + const colorPicker = settingsLeft.addColorInput( + I18n.getTranslation("profileColor"), + _=>{}, + { initColor: color } + ); + colorPicker.watchForChange(_=>{ + console.log(); + color = _; + hypomember.accent_color = Number.parseInt("0x" + _.substr(1), 16); + changed = true; + regen(); + }); + } + } update(memberjson: memberjson){ this.roles=[]; for(const key of Object.keys(memberjson)){ @@ -114,11 +363,16 @@ class Member extends SnowFlake{ owner.members.add(memb); return memb; }else if(memb instanceof Promise){ - return await memb; //I should do something else, though for now this is "good enough" + const member=await memb; //I should do something else, though for now this is "good enough"; + if(member){ + member.update(memberjson); + } + return member; }else{ if(memberjson.presence){ memb.getPresence(memberjson.presence); } + memb.update(memberjson); return memb; } }else{ diff --git a/src/webpage/user.ts b/src/webpage/user.ts index 3126ebd..fa6addb 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -9,6 +9,7 @@ import { Role } from "./role.js"; import { Search } from "./search.js"; import { I18n } from "./i18n.js"; import { Direct } from "./direct.js"; +import { Settings } from "./settings.js"; class User extends SnowFlake{ owner: Localuser; @@ -19,7 +20,7 @@ class User extends SnowFlake{ relationshipType: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; bio!: MarkDown; discriminator!: string; - pronouns!: string; + pronouns?: string; bot!: boolean; public_flags!: number; accent_color!: number; @@ -57,11 +58,10 @@ class User extends SnowFlake{ } } - clone(): User{ - return new User( - { + tojson():userjson{ + return { username: this.username, - id: this.id + "#clone", + id: this.id, public_flags: this.public_flags, discriminator: this.discriminator, avatar: this.avatar, @@ -74,7 +74,14 @@ class User extends SnowFlake{ theme_colors: this.theme_colors, pronouns: this.pronouns, badge_ids: this.badge_ids, - }, + } + } + + clone(): User{ + const json=this.tojson(); + json.id+="#clone"; + return new User( + json, this.owner ); } @@ -189,6 +196,20 @@ class User extends SnowFlake{ return us.hasPermission("KICK_MEMBERS") || false; } ); + + this.contextmenu.addbutton( + ()=>I18n.getTranslation("user.editServerProfile"), + function(this: User, member: Member | undefined){ + if(!member) return; + const settings=new Settings(I18n.getTranslation("user.editServerProfile")); + member.editProfile(settings.addButton(I18n.getTranslation("user.editServerProfile"),{ltr:true})); + settings.show(); + }, + null, + member=>{ + return !!member; + } + ); this.contextmenu.addbutton( ()=>I18n.getTranslation("user.ban"), function(this: User, member: Member | undefined){ @@ -319,19 +340,32 @@ class User extends SnowFlake{ } } - buildpfp(): HTMLImageElement{ + buildpfp(guild:Guild|void|Member|null): HTMLImageElement{ const pfp = document.createElement("img"); pfp.loading = "lazy"; pfp.src = this.getpfpsrc(); pfp.classList.add("pfp"); pfp.classList.add("userid:" + this.id); + if(guild){ + (async()=>{ + if(guild instanceof Guild){ + const memb= await Member.resolveMember(this,guild) + if(!memb) return; + pfp.src = memb.getpfpsrc(); + }else{ + pfp.src = guild.getpfpsrc(); + } + + })(); + + } return pfp; } - async buildstatuspfp(): Promise{ + async buildstatuspfp(guild:Guild|void|Member|null): Promise{ const div = document.createElement("div"); div.classList.add("pfpDiv") - const pfp = this.buildpfp(); + const pfp = this.buildpfp(guild); div.append(pfp); const status = document.createElement("div"); status.classList.add("statusDiv"); @@ -423,11 +457,19 @@ class User extends SnowFlake{ } } } - - getpfpsrc(): string{ + /** + * @param guild this is an optional thing that'll get the src of the member if it exists, otherwise ignores it, this is meant to be fast, not accurate + */ + getpfpsrc(guild:Guild|void): string{ if(this.hypotheticalpfp && this.avatar){ return this.avatar; } + if(guild){ + const member=this.members.get(guild) + if(member instanceof Member){ + return member.getpfpsrc(); + } + } if(this.avatar !== null){ return`${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${ this.avatar @@ -441,12 +483,21 @@ class User extends SnowFlake{ async buildprofile( x: number, y: number, - guild: Guild | null = null + guild: Guild | null | Member = null ): Promise{ if(Contextmenu.currentmenu != ""){ Contextmenu.currentmenu.remove(); } - + const membres=(async ()=>{ + if(!guild) return; + let member:Member|undefined; + if(guild instanceof Guild){ + member=await Member.resolveMember(this,guild) + }else{ + member=guild; + } + return member; + })() const div = document.createElement("div"); if(this.accent_color){ @@ -457,20 +508,18 @@ class User extends SnowFlake{ }else{ div.style.setProperty("--accent_color", "transparent"); } - if(this.banner){ - const banner = document.createElement("img"); - let src: string; - if(!this.hypotheticalbanner){ - src = `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${ - this.banner - }.png`; - }else{ - src = this.banner; + const banner=this.getBanner(guild); + div.append(banner); + membres.then(member=>{ + if(!member) return; + if(member.accent_color&&member.accent_color!==0){ + div.style.setProperty( + "--accent_color", + `#${member.accent_color.toString(16).padStart(6, "0")}` + ); } - banner.src = src; - banner.classList.add("banner"); - div.append(banner); - } + }) + if(x !== -1){ div.style.left = `${x}px`; div.style.top = `${y}px`; @@ -501,7 +550,7 @@ class User extends SnowFlake{ } } })(); - const pfp = await this.buildstatuspfp(); + const pfp = await this.buildstatuspfp(guild); div.appendChild(pfp); const userbody = document.createElement("div"); userbody.classList.add("flexttb","infosection"); @@ -516,16 +565,33 @@ class User extends SnowFlake{ userbody.appendChild(discrimatorhtml); const pronounshtml = document.createElement("p"); - pronounshtml.textContent = this.pronouns; + pronounshtml.textContent = this.pronouns||""; pronounshtml.classList.add("pronouns"); userbody.appendChild(pronounshtml); + membres.then(member=>{ + if(!member) return; + if(member.pronouns&&member.pronouns!==""){ + pronounshtml.textContent=member.pronouns; + } + }); + const rule = document.createElement("hr"); userbody.appendChild(rule); const biohtml = this.bio.makeHTML(); userbody.appendChild(biohtml); + + membres.then(member=>{ + if(!member)return; + if(member.bio&&member.bio!==""){ + //TODO make markdown take Guild + userbody.insertBefore(new MarkDown(member.bio,this.localuser).makeHTML(),biohtml); + biohtml.remove(); + } + }); + if(guild){ - Member.resolveMember(this, guild).then(member=>{ + membres.then(member=>{ if(!member)return; usernamehtml.textContent=member.name; const roles = document.createElement("div"); @@ -556,7 +622,48 @@ class User extends SnowFlake{ } return div; } + getBanner(guild:Guild|null|Member):HTMLImageElement{ + const banner = document.createElement("img"); + const bsrc=this.getBannerUrl(); + if(bsrc){ + banner.src = bsrc; + banner.classList.add("banner"); + } + + if(guild){ + if(guild instanceof Member){ + const bsrc=guild.getBannerUrl(); + if(bsrc){ + banner.src = bsrc; + banner.classList.add("banner"); + } + }else{ + Member.resolveMember(this,guild).then(memb=>{ + if(!memb) return; + const bsrc=memb.getBannerUrl(); + if(bsrc){ + banner.src = bsrc; + banner.classList.add("banner"); + } + }) + } + } + return banner + } + getBannerUrl():string|undefined{ + if(this.banner){ + if(!this.hypotheticalbanner){ + return `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${ + this.banner + }.png`; + }else{ + return this.banner; + } + }else{ + return undefined; + } + } profileclick(obj: HTMLElement, guild?: Guild): void{ obj.onclick = (e: MouseEvent)=>{ this.buildprofile(e.clientX, e.clientY, guild); diff --git a/translations/en.json b/translations/en.json index 1a16dee..578933a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -396,7 +396,8 @@ "kick":"Kick member", "ban":"Ban member", "addRole":"Add roles", - "removeRole":"Remove roles" + "removeRole":"Remove roles", + "editServerProfile":"Edit server profile" }, "login":{ "checking":"Checking Instance", @@ -407,7 +408,8 @@ "member":{ "kick":"Kick $1 from $2", "reason:":"Reason:", - "ban":"Ban $1 from $2" + "ban":"Ban $1 from $2", + "nick:":"Nickname:" }, "uploadFilesText":"Upload your files here!", "errorReconnect":"Unable to connect to the server, retrying in **$1** seconds...",