import {Contextmenu} from "./contextmenu.js"; import {User} from "./user.js"; import {Member} from "./member.js"; import {MarkDown, saveCaretPosition} from "./markdown.js"; import {Embed} from "./embed.js"; import {Channel} from "./channel.js"; import {Localuser} from "./localuser.js"; import {Role} from "./role.js"; import {File} from "./file.js"; import {SnowFlake} from "./snowflake.js"; import { channeljson, emojijson, interactionEvents, memberjson, messagejson, userjson, } from "./jsontypes.js"; import {Emoji} from "./emoji.js"; import {mobile} from "./utils/utils.js"; import {I18n} from "./i18n.js"; import {Hover} from "./hover.js"; import {Dialog} from "./settings.js"; import {Sticker} from "./sticker.js"; import {Components} from "./interactions/compontents.js"; import {ImagesDisplay} from "./disimg"; import {ReportMenu} from "./reporting/report.js"; import {getDeveloperSettings} from "./utils/storage/devSettings.js"; class Message extends SnowFlake { static contextmenu = new Contextmenu("message menu"); stickers!: Sticker[]; owner: Channel; headers: Localuser["headers"]; embeds: Embed[] = []; author!: User; mentions: userjson[] = []; mention_roles!: Role[]; attachments: File[] = []; //probably should be its own class tbh, should be Attachments[] message_reference?: { guild_id: string; channel_id: string; message_id: string; }; type!: number; private timestamp!: number | string; content!: MarkDown; static del: Promise; static resolve: Function; /* weakdiv:WeakRef; set div(e:HTMLDivElement){ if(!e){ this.weakdiv=null; return; } this.weakdiv=new WeakRef(e); } get div(){ return this.weakdiv?.deref(); } */ div: HTMLDivElement | undefined; member: Member | undefined; reactions: { count: number; emoji: emojijson; me: boolean; }[] = []; pinned!: boolean; flags: number = 0; getTimeStamp() { return new Date(this.timestamp).getTime(); } static setup() { this.del = new Promise((_) => { this.resolve = _; }); Message.setupcmenu(); } static setupcmenu() { const editTypes = new Set([0, 19]); Message.contextmenu.addButton( () => I18n.message.edit(), function (this: Message) { this.setEdit(); }, { visible: function () { return this.author.id === this.localuser.user.id && editTypes.has(this.type); }, icon: { css: "svg-edit", }, }, ); Message.contextmenu.addButton( () => I18n.reply(), function (this: Message) { this.channel.setReplying(this); }, { icon: { css: "svg-reply", }, visible: function () { return !this.ephemeral && this.channel.hasPermission("SEND_MESSAGES"); }, }, ); Message.contextmenu.addButton( () => I18n.message.reactionAdd(), function (this: Message, _, e: MouseEvent) { Emoji.emojiPicker(e.x, e.y, this.localuser).then((_) => { this.reactionToggle(_); }); }, { icon: { css: "svg-emoji", }, visible: function () { return this.channel.hasPermission("ADD_REACTIONS"); }, }, ); Message.contextmenu.addButton( () => I18n.message.createThread(), function () { const dio = new Dialog(I18n.message.threadOptions()); const opt = dio.options.addForm( "", (body) => { const channelJson = body as channeljson; this.guild.goToThread(channelJson.id); dio.hide(); }, { fetchURL: this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id + "/threads", headers: this.headers, }, ); opt.addTextInput(I18n.threads.name(), "name"); dio.show(); }, { visible: function () { return this.channel.hasPermission("CREATE_PUBLIC_THREADS"); }, }, ); Message.contextmenu.addButton( () => I18n.message.reactions(), function (this: Message) { this.viewReactions(); }, { visible: function () { return !!this.reactions.length; }, }, ); Message.contextmenu.addSeperator(); Message.contextmenu.addButton( () => I18n.copyrawtext(), function (this: Message) { navigator.clipboard.writeText(this.content.rawString); }, { icon: { css: "svg-copy", }, }, ); Message.contextmenu.addGroup("copyLink"); Message.contextmenu.addButton( () => I18n.copyLink(), function (this: Message) { navigator.clipboard.writeText( `${window.location.origin}/channels/${this.guild.id}/${this.channel.id}/${this.id}`, ); }, { //TODO make icon }, ); Message.contextmenu.addButton( () => I18n.pinMessage(), async function (this: Message) { const f = await fetch(`${this.info.api}/channels/${this.channel.id}/pins/${this.id}`, { method: "PUT", headers: this.headers, }); if (!f.ok) alert(I18n.unableToPin()); }, { icon: { css: "svg-pin", }, visible: function () { if (this.pinned) return false; if (this.channel.guild.id === "@me") return true; return this.channel.hasPermission("MANAGE_MESSAGES"); }, }, ); Message.contextmenu.addButton( () => I18n.unpinMessage(), async function (this: Message) { const f = await fetch(`${this.info.api}/channels/${this.channel.id}/pins/${this.id}`, { method: "DELETE", headers: this.headers, }); if (!f.ok) alert(I18n.unableToPin()); }, { icon: { css: "svg-pin", }, visible: function () { if (!this.pinned) return false; if (this.channel.guild.id === "@me") return true; return this.channel.hasPermission("MANAGE_MESSAGES"); }, }, ); Message.contextmenu.addButton( () => I18n.copymessageid(), function (this: Message) { navigator.clipboard.writeText(this.id); }, ); Message.contextmenu.addSeperator(); Message.contextmenu.addButton( () => I18n.message.delete(), function (this: Message) { this.confirmDelete(); }, { visible: function () { return this.canDelete(); }, icon: { css: "svg-delete", }, color: "red", }, ); Message.contextmenu.addButton( () => I18n.message.report(), async function () { const menu = await ReportMenu.makeReport("message", this.localuser, {message: this}); menu?.spawnMenu(); }, { visible: function () { const settings = getDeveloperSettings(); return this.author.id !== this.localuser.user.id && settings.reportSystem; }, color: "red", }, ); Message.contextmenu.addSeperator(); Message.contextmenu.addButton( () => I18n.usedFermi(), () => {}, { visible: function () { return !!this.nonce && this.nonce.length <= 9 && this.nonce.length !== 0; }, enabled: () => false, }, ); } viewReactions() { const dio = new Dialog(I18n.message.reactionsTitle()); const div = document.createElement("div"); div.classList.add("flexltr"); const reactions = document.createElement("div"); reactions.classList.add("flexttb", "reactionList"); const list = document.createElement("div"); list.classList.add("flexttb", "reactionUserList"); let curSelect = document.createElement("div"); reactions.append( ...this.reactions.map((reaction) => { const button = document.createElement("div"); console.log(reaction); const emoji = new Emoji(reaction.emoji, this.guild); button.append(emoji.getHTML(), `(${reaction.count})`); let users: User[] | undefined = undefined; button.onclick = async () => { curSelect.classList.remove("current"); curSelect = button; curSelect.classList.add("current"); if (!users) { const f = await fetch( `${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reaction.emoji.name}?limit=50&type=0`, {headers: this.headers}, ); users = ((await f.json()) as userjson[]).map((_) => new User(_, this.localuser)); } list.innerHTML = ""; list.append( ...users.map((user) => { return user.createWidget(this.guild); }), ); }; return button; }), ); //@ts-ignore [...reactions.children][0].click(); div.append(reactions, list); dio.options.addHTMLArea(div); dio.show(); } nonce: string = ""; setEdit() { const prev = this.channel.editing; this.channel.editing = this; if (prev) prev.generateMessage(); this.generateMessage(undefined, false); } constructor(messagejson: messagejson, owner: Channel, dontStore = false) { super(messagejson.id); this.owner = owner; this.headers = this.owner.headers; this.giveData(messagejson); if (!dontStore) { this.owner.messages.set(this.id, this); if (messagejson.referenced_message) this.localuser.giveMessage(messagejson.referenced_message); } } reactionToggle(emoji: string | Emoji) { if (emoji instanceof Emoji && !emoji.id && emoji.emoji) { emoji = emoji.emoji; } let remove = !!this.reactions.find((_) => typeof emoji === "string" ? _.emoji.name === emoji : _.emoji.name === emoji.name, )?.me; let reactiontxt: string; if (emoji instanceof Emoji) { if (emoji.id) { reactiontxt = `${emoji.name}:${emoji.id}`; } else { reactiontxt = encodeURIComponent(emoji.name); } } else { reactiontxt = encodeURIComponent(emoji); } if (!remove) { this.localuser.favorites.addReactEmoji( emoji instanceof Emoji ? emoji.id || (emoji.emoji as string) : emoji, ); } fetch( `${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reactiontxt}/@me`, { method: remove ? "DELETE" : "PUT", headers: this.headers, }, ); } components?: Components; edited_timestamp: string | null = null; thread?: Channel; giveData(messagejson: messagejson) { const func = this.channel.infinite.snapBottom(); for (const thing of Object.keys(messagejson)) { if (thing === "attachments") { this.attachments = []; for (const thing of messagejson.attachments) { this.attachments.push(new File(thing, this)); } continue; } else if (thing === "content") { this.content = new MarkDown(messagejson[thing] || "", this.channel); continue; } else if (thing === "id") { continue; } else if (thing === "member") { Member.new(messagejson.member as memberjson, this.guild).then((_) => { this.member = _ as Member; }); continue; } else if (thing === "embeds") { this.embeds = []; for (const thing in messagejson.embeds) { this.embeds[thing] = new Embed(messagejson.embeds[thing], this); } continue; } else if (thing === "author") { continue; } else if (thing === "sticker_items") { this.stickers = messagejson.sticker_items.map((_) => { const guild = this.localuser.guildids.get(_.guild_id as string); return new Sticker(_, guild || this.localuser); }); } else if (thing === "components" && messagejson.components) { this.components = new Components(messagejson.components, this); continue; } else if (thing === "thread" && messagejson.thread) { let thread = this.localuser.channelids.get(messagejson.thread.id); if (!thread) { thread = new Channel(messagejson.thread, this.guild); thread.resolveparent(); this.localuser.channelids.set(thread.id, thread); } this.thread = thread; continue; } (this as any)[thing] = (messagejson as any)[thing]; } this.stickers ||= []; if (messagejson.reactions?.length) { console.log(messagejson.reactions, ":3"); } if (messagejson.webhook) { messagejson.author.webhook = messagejson.webhook; } if (messagejson.author.id) { this.author = new User(messagejson.author, this.localuser, false); } if (messagejson.mentions) this.mentions = messagejson.mentions; this.mention_roles = (messagejson.mention_roles || []) .map((role: string | {id: string}) => { return this.guild.roleids.get(role instanceof Object ? role.id : role); }) .filter((_) => _ !== undefined); if (!this.member && this.guild.id !== "@me") { this.author.resolvemember(this.guild).then((_) => { this.member = _; }); } if (this.div) { this.generateMessage(); return; } if (+this.id > +(this.channel.lastmessageid || "0")) { func(); } } canDelete() { return this.channel.hasPermission("MANAGE_MESSAGES") || this.author === this.localuser.user; } get channel() { return this.owner; } get guild() { return this.owner.guild; } get localuser() { return this.owner.localuser; } get info() { return this.owner.info; } interactionDiv?: HTMLDivElement; interactionEvents(event: interactionEvents) { if (!this.interactionDiv) return; this.interactionDiv.classList.remove("failed"); switch (event.t) { case "INTERACTION_CREATE": this.interactionDiv.textContent = I18n.interactions.started(); break; case "INTERACTION_SUCCESS": this.interactionDiv.textContent = I18n.interactions.worked(); setTimeout(() => { if (this.interactionDiv?.textContent === I18n.interactions.worked()) { this.interactionDiv.textContent = ""; } }, 1000); break; case "INTERACTION_FAILURE": this.interactionDiv.textContent = I18n.interactions.failed(); this.interactionDiv.classList.add("failed"); setTimeout(() => { if (this.interactionDiv?.textContent === I18n.interactions.failed()) { this.interactionDiv.textContent = ""; } }, 5000); break; } } messageevents(obj: HTMLDivElement) { let drag = false; Message.contextmenu.bindContextmenu( obj, this, undefined, (x) => { //console.log(x); if (x < -20) { obj.style.translate = x + 20 + "px 0px"; } else obj.style.translate = 0 + "px"; if (!drag && x < 20) { return; } drag = true; this.channel.moveForDrag(Math.max(x, 0)); }, (x, y) => { drag = false; console.log(x, y); obj.style.translate = 0 + "px"; this.channel.moveForDrag(-1); if (x > 60) { console.log("In here?"); const toggle = document.getElementById("maintoggle") as HTMLInputElement; toggle.checked = false; console.log(toggle); } if (x < -40) { this.channel.setReplying(this); } }, ); this.div = obj; obj.classList.add("messagediv"); } deleteDiv() { if (!this.div) return; try { this.div.remove(); this.div = undefined; } catch (e) { console.error(e); } } mention_everyone!: boolean; mentionsuser(userd: User | Member) { if (this.mention_everyone) return true; if (userd instanceof User) { return !!this.mentions.find(({id}) => id == userd.id); } else if (userd instanceof Member) { if (!!this.mentions.find(({id}) => id == userd.id)) { return true; } else { return !new Set(this.mention_roles).isDisjointFrom(new Set(userd.roles)); //if the message mentions a role the user has } } else { return false; } } getimages() { const build: File[] = []; for (const thing of this.attachments) { if (thing.content_type.startsWith("image/")) { build.push(thing); } } return build; } getUnixTime(): number { return new Date(this.timestamp).getTime(); } async edit(content: string) { if (content === this.content.textContent) { return; } return await fetch(this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id, { method: "PATCH", headers: this.headers, body: JSON.stringify({content}), }); } async delete() { await fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`, { headers: this.headers, method: "DELETE", }); } deleteEvent() { if (!this.channel.messages.has(this.id)) return; if (!this.id.includes("fake") && this.channel.messageCount !== undefined) this.channel.messageCount--; console.log("deleted"); this.channel.infinite.deleteId(this.id); if (this.div) { this.div.remove(); this.div.innerHTML = ""; this.div = undefined; } const prev = this.channel.idToPrev.get(this.id); const next = this.channel.idToNext.get(this.id); this.channel.messages.delete(this.id); if (this.channel.idToPrev.has(this.id) && this.channel.idToNext.has(this.id)) { if (next) this.channel.idToPrev.set(next, prev); if (prev) this.channel.idToNext.set(prev, next); } else if (prev) { this.channel.idToNext.delete(prev); } else if (next) { this.channel.idToPrev.delete(next); } if (prev) { const prevMessage = this.channel.messages.get(prev); if (prevMessage) { prevMessage.generateMessage(); } } if (next) { const nextMessage = this.channel.messages.get(next); if (nextMessage) { nextMessage.generateMessage(); } } if (this.channel.lastmessage === this || this.channel.lastmessageid === this.id) { if (prev) { this.channel.lastmessage = this.channel.messages.get(prev); this.channel.lastmessageid = prev; } else { this.channel.lastmessage = undefined; this.channel.lastmessageid = undefined; } } if (this.channel.lastreadmessageid === this.id) { if (prev) { this.channel.lastreadmessageid = prev; } else { this.channel.lastreadmessageid = undefined; } } console.log("deleted done"); } reactdiv!: WeakRef; blockedPropigate() { const previd = this.channel.idToPrev.get(this.id); if (!previd) { this.generateMessage(); return; } const premessage = this.channel.messages.get(previd); if (premessage?.author === this.author) { premessage.blockedPropigate(); } else { this.generateMessage(); } } interaction: messagejson["interaction"]; get ephemeral() { return !!(this.flags & (1 << 6)); } generateMessage( premessage?: Message | undefined, ignoredblock = false, dupe: false | HTMLDivElement = false, ) { const div = dupe || this.div; if (!div) return; if (div === this.div) { this.div.classList.add("messagediv"); } if (this.type === 21) { this.channel.parent?.getmessage(this.message_reference?.message_id as string).then((_) => { const gen = _?.generateMessage(undefined, undefined, div); if (!gen) { //idk } }); return div; } const editmode = this.channel.editing === this; if (!premessage && !dupe) { premessage = this.channel.messages.get(this.channel.idToPrev.get(this.id) as string); } if ( this.mentionsuser(this.guild.member) || this.ephemeral || this.interaction?.user.id === this.localuser.user.id ) { div.classList.add("mentioned"); } div.style.setProperty( "--time-text", JSON.stringify( new Date(this.getUnixTime()).toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}), ), ); if (this === this.channel.replyingto) { div.classList.add("replying"); } div.innerHTML = ""; const build = document.createElement("div"); build.classList.add("flexltr", "message"); if (this.interaction) { const replyline = document.createElement("div"); const minipfp = document.createElement("img"); minipfp.classList.add("replypfp"); replyline.appendChild(minipfp); const username = document.createElement("span"); username.classList.add("username"); replyline.appendChild(username); const reply = document.createElement("div"); reply.classList.add("replytext", "ellipsis"); reply.textContent = I18n.interactions.replyline(); replyline.appendChild(reply); const user = new User(this.interaction.user, this.localuser); minipfp.src = user.getpfpsrc(); Member.resolveMember(user, this.guild).then((member) => { if (member) { minipfp.src = member.getpfpsrc(); username.textContent = member.name; member.subName(username); } else { user.subName(username); } }); user.bind(minipfp, this.guild); username.textContent = user.name; user.bind(username, this.guild); replyline.classList.add("flexltr", "replyflex"); div.appendChild(replyline); } div.classList.remove("zeroheight"); if (this.author.relationshipType === 2) { if (ignoredblock) { if (premessage?.author !== this.author) { const span = document.createElement("span"); span.textContent = I18n.hideBlockedMessages(); div.append(span); span.classList.add("blocked"); span.onclick = (_) => { let next: Message | undefined = this; while (next?.author === this.author) { next.generateMessage(); next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); } }; } } else { div.classList.remove("topMessage"); if (premessage?.author === this.author) { div.classList.add("zeroheight"); premessage.blockedPropigate(); div.appendChild(build); return div; } else { build.classList.add("blocked", "topMessage"); const span = document.createElement("span"); let count = 1; let next = this.channel.messages.get(this.channel.idToNext.get(this.id) as string); while (next?.author === this.author) { count++; next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); } span.textContent = I18n.showBlockedMessages(count + ""); build.append(span); span.onclick = (_) => { const func = this.channel.infinite.snapBottom(); let next: Message | undefined = this; while (next?.author === this.author) { next.generateMessage(undefined, true); next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); console.log("loopy"); } func(); }; div.appendChild(build); return div; } } } if (this.message_reference && this.type !== 6 && this.type !== 18) { const replyline = document.createElement("div"); const minipfp = document.createElement("img"); minipfp.classList.add("replypfp"); replyline.appendChild(minipfp); const username = document.createElement("span"); replyline.appendChild(username); const reply = document.createElement("div"); username.classList.add("username"); reply.classList.add("replytext", "ellipsis"); replyline.appendChild(reply); replyline.classList.add("flexltr", "replyflex"); // TODO: Fix this this.channel.getmessage(this.message_reference.message_id).then((message) => { if (!message) { minipfp.remove(); username.textContent = I18n.message.deleted(); username.classList.remove("username"); return; } if (message.author.relationshipType === 2) { username.textContent = "Blocked user"; return; } if (message.attachments?.length || message.embeds?.length || message.stickers.length) { const b = document.createElement("b"); b.innerText = I18n.message.attached(); b.style.paddingRight = "4px"; reply.append(b); } const author = message.author; reply.appendChild(message.content.makeHTML({stdsize: true})); minipfp.src = author.getpfpsrc(); author.bind(minipfp, this.guild); username.textContent = author.name; author.bind(username, this.guild); Member.resolveMember(author, this.guild).then((member) => { if (member) { username.textContent = member.name; minipfp.src = member.getpfpsrc(); member.subName(username); } else { author.subName(username); } }); }); reply.onclick = (_) => { if (!this.message_reference) return; // TODO: FIX this this.channel.focus(this.message_reference.message_id); }; div.appendChild(replyline); } div.appendChild(build); const text = document.createElement("div"); text.classList.add("commentrow", "flexttb"); const messageTypes = new Set([0, 19, 20]); if (messageTypes.has(this.type) || this.attachments.length !== 0) { const pfpRow = document.createElement("div"); let current = true; if (premessage !== undefined) { const old = new Date(premessage.timestamp).getTime() / 1000; const newt = new Date(this.timestamp).getTime() / 1000; current = newt - old > 600; } const combine = premessage?.author != this.author || current || this.message_reference || !messageTypes.has(premessage.type) || this.interaction; if (combine) { const pfp = this.author.buildpfp(this.guild, div); this.author.bind(pfp, this.guild, false); pfpRow.appendChild(pfp); } pfpRow.classList.add("pfprow"); build.appendChild(pfpRow); if (combine) { const username = document.createElement("span"); username.classList.add("username", "ellipsis"); this.author.bind(username, this.guild); const membProm = Member.resolveMember(this.author, this.guild); membProm.then((member) => { if (member) { username.textContent = member.name; member.subName(username); const icon = member.getRoleIcon(); if (icon) username.after(icon); } else { this.author.subName(username); } }); div.classList.add("topMessage"); username.textContent = this.author.name; const userwrap = document.createElement("div"); userwrap.classList.add("userwrap"); userwrap.appendChild(username); if (this.author.bot) { const username = document.createElement("span"); username.classList.add("bot"); username.textContent = this.author.webhook ? I18n.webhook() : I18n.bot(); userwrap.appendChild(username); } const time = document.createElement("span"); time.textContent = " " + formatTime(new Date(this.timestamp)); time.classList.add("timestamp"); userwrap.appendChild(time); const hover = new Hover(new Date(this.timestamp).toString()); hover.addEvent(time); if (this.edited_timestamp) { const edit = document.createElement("span"); edit.classList.add("timestamp"); edit.textContent = I18n.message.edited(); const hover = new Hover(new Date(this.edited_timestamp).toString()); hover.addEvent(edit); userwrap.append(edit); } membProm.then((memb) => { if (memb) { if (memb.commuicationDisabledLeft()) { const icon = document.createElement("span"); icon.classList.add("svg-timeout"); username.after(icon); const date = memb.communication_disabled_until as Date; new Hover(I18n.channel.timedOutUntil(date.toLocaleString())).addEvent(icon); } } }); text.appendChild(userwrap); } else { div.classList.remove("topMessage"); } const messagedwrap = document.createElement("div"); if (editmode) { const box = document.createElement("div"); box.classList.add("messageEditContainer"); const area = document.createElement("div"); const sb = document.createElement("div"); sb.style.position = "absolute"; sb.style.width = "100%"; const search = document.createElement("div"); search.classList.add("searchOptions", "flexttb"); area.classList.add("editMessage"); try { area.contentEditable = "plaintext-only"; } catch { area.contentEditable = "true"; } const md = new MarkDown(this.content.rawString, this.owner, {keep: true}); area.append(md.makeHTML()); area.addEventListener("keyup", (event) => { if (this.localuser.keyup(event)) return; if (event.key === "Enter" && !event.shiftKey) { this.edit(md.rawString); this.channel.editing = null; this.generateMessage(); } }); area.addEventListener("keydown", (event) => { this.localuser.keydown(event); if (event.key === "Enter" && !event.shiftKey) event.preventDefault(); if (event.key === "Escape") { this.channel.editing = null; this.generateMessage(); } }); md.giveBox(area, (str, pre) => { this.localuser.search(search, md, str, pre); }); sb.append(search); box.append(sb, area); messagedwrap.append(box); setTimeout(() => { area.focus(); const fun = saveCaretPosition(area, Infinity); if (fun) fun(); }); box.oncontextmenu = (e) => { e.stopImmediatePropagation(); }; } else { this.content.onUpdate = () => {}; const messaged = this.content.makeHTML(); if (!this.embeds.find((_) => _.json.url === messaged.textContent)) { messagedwrap.classList.add("flexttb"); messagedwrap.appendChild(messaged); if (!combine && this.edited_timestamp) { const edit = document.createElement("span"); edit.classList.add("timestamp"); edit.textContent = I18n.message.edited(); const hover = new Hover(new Date(this.edited_timestamp).toString()); hover.addEvent(edit); messaged.append(edit); } } } text.appendChild(messagedwrap); if (this.attachments.length) { const attach = document.createElement("div"); attach.classList.add("flexltr", "attachments"); for (const thing of this.attachments) { attach.appendChild(thing.getHTML()); } messagedwrap.appendChild(attach); } if (this.embeds.length) { const embeds = document.createElement("div"); for (const thing of this.embeds) { embeds.appendChild(thing.generateHTML()); } messagedwrap.appendChild(embeds); } // } else if (this.type === 7) { const messages = I18n.welcomeMessages("|||").split("\n"); const message = messages[Number(BigInt(this.id) % BigInt(messages.length))]; const [first, second] = message.split("|||"); const welcome = document.createElement("div"); text.appendChild(welcome); const firstspan = document.createElement("span"); firstspan.textContent = first; welcome.appendChild(firstspan); // TODO: settings how? if (false) { const img = document.createElement("img"); img.classList.add("avatar"); img.style.height = "1em"; img.style.width = "1em"; img.style.objectFit = "cover"; img.src = this.author.getpfpsrc(this.guild) + "?size=1"; img.loading = "lazy"; img.decoding = "async"; img.addEventListener( "load", () => { img.src = this.author.getpfpsrc(this.guild) + "?size=" + firstspan.clientHeight; }, {once: true}, ); img.onclick = () => { const full = new ImagesDisplay([ new File( { content_type: "image/webp", filename: "0", id: "0", size: 0, url: this.author.getpfpsrc(this.guild), }, this, ), ]); full.show(); }; welcome.appendChild(img); } const username = document.createElement("span"); username.textContent = this.author.name; //this.author.profileclick(username); this.author.bind(username, this.guild); welcome.appendChild(username); username.classList.add("username"); const secondspan = document.createElement("span"); secondspan.textContent = second; welcome.appendChild(secondspan); const time = document.createElement("span"); time.textContent = " " + formatTime(new Date(this.timestamp)); time.classList.add("timestamp"); welcome.append(time); div.classList.add("topMessage"); } else if (this.type === 6) { const pinText = document.createElement("div"); text.appendChild(pinText); const m = I18n.message.pin("||").split("||"); if (m.length === 2) pinText.append(m.shift() as string); const username = document.createElement("span"); username.textContent = this.author.name; //this.author.profileclick(username); this.author.bind(username, this.guild); pinText.appendChild(username); username.classList.add("username"); const afterText = document.createElement("span"); afterText.textContent = m[0]; afterText.onclick = (_) => { if (!this.message_reference) return; this.channel.infinite.focus(this.message_reference.message_id); }; afterText.classList.add("pinText"); pinText.append(afterText); const time = document.createElement("span"); time.textContent = " " + formatTime(new Date(this.timestamp)); time.classList.add("timestamp"); pinText.append(time); div.classList.add("topMessage"); } else if (this.type === 18) { const text = document.createElement("div"); build.appendChild(text); const m = I18n.message.thread("|||", "???").split("|||"); if (m.length === 2) text.append(m.shift() as string); const username = document.createElement("span"); username.textContent = this.author.name; //this.author.profileclick(username); this.author.bind(username, this.guild); text.appendChild(username); username.classList.add("username"); const midText = m[0].split("???"); const afterText = document.createElement("span"); afterText.textContent = midText[0]; text.append(afterText); const threadName = document.createElement("span"); threadName.textContent = this.content.rawString; threadName.classList.add("pinText"); text.append(threadName); threadName.onclick = () => { this.guild.goToThread(this.message_reference?.channel_id as string); }; const time = document.createElement("span"); time.textContent = " " + formatTime(new Date(this.timestamp)); time.classList.add("timestamp"); text.append(time); div.classList.add("topMessage"); } build.appendChild(text); const stickerArea = document.createElement("div"); stickerArea.classList.add("flexltr", "stickerMArea"); for (const sticker of this.stickers) { stickerArea.append(sticker.getHTML()); } div.append(stickerArea); if (!dupe) { if (this.components && this.components.components.length) { const cdiv = this.components.getHTML(); cdiv.classList.add("messageComps"); text.append(cdiv); const ndiv = document.createElement("div"); ndiv.classList.add("compAppStatus"); this.interactionDiv = ndiv; text.append(ndiv); } const reactions = document.createElement("div"); reactions.classList.add("flexltr", "reactiondiv"); this.reactdiv = new WeakRef(reactions); this.updateReactions(); div.append(reactions); } if (this.ephemeral) { const ephemeral = document.createElement("div"); ephemeral.classList.add("flexltr", "ephemeralDiv"); const span = document.createElement("span"); span.textContent = I18n.interactions.onlyYou(); const a = document.createElement("a"); a.onclick = () => { this.deleteEvent(); }; a.textContent = I18n.interactions.ephemeralDismiss(); ephemeral.append(span, a); div.append(ephemeral); } const unreadLine = premessage && premessage.id === this.channel.lastreadmessageid; let datelineNeeded = false; if ((premessage || unreadLine) && !dupe) { const thisTime = new Date(this.getUnixTime()); if (premessage && !unreadLine) { const prevTime = new Date(premessage.getUnixTime()); datelineNeeded = thisTime.getDay() !== prevTime.getDay() || thisTime.getMonth() !== prevTime.getMonth() || thisTime.getFullYear() !== prevTime.getFullYear(); } else { datelineNeeded = true; } if (datelineNeeded) { const dateline = document.createElement("div"); if (unreadLine) { dateline.classList.add("unreadDateline"); } dateline.classList.add("flexltr", "dateline"); dateline.append(document.createElement("hr")); const span = document.createElement("span"); span.innerText = Intl.DateTimeFormat(I18n.lang, { year: "numeric", month: "long", day: "2-digit", }).format(thisTime); dateline.append(span); dateline.append(document.createElement("hr")); const messageDiv = document.createElement("div"); messageDiv.append(...Array.from(div.children)); messageDiv.classList = div.classList + ""; div.classList = ""; div.append(dateline, messageDiv); } } this.bindButtonEvent(); if (!dupe && this.thread) { const thread = this.thread; const threadBox = document.createElement("div"); threadBox.classList.add("flexttb", "threadBox"); const topRow = document.createElement("div"); topRow.classList.add("flexltr"); const title = document.createElement("span"); title.textContent = thread.name; topRow.onclick = () => { this.localuser.goToChannel(thread.id); }; const messages = document.createElement("span"); messages.classList.add("clickable"); messages.textContent = I18n.message.messages(thread.messageCount + ""); console.log(thread); topRow.append(title, messages); threadBox.append(topRow); div.append(threadBox); } return div; } bindButtonEvent() { if (!this.div) return; const div = this.div.classList.contains("messagediv") ? this.div : (this.div.getElementsByClassName("messagediv")[0] as HTMLDivElement); if (!div) return; let buttons: HTMLDivElement | undefined; div.onmouseenter = (_) => { if (mobile) return; if (buttons) { buttons.remove(); buttons = undefined; } if (div) { buttons = document.createElement("div"); buttons.classList.add("messageButtons", "flexltr"); let addedRec = false; if (this.channel.hasPermission("ADD_REACTIONS")) { const favs = this.localuser.favorites .emojiReactFreq() .slice(0, 6) .filter(([emoji]) => { return !this.reactions.find( (_) => _.emoji.id === emoji || _.emoji.emoji === emoji || _.emoji.name === emoji, )?.me; }) .slice(0, 3); for (const [emoji] of favs) { addedRec = true; const container = document.createElement("button"); if (isNaN(+emoji)) { container.append(emoji); } else { const emj = Emoji.getEmojiFromIDOrString(emoji, this.localuser); container.append(emj.getHTML(false, false)); } container.onclick = () => { this.reactionToggle(emoji); }; buttons.append(container); } } if (addedRec) { Array.from(buttons.children).at(-1)?.classList.add("vr-message"); } if (this.channel.hasPermission("SEND_MESSAGES") && !this.ephemeral) { const container = document.createElement("button"); const reply = document.createElement("span"); reply.classList.add("svg-reply", "svgicon"); container.append(reply); buttons.append(container); container.onclick = (_) => { this.channel.setReplying(this); }; } if (this.channel.hasPermission("ADD_REACTIONS")) { const container = document.createElement("button"); const reply = document.createElement("span"); reply.classList.add("svg-emoji", "svgicon"); container.append(reply); buttons.append(container); container.onclick = (e) => { e.stopImmediatePropagation(); e.preventDefault(); Emoji.emojiPicker(e.x, e.y, this.localuser).then((_) => { this.reactionToggle(_); }); }; } if (this.author === this.localuser.user) { const container = document.createElement("button"); const edit = document.createElement("span"); edit.classList.add("svg-edit", "svgicon"); container.append(edit); buttons.append(container); container.onclick = (_) => { this.setEdit(); }; } if (this.canDelete()) { const container = document.createElement("button"); const reply = document.createElement("span"); reply.classList.add("svg-delete", "svgicon"); container.append(reply); buttons.append(container); container.onclick = (_) => { if (_.shiftKey) { this.delete(); return; } this.confirmDelete(); }; } if (buttons.childNodes.length !== 0) { div.append(buttons); } } }; div.onmouseleave = (_) => { if (buttons) { buttons.remove(); buttons = undefined; } }; } confirmDelete() { const diaolog = new Dialog(""); diaolog.options.addTitle(I18n.deleteConfirm()); const options = diaolog.options.addOptions("", {ltr: true}); options.addButtonInput("", I18n.yes(), () => { this.delete(); diaolog.hide(); }); options.addButtonInput("", I18n.no(), () => { diaolog.hide(); }); diaolog.show(); } updateReactions() { const reactdiv = this.reactdiv.deref(); if (!reactdiv) return; const func = this.channel.infinite.snapBottom(); reactdiv.innerHTML = ""; for (const thing of this.reactions) { const reaction = document.createElement("div"); reaction.classList.add("reaction"); if (thing.me) { reaction.classList.add("meReacted"); } let emoji: HTMLElement; if (thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)) { if (/\d{17,21}/.test(thing.emoji.name)) { thing.emoji.id = thing.emoji.name; //Should stop being a thing once the server fixes this bug } const emo = new Emoji( thing.emoji as {name: string; id: string; animated: boolean}, this.guild, ); emoji = emo.getHTML(false, false); } else { emoji = document.createElement("p"); emoji.textContent = thing.emoji.name; } const h = new Hover(async () => { //TODO this can't be real, name conflicts must happen, but for now it's fine const f = await fetch( `${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${thing.emoji.name}?limit=3&type=0`, {headers: this.headers}, ); const json = (await f.json()) as userjson[]; let build = ""; let users = json.map((_) => new User(_, this.localuser)); //FIXME this is a spacebar bug, I can't fix this the api ignores limit and just sends everything. users = users.splice(0, 3); let first = true; for (const user of users) { if (!first) { build += ", "; } build += user.name; first = false; } if (thing.count > 3) { build = I18n.message.andMore(build); } else { } build += "\n" + I18n.message.reactedWith(thing.emoji.name); return build; }); h.addEvent(reaction); const count = document.createElement("p"); count.textContent = "" + thing.count; count.classList.add("reactionCount"); reaction.append(count); reaction.append(emoji); reactdiv.append(reaction); reaction.onclick = (_) => { this.reactionToggle(new Emoji(thing.emoji, this.guild)); }; } func(); } reactionAdd(data: {name: string; id?: string}, member: Member | {id: string}) { for (const thing of this.reactions) { if (thing.emoji.name === data.name || thing.emoji.id === data.id) { thing.count++; if (member.id === this.localuser.user.id) { thing.me = true; } this.updateReactions(); return; } } console.log(data, this.reactions); this.reactions.push({ count: 1, emoji: data, me: member.id === this.localuser.user.id, }); this.updateReactions(); } reactionRemove(data: {name: string}, id: string) { console.log("test"); for (const i in this.reactions) { const thing = this.reactions[i]; console.log(thing, data); if (thing.emoji.name === data.name) { thing.count--; if (thing.count === 0) { this.reactions.splice(Number(i), 1); this.updateReactions(); return; } if (id === this.localuser.user.id) { thing.me = false; this.updateReactions(); return; } } } } reactionRemoveAll() { this.reactions = []; this.updateReactions(); } reactionRemoveEmoji(emoji: Emoji) { for (const i in this.reactions) { const reaction = this.reactions[i]; if ( (reaction.emoji.id && reaction.emoji.id == emoji.id) || (!reaction.emoji.id && reaction.emoji.name == emoji.name) ) { this.reactions.splice(Number(i), 1); this.updateReactions(); break; } } } buildhtml(premessage?: Message | undefined, dupe = false): HTMLElement { const id = this.channel.nonceMap.get(this.nonce); if (id && !dupe) { this.channel.destroyFakeMessage(id); } if (dupe) { return this.generateMessage(premessage, false, document.createElement("div")) as HTMLElement; } if (this.div) { console.error(`HTML for ${this.id} already exists, aborting`); return this.div; } try { const div = document.createElement("div"); this.div = div; this.messageevents(div); return this.generateMessage(premessage) as HTMLElement; } catch (e) { console.error(e); } return this.div as HTMLElement; } } let now: string; let yesterdayStr: string; function formatTime(date: Date) { updateTimes(); const datestring = date.toLocaleDateString(); const formatTime = (date: Date) => date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); if (datestring === now) { return I18n.todayAt(formatTime(date)); } else if (datestring === yesterdayStr) { return I18n.yesterdayAt(formatTime(date)); } else { return I18n.otherAt(date.toLocaleDateString(), formatTime(date)); } } let tomorrow = 0; updateTimes(); function updateTimes() { if (tomorrow < Date.now()) { const d = new Date(); tomorrow = d.setHours(24, 0, 0, 0); now = new Date().toLocaleDateString(); const yesterday = new Date(now); yesterday.setDate(new Date().getDate() - 1); yesterdayStr = yesterday.toLocaleDateString(); } } Message.setup(); export {Message};