diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index dd9c580..74ddf6c 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -27,16 +27,16 @@ import {Member} from "./member.js"; import {Voice} from "./voice.js"; import {User, userVolMenu} from "./user.js"; import {I18n} from "./i18n.js"; -import {mobile, createImg, safeImg} from "./utils/utils.js"; +import {createImg, safeImg} from "./utils/utils.js"; import {webhookMenu} from "./webhooks.js"; import {File} from "./file.js"; import {Sticker} from "./sticker.js"; -import {CustomHTMLDivElement} from "./index.js"; import {Direct} from "./direct.js"; import {NotificationHandler} from "./notificationHandler.js"; import {Command} from "./interactions/commands.js"; import {Tag} from "./tag.js"; import {CDNParams} from "./utils/cdnParams.js"; +import {TypeBox} from "./typeBox.js"; class Channel extends SnowFlake { editing!: Message | null; @@ -1437,40 +1437,13 @@ class Channel extends SnowFlake { this.replyingto.div.classList.remove("replying"); } this.replyingto = message; - const typebox = document.getElementById("typebox") as HTMLElement; - typebox.focus(); + TypeBox.focus(); if (!this.replyingto?.div) return; - console.log(message); this.replyingto.div.classList.add("replying"); this.makereplybox(); } makereplybox() { - const replybox = document.getElementById("replybox") as HTMLElement; - const typebox = document.getElementById("typebox") as HTMLElement; - if (this.replyingto) { - replybox.innerHTML = ""; - const span = document.createElement("span"); - span.textContent = I18n.replyingTo(this.replyingto.author.username); - const X = document.createElement("button"); - X.onclick = (_) => { - if (this.replyingto?.div) { - this.replyingto.div.classList.remove("replying"); - } - replybox.classList.add("hideReplyBox"); - this.replyingto = null; - replybox.innerHTML = ""; - typebox.classList.remove("typeboxreplying"); - }; - replybox.classList.remove("hideReplyBox"); - X.classList.add("cancelReply", "svgicon", "svg-x"); - replybox.append(span); - replybox.append(X); - typebox.classList.add("typeboxreplying"); - } else { - replybox.classList.add("hideReplyBox"); - replybox.innerHTML = ""; - typebox.classList.remove("typeboxreplying"); - } + TypeBox.updateReplying(); } async getmessage(id: string): Promise { const message = this.messages.get(id); @@ -1543,9 +1516,6 @@ class Channel extends SnowFlake { } static genid: number = 0; nsfwPannel() { - (document.getElementById("typebox") as HTMLDivElement).contentEditable = "" + false; - (document.getElementById("upload") as HTMLElement).style.visibility = "hidden"; - (document.getElementById("typediv") as HTMLElement).style.visibility = "hidden"; const messages = document.getElementById("scrollWrap") as HTMLDivElement; const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer")); for (const thing of messageContainers) { @@ -1953,49 +1923,35 @@ class Channel extends SnowFlake { } }; } - files: Blob[] = []; - htmls = new WeakMap(); - textSave = ""; - collectBox() { - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - const [files, html] = this.localuser.fileExtange([], new WeakMap()); - this.files = files; - this.htmls = html; - this.textSave = MarkDown.gatherBoxText(typebox); - typebox.textContent = ""; - } curCommand?: Command; curWatch = () => {}; async submitCommand() { if (!this.curCommand) return; - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - if (await this.curCommand.submit(typebox, this)) { + if (await this.curCommand.submit(TypeBox.box, this)) { this.curCommand = undefined; - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - typebox.markdown.boxEnabled = true; - typebox.innerHTML = ""; - typebox.markdown.boxupdate(); - typebox.removeEventListener("keyup", this.curWatch); + TypeBox.markdown.boxEnabled = true; + TypeBox.box.innerHTML = ""; + TypeBox.markdown.boxupdate(); + TypeBox.box.removeEventListener("keyup", this.curWatch); } } startCommand(command: Command) { this.curCommand = command; - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - typebox.markdown.boxEnabled = false; + TypeBox.markdown.boxEnabled = false; const func = () => { const node = window.getSelection()?.focusNode; if (this.localuser.focusChannel === this) { - const out = command.collect(typebox, this, node || undefined); + const out = command.collect(TypeBox.box, this, node || undefined); if (!out) { - typebox.markdown.boxEnabled = true; - typebox.markdown.boxupdate(); - typebox.removeEventListener("keyup", func); + TypeBox.markdown.boxEnabled = true; + TypeBox.markdown.boxupdate(); + TypeBox.box.removeEventListener("keyup", func); } } }; this.curWatch = func; - typebox.addEventListener("keyup", func); - command.render(typebox, this); + TypeBox.box.addEventListener("keyup", func); + command.render(TypeBox.box, this); } isForum() { return this.type === 15 || this.type === 16; @@ -2624,28 +2580,18 @@ class Channel extends SnowFlake { if (this.owner instanceof Direct) { this.owner.freindDiv?.classList.remove("viewChannel"); } - if (this.localuser.focusChannel) { - this.localuser.focusChannel.collectBox(); + TypeBox.saveBox(); + if (!this.curCommand && !this.isForum()) { + TypeBox.restoreBox(this); } - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - typebox.markdown.boxEnabled = !this.curCommand; + TypeBox.markdown.boxEnabled = !this.curCommand; if (this.curCommand) { - this.curCommand.render(typebox, this); + this.curCommand.render(TypeBox.box, this); } - typebox.style.setProperty( + TypeBox.box.style.setProperty( "--channel-text", JSON.stringify(I18n.channel.typebox(this.shortName)), ); - if (!this.curCommand && !this.isForum()) { - const md = typebox.markdown; - md.owner = this; - typebox.textContent = this.textSave; - md.boxupdate(Infinity); - } - if (this.isForum()) { - typebox.textContent = ""; - } - this.localuser.fileExtange(this.files, this.htmls); if (getMessages === undefined) { getMessages = this.type !== 2 || !this.localuser.voiceAllowed; @@ -2761,37 +2707,17 @@ class Channel extends SnowFlake { } this.rendertyping(); + TypeBox.changeWrite(); + try { - (document.getElementById("typebox") as HTMLDivElement).contentEditable = this.canMessage - ? "plaintext-only" - : "false"; - } catch { - (document.getElementById("typebox") as HTMLDivElement).contentEditable = this.canMessage - ? "true" - : "false"; + if (getMessages) await this.putmessages(); + } catch (e) { + if (e instanceof Error) { + const d = new Dialog(e.message); + d.show(); + } + return; } - (document.getElementById("upload") as HTMLElement).style.visibility = this.canMessage - ? "visible" - : "hidden"; - (document.getElementById("gifTB") as HTMLElement).style.display = this.canMessage - ? "block" - : "none"; - (document.getElementById("stickerTB") as HTMLElement).style.display = this.canMessage - ? "block" - : "none"; - (document.getElementById("emojiTB") as HTMLElement).style.display = this.canMessage - ? "block" - : "none"; - (document.getElementById("mobileSend") as HTMLElement).style.display = this.canMessage - ? "block" - : "none"; - (document.getElementById("typediv") as HTMLElement).style.visibility = "visible"; - if (!mobile) { - (document.getElementById("typebox") as HTMLDivElement).focus(); - } else { - (document.getElementById("typebox") as HTMLDivElement).blur(); - } - if (getMessages) await this.putmessages(); await prom; if (id !== Channel.genid) { @@ -2938,11 +2864,20 @@ class Channel extends SnowFlake { if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { return; } - const j = await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100", { - headers: this.headers, - }); + let response: messagejson[] | undefined = undefined; + for (let i = 0; i < 5; i++) { + try { + const j = await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100", { + headers: this.headers, + }); - const response = (await j.json()) as messagejson[]; + response = (await j.json()) as messagejson[]; + break; + } catch { + await new Promise((res) => setTimeout(res, 1000)); + } + } + if (!response) throw new Error(I18n.messageNotLoad()); if (response.length !== 100) { this.allthewayup = true; } diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts index 6ab38a8..cfdc3db 100644 --- a/src/webpage/direct.ts +++ b/src/webpage/direct.ts @@ -492,7 +492,15 @@ class Group extends Channel { ); this.groupcontextmenu.addSeperator(undefined, "dm"); - + this.groupcontextmenu.addButton( + I18n.DMs.copyURL(), + function (this: Group) { + navigator.clipboard.writeText(`${location.origin}/channels/@me/${this.id}`); + }, + { + group: "id", + }, + ); this.groupcontextmenu.addButton( () => I18n.DMs.copyId(), function (this: Group) { diff --git a/src/webpage/discovery.ts b/src/webpage/discovery.ts index 21dc185..fcde1a1 100644 --- a/src/webpage/discovery.ts +++ b/src/webpage/discovery.ts @@ -4,6 +4,7 @@ import {I18n} from "./i18n.js"; import {guildjson} from "./jsontypes.js"; import {ReportMenu} from "./reporting/report.js"; import {Dialog} from "./settings.js"; +import {TypeBox} from "./typeBox.js"; import {CDNParams} from "./utils/cdnParams.js"; import {getDeveloperSettings} from "./utils/storage/devSettings.js"; import {createImg} from "./utils/utils.js"; @@ -51,7 +52,7 @@ export class Discovery { this.owner.freindDiv?.classList.remove("viewChannel"); } if (this.localuser.focusChannel) { - this.localuser.focusChannel.collectBox(); + TypeBox.saveBox(); } history.pushState(["@me", "discover"], "", "/channels/@me/discover"); this.localuser.pageTitle(I18n.discovery()); diff --git a/src/webpage/embed.ts b/src/webpage/embed.ts index 3b2e32c..ef3857d 100644 --- a/src/webpage/embed.ts +++ b/src/webpage/embed.ts @@ -210,7 +210,6 @@ class Embed { const div = document.createElement("div"); div.classList.add("messageimgdiv"); const img = createImg(this.json.thumbnail.proxy_url); - img.classList.add("messageimg"); img.onclick = () => { const full = new ImagesDisplay([ new File( @@ -442,7 +441,6 @@ class Embed { video.controls = true; video.preload = "metadata"; video.src = videoUrl; - video.classList.add("messageimg"); if (this.json.video?.width) { let scale = 1; const max = 96 * 3; diff --git a/src/webpage/emoji.ts b/src/webpage/emoji.ts index 13cde44..0508ac2 100644 --- a/src/webpage/emoji.ts +++ b/src/webpage/emoji.ts @@ -6,7 +6,7 @@ import {emojijson, emojiSource} from "./jsontypes.js"; import {Localuser} from "./localuser.js"; import {BinRead} from "./utils/binaryUtils.js"; import {CDNParams} from "./utils/cdnParams.js"; -import {removeAni} from "./utils/utils.js"; +import {createImg, removeAni} from "./utils/utils.js"; //I need to recompile the emoji format for translation class Emoji { @@ -89,17 +89,20 @@ class Emoji { getHTML(bigemoji: boolean = false, click = true) { if (this.id) { if (!this.owner) throw new Error("owner is missing for custom emoji!"); - const emojiElem = document.createElement("img"); + const emojiElem = createImg( + this.owner.info.cdn + + "/emojis/" + + this.id + + "." + + (this.animated ? "gif" : "png") + + new CDNParams({expectedSize: 32, animated: this.animated}), + undefined, + undefined, + "emoji", + ); emojiElem.classList.add("md-emoji"); emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji"); emojiElem.crossOrigin = "anonymous"; - emojiElem.src = - this.owner.info.cdn + - "/emojis/" + - this.id + - "." + - (this.animated ? "gif" : "png") + - new CDNParams({expectedSize: 32, animated: this.animated}); emojiElem.alt = this.name; emojiElem.loading = "lazy"; @@ -157,9 +160,11 @@ class Emoji { guildText.classList.add("flexttb", "guildEmojiText"); const guildName = document.createElement("span"); + guildName.classList.add("guildName"); guildName.textContent = lookup.guild.name; const guildDesc = document.createElement("span"); + guildDesc.classList.add("guildDesc"); const discoverable = lookup.guild.features.find((_) => _ === "DISCOVERABLE"); if (discoverable) { if (lookup.guild.description) { diff --git a/src/webpage/file.ts b/src/webpage/file.ts index f0d3138..2bfeb6a 100644 --- a/src/webpage/file.ts +++ b/src/webpage/file.ts @@ -44,7 +44,7 @@ class File { this.content_type = fileJSON.content_type; this.size = fileJSON.size; } - getHTML(temp: boolean = false, fullScreen = false, OSpoiler = false, max = 96 * 3): HTMLElement { + getHTML(temp: boolean = false, fullScreen = false, OSpoiler = false, max = 96 * 3, gallery = false): HTMLElement { function makeSpoilerHTML(): HTMLElement { const spoil = document.createElement("div"); spoil.classList.add("fSpoil"); @@ -80,8 +80,10 @@ class File { img.height = this.height; } if (!fullScreen) { - img.classList.add("messageimg"); div.classList.add("messageimgdiv"); + if (gallery) { + div.classList.add("messagegallerydiv"); + } img.onclick = () => { if (this.owner) { const full = new ImagesDisplay( @@ -106,12 +108,13 @@ class File { img.setSrcs(src); }); div.append(img); - if (this.width && !fullScreen) { + // Non-gallery images are sized according to their actual size + if (!gallery && this.width && !fullScreen) { img.style.width = div.style.width = this.width + "px"; img.style.height = div.style.height = this.height + "px"; - } else if (!fullScreen) { - img.style.maxWidth = div.style.maxWidth = 96 * 3 + "px"; - img.style.maxHeight = div.style.maxHeight = 96 * 3 + "px"; + } else if (!gallery && !fullScreen) { + img.style.maxWidth = div.style.maxWidth = max + "px"; + img.style.maxHeight = div.style.maxHeight = max + "px"; } img.isAnimated().then((animated) => { if (!animated || !this.owner || fullScreen) return; @@ -158,6 +161,7 @@ class File { video.append(source); //source.type = this.content_type; video.controls = !temp; + video.preload = "metadata"; if (this.width) video.width = this.width; if (this.height) video.height = this.height; diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 962b479..56c6e4d 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -35,6 +35,7 @@ import {Hover} from "./hover.js"; import {ReportMenu} from "./reporting/report.js"; import {getDeveloperSettings} from "./utils/storage/devSettings.js"; import {CDNParams} from "./utils/cdnParams.js"; +import {TypeBox} from "./typeBox.js"; export async function makeInviteMenu(inviteMenu: Options, guild: Guild, url: string) { const invDiv = document.createElement("div"); const bansp = ProgessiveDecodeJSON(url, { @@ -197,6 +198,14 @@ class Guild extends SnowFlake { welcomeScreen?: welcomeScreen; static readonly contextmenu = new Contextmenu("guild menu"); static setupcontextmenu() { + Guild.contextmenu.addButton( + () => I18n.guild.markRead(), + function (this: Guild) { + this.markAsRead(); + }, + ); + Guild.contextmenu.addSeperator(); + Guild.contextmenu.addButton( () => I18n.guild.makeInvite(), function (this: Guild) { @@ -211,14 +220,6 @@ class Guild extends SnowFlake { color: "blue", }, ); - Guild.contextmenu.addSeperator(); - - Guild.contextmenu.addButton( - () => I18n.guild.markRead(), - function (this: Guild) { - this.markAsRead(); - }, - ); Guild.contextmenu.addButton( () => I18n.guild.notifications(), @@ -1955,16 +1956,9 @@ class Guild extends SnowFlake { if (this.localuser.focusChannel && this.localuser.focusChannel.myhtml) { this.localuser.focusChannel.myhtml.classList.remove("viewChannel"); } + TypeBox.changeVisablity(false); this.prevchannel = undefined; this.localuser.focusChannel = undefined; - const replybox = document.getElementById("replybox") as HTMLElement; - const typebox = document.getElementById("typebox") as HTMLElement; - replybox.classList.add("hideReplyBox"); - typebox.classList.remove("typeboxreplying"); - (document.getElementById("typebox") as HTMLDivElement).contentEditable = "false"; - (document.getElementById("upload") as HTMLElement).style.visibility = "hidden"; - (document.getElementById("typediv") as HTMLElement).style.visibility = "hidden"; - (document.getElementById("sideDiv") as HTMLElement).innerHTML = ""; } noChannel(addstate: boolean) { for (const c of this.channels) { diff --git a/src/webpage/highlighter/clike/langs.json b/src/webpage/highlighter/clike/langs.json index 40b8743..1624a79 100644 --- a/src/webpage/highlighter/clike/langs.json +++ b/src/webpage/highlighter/clike/langs.json @@ -85,5 +85,13 @@ "doubleSlashComments": true, "multilineSlashComments": true, "multiLine":"\"\"\"" +}, +{ + "names":["bash","sh","ksh","csh","shell"], + "keywords":["if","fi","else","then","while","do","for","continue","done","break","case","esac","in","local","return","declare","EOF","readonly","true","false"], + "firstLineShebang":true, + "hashComments": true, + "doubleSlashComments": false, + "multilineSlashComments": false } ] diff --git a/src/webpage/highlighter/clike/lex.ts b/src/webpage/highlighter/clike/lex.ts index 34dde2c..0de49b9 100644 --- a/src/webpage/highlighter/clike/lex.ts +++ b/src/webpage/highlighter/clike/lex.ts @@ -25,7 +25,7 @@ function regex(config: clikeConf): RegExp { `(${RegExp.escape(config.multiLine)}(?:${matchOpts.join("|")})*(?:${RegExp.escape(config.multiLine)})?)`, ); } - conds.push(`\'(\\\\(.|\\n)|[^"\\n\\\\])*\'?`); + conds.push(`\'(\\\\(.|\\n)|[^\'\\n\\\\])*\'?`); conds.push(`"(\\\\(.|\\n)|[^"\\n\\\\])*"?`); if (config.hashComments) { conds.push("#.*"); diff --git a/src/webpage/hover.ts b/src/webpage/hover.ts index 490289a..01630c5 100644 --- a/src/webpage/hover.ts +++ b/src/webpage/hover.ts @@ -68,9 +68,13 @@ class Hover { this.elm2.remove(); }); } + static prevDiv = new WeakRef(document.createElement("div")); async makeHover(elm: HTMLElement) { + const prev = Hover.prevDiv.deref(); + if (prev) prev.remove(); if (!document.contains(elm)) return document.createElement("div"); const div = document.createElement("div"); + Hover.prevDiv = new WeakRef(div); if (this.customHTML) { div.append(this.customHTML()); diff --git a/src/webpage/icons/link.svg b/src/webpage/icons/link.svg new file mode 100644 index 0000000..e6e660d --- /dev/null +++ b/src/webpage/icons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/index.ts b/src/webpage/index.ts index e6aa6ff..2388a98 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -3,8 +3,6 @@ import {Contextmenu} from "./contextmenu.js"; import {mobile, Specialuser} from "./utils/utils.js"; import {setTheme} from "./utils/utils.js"; import {MarkDown} from "./markdown.js"; -import {Message} from "./message.js"; -import {File} from "./file.js"; import {I18n} from "./i18n.js"; import "./utils/pollyfills.js"; import {makeLogin} from "./login.js"; @@ -17,8 +15,8 @@ import "./invite.js"; import "./oauth2/auth.js"; import "./audio/page.js"; import "./404.js"; -import {Channel} from "./channel.js"; import type * as C from "./typeChecker/chekerIndex.js"; +import {TypeBox} from "./typeBox.js"; if (window.location.pathname === "/app") { window.location.pathname = "/channels/@me"; @@ -72,35 +70,7 @@ if (window.location.pathname.startsWith("/channels")) { }); let thisUser: Localuser | null = null; - function regSwap(l: Localuser) { - l.onswap = (l) => { - thisUser = l; - regSwap(l); - }; - l.fileExtange = (img, html) => { - const blobArr: Blob[] = []; - const htmlArr = imagesHtml; - let i = 0; - for (const file of images) { - const img = imagesHtml.get(file); - if (!img) continue; - if (pasteImageElement.contains(img)) { - pasteImageElement.removeChild(img); - blobArr.push(images[i]); - } else { - i++; - } - } - images = img; - imagesHtml = html; - for (const file of images) { - const img = imagesHtml.get(file); - if (!img) throw new Error("Image without HTML, exiting"); - pasteImageElement.append(img); - } - return [blobArr, htmlArr]; - }; - } + const loaddesc = document.getElementById("load-desc") as HTMLSpanElement; try { const current = sessionStorage.getItem("currentuser") || Localuser.users.currentuser; @@ -110,8 +80,8 @@ if (window.location.pathname.startsWith("/channels")) { thisUser = new Localuser(Localuser.users.users[current]); } - regSwap(thisUser); thisUser.initwebsocket().then(async () => { + if (thisUser) TypeBox.regSwap(thisUser); thisUser?.loaduser(); console.warn("huh"); await thisUser?.init(); @@ -160,151 +130,16 @@ if (window.location.pathname.startsWith("/channels")) { }, }, ); - const channelw = document.getElementById("channelw"); - if (channelw) - channelw.addEventListener("keypress", (e) => { - if (e.ctrlKey || e.altKey || e.metaKey || e.metaKey) return; - let owner = e.target as HTMLElement; - while (owner !== channelw) { - if (owner.tagName === "input" || owner.contentEditable !== "false") { - return; - } - owner = owner.parentElement as HTMLElement; - } - typebox.markdown.boxupdate(Infinity); - }); + menu.bindContextmenu(document.getElementById("channels") as HTMLDivElement); - const pasteImageElement = document.getElementById("pasteimage") as HTMLDivElement; - let replyingTo: Message | null = null; window.addEventListener("popstate", (e) => { if (e.state instanceof Object) { thisUser?.goToState(e.state); } //console.log(e.state,"state:3") }); - let nonceMap = new Map(); - //@ts-expect-error unused right now, not needed - function getNonce(id: string) { - const nonce = nonceMap.get(id) || Math.floor(Math.random() * 1000000000) + ""; - nonceMap.set(id, nonce); - return nonce; - } - const markdown = new MarkDown("", thisUser ?? undefined); - async function sendMessage(channel: Channel, content: string) { - if (!channel.canMessageRightNow()) return; - if (channel.curCommand) { - channel.submitCommand(); - return; - } - markdown.onUpdate("", false); - replyingTo = thisUser?.focusChannel ? thisUser.focusChannel.replyingto : null; - if (replyingTo?.div) { - replyingTo.div.classList.remove("replying"); - } - if (thisUser?.focusChannel) { - thisUser.focusChannel.replyingto = null; - thisUser.focusChannel.makereplybox(); - } - const attachments = images.filter((_) => document.contains(imagesHtml.get(_) || null)); - while (images.length) { - const elm = imagesHtml.get(images.pop() as Blob) as HTMLElement; - if (pasteImageElement.contains(elm)) pasteImageElement.removeChild(elm); - } - typebox.innerHTML = ""; - typebox.markdown.txt = []; - try { - await new Promise((mres, rej) => - channel.sendMessage( - content, - { - attachments, - embeds: [], // Add an empty array for the embeds property - replyingto: replyingTo, - sticker_ids: [], - //nonce: getNonce(channel.id), - }, - (res) => { - if (res === "Ok") { - mres(); - } else { - rej(); - } - }, - ), - ); - } catch { - images = attachments; - for (const file of images) { - const img = imagesHtml.get(file); - if (!img) continue; - pasteImageElement.append(img); - } - channel.replyingto = replyingTo; - channel.makereplybox(); - typebox.textContent = content; - typebox.markdown.txt = content.split(""); - typebox.markdown.boxupdate(Infinity); - } - nonceMap.delete(channel.id); - } - const mobileSend = document.getElementById("mobileSend"); - if (mobileSend) { - mobileSend.onclick = () => { - const channel = thisUser?.focusChannel; - if (!channel) return; - const content = MarkDown.gatherBoxText(typebox); - sendMessage(channel, content); - }; - } - async function handleEnter(event: KeyboardEvent): Promise { - if (event.isComposing) return; - if (event.key === "Escape" && (images.length || thisUser?.focusChannel?.replyingto)) { - while (images.length) { - const elm = imagesHtml.get(images.pop() as Blob) as HTMLElement; - if (pasteImageElement.contains(elm)) pasteImageElement.removeChild(elm); - } - if (thisUser?.focusChannel) { - thisUser.focusChannel?.replyingto?.div?.classList.remove("replying"); - thisUser.focusChannel.replyingto = null; - thisUser.focusChannel.makereplybox(); - } - thisUser?.updateSend(); - return; - } - if (thisUser?.handleKeyUp(event)) { - return; - } - - const channel = thisUser?.focusChannel; - if (!channel) return; - const content = MarkDown.gatherBoxText(typebox); - if (content === "" && event.key === "ArrowUp") { - channel.editLast(); - return; - } - channel.typingstart(); - - if (event.key === "Enter" && !event.shiftKey && window.innerWidth > 600) { - event.preventDefault(); - await sendMessage(channel, content); - } - } - - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - - typebox.markdown = markdown; - typebox.addEventListener("keyup", handleEnter); - typebox.addEventListener("keydown", (event) => { - if (event.isComposing) return; - thisUser?.keydown(event); - if (event.key === "Enter" && !event.shiftKey && window.innerWidth > 600) { - event.preventDefault(); - event.stopImmediatePropagation(); - } - }); - markdown.giveBox(typebox); { const searchBox = document.getElementById("searchBox") as CustomHTMLDivElement; const markdown = new MarkDown("", thisUser ?? undefined); @@ -352,25 +187,6 @@ if (window.location.pathname.startsWith("/channels")) { return span; }); } - let images: Blob[] = []; - let imagesHtml = new WeakMap(); - - document.addEventListener("paste", async (e: ClipboardEvent) => { - if (!thisUser?.focusChannel) return; - if (!e.clipboardData) return; - - for (const file of Array.from(e.clipboardData.files)) { - const fileInstance = File.initFromBlob(file); - e.preventDefault(); - const html = fileInstance.upHTML(images, imagesHtml, file, () => { - thisUser?.updateSend(); - }); - pasteImageElement.appendChild(html); - images.push(file); - imagesHtml.set(file, html); - } - thisUser?.updateSend(); - }); await setTheme(); @@ -396,60 +212,7 @@ if (window.location.pathname.startsWith("/channels")) { }; memberListToggle.checked = false; } - let dragendtimeout = setTimeout(() => {}); - document.addEventListener("dragover", (e) => { - clearTimeout(dragendtimeout); - const data = e.dataTransfer; - const bg = document.getElementById("gimmefile") as HTMLDivElement; - if (data) { - const isfile = data.types.includes("Files") || data.types.includes("application/x-moz-file"); - if (!isfile) { - bg.hidden = true; - return; - } - e.preventDefault(); - bg.hidden = false; - //console.log(data.types,data) - } else { - bg.hidden = true; - } - }); - document.addEventListener("dragleave", (_) => { - dragendtimeout = setTimeout(() => { - const bg = document.getElementById("gimmefile") as HTMLDivElement; - bg.hidden = true; - }, 1000); - }); - document.addEventListener("dragenter", (e) => { - e.preventDefault(); - }); - document.addEventListener("drop", (e) => { - const data = e.dataTransfer; - const bg = document.getElementById("gimmefile") as HTMLDivElement; - bg.hidden = true; - if (!thisUser?.focusChannel) { - e.preventDefault(); - return; - } - if (data) { - const isfile = data.types.includes("Files") || data.types.includes("application/x-moz-file"); - if (isfile) { - e.preventDefault(); - console.log(data.files); - for (const file of Array.from(data.files)) { - const fileInstance = File.initFromBlob(file); - const html = fileInstance.upHTML(images, imagesHtml, file, () => { - thisUser?.updateSend(); - }); - pasteImageElement.appendChild(html); - images.push(file); - imagesHtml.set(file, html); - } - thisUser?.updateSend(); - } - } - }); const pinnedM = document.getElementById("pinnedM") as HTMLElement; pinnedM.onclick = (e) => { thisUser?.pinnedClick(pinnedM.getBoundingClientRect()); @@ -468,26 +231,7 @@ if (window.location.pathname.startsWith("/channels")) { }, ); umenu.addButton(I18n.upload(), () => { - const input = document.createElement("input"); - input.type = "file"; - input.click(); - input.multiple = true; - console.log("clicked"); - if (!thisUser?.focusChannel) return; - input.onchange = () => { - if (input.files) { - for (const file of Array.from(input.files)) { - const fileInstance = File.initFromBlob(file); - const html = fileInstance.upHTML(images, imagesHtml, file, () => { - thisUser?.updateSend(); - }); - pasteImageElement.appendChild(html); - images.push(file); - imagesHtml.set(file, html); - } - thisUser?.updateSend(); - } - }; + TypeBox.uploadFiles(); }); umenu.bindContextmenu( document.getElementById("upload")!, diff --git a/src/webpage/invite.ts b/src/webpage/invite.ts index 9c29bce..4bb3c53 100644 --- a/src/webpage/invite.ts +++ b/src/webpage/invite.ts @@ -12,6 +12,7 @@ if (window.location.pathname.startsWith("/invite")) } console.log(m); well = m.get(well.toLowerCase()) || (await getInstanceInfo(well))?.api || well; + well = well.replace(/\/*$/gm, ""); const joinable: Specialuser[] = []; for (const key in users.users) { @@ -20,7 +21,6 @@ if (window.location.pathname.startsWith("/invite")) if (well && user.serverurls.wellknown.includes(well)) { joinable.push(user); } - console.log(user); } } diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 7d7a182..67423b4 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -23,7 +23,7 @@ import { } from "./jsontypes.js"; import {Member} from "./member.js"; import {buttonColor, Dialog, Form, FormError, Options, Settings} from "./settings.js"; -import {getTextNodeAtPosition, MarkDown, saveCaretPosition} from "./markdown.js"; +import {getTextNodeAtPosition, MarkDown} from "./markdown.js"; import {Bot} from "./bot.js"; import {Role} from "./role.js"; import {VoiceFactory, voiceStatusStr} from "./voice.js"; @@ -53,15 +53,14 @@ import {trimTrailingSlashes} from "./utils/netUtils.js"; import {Versions} from "./versions.js"; import {Shortcut} from "./shortcuts/shortcut.js"; import {getShortcuts, setShortcuts} from "./utils/storage/shortcuts.js"; +import {TypeBox} from "./typeBox.js"; type traceObj = { micros: number; calls?: (string | traceObj)[]; }; type trace = [string, traceObj]; const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]); -interface CustomHTMLDivElement extends HTMLDivElement { - markdown: MarkDown; -} + interface MDSearchOption { name: string; replace: string; @@ -1755,6 +1754,7 @@ class Localuser { } loadGuild(id: string, forceReload = false): Guild | undefined { + TypeBox.saveBox(); this.searching = false; let guild = this.guilds.get(id); if (!guild) { @@ -3305,6 +3305,24 @@ class Localuser { AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()), {defaultIndex: AnimateTristateValues.indexOf(prefs.animateIcons)}, ); + animations.addSelect( + I18n.accessibility.playEmoji(), + async (i) => { + prefs.animateEmoji = AnimateTristateValues[i]; + await setPreferences(prefs); + }, + AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()), + {defaultIndex: AnimateTristateValues.indexOf(prefs.animateEmoji)}, + ); + animations.addSelect( + I18n.accessibility.playSticker(), + async (i) => { + prefs.animateSticker = AnimateTristateValues[i]; + await setPreferences(prefs); + }, + AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()), + {defaultIndex: AnimateTristateValues.indexOf(prefs.animateSticker)}, + ); } settings.addButton(I18n.localuser.security(), {head: true}); { @@ -4280,21 +4298,12 @@ class Localuser { }); } updateSend() { - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - if ( - (typebox.markdown.rawString && typebox.markdown.rawString !== "\n") || - document.getElementById("pasteimage")?.children.length - ) { - typebox.parentElement!.classList.remove("noConent"); - } else { - typebox.parentElement!.classList.add("noConent"); - } + TypeBox.updateSend(); } //TODO make this an option readonly autofillregex = Object.freeze(/(^|\s|\n)[@#:]([a-zA-Z0-9]*)$/i); mdBox() { - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - const typeMd = typebox.markdown; + const typeMd = TypeBox.markdown; typeMd.owner = this; typeMd.onUpdate = (str, pre) => { this.search(document.getElementById("searchOptions") as HTMLDivElement, typeMd, str, pre); @@ -4529,8 +4538,7 @@ class Localuser { search.focus(); } async TBEmojiMenu(rect: DOMRect) { - const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - const p = saveCaretPosition(typebox); + const p = TypeBox.saveCarrot(); if (!p) return; const original = MarkDown.getText(); @@ -4541,7 +4549,7 @@ class Localuser { ); this.favorites.addEmoji(emoji.id || (emoji.emoji as string)); p(); - const md = typebox.markdown; + const md = TypeBox.markdown; this.MDReplace( emoji.id ? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>` @@ -4576,10 +4584,6 @@ class Localuser { ); } } - fileExtange!: ( - files: Blob[], - html: WeakMap, - ) => [Blob[], WeakMap]; MDSearchOptions( options: MDSearchOption[], original: string, diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts index f826ca7..39a6986 100644 --- a/src/webpage/markdown.ts +++ b/src/webpage/markdown.ts @@ -6,6 +6,7 @@ import {I18n} from "./i18n.js"; import {Dialog} from "./settings.js"; import {Contextmenu} from "./contextmenu.js"; import {highlight} from "./highlighter/index.js"; +import {TypeBox} from "./typeBox.js"; const linkMenu = new Contextmenu("copyLink", true); linkMenu.addButton( () => I18n.copyRegLink(), @@ -14,7 +15,7 @@ linkMenu.addButton( }, {group: "copyLink"}, ); -const isFirefox = navigator.userAgent.toLowerCase().includes("firefox"); +const isFirefox = Error.prototype.stack === ""; class MarkDown { static emoji?: typeof Emoji; txt: string[]; @@ -351,19 +352,20 @@ class MarkDown { if (stdsize) { build = build.replaceAll("\n", ""); } - if (find === count) { + if (find === count || (keep && count === 3)) { appendcurrent(); i = j; if (keep) { build += "`".repeat(find); } + if (build.endsWith("\n")) build += "\n"; if (count !== 3 && !stdsize) { const samp = document.createElement("samp"); samp.textContent = build; span.appendChild(samp); } else { const pre = document.createElement("pre"); - if (build.at(-1) === "\n") { + if (build.at(-1) === "\n" && !isFirefox) { build = build.substring(0, build.length - 1); } if (txt[i] === "\n") { @@ -974,14 +976,20 @@ class MarkDown { } appendcurrent(); const last = getCurLast(); - + function addSpacer() { + const s = document.createElement("span"); + s.style.setProperty("white-space", "pre"); + s.textContent = "​"; + s.setAttribute("real", ""); + span.append(s); + } if ( last && - last instanceof Text && - last.textContent === "\n" && - Error.prototype.stack === "" && - !isFirefox + isFirefox && + (last instanceof Text || last instanceof HTMLSpanElement) && + last.textContent === "\n" ) { + addSpacer(); span.append(current); } if ( @@ -1052,17 +1060,6 @@ class MarkDown { box.addEventListener("keydown", (_) => { if (_.isComposing) return; if (Error.prototype.stack !== "") return; - if (_.key === "Enter") { - const selection = window.getSelection() as Selection; - if (!selection) return; - const range = selection.getRangeAt(0); - const node = new Text("\n"); - range.insertNode(node); - const g = node.nextSibling; - if (g) range.setStart(g, 0); - _.preventDefault(); - return; - } }); let prevcontent = ""; const gatherBoxContents = (isBackSpace: boolean) => { @@ -1083,28 +1080,29 @@ class MarkDown { const content = this.rawString; if (content) { let txti = text; - if (trim) { + if (trim && TypeBox.inPre()) { txti = txti.replace(/\n$/, ""); } - const [_first, end] = content.split(txti); - console.log([txti, txt, end]); + const [first, ...ends] = content.split(txti); + const end = ends.join(txti); if (rstr) { const tw = text.split(rstr); tw.pop(); text = tw.join(""); } - const boxText = txti + txt + (end ?? ""); - box.textContent = boxText; + const boxText = first + txti + txt + (end ?? ""); + box.innerHTML = ""; + this.txt = boxText.split(""); + box.append(this.makeHTML({keep: true})); const len = txti.length + txt.length; text = boxText; - this.txt = text.split(""); this.boxupdate(len, false, 0); - console.log(this.rawString); } else { - box.textContent = txt; + this.txt = txt.split(""); + box.innerHTML = ""; + box.append(this.makeHTML({keep: true})); text = txt; - this.txt = text.split(""); this.boxupdate(txt.length, false, 0); } }; @@ -1114,7 +1112,7 @@ class MarkDown { gatherBoxContents(_.key === "Backspace"); }; box.onkeydown = (_) => { - if (isFirefox && _.key === "Enter" && !text.endsWith("\n")) { + if (isFirefox && _.key === "Enter" && (TypeBox.inPre() || _.shiftKey)) { _.preventDefault(); _.stopImmediatePropagation(); @@ -1191,6 +1189,7 @@ class MarkDown { html.childNodes[0].childNodes.length === 1 && html.childNodes[0].childNodes[0]; //console.log(box.cloneNode(true), html.cloneNode(true)); + if (isFirefox) allowLazy = false; //TODO this may be slow, may want to check in on this in the future if it is if ((!box.hasChildNodes() || html.isEqualNode(Array.from(box.childNodes)[0])) && allowLazy) { //console.log("no replace needed"); @@ -1233,6 +1232,11 @@ class MarkDown { if (thing instanceof Text) { const text = thing.textContent; build += text; + if (element.tagName.toLowerCase() === "pre") { + if (build.endsWith("\n") && isFirefox) { + build = build.replace(/\n$/, ""); + } + } continue; } @@ -1431,13 +1435,13 @@ function saveCaretPosition( build += node.textContent; } } else { - //console.error(node,"This shouldn't happen"); + //console.error(node, "This shouldn't happen"); } } } crawlForText(context); if (baseString === "\n") { - build += baseString; + //build += baseString; } text = build; len += build.length; @@ -1447,15 +1451,14 @@ function saveCaretPosition( len = Math.min(len, txtLengthFunc(context).length); len += offset; - return function restore(backspace = false) { + return function restore(_backspace = false) { if (!selection) return; const pos = getTextNodeAtPosition(context, len, txtLengthFunc); if ( pos.node instanceof Text && pos.node.textContent === "\n" && pos.node.nextSibling && - Error.prototype.stack === "" && - !backspace + isFirefox ) { if (pos.node.nextSibling instanceof Text && pos.node.nextSibling.textContent === "\n") { pos.position = 1; diff --git a/src/webpage/message.ts b/src/webpage/message.ts index 5f852e2..d2478d1 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -28,6 +28,7 @@ import {ImagesDisplay} from "./disimg"; import {ReportMenu} from "./reporting/report.js"; import {getDeveloperSettings} from "./utils/storage/devSettings.js"; import {getPreferences} from "./utils/storage/userPreferences.js"; +import {TypeBox} from "./typeBox.js"; class Message extends SnowFlake { static contextmenu = new Contextmenu("message menu"); stickers!: Sticker[]; @@ -193,7 +194,9 @@ class Message extends SnowFlake { ); }, { - //TODO make icon + icon: { + css: "svg-link", + }, }, ); Message.contextmenu.addButton( @@ -1015,7 +1018,7 @@ class Message extends SnowFlake { area.append(md.makeHTML()); area.addEventListener("keyup", (event) => { if (this.localuser.keyup(event)) return; - if (event.key === "Enter" && !event.shiftKey) { + if (event.key === "Enter" && !event.shiftKey && !TypeBox.inPre()) { this.edit(MarkDown.gatherBoxText(area)); this.channel.editing = null; this.generateMessage(); @@ -1023,7 +1026,7 @@ class Message extends SnowFlake { }); area.addEventListener("keydown", (event) => { this.localuser.keydown(event); - if (event.key === "Enter" && !event.shiftKey) event.preventDefault(); + if (event.key === "Enter" && !event.shiftKey && !TypeBox.inPre()) event.preventDefault(); if (event.key === "Escape") { this.channel.editing = null; this.generateMessage(); @@ -1080,8 +1083,9 @@ class Message extends SnowFlake { if (this.attachments.length) { const attach = document.createElement("div"); attach.classList.add("flexltr", "attachments"); + const isGallery = this.attachments.length > 1; for (const thing of this.attachments) { - attach.appendChild(thing.getHTML()); + attach.appendChild(thing.getHTML(false, false, false, 96 * 3, isGallery)); } messagedwrap.appendChild(attach); } @@ -1291,7 +1295,7 @@ class Message extends SnowFlake { for (const sticker of this.stickers) { stickerArea.append(sticker.getHTML()); } - div.append(stickerArea); + text.append(stickerArea); if (this.poll) { const pollbody = document.createElement("div"); @@ -1361,7 +1365,7 @@ class Message extends SnowFlake { count.textContent = I18n.poll.count("" + c, per + ""); aarea.append(count); if (per) - aarea.style.background = `linear-gradient(to right, var(--green) ${per}%, var(--bg) ${per}%)`; + aarea.style.background = `linear-gradient(to right, var(--accent-color) ${per}%, var(--bg) ${per}%)`; } aarea.onclick = () => check.click(); pollbody.append(aarea); diff --git a/src/webpage/notificationHandler.ts b/src/webpage/notificationHandler.ts index b7496ee..5a33c1f 100644 --- a/src/webpage/notificationHandler.ts +++ b/src/webpage/notificationHandler.ts @@ -20,6 +20,17 @@ export class NotificationHandler { noticontent ||= message.embeds[0]?.json.title; noticontent ||= message.content.textContent; } + if (message.attachments.length > 0) { + noticontent ||= I18n.sentAttachment(); + } + if (message.poll) { + noticontent ||= I18n.sentPoll(); + } + if (message.stickers.length > 0) { + noticontent ||= I18n.sentSticker(); + } + // exhausted all options, sorry, we just send the raw thing! + noticontent ||= message.content.rawString; noticontent ||= I18n.blankMessage(); const image = message.getimages()[0]; diff --git a/src/webpage/sticker.ts b/src/webpage/sticker.ts index e361ba6..4c8f287 100644 --- a/src/webpage/sticker.ts +++ b/src/webpage/sticker.ts @@ -36,6 +36,9 @@ class Sticker extends SnowFlake { getHTML(): HTMLElement { const img = createImg( this.owner.info.cdn + "/stickers/" + this.id + ".webp" + new CDNParams({expectedSize: 160}), + undefined, + undefined, + "sticker", ); img.classList.add("sticker"); const hover = new Hover(this.name); diff --git a/src/webpage/style.css b/src/webpage/style.css index f326edd..f413d1c 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -7,6 +7,7 @@ body { color: var(--primary-text); overflow: hidden; /* avoid "bounce" */ -webkit-text-size-adjust: 100%; + scrollbar-color: var(--button-bg) var(--primary-bg); } #page { height: 100svh; @@ -274,7 +275,7 @@ body { flex-direction: column; } .pollBody { - background: #00000059; + background: var(--secondary-bg); padding: 10px; border-radius: 4px; margin-top: 2px; @@ -284,13 +285,12 @@ body { } } .answerArea { - --bg: #0000004a; + --bg: color-mix(in srgb, var(--accent-color) 15%, transparent); background: var(--bg); - margin-top: 3px; + margin-bottom: 6px; padding: 12px; cursor: pointer; position: relative; - input { margin-left: auto; cursor: pointer; @@ -1058,10 +1058,12 @@ textarea { right: 5px; transition: top 0.2s; transition-timing-function: ease-in; - background: white; + backdrop-filter: blur(20px); + mix-blend-mode: difference; + background: #969696; cursor: pointer; &.favorited { - background: yellow; + background: orange; } } .visually-hidden { @@ -1132,6 +1134,10 @@ textarea { mask: url(./icons/pin.svg); mask-size: cover; } +.svg-link { + mask: url(./icons/link.svg); + mask-size: cover; +} .svg-rules { mask: url(./icons/rules.svg); mask-size: cover; @@ -1304,7 +1310,7 @@ textarea { position: absolute; right: 10px; top: 10px; - background: #0000009e; + background: var(--secondary-bg); padding: 6px; border-radius: 100%; cursor: pointer; @@ -1598,6 +1604,12 @@ textarea { display: none; } } +#filedroptext { + max-width: 75%; + max-height: 75vh; + padding: 8em 8em 8em 8em; + text-align: center; +} .commandError { position: absolute; top: -36px; @@ -2076,6 +2088,7 @@ span.instanceStatus { align-items: start; padding-top: 10px; background-size: cover; + color: white; .ellipsis { text-shadow: 0px 0px 8px black; } @@ -2146,6 +2159,9 @@ span.instanceStatus { display: flex; align-items: center; transition: font-weight 0.1s; + > .ellipsis { + margin-bottom: 0.25em; + } } .channelbutton:hover { background: var(--channel-hover); @@ -2362,7 +2378,7 @@ span.instanceStatus { } #userdock { - padding: 4px 6px; + padding: 11.75px 6px; background: var(--dock-bg); align-items: center; justify-content: space-between; @@ -2452,7 +2468,7 @@ span.instanceStatus { flex: 0; } #pasteimage { - height: 30%; + max-height: 30%; padding: 12px; margin: 16px; background: var(--typebox-bg); @@ -2477,13 +2493,14 @@ span.instanceStatus { border-radius: 8px; overflow: hidden; height: 192px; + display: flex; + align-items: center; + justify-content: center; .unknownfile { width: 100%; height: 100%; display: flex; flex-direction: column; - align-items: center; - justify-content: center; } } .messageimgdiv { @@ -2491,16 +2508,13 @@ span.instanceStatus { overflow: clip; width: fit-content; height: fit-content; -} -.messageimg { - height: 100%; - width: 100%; - object-fit: contain; - user-select: none; - cursor: pointer; -} -.attachments .messageimg { - border-radius: 4px; + img, div { + height: 100%; + width: 100%; + object-fit: contain; + user-select: none; + cursor: pointer; + } } #replybox { height: 32px; @@ -2551,6 +2565,7 @@ span.instanceStatus { flex-shrink: 1; text-wrap: auto; overflow-y: auto; + word-break: break-word; margin-right: 0.03in; padding: 10px 0; } @@ -2866,12 +2881,13 @@ span.instanceStatus { background: color-mix(in srgb, var(--accent-color) 75%, transparent) !important; font-size: 0.75em !important; font-weight: bold; - color: var(--primary-text-prominent) !important; + color: white !important; border-radius: 4px; flex: none; width: fit-content; background-clip: border-box !important; align-self: center; + vertical-align: center; } .membererror { display: inline-block; @@ -3087,11 +3103,11 @@ span .quote:last-of-type .quoteline { aspect-ratio: 1.9; } .mentionMD { - color: transparent; background-clip: text; padding: 1px 2px; border-radius: 4px; cursor: pointer; + color: color-mix(in srgb, var(--userbg) 75%, var(--primary-text)); background-image: var(--userbg, linear-gradient(var(--primary-text), var(--primary-text))); position: relative; z-index: 1; @@ -3109,6 +3125,24 @@ span .quote:last-of-type .quoteline { .mentionMD:hover::before { background: var(--mention); } + +.smallemoji { + height: auto; + width: auto; + max-height: 1.25em; + max-width: 1.25em; + vertical-align: middle; +} +.bigemoji { + width: 0.6in; + object-fit: contain; + height: 0.6in; + vertical-align: middle; +} +.bigemojiUni { + font-size: 47px; +} + .reactiondiv { padding-left: 52px; gap: 0 4px; @@ -3116,7 +3150,8 @@ span .quote:last-of-type .quoteline { user-select: none; } .reaction { - min-width: 30px; + min-width: 1.25em; + min-height: 1.25em; padding: 3px 4px; margin-top: 4px; background: var(--reaction-bg); @@ -3127,6 +3162,7 @@ span .quote:last-of-type .quoteline { align-items: center; cursor: pointer; gap: 4px; + font-size: 1.25em; } .reaction p { line-height: 1em; @@ -3138,12 +3174,6 @@ span .quote:last-of-type .quoteline { background: var(--reaction-reacted-bg); border-color: color-mix(in srgb, var(--accent-color), transparent); } -.smallemoji { - height: auto; - width: auto; - max-height: 16px; - max-width: 16px; -} /* Message External */ .filename a { @@ -3154,8 +3184,25 @@ span .quote:last-of-type .quoteline { gap: 4px; flex-wrap: wrap; video { - max-height: 288px; - max-width: 288px; + min-height: 200px; + min-width: 200px; + max-height: 25%; + max-width: 50%; + } + .messageimgdiv { + img, video { + border-radius: 4px; + } + } + .messagegallerydiv { + object-fit: fill; + height: 200px; + max-width: 50%; + overflow: hidden; + img, video { + height: 100%; + object-fit: cover; + } } } .embed { @@ -3235,6 +3282,8 @@ img.bigembedimg { margin-top: 8px; border-radius: 4px; cursor: pointer; + width: -webkit-fill-available; + width: -moz-available; } .inviteEmbed { width: 400px; @@ -3302,7 +3351,7 @@ img.bigembedimg { /* Sidebar */ #sideContainDiv { - padding: 16px 8px; + padding: 16px 8px 0px 8px; display: none; flex: none; width: 240px; @@ -3372,33 +3421,34 @@ img.bigembedimg { } } ::highlight(highkeyword) { - color: yellow; + color: var(--code-keyword); } ::highlight(highnumber) { - color: orange; + color: var(--code-number); } ::highlight(highstring) { - color: red; + color: var(--code-string); } ::highlight(highsymbol) { - color: white; + color: var(--primary-text-prominent); font-weight: bold; } ::highlight(highcomment) { - color: green; + color: var(--code-comment); } ::highlight(highidentifier) { - color: white; + color: var(--primary-text); } ::highlight(highClass) { - color: #6741ff; + color: var(--code-class); } ::highlight(highProp) { - color: #33dcff; + color: var(--code-prop); } ::highlight(highFunc) { - color: #d564ff; + color: var(--code-func); } + #memberlisttoggle.svgicon { -moz-appearance: initial; &::before { @@ -3489,9 +3539,12 @@ img.bigembedimg { border-radius: 8px; overflow: hidden; align-items: flex-start; - z-index: 3; + z-index: 4; display: flex; flex-direction: column; + .banner { + height: 64px; + } .infosection { background-color: color-mix(in srgb, var(--secondary-bg) 75%, transparent); } @@ -4488,32 +4541,34 @@ fieldset input[type="radio"] { position: relative; padding: 0px; cursor: pointer; + flex-direction: row-reverse; + &::after { content: ""; background: var(--secondary-bg); - transition: left 0.2s; + transition: right 0.2s; border-radius: 4px; width: 18px; height: 18px; display: block; top: 3px; - left: 2px; + right: 2px; position: absolute; } - :first-child { + :last-child { border-radius: 5px 0px 0px 5px; } - :last-child { + :first-child { border-radius: 0px 5px 5px 0px; } &:has(:first-child.selected)::after { - left: 3px; + right: 3px; } &:has(:nth-child(2).selected)::after { - left: 27px; + right: 27px; } &:has(:nth-child(3).selected)::after { - left: 51px; + right: 51px; } } .tritoggle > .triOpt { @@ -4854,18 +4909,15 @@ fieldset input[type="radio"] { .friendcontainer { display: flex; width: 100%; - padding: 0.2in; + padding: 0.15in; overflow-y: auto; height: 100%; align-items: stretch; box-sizing: border-box; - > div { - background: #00000030; - margin-bottom: 0.1in; padding: 0.06in 0.1in; - border-radius: 0.1in; - border: solid 1px var(--black); + margin: 0 0; + border-bottom: solid 1px var(--secondary-bg); } } .fixedsearch { @@ -4922,26 +4974,17 @@ fieldset input[type="radio"] { transition: background-color 0.2s; padding: 0.08in; } -.bigemoji { - width: 0.6in; - object-fit: contain; - height: 0.6in; -} -.bigemojiUni { - font-size: 47px; -} .friendlyButton { padding: 0.07in; - background: #00000045; transition: background 0.2s; border-radius: 1in; - border: solid 1px var(--black); + border: solid 1px var(--secondary-bg); width: 24px; height: 24px; margin: 0 0.05in; } .friendlyButton:hover { - background: black; + background: var(--secondary-hover); } .stickerOption { border: solid 1px var(--black); @@ -4993,14 +5036,15 @@ fieldset input[type="radio"] { .gifPreviewBox { position: relative; width: 2in; + height: 1in; margin-bottom: 10px; border-radius: 7px; overflow: hidden; cursor: pointer; img { - width: 2in; - height: 1in; + width: 100%; + height: 100%; object-fit: cover; } span { @@ -5012,8 +5056,9 @@ fieldset input[type="radio"] { display: inline-flex; align-items: center; justify-content: center; - background: color-mix(in srgb, var(--card-bg) 60%, transparent); font-weight: bold; + color: white; + background: color-mix(in srgb, black 50%, transparent); } } .gifbox { @@ -5150,7 +5195,6 @@ img.error::after { border-radius: 2in; } .stickerMArea { - padding-left: 48px; } .solidBackground { background: var(--secondary-bg); @@ -5188,6 +5232,13 @@ img.error::after { .guildEmojiText { display: flex; justify-content: center; + word-break: break-all; +} +.guildDesc { + color: var(--secondary-text-soft); + font-size: 0.9em; + max-height: 15em; + overflow-y: scroll; } .optionElement:has(.friendGroupSelect) { @@ -5259,7 +5310,7 @@ img.error::after { background: transparent; width: 48px; height: 48px; - img { + .pfpDiv { width: 24px; height: 24px; &:nth-child(1) { diff --git a/src/webpage/themes.css b/src/webpage/themes.css index fd8ab6c..ab048c1 100644 --- a/src/webpage/themes.css +++ b/src/webpage/themes.css @@ -12,6 +12,14 @@ --blue: #779bff; --grey: #b5b5b5; --update: var(--green); + + --code-keyword: yellow; + --code-number: orange; + --code-string: red; + --code-comment: green; + --code-class: #6741ff; + --code-prop: #33dcff; + --code-func: #d564ff; } /* Default theme that looks "just about grey" */ @@ -43,7 +51,7 @@ --primary-button-bg: color-mix(in srgb, #000000 10%, var(--accent-color)); --primary-bg: #fefefe; --primary-hover: #f6f6f9; - --primary-text: #4b4b59; + --primary-text: black; --primary-text-soft: #656575; --secondary-bg: #e0e0ea; @@ -66,6 +74,15 @@ --secondary-text-soft: #4c4c5a; --dock-bg: #d1d1df; --dock-hover: #b8b8d0; + + --code-bg: #fdfdf4; + --code-keyword: rgb(180, 180, 76); + --code-number: rgb(161, 109, 12); + --code-string: rgb(153, 4, 4); + --code-comment: rgb(9, 116, 9); + --code-class: #230a86; + --code-prop: #027d96; + --code-func: #6c0091; } .Light-theme { diff --git a/src/webpage/typeBox.ts b/src/webpage/typeBox.ts new file mode 100644 index 0000000..6c6ed85 --- /dev/null +++ b/src/webpage/typeBox.ts @@ -0,0 +1,376 @@ +import {Channel} from "./channel"; +import {Localuser} from "./localuser"; +import {MarkDown, saveCaretPosition} from "./markdown"; +import {File} from "./file"; +import {I18n} from "./i18n"; +import {mobile} from "./utils/utils"; +class BoxState { + text: string; + files: globalThis.File[]; + constructor(text: string, files: globalThis.File[]) { + this.text = text; + this.files = files; + } +} +export class TypeBox { + static box = document.getElementById("typebox") as HTMLDivElement; + private static localuser?: Localuser; + static markdown = new MarkDown(""); + private static files: globalThis.File[] = []; + private static imagesHtml = new WeakMap(); + private static pasteImageElement = document.getElementById("pasteimage") as HTMLDivElement; + private static nonceMap = new Map(); + static getNonce(id: string) { + const nonce = this.nonceMap.get(id) || Math.floor(Math.random() * 1000000000) + ""; + this.nonceMap.set(id, nonce); + return nonce; + } + static focus() { + this.box.focus(); + } + static init() { + this.box.addEventListener("keyup", this.handleEnter.bind(this)); + this.box.addEventListener("keydown", (event) => { + if (event.isComposing) return; + this.localuser?.keydown(event); + if (event.key === "Enter" && !event.shiftKey && window.innerWidth > 600 && !TypeBox.inPre()) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + }); + this.markdown.giveBox(this.box); + const mobileSend = document.getElementById("mobileSend"); + if (mobileSend) { + mobileSend.onclick = () => { + const channel = this.localuser?.focusChannel; + if (!channel) return; + const content = MarkDown.gatherBoxText(this.box); + this.sendMessage(channel, content); + }; + } + const channelw = document.getElementById("channelw"); + if (channelw) + channelw.addEventListener("keypress", (e) => { + if (e.ctrlKey || e.altKey || e.metaKey || e.metaKey) return; + let owner = e.target as HTMLElement; + while (owner !== channelw) { + if (owner.tagName === "input" || owner.contentEditable !== "false") { + return; + } + owner = owner.parentElement as HTMLElement; + } + this.markdown.boxupdate(Infinity); + }); + document.addEventListener("paste", async (e: ClipboardEvent) => { + if (!this.localuser?.focusChannel) return; + if (!e.clipboardData) return; + + for (const file of Array.from(e.clipboardData.files)) { + e.preventDefault(); + this.addFile(file); + } + this.localuser?.updateSend(); + }); + let dragendtimeout = setTimeout(() => {}); + document.addEventListener("dragover", (e) => { + clearTimeout(dragendtimeout); + const data = e.dataTransfer; + const bg = document.getElementById("gimmefile") as HTMLDivElement; + + if (data) { + const isfile = + data.types.includes("Files") || data.types.includes("application/x-moz-file"); + if (!isfile) { + bg.hidden = true; + return; + } + e.preventDefault(); + bg.hidden = false; + //console.log(data.types,data) + } else { + bg.hidden = true; + } + }); + document.addEventListener("dragleave", (_) => { + dragendtimeout = setTimeout(() => { + const bg = document.getElementById("gimmefile") as HTMLDivElement; + bg.hidden = true; + }, 1000); + }); + document.addEventListener("dragenter", (e) => { + e.preventDefault(); + }); + document.addEventListener("drop", (e) => { + const data = e.dataTransfer; + const bg = document.getElementById("gimmefile") as HTMLDivElement; + bg.hidden = true; + if (!this.localuser?.focusChannel) { + e.preventDefault(); + return; + } + if (data) { + const isfile = + data.types.includes("Files") || data.types.includes("application/x-moz-file"); + if (isfile) { + e.preventDefault(); + console.log(data.files); + for (const file of Array.from(data.files)) { + this.addFile(file); + } + this.localuser?.updateSend(); + } + } + }); + } + private static cMap = new Map(); + private static channelToID(c: Channel) { + return c.id; + } + static saveBox() { + if (!this.localuser?.focusChannel) return; + const state = new BoxState(MarkDown.gatherBoxText(this.box), this.files); + this.pasteImageElement.textContent = ""; + this.box.textContent = ""; + this.files = []; + this.markdown.txt = []; + this.cMap.set(this.channelToID(this.localuser.focusChannel), state); + } + static restoreBox(c = this.localuser?.focusChannel) { + if (!c) return; + const state = this.cMap.get(this.channelToID(c)); + this.box.textContent = ""; + this.pasteImageElement.textContent = ""; + if (state) { + this.markdown.txt = [...state.text]; + this.files = state.files; + } else { + this.markdown.txt = []; + this.files = []; + } + this.box.append(this.markdown.makeHTML({keep: true})); + this.markdown.owner = c; + this.markdown.boxupdate(Infinity); + for (const file of this.files) { + this.addFile(file, false); + } + } + static changeWrite() { + if (!this.localuser?.focusChannel) return; + const c = this.localuser.focusChannel; + const canMessage = c.canMessage; + try { + this.box.contentEditable = canMessage ? "plaintext-only" : "false"; + } catch { + this.box.contentEditable = canMessage ? "true" : "false"; + } + (document.getElementById("upload") as HTMLElement).style.visibility = canMessage + ? "visible" + : "hidden"; + (document.getElementById("gifTB") as HTMLElement).style.display = canMessage ? "block" : "none"; + (document.getElementById("stickerTB") as HTMLElement).style.display = canMessage + ? "block" + : "none"; + (document.getElementById("emojiTB") as HTMLElement).style.display = canMessage + ? "block" + : "none"; + (document.getElementById("mobileSend") as HTMLElement).style.display = canMessage + ? "block" + : "none"; + (document.getElementById("typediv") as HTMLElement).style.visibility = "visible"; + if (!mobile) { + this.box.focus(); + } else { + this.box.blur(); + } + } + static saveCarrot() { + return saveCaretPosition(this.box); + } + static updateSend() { + if ( + (this.markdown.rawString && this.markdown.rawString !== "\n") || + document.getElementById("pasteimage")?.children.length + ) { + this.box.parentElement!.classList.remove("noConent"); + } else { + this.box.parentElement!.classList.add("noConent"); + } + } + static changeVisablity(visable: boolean) { + if (visable) { + } else { + this.box.contentEditable = "" + false; + const replybox = document.getElementById("replybox") as HTMLElement; + replybox.classList.add("hideReplyBox"); + this.box.classList.remove("typeboxreplying"); + (document.getElementById("upload") as HTMLElement).style.visibility = "hidden"; + (document.getElementById("typediv") as HTMLElement).style.visibility = "hidden"; + (document.getElementById("sideDiv") as HTMLElement).innerHTML = ""; + } + } + static addFile(blob: globalThis.File, add = true) { + const file = File.initFromBlob(blob); + const html = file.upHTML(this.files, this.imagesHtml, blob, () => { + this.localuser?.updateSend(); + }); + this.pasteImageElement.appendChild(html); + if (add) this.files.push(blob); + this.imagesHtml.set(blob, html); + } + static updateReplying() { + const c = this.localuser?.focusChannel; + const replybox = document.getElementById("replybox") as HTMLElement; + if (c && c.replyingto) { + this.box.classList.add("typeboxreplying"); + replybox.innerHTML = ""; + const span = document.createElement("span"); + span.textContent = I18n.replyingTo(c.replyingto.author.username); + const X = document.createElement("button"); + X.onclick = (_) => { + if (c.replyingto?.div) { + c.replyingto.div.classList.remove("replying"); + } + replybox.classList.add("hideReplyBox"); + c.replyingto = null; + replybox.innerHTML = ""; + TypeBox.updateReplying(); + }; + replybox.classList.remove("hideReplyBox"); + X.classList.add("cancelReply", "svgicon", "svg-x"); + replybox.append(span); + replybox.append(X); + } else { + replybox.classList.add("hideReplyBox"); + replybox.innerHTML = ""; + this.box.classList.remove("typeboxreplying"); + } + } + static uploadFiles() { + const input = document.createElement("input"); + input.type = "file"; + input.click(); + input.multiple = true; + console.log("clicked"); + if (!this.localuser?.focusChannel) return; + input.onchange = () => { + if (input.files) { + for (const file of Array.from(input.files)) { + this.addFile(file); + } + this.localuser?.updateSend(); + } + }; + } + static regSwap(l: Localuser) { + l.onswap = (l) => { + this.localuser = l; + this.regSwap(l); + }; + this.localuser = l; + } + static inPre() { + const selection = window.getSelection(); + if (!selection) return false; + let node = selection.anchorNode; + while (node) { + if (node instanceof HTMLPreElement) return true; + node = node.parentElement; + } + return false; + } + private static async handleEnter(event: KeyboardEvent): Promise { + if (event.isComposing) return; + if (event.key === "Escape" && (this.files.length || this.localuser?.focusChannel?.replyingto)) { + while (this.files.length) { + const elm = this.imagesHtml.get(this.files.pop() as Blob) as HTMLElement; + if (this.pasteImageElement.contains(elm)) this.pasteImageElement.removeChild(elm); + } + if (this.localuser?.focusChannel) { + this.localuser.focusChannel?.replyingto?.div?.classList.remove("replying"); + this.localuser.focusChannel.replyingto = null; + this.localuser.focusChannel.makereplybox(); + } + this.localuser?.updateSend(); + return; + } + if (this.localuser?.handleKeyUp(event)) { + return; + } + + const channel = this.localuser?.focusChannel; + if (!channel) return; + const content = MarkDown.gatherBoxText(this.box); + if (content === "" && event.key === "ArrowUp") { + channel.editLast(); + return; + } + channel.typingstart(); + + if (event.key === "Enter" && !event.shiftKey && window.innerWidth > 600 && !this.inPre()) { + event.preventDefault(); + await this.sendMessage(channel, content); + } + } + private static async sendMessage(channel: Channel, content: string) { + if (!channel.canMessageRightNow()) return; + if (channel.curCommand) { + channel.submitCommand(); + return; + } + this.markdown.onUpdate("", false); + + let replyingTo = this.localuser?.focusChannel ? this.localuser.focusChannel.replyingto : null; + if (replyingTo?.div) { + replyingTo.div.classList.remove("replying"); + } + if (this.localuser?.focusChannel) { + this.localuser.focusChannel.replyingto = null; + this.localuser.focusChannel.makereplybox(); + } + const attachments = this.files.filter((_) => document.contains(this.imagesHtml.get(_) || null)); + while (this.files.length) { + const elm = this.imagesHtml.get(this.files.pop() as Blob) as HTMLElement; + if (this.pasteImageElement.contains(elm)) this.pasteImageElement.removeChild(elm); + } + this.box.innerHTML = ""; + this.markdown.txt = []; + try { + await new Promise((mres, rej) => + channel.sendMessage( + content, + { + attachments, + embeds: [], // Add an empty array for the embeds property + replyingto: replyingTo, + sticker_ids: [], + //nonce: getNonce(channel.id), + }, + (res) => { + if (res === "Ok") { + mres(); + } else { + rej(); + } + }, + ), + ); + } catch { + this.files = attachments; + for (const file of this.files) { + const img = this.imagesHtml.get(file); + if (!img) continue; + this.pasteImageElement.append(img); + } + channel.replyingto = replyingTo; + channel.makereplybox(); + this.box.textContent = content; + this.markdown.txt = content.split(""); + this.markdown.boxupdate(Infinity); + } + this.nonceMap.delete(channel.id); + } +} + +if (TypeBox.box) { + TypeBox.init(); +} diff --git a/src/webpage/utils/storage/userPreferences.ts b/src/webpage/utils/storage/userPreferences.ts index 6373e56..e52d322 100644 --- a/src/webpage/utils/storage/userPreferences.ts +++ b/src/webpage/utils/storage/userPreferences.ts @@ -30,6 +30,8 @@ export class UserPreferences { // render settings animateIcons: AnimateTristateValue = AnimateTristateValue.OnlyOnHover; animateGifs: AnimateTristateValue = AnimateTristateValue.OnlyOnHover; + animateEmoji: AnimateTristateValue = AnimateTristateValue.Always; + animateSticker: AnimateTristateValue = AnimateTristateValue.Always; renderJoinAvatars: boolean = true; theme: ThemeOption = ThemeOption.Dark; accentColor: string = "#5865F2"; diff --git a/src/webpage/utils/utils.ts b/src/webpage/utils/utils.ts index 837b4e7..3283089 100644 --- a/src/webpage/utils/utils.ts +++ b/src/webpage/utils/utils.ts @@ -290,87 +290,6 @@ export function adduser(user: typeof Specialuser.prototype.json): Specialuser { localStorage.setItem("userinfos", JSON.stringify(info)); return suser; } -class Directory { - static home = this.createHome(); - handle: FileSystemDirectoryHandle; - writeWorker?: Worker; - private constructor(handle: FileSystemDirectoryHandle) { - this.handle = handle; - } - static async createHome(): Promise { - navigator.storage.persist(); - const home = new Directory(await navigator.storage.getDirectory()); - return home; - } - async *getAllInDir() { - for await (const [name, handle] of this.handle.entries()) { - if (handle instanceof FileSystemDirectoryHandle) { - yield [name, new Directory(handle)] as [string, Directory]; - } else if (handle instanceof FileSystemFileHandle) { - yield [name, await handle.getFile()] as [string, File]; - } else { - console.log(handle, "oops :3"); - } - } - console.log("done"); - } - async getRawFileHandler(name: string) { - return await this.handle.getFileHandle(name); - } - async getRawFile(name: string) { - try { - return await (await this.handle.getFileHandle(name)).getFile(); - } catch { - return undefined; - } - } - async getString(name: string): Promise { - try { - return await (await this.getRawFile(name))!.text(); - } catch { - return undefined; - } - } - initWorker() { - if (this.writeWorker) return this.writeWorker; - this.writeWorker = new Worker("/utils/dirrWorker.js"); - this.writeWorker.onmessage = (event) => { - const res = this.wMap.get(event.data[0]); - this.wMap.delete(event.data[0]); - if (!res) throw new Error("Res is not defined here somehow"); - res(event.data[1]); - }; - return this.writeWorker; - } - wMap = new Map void>(); - async setStringWorker(name: FileSystemFileHandle, value: ArrayBuffer) { - const worker = this.initWorker(); - const random = Math.random(); - worker.postMessage([name, value, random]); - return new Promise((res) => { - this.wMap.set(random, res); - }); - } - async setString(name: string, value: string): Promise { - const file = await this.handle.getFileHandle(name, {create: true}); - const contents = new TextEncoder().encode(value); - - if (file.createWritable as unknown) { - const stream = await file.createWritable({keepExistingData: false}); - await stream.write(contents); - await stream.close(); - return true; - } else { - //Curse you webkit! - return await this.setStringWorker(file, contents.buffer as ArrayBuffer); - } - } - async getDir(name: string) { - return new Directory(await this.handle.getDirectoryHandle(name, {create: true})); - } -} - -export {Directory}; const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || @@ -743,18 +662,28 @@ export type safeImg = HTMLImageElement & { setSrcs: (nsrc: string, nstaticsrc: string | void) => void; isAnimated: () => Promise; }; +function getSetting(type: "gif" | "icon" | "emoji" | "sticker") { + const prefs = getPreferences(); + switch (type) { + case "gif": + return prefs.animateGifs; + case "icon": + return prefs.animateIcons; + case "emoji": + return prefs.animateEmoji; + case "sticker": + return prefs.animateSticker; + default: + return "hover"; + } +} export function createImg( src: string | undefined, staticsrc: string | void, elm: HTMLElement | void, - type: "gif" | "icon" = "gif", + type: "gif" | "icon" | "emoji" | "sticker" = "gif", ): safeImg { - const prefs = getPreferences(); - const aniOpt = - (type === "gif" ? prefs.animateGifs : prefs.animateIcons) || - ("hover" as "hover") || - "always" || - "never"; + const aniOpt = getSetting(type); const img = document.createElement("img"); img.loading = "lazy"; img.decoding = "async"; diff --git a/translations/en.json b/translations/en.json index 567f277..9532dc1 100644 --- a/translations/en.json +++ b/translations/en.json @@ -39,6 +39,7 @@ "DMs": { "add": "Add someone to this DM", "close": "Close DM", + "copyURL":"Copy DM URL", "copyId": "Copy DM id", "markRead": "Mark as read", "name": "Direct Messages" @@ -75,6 +76,8 @@ "animations":"Animations", "playGif": "Play GIFs:", "playIcon": "Play animated icons:", + "playEmoji":"Play animated emoji:", + "playSticker":"Play animated stickers:", "roleColors": "Disable role colors:", "gradientColors":"Disable gradient coloring:", "decorations":"Enable avatar decorations:" @@ -116,6 +119,9 @@ }, "bio": "Bio:", "blankMessage": "Blank message", + "sentAttachment": "Sent an attachment", + "sentPoll": "Sent a poll", + "sentSticker": "Sent a sticker", "blog": { "blog": "Blog", "blogUpdates": "Get blog updates when you open the app:", @@ -816,6 +822,7 @@ "userInfo": "User info", "notes":"User notes" }, + "messageNotLoad":"Messages failed to load", "profileColor": "Profile color", "pronouns": "Pronouns:", "readableName": "English", diff --git a/translations/fr.json b/translations/fr.json index 740b33c..93a27a7 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -16,6 +16,7 @@ "Majda GHARRABOU", "McDutchie", "Momo50WM", + "NClara", "Od1n", "Oh64", "Pyschobbens", @@ -147,20 +148,21 @@ "cancel": "Annuler", "channel": { "SlowmodeCool": "Temporisation du mode lent : $1", + "copyURL": "Copier l'URL du canal", "TimeOutCool": "Exclu jusqu'à : $1", "allowIcons": "Autoriser les icônes de chaîne personnalisées", "announcement": "Annonces", - "copyId": "Copier l'identifiant de la chaîne", + "copyId": "Copier l'identifiant du canal", "copyIdCat": "Copier l'identifiant de la catégorie", "createCatagory": "Créer une catégorie", - "createChannel": "Créer un chaîne", - "creating": "Création du Chaîne", + "createChannel": "Créer un canal", + "creating": "Création du canal", "delete": "Supprimer la chaîne", "deleteCat": "Supprimer la catégorie", "icon": "Icône :", "makeInvite": "Créer une invitation", "markRead": "Marquer comme lu", - "mute": "Rendre la chaîne muette", + "mute": "Rendre le canal muet", "name": "Chaîne", "name:": "Nom de la Chaîne :", "nsfw:": "Avertissement contenu mature :", @@ -174,7 +176,7 @@ "text": "Texte", "timedOutUntil": "Exclu jusqu'à : $1", "topic:": "Sujet :", - "typebox": "Envoyer un message dans $1", + "typebox": "Message dans # $1", "unmute": "Rétablir les notifications de la chaîne", "voice": "Voix", "deleteThread": "Supprimer le fil", @@ -763,7 +765,8 @@ "REQUEST_TO_SPEAK": "Permet aux membres du rôle de demander à parler dans le canal de scène.", "USE_EMBEDDED_ACTIVITIES": "Permet aux membres du rôle d'utiliser les activités intégrées.", "USE_APPLICATION_COMMANDS": "Permet aux membres du rôle d'utiliser les commandes d'application.", - "USE_EXTERNAL_APPS": "Permet aux applications des membres du rôle d'envoyer des réponses publiques. (Si désactivé, les membres du rôle seront toujours autorisés à utiliser leurs applications mais les réponses seront visible que d'eux-mêmes. Cela s'applique uniquement aux applications qui ne sont pas également installées dans la guilde)." + "USE_EXTERNAL_APPS": "Permet aux applications des membres du rôle d'envoyer des réponses publiques. (Si désactivé, les membres du rôle seront toujours autorisés à utiliser leurs applications mais les réponses seront visible que d'eux-mêmes. Cela s'applique uniquement aux applications qui ne sont pas également installées dans la guilde).", + "SET_VOICE_CHANNEL_STATUS": "Définir le statut du canal vocal" }, "readableNames": { "ADD_REACTIONS": "Ajouter des réactions", @@ -816,7 +819,8 @@ "VIEW_AUDIT_LOG": "Afficher le journal d'audit", "VIEW_CHANNEL": "Voir les canaux", "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Consulter les analyses de monétisation des créateurs", - "VIEW_GUILD_INSIGHTS": "Voir les informations sur la guilde" + "VIEW_GUILD_INSIGHTS": "Voir les informations sur la guilde", + "SET_VOICE_CHANNEL_STATUS": "Définir le statut du canal vocal" } }, "pinMessage": "Épingler le message", @@ -824,12 +828,13 @@ "bio": "À propos de moi :", "joined": "Compte créé : $1", "joinedMember": "A rejoint $1 : $2", - "mut": "Guildes en commun", + "mut": "Guildes mutuelles ($1)", "mutFriends": "Amis en commun", "permInfo": "Permissions", "userInfo": "Informations sur l’utilisateur", "notes": "Notes d'utilisateur" }, + "messageNotLoad": "Échec du chargement des messages", "profileColor": "Couleur du profil", "pronouns": "Pronoms :", "readableName": "Français", diff --git a/translations/hu.json b/translations/hu.json new file mode 100644 index 0000000..738ad31 --- /dev/null +++ b/translations/hu.json @@ -0,0 +1,408 @@ +{ + "@metadata": { + "authors": [ + "Dj", + "Koko1998" + ] + }, + "2faCode": "Kétfaktoros hitelesítési kód:", + "404": { + "404": "Hiba: 404 – Az oldal nem található", + "app": "Az alkalmazáshoz", + "home": "Kezdőlap", + "login": "Bejelentkezés", + "reset": "Jelszó-visszaállítási oldal", + "title": "Úgy tűnik, elvesztél", + "whereever": "Bárhol is legyen ez" + }, + "filesize": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB" + }, + "onboarding": { + "addChannel": "Csatorna hozzáadása", + "channel": "Csatorna:", + "desc": "Leírás:" + }, + "DMs": { + "markRead": "Megjelölés olvasottként", + "name": "Közvetlen üzenetek" + }, + "ok": "Rendben", + "dismiss": "Elvetés", + "accessibility": { + "gifSettings": { + "always": "Mindig", + "never": "Soha" + }, + "name": "Akadálymentesség", + "visuals": "Vizuális elemek", + "animations": "Animációk", + "playGif": "GIF-ek lejátszása:", + "playIcon": "Animált ikonok lejátszása:" + }, + "badge": { + "premium": "Prémium", + "verified_developer": "Ellenőrzött fejlesztő" + }, + "blog": { + "blog": "Blog", + "gotoPost": "Ugrás a bejegyzéshez" + }, + "cancel": "Mégse", + "channel": { + "announcement": "Bejelentések", + "createCatagory": "Kategória létrehozása", + "createChannel": "Csatorna létrehozása", + "creating": "Csatorna létrehozása", + "delete": "Csatorna törlése", + "deleteCat": "Kategória törlése", + "icon": "Ikon:", + "makeInvite": "Meghívó küldése", + "markRead": "Megjelölés olvasottként", + "mute": "Csatorna némítása", + "name": "Csatorna", + "name:": "Csatorna neve:", + "permissions": "Engedélyek", + "selectCatName": "Kategória neve", + "selectName": "Csatorna neve", + "selectType": "Csatornatípus kiválasztása", + "settings": "Beállítások", + "slowmode": "Lassú üzemmód:", + "text": "Szöveg", + "topic:": "Téma:", + "forum": "Fórum" + }, + "forum": { + "next": "Következő", + "back": "Vissza", + "newPost": "Új bejegyzés létrehozása", + "post": "Bejegyzés", + "sortOptions": { + "sortby": { + "title": "Rendezés", + "recent": "Nemrég aktív", + "posted": "Közzététel dátuma" + }, + "sortOrder": { + "title": "Rendezési sorrend" + } + } + }, + "copyRegLink": "Hivatkozás másolása", + "createAccount": "Fiók létrehozása", + "delete": "Törlés", + "deleteConfirm": "Biztosan törölni szeretnéd ezt?", + "devSettings": { + "name": "Fejlesztői beállítások" + }, + "deviceManage": { + "city": "Város: $1", + "continent": "Kontinens: $1", + "country": "Ország: $1", + "ip": "Utolsó ismert IP-cím: $1", + "logout": "Kijelentkezés", + "manageDev": "Eszköz kezelése", + "region": "Régió: $1" + }, + "dms": "Közvetlen üzenetek", + "edit": "Szerkesztés", + "emoji": { + "confirmDel": "Biztos, hogy törölni akarod ezt az emojit?", + "image:": "Kép:", + "name:": "Név:", + "title": "Emojik", + "upload": "Emojik feltöltése" + }, + "emojiSelect": "Emoji:", + "favoriteGifs": "Kedvenc GIF-ek", + "folder": { + "color": "Mappa színe:", + "create": "Új mappa létrehozása", + "edit": "Mappa szerkesztése", + "name": "Mappa neve:" + }, + "form": { + "captcha": "Várjunk csak, ember vagy?" + }, + "friends": { + "addfriend": "Barát hozzáadása", + "addfriendpromt": "Barátok hozzáadása felhasználónév alapján:", + "requestsent": "Kérés elküldve!", + "all": "Mind", + "all:": "Minden barát:", + "blocked": "Blokkolva", + "blockedusers": "Blokkolt felhasználók", + "bu": "Blokkolt felhasználók", + "friendlist": "Barátlista", + "friends": "Barátok", + "notfound": "A felhasználó nem található", + "online": "Elérhető", + "online:": "Elérhető barátok:", + "pending": "Függőben", + "pending:": "Függőben lévő barátkérelmek:", + "removeFriend": "Barát eltávolítása" + }, + "group": { + "edit": "Csoportos csevegés szerkesztése", + "select": "Barátok kijelölése" + }, + "guild": { + "INVITES_DISABLED": "Csak meghívottak számára", + "adminMenu": { + "ownName": "Tulajdonos", + "permission": "Engedélyek:" + }, + "banReason": "Kitiltás oka: $1", + "bannedBy": "Kitiltotta:", + "bans": "Kitiltások", + "idSel": "Azonosító:", + "community": "Közösség", + "createNewTemplate": "Sablon létrehozása", + "description:": "Leírás:", + "icon:": "Ikon:", + "invites": "Meghívók", + "loadingDiscovery": "Betöltés…", + "makeInvite": "Meghívó küldése", + "markRead": "Megjelölés olvasottként", + "name:": "Név:", + "nameNoMatch": "A nevek nem egyeznek", + "noDelete": "Mindegy", + "noLeave": "Mindegy", + "none": "Egyik sem", + "notifications": "Értesítések", + "overview": "Áttekintés", + "region:": "Régió:", + "roles": "Szerepkörök", + "settings": "Beállítások", + "tempCreatedBy": "A sablont készítette:", + "templateDesc": "A sablon leírása:", + "templateName": "A sablon neve:", + "templateURL": "Sablon linkje: $1", + "templates": "Sablonok", + "topic:": "Téma:", + "viewTemplate": "Sablon megtekintése", + "yesDelete": "Igen, biztos vagyok benne", + "yesLeave": "Igen, biztos vagyok benne" + }, + "donate": { + "title": "Adományozási lehetőségek", + "donate": "Adományozás" + }, + "htmlPages": { + "createAccount": "Fiók létrehozása", + "dobField": "Születési dátum", + "emailField": "E-mail:", + "instanceField": "Példa:", + "loaddesc": "Ez nem fog sokáig tartani", + "loginButton": "Bejelentkezés", + "noAccount": "Nincs még fiókod?", + "pw2Field": "Írd be a jelszót újra:", + "pwField": "Jelszó:", + "switchaccounts": "Fiókváltás", + "trans": "Fordítás", + "userField": "Felhasználónév:" + }, + "instanceStats": { + "members": "Tagok: $1", + "messages": "Üzenetek: $1", + "users": "Regisztrált felhasználók: $1" + }, + "invite": { + "channel:": "Csatorna:", + "createInvite": "Meghívó létrehozása", + "createdAt": "Létrehozva ekkor $1", + "expireAfter": "Lejár ezután:", + "expires": "Lejár ekkor: $1", + "never": "Soha" + }, + "inviteOptions": { + "12h": "12 óra", + "1d": "1 nap", + "1h": "1 óra", + "30d": "30 nap", + "30m": "30 perc", + "6h": "6 óra", + "7d": "7 nap", + "never": "Soha", + "noLimit": "Nincs korlát", + "title": "Hívj meg embereket" + }, + "localuser": { + "2faCode:": "Kód:", + "2fa": "Kétfaktoros hitelesítés beállításai", + "2faDisable": "A kétfaktoros hitelesítés letiltása", + "2faEnable": "A kétfaktoros hitelesítés engedélyezése", + "CheckUpdate": "Frissítések ellenőrzése", + "PasswordsNoMatch": "A jelszavak nem egyeznek", + "accountSettings": "Fiók beállításai", + "badCode": "Érvénytelen kód", + "badPassword": "Érvénytelen jelszó", + "changeEmail": "E-mail cím módosítása", + "changePassword": "Jelszó módosítása", + "changeUsername": "Felhasználónév módosítása", + "clearCache": "Gyorsítótár törlése", + "newEmail:": "Új e-mail", + "newPassword:": "Új jelszó:", + "newUsername": "Új felhasználónév:", + "noUpdates": "Nem található frissítés", + "notisound": "Értesítési hang:", + "oldPassword:": "Régi jelszó:", + "password:": "Jelszó", + "setUp2fa": "A kétfaktoros hitelesítés beállítása", + "settings": "Beállítások", + "status": "Állapot", + "team:": "Csapat:", + "theme:": "Téma", + "themesAndSounds": "Témák és hangok", + "updateSettings": "Beállítások frissítése", + "updatesYay": "Frissítések találhatók!", + "userSettings": "A nyilvános profilod", + "general": "Általános" + }, + "login": { + "enterPAgain": "Írd be a jelszót újra:", + "login": "Bejelentkezés", + "newPassword": "Új jelszó:", + "recover": "Elfelejtett jelszó?", + "recovery": "Elfelejtett jelszó" + }, + "logout": { + "error": { + "cancel": "Mégse", + "cont": "Folytatás mindenképpen" + }, + "logout": "Kijelentkezés" + }, + "manageInstance": { + "format": "Formátum:", + "length": "Hossz:" + }, + "media": { + "artist": "Művész: $1", + "composer": "Zeneszerző: $1", + "download": "Média letöltése", + "length": "Hossz: $1 perc és $2 másodperc", + "loading": "Betöltés", + "moreInfo": "További információ", + "notFound": "Média nem található" + }, + "member": { + "nick:": "Becenév:", + "reason:": "Indoklás:" + }, + "message": { + "delete": "Üzenet törlése", + "report": "Üzenet jelentése", + "deleted": "Törölt üzenet", + "edit": "Üzenet szerkesztése", + "edited": "(szerkesztve)", + "fullMessage": "Teljes üzenet:", + "reactionAdd": "Reakció hozzáadása", + "reactionsTitle": "Reakciók" + }, + "report": { + "back": "Vissza", + "next": "Következő", + "cancel": "Mégse" + }, + "nevermind": "Mindegy", + "notiVolume": "Értesítés hangereje:", + "pinMessage": "Üzenet rögzítése", + "profile": { + "bio": "Rólam:", + "mutFriends": "Közös barátok", + "permInfo": "Engedélyek", + "userInfo": "Felhasználói információk", + "notes": "Felhasználói megjegyzések" + }, + "profileColor": "Profil színe", + "recentEmoji": "Legutóbbi emojik", + "register": { + "DOBError": "Születési dátum: $1", + "agreeTOS": "Elfogadom a [Szolgáltatási feltételeket]($1):", + "emailError": "E-mail: $1", + "passwordError:": "Jelszó: $1", + "usernameError": "Felhasználónév: $1", + "instURL": "URL:" + }, + "remove": "Eltávolítás", + "role": { + "displaySettings": "Megjelenítési beállítások", + "name": "Szerepkör neve:", + "perms": "Engedélyek", + "remove": "Szerepkör eltávolítása", + "roleEmoji": "Szerepkör emoji:", + "roleFileIcon": "Szerepkör ikon:", + "roles": "Szerepkörök" + }, + "upload": "Fájlok feltöltése", + "makePoll": "Szavazás létrehozása", + "poll": { + "question": "Kérdés:", + "answers": "Válaszok:", + "newAnswer": "Új válasz", + "duration": "Időtartam:", + "durCount": { + "1": "1 óra", + "4": "4 óra", + "8": "8 óra", + "24": "24 óra", + "72": "3 nap", + "168": "7 nap", + "336": "14 nap" + }, + "expires": "Lejár ekkor: $1" + }, + "search": { + "back": "Vissza", + "new": "Új", + "next": "Következő", + "old": "Régi", + "search": "Keresés", + "settings": "Keresési beállítások", + "authors": "Szerzők:", + "mentions": "Említések:", + "channels": "Csatornák:" + }, + "settings": { + "img": "Kép feltöltése", + "save": "Módosítások mentése", + "unsaved": "Vigyázz, vannak nem mentett módosításaid" + }, + "sticker": { + "desc": "Leírás", + "image": "Kép:", + "name": "Név:" + }, + "switchAccounts": "Fiókváltás ⇌", + "connections": { + "since": "Tagság kezdete: $1", + "delete": "Kapcsolat törlése", + "sure": "Biztos vagy benne?" + }, + "unpinMessage": "Üzenet rögzítésének megszüntetése", + "updateAv": "Frissítések elérhetők", + "useTemplateButton": "Sablon használata", + "user": { + "dnd": "Ne zavarj", + "editNick": "Becenév szerkesztése", + "friendReq": "Barátkérelem", + "idle": "Tétlen", + "invisible": "Láthatatlan", + "offline": "Offline", + "online": "Online", + "remove": "Felhasználó eltávolítása", + "unblock": "Felhasználó blokkolásának feloldása", + "viewProfile": "Profil megtekintése" + }, + "keyboard": { + "empty": "", + "descs": { + "gifSearch": "GIF-ek keresése:" + } + }, + "yes": "Igen" +} diff --git a/translations/it.json b/translations/it.json index 2cc88c3..4e75b20 100644 --- a/translations/it.json +++ b/translations/it.json @@ -25,6 +25,12 @@ "whatelse": "Cos'altro pensi che dovrebbe succedere?", "whereever": "Ovunque questo sia" }, + "filesize": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB" + }, "onboarding": { "name": "Inserimento", "addChannel": "Aggiungi canale", @@ -109,6 +115,7 @@ }, "bio": "Biografia:", "blankMessage": "Messaggio vuoto", + "sentPoll": "Inviato un sondaggio", "blog": { "blog": "Blog", "blogUpdates": "Ricevi aggiornamenti dal blog quando apri l'app:", @@ -123,6 +130,7 @@ "cancel": "Annulla", "channel": { "SlowmodeCool": "Tempo di attesa slowmode: $1", + "copyURL": "Copia l'URL del canale", "TimeOutCool": "In time-out fino: $1", "allowIcons": "Consenti di mettere icone del canale personalizzati", "announcement": "Annunci", diff --git a/translations/ko.json b/translations/ko.json index e2ad31f..b13ed59 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -24,6 +24,12 @@ "whatelse": "어떻게 되어야 한다고 생각시나요?", "whereever": "이곳이 무엇이든" }, + "filesize": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB" + }, "onboarding": { "name": "온보딩", "disable": "온보딩 비활성화", @@ -38,6 +44,7 @@ "DMs": { "add": "이 메시지에 누군가를 추가", "close": "DM 닫기", + "copyURL": "DM URL 복사", "copyId": "DM ID 복사", "markRead": "읽은 것으로 표시", "name": "직신" @@ -115,6 +122,9 @@ }, "bio": "자기 소개:", "blankMessage": "빈 메시지", + "sentAttachment": "첨부 파일을 보냈습니다", + "sentPoll": "설문 조사를 보냈습니다", + "sentSticker": "스티커를 보냈습니다", "blog": { "blog": "블로그", "blogUpdates": "앱을 열었을 때 블로그 업데이트 받기:", @@ -129,6 +139,7 @@ "cancel": "취소", "channel": { "SlowmodeCool": "저속 모드 쿨다운: $1", + "copyURL": "채널 URL 복사", "TimeOutCool": "다음 시간까지 타임아웃: $1", "allowIcons": "사용자 지정 채널 아이콘 허용", "announcement": "알림", @@ -156,7 +167,7 @@ "text": "텍스트", "timedOutUntil": "다음 시간까지 타임아웃: $1", "topic:": "주제:", - "typebox": "$1에서의 메시지", + "typebox": "# $1에서의 메시지", "unmute": "채널 알림 표시", "voice": "음성", "deleteThread": "스레드 삭제", @@ -745,7 +756,8 @@ "REQUEST_TO_SPEAK": "역할 멤버가 스테이지 채널에서 말할 수 있도록 허용합니다.", "USE_EMBEDDED_ACTIVITIES": "역할 멤버가 임베디드 활동을 사용할 수 있도록 허용합니다.", "USE_APPLICATION_COMMANDS": "역할 멤버가 애플리케이션 명령을 사용할 수 있도록 허용합니다.", - "USE_EXTERNAL_APPS": "역할 멤버가 애플리케이션 응답을 채널에 공개적으로 표시할 수 있도록 허용합니다. (비활성화하더라도 사용자는 앱을 계속 사용할 수 있지만 응답은 본인에게만 표시됩니다. 이 기능은 길드에 설치되지 않은 앱에만 적용됩니다.)" + "USE_EXTERNAL_APPS": "역할 멤버가 애플리케이션 응답을 채널에 공개적으로 표시할 수 있도록 허용합니다. (비활성화하더라도 사용자는 앱을 계속 사용할 수 있지만 응답은 본인에게만 표시됩니다. 이 기능은 길드에 설치되지 않은 앱에만 적용됩니다.)", + "SET_VOICE_CHANNEL_STATUS": "음성 채널 상태 설정" }, "readableNames": { "ADD_REACTIONS": "반응 추가", @@ -798,7 +810,8 @@ "VIEW_AUDIT_LOG": "감사 로그 보기", "VIEW_CHANNEL": "채널 보기", "VIEW_CREATOR_MONETIZATION_ANALYTICS": "크리에이터 수익 창출 분석 보기", - "VIEW_GUILD_INSIGHTS": "길드 분석 정보 보기" + "VIEW_GUILD_INSIGHTS": "길드 분석 정보 보기", + "SET_VOICE_CHANNEL_STATUS": "음성 채널 상태 설정" } }, "pinMessage": "메시지 고정", @@ -806,12 +819,13 @@ "bio": "내 정보:", "joined": "계정 생성일: $1", "joinedMember": "가입일 $1: $2", - "mut": "상호 길드", + "mut": "상호 길드 ($1)", "mutFriends": "상호 친구", "permInfo": "권한", "userInfo": "사용자 정보", "notes": "사용자 메모" }, + "messageNotLoad": "메시지를 불러오는 데 실패했습니다", "profileColor": "프로필 색상", "pronouns": "대명사:", "readableName": "한국어", @@ -996,7 +1010,14 @@ "unknown": "@unknown-user" }, "keyboard": { - "shortcuts": "단축키" + "shortcuts": "단축키", + "descs": { + "gifSearch": "GIF 검색:" + } + }, + "domain": { + "title": "도메인 검증", + "domain": "도메인:" }, "vc": { "joinForStream": "음성 채널에 참여해 시청", diff --git a/translations/lb.json b/translations/lb.json index 2ead5b2..983f0e2 100644 --- a/translations/lb.json +++ b/translations/lb.json @@ -11,6 +11,12 @@ "home": "Haaptsäit", "login": "Aloggen" }, + "filesize": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB" + }, "onboarding": { "addChannel": "Kanal derbäisetzen", "channel": "Kanal:", @@ -19,6 +25,7 @@ }, "copyMedia": "Medien-URL kopéieren", "DMs": { + "copyURL": "DM-URL kopéieren", "markRead": "Als gelies markéieren" }, "ok": "OK", @@ -54,6 +61,7 @@ "botSettings": "Bot-Astellungen", "cancel": "Ofbriechen", "channel": { + "copyURL": "Kanal-URL kopéieren", "announcement": "Ukënnegungen", "copyId": "Kanal-ID kopéieren", "createCatagory": "Kategorie uleeën", @@ -316,6 +324,7 @@ "bio": "Iwwer mech:", "userInfo": "Benotzerinformatiounen" }, + "messageNotLoad": "Messagë konnten net geluede ginn", "profileColor": "Profilfaarf", "pronouns": "Pronomen:", "readableName": "Lëtzebuergesch", @@ -404,7 +413,7 @@ "since": "Member zanter: $1", "sure": "Sidd Dir sécher?" }, - "uploadFilesText": "Lued Är Fichieren hei erop!", + "uploadFilesText": "Luet Är Fichieren hei erop!", "useTemplate": "$1 als Schabloun benotzen", "useTemplateButton": "Schabloun benotzen", "user": { diff --git a/translations/nl.json b/translations/nl.json index c3e5777..0d8cea5 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -21,6 +21,12 @@ "whatelse": "Wat denkt u dat er nog meer moet gebeuren?", "whereever": "Waar dit ook is" }, + "filesize": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB" + }, "onboarding": { "name": "Inwerken", "disable": "Inwerken uitschakelen", @@ -35,6 +41,7 @@ "DMs": { "add": "Iemand aan dit directe bericht toevoegen", "close": "Directe bericht sluiten", + "copyURL": "DM-url kopiëren", "copyId": "Directe bericht-ID kopiëren", "markRead": "Markeren als gelezen", "name": "Directe berichten" @@ -126,6 +133,7 @@ "cancel": "Annuleren", "channel": { "SlowmodeCool": "Afkoeltijd voor langzame modus: $1", + "copyURL": "Kanaal-url kopiëren", "TimeOutCool": "Time-out bereikt na: $1", "allowIcons": "Aangepaste kanaalpictogrammen toestaan", "announcement": "Aankondigingen", @@ -153,7 +161,7 @@ "text": "Tekst", "timedOutUntil": "Time-out bereikt na: $1", "topic:": "Onderwerp:", - "typebox": "Bericht in $1", + "typebox": "Bericht in # $1", "unmute": "Dempen kanaal opheffen", "voice": "Spraak", "deleteThread": "Onderwerp verwijderen", @@ -742,7 +750,8 @@ "REQUEST_TO_SPEAK": "Laat rolleden een spreekverzoek indienen in de podiumkanalen.", "USE_EMBEDDED_ACTIVITIES": "Hiermee kunnen rolleden ingebedde activiteiten gebruiken.", "USE_APPLICATION_COMMANDS": "Hiermee kunnen rolleden toepassingsopdrachten gebruiken.", - "USE_EXTERNAL_APPS": "Hiermee kunnen rolleden hun toepassingenreacties openbaar in het kanaal weergeven. Indien uitgeschakeld, kunnen gebruikers hun apps nog steeds gebruiken, maar zijn de reacties alleen voor henzelf zichtbaar. Dit geldt alleen voor apps die niet ook in het gilde zijn geïnstalleerd." + "USE_EXTERNAL_APPS": "Hiermee kunnen rolleden hun toepassingenreacties openbaar in het kanaal weergeven. Indien uitgeschakeld, kunnen gebruikers hun apps nog steeds gebruiken, maar zijn de reacties alleen voor henzelf zichtbaar. Dit geldt alleen voor apps die niet ook in het gilde zijn geïnstalleerd.", + "SET_VOICE_CHANNEL_STATUS": "Spraakkanaalstatus instellen" }, "readableNames": { "ADD_REACTIONS": "Reacties toevoegen", @@ -795,7 +804,8 @@ "VIEW_AUDIT_LOG": "Inspectielogboek bekijken", "VIEW_CHANNEL": "Kanalen bekijken", "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Monetisatie-analyses voor makers bekijken", - "VIEW_GUILD_INSIGHTS": "Gilde-inzichten bekijken" + "VIEW_GUILD_INSIGHTS": "Gilde-inzichten bekijken", + "SET_VOICE_CHANNEL_STATUS": "Spraakkanaalstatus instellen" } }, "pinMessage": "Bericht vastzetten", @@ -803,12 +813,13 @@ "bio": "Over mij:", "joined": "Account aangemaakt: $1", "joinedMember": "Toegetreden tot $1: $2", - "mut": "Gedeelde gilden", + "mut": "Gedeelde gilden ($1)", "mutFriends": "Wederzijdse vrienden", "permInfo": "Rechten", "userInfo": "Gebruikersinfo", "notes": "Gebruikersopmerkingen" }, + "messageNotLoad": "Berichten konden niet worden geladen", "profileColor": "Profielkleur", "pronouns": "Voornaamwoorden:", "readableName": "Nederlands", diff --git a/translations/qqq.json b/translations/qqq.json index 9eb1ea0..5c7334b 100644 --- a/translations/qqq.json +++ b/translations/qqq.json @@ -119,6 +119,9 @@ }, "bio": "Label for the bio/about-me text field in user settings.", "blankMessage": "Placeholder text shown when a message has no content.", + "sentAttachment": "Notification message when someone sends an attachment", + "sentPoll": "Notification message when someone sends a poll", + "sentSticker": "Notification message when someone sends a sticker", "blog": { "blog": "Settings section title for the blog feature.", "blogUpdates": "Label for the toggle to enable/disable blog update notifications.", diff --git a/translations/zh-hans.json b/translations/zh-hans.json index 785f913..4aae249 100644 --- a/translations/zh-hans.json +++ b/translations/zh-hans.json @@ -29,6 +29,12 @@ "whatelse": "您认为应该还会发生什么事?", "whereever": "无论这在哪里" }, + "filesize": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB" + }, "onboarding": { "name": "入门", "disable": "禁用入门", @@ -43,6 +49,7 @@ "DMs": { "add": "将某人添加到此私信", "close": "关闭私信", + "copyURL": "复制私信 URL", "copyId": "复制私信 ID", "markRead": "标记为已读", "name": "私信" @@ -120,6 +127,9 @@ }, "bio": "个人简介:", "blankMessage": "空白消息", + "sentAttachment": "发送了一个附件", + "sentPoll": "发送了一个投票", + "sentSticker": "发送了一张贴纸", "blog": { "blog": "博客", "blogUpdates": "打开应用时获取博客更新:", @@ -134,6 +144,7 @@ "cancel": "取消", "channel": { "SlowmodeCool": "慢速模式冷却时间:$1", + "copyURL": "复制频道 URL", "TimeOutCool": "超时至:$1", "allowIcons": "允许自定义频道图标", "announcement": "公告", @@ -813,12 +824,13 @@ "bio": "关于我:", "joined": "账户已创建: $1", "joinedMember": "已加入$1:$2", - "mut": "互助公会", + "mut": "互助公会($1)", "mutFriends": "共同好友", "permInfo": "权限", "userInfo": "用户信息", "notes": "用户笔记" }, + "messageNotLoad": "消息加载失败", "profileColor": "个人档案色", "pronouns": "称谓:", "readableName": "简体中文",