diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 6883d55..1ec188a 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -928,12 +928,35 @@ class Channel extends SnowFlake { } } async focus(id: string) { - console.time(); + const m = this.messages.get(id); + if (m && m.div) { + if (document.contains(m.div)) { + m.div.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + m.div.classList.remove("jumped"); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + m.div.classList.add("jumped"); + return; + } + } console.log(await this.getmessage(id)); - await this.getHTML(); - console.timeEnd(); + + if (this.localuser.channelfocus === this) { + this.localuser.channelfocus?.infinite.delete(); + this.localuser.channelfocus = undefined; + } + await this.getHTML(true, false); console.warn(id); - this.infinite.focus(id); + await this.buildmessages(id); + //debugger; + this.infinite.focus(id, true, true); } editLast() { let message: Message | undefined = this.lastmessage; @@ -987,7 +1010,7 @@ class Channel extends SnowFlake { if (!this.last_pin_timestamp && !this.lastpin) return false; return this.last_pin_timestamp !== this.lastpin; } - async getHTML(addstate = true) { + async getHTML(addstate = true, getMessages = true) { const pinnedM = document.getElementById("pinnedMDiv"); if (pinnedM) { if (this.unreadPins()) { @@ -1059,14 +1082,14 @@ class Channel extends SnowFlake { if (!mobile) { (document.getElementById("typebox") as HTMLDivElement).focus(); } - await this.putmessages(); + if (getMessages) await this.putmessages(); await prom; if (id !== Channel.genid) { return; } this.makereplybox(); - await this.buildmessages(); + if (getMessages) await this.buildmessages(); //loading.classList.remove("loading"); } typingmap: Map = new Map(); @@ -1354,12 +1377,12 @@ class Channel extends SnowFlake { } }); } - async buildmessages() { + async buildmessages(id: string | void) { this.infinitefocus = false; - await this.tryfocusinfinate(); + await this.tryfocusinfinate(id); } infinitefocus = false; - async tryfocusinfinate() { + async tryfocusinfinate(id: string | void) { if (this.infinitefocus) return; this.infinitefocus = true; const messages = document.getElementById("channelw") as HTMLDivElement; @@ -1370,12 +1393,13 @@ class Channel extends SnowFlake { const loading = document.getElementById("loadingdiv") as HTMLDivElement; const removetitle = document.getElementById("removetitle"); //messages.innerHTML=""; - let id: string | undefined; - if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { - id = this.lastreadmessageid; - } else if (this.lastreadmessageid && (id = this.findClosest(this.lastreadmessageid))) { - } else if (this.lastmessageid && this.messages.has(this.lastmessageid)) { - id = this.goBackIds(this.lastmessageid, 50); + if (!id) { + if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { + id = this.lastreadmessageid; + } else if (this.lastreadmessageid && (id = this.findClosest(this.lastreadmessageid))) { + } else if (this.lastmessageid && this.messages.has(this.lastmessageid)) { + id = this.goBackIds(this.lastmessageid, 50); + } } if (!id) { if (!removetitle) { @@ -1400,8 +1424,9 @@ class Channel extends SnowFlake { console.warn("rouge element detected and removed"); } messages.append(await this.infinite.getDiv(id)); + this.infinite.updatestuff(); - this.infinite.watchForChange().then(async (_) => { + await this.infinite.watchForChange().then(async (_) => { //await new Promise(resolve => setTimeout(resolve, 0)); this.infinite.focus(id, false); //if someone could figure out how to make this work correctly without this, that's be great :P loading.classList.remove("loading"); diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts index b7be973..9231c0d 100644 --- a/src/webpage/direct.ts +++ b/src/webpage/direct.ts @@ -54,6 +54,11 @@ class Direct extends Guild { } } getHTML() { + const sideContainDiv = document.getElementById("sideContainDiv"); + if (sideContainDiv) sideContainDiv.classList.remove("searchDiv"); + const searchBox = document.getElementById("searchBox"); + if (searchBox) searchBox.textContent = ""; + const ddiv = document.createElement("div"); const build = super.getHTML(); const freindDiv = document.createElement("div"); diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 8e48644..e9f5eea 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -855,6 +855,11 @@ class Guild extends SnowFlake { } } getHTML() { + const sideContainDiv = document.getElementById("sideContainDiv"); + if (sideContainDiv) sideContainDiv.classList.remove("searchDiv"); + const searchBox = document.getElementById("searchBox"); + if (searchBox) searchBox.textContent = ""; + //this.printServers(); this.sortchannels(); this.printServers(); diff --git a/src/webpage/icons/plainx.svg b/src/webpage/icons/plainx.svg new file mode 100644 index 0000000..e7406fd --- /dev/null +++ b/src/webpage/icons/plainx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/search.svg b/src/webpage/icons/search.svg new file mode 100644 index 0000000..42ccd12 --- /dev/null +++ b/src/webpage/icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/index.html b/src/webpage/index.html index d07f3f3..ac7e8cb 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -78,7 +78,10 @@
- +
+ + +
diff --git a/src/webpage/index.ts b/src/webpage/index.ts index f3a3c9d..b2184be 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -149,13 +149,32 @@ import {I18n} from "./i18n.js"; const searchBox = document.getElementById("searchBox") as CustomHTMLDivElement; const markdown = new MarkDown("", thisUser); searchBox.markdown = markdown; - + const searchX = document.getElementById("searchX") as HTMLElement; searchBox.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); thisUser.mSearch(markdown.rawString); } }); + searchBox.addEventListener("keyup", () => { + if (searchBox.textContent === "") { + setTimeout(() => (searchBox.innerHTML = ""), 0); + searchX.classList.add("svg-search"); + searchX.classList.remove("svg-plainx"); + } else { + searchX.classList.remove("svg-search"); + searchX.classList.add("svg-plainx"); + } + }); + searchX.onclick = () => { + if (searchX.classList.contains("svg-plainx")) { + markdown.txt = []; + searchBox.innerHTML = ""; + searchX.classList.add("svg-search"); + searchX.classList.remove("svg-plainx"); + thisUser.mSearch(""); + } + }; markdown.giveBox(searchBox); markdown.setCustomBox((e) => { diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index eb7e561..7e184a9 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -242,7 +242,18 @@ class InfiniteScroller { } } - async watchForChange(): Promise { + async watchForChange(stop = false): Promise { + if (stop == true) { + let prom = this.changePromise; + while (this.changePromise) { + prom = this.changePromise; + await this.changePromise; + if (prom === this.changePromise) { + this.changePromise = undefined; + break; + } + } + } if (this.changePromise) { this.watchtime = true; return await this.changePromise; @@ -268,6 +279,10 @@ class InfiniteScroller { console.error(e); res(false); } finally { + if (stop === true) { + this.changePromise = undefined; + return; + } setTimeout(() => { this.changePromise = undefined; if (this.watchtime) { @@ -279,17 +294,18 @@ class InfiniteScroller { return await this.changePromise; } - async focus(id: string, flash = true): Promise { + async focus(id: string, flash = true, sec = false): Promise { let element: HTMLElement | undefined; for (const thing of this.HTMLElements) { if (thing[1] === id) { element = thing[0]; } } - if (element) { + if (sec && element) { if (flash) { element.scrollIntoView({ behavior: "smooth", + inline: "center", block: "center", }); await new Promise((resolve) => { @@ -301,9 +317,11 @@ class InfiniteScroller { }); element.classList.add("jumped"); } else { - element.scrollIntoView(); + element.scrollIntoView({ + block: "center", + }); } - } else { + } else if (!sec) { this.resetVars(); //TODO may be a redundant loop, not 100% sure :P for (const thing of this.HTMLElements) { @@ -312,11 +330,16 @@ class InfiniteScroller { this.HTMLElements = []; await this.firstElement(id); this.updatestuff(); - await this.watchForChange(); - await new Promise((resolve) => { - setTimeout(resolve, 100); + await this.watchForChange(true); + this.changePromise = new Promise((resolve) => { + setTimeout(() => { + this.changePromise = undefined; + resolve(true); + }, 1000); }); - await this.focus(id, true); + await this.focus(id, !element, true); + } else { + console.warn("elm not exist"); } } diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 3c362d4..7128bb5 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -240,6 +240,7 @@ class Localuser { this.rights = new Rights(rights); if (this.perminfo.user.disableColors === undefined) this.perminfo.user.disableColors = true; + this.updateTranslations(); } async gottenReady(ready: readyjson): Promise { await I18n.done; @@ -1751,6 +1752,7 @@ class Localuser { I18n.getTranslation("localuser.language"), (e) => { I18n.setLanguage(I18n.options()[e]); + this.updateTranslations(); }, [...langmap.values()], { @@ -2671,51 +2673,140 @@ class Localuser { box.innerHTML = ""; } searching = false; + updateTranslations() { + const searchBox = document.getElementById("searchBox") as HTMLDivElement; + searchBox.style.setProperty("--hint-text", JSON.stringify(I18n.search.search())); + } + curSearch?: Symbol; mSearch(query: string) { + const searchy = Symbol("search"); + this.curSearch = searchy; const p = new URLSearchParams("?"); this.searching = true; p.set("content", query.trim()); - fetch(this.info.api + `/guilds/${this.lookingguild?.id}/messages/search/?` + p.toString(), { - headers: this.headers, - }) - .then((_) => _.json()) - .then((json: {messages: [messagejson][]; total_results: number}) => { - //FIXME total_results shall be ignored as it's known to be bad, spacebar bug. - const messages = json.messages - .map(([m]) => { - const c = this.channelids.get(m.channel_id); - if (!c) return; - if (c.messages.get(m.id)) { - return c.messages.get(m.id); - } - return new Message(m, c, true); - }) - .filter((_) => _ !== undefined); - const sideDiv = document.getElementById("sideDiv"); - const sideContainDiv = document.getElementById("sideContainDiv"); - if (!sideDiv || !sideContainDiv) return; - sideDiv.innerHTML = ""; - sideContainDiv.classList.add("searchDiv"); - let channel: Channel | undefined = undefined; - for (const message of messages) { - if (channel !== message.channel) { - channel = message.channel; - const h3 = document.createElement("h3"); - h3.textContent = channel.name; - h3.classList.add("channelSTitle"); - sideDiv.append(h3); + p.set("sort_by", "timestamp"); + p.set("sort_order", "desc"); + let maxpage: undefined | number = undefined; + const sideDiv = document.getElementById("sideDiv"); + const sideContainDiv = document.getElementById("sideContainDiv"); + if (!sideDiv || !sideContainDiv) return; + const genPage = (page: number) => { + p.set("offset", page * 50 + ""); + fetch(this.info.api + `/guilds/${this.lookingguild?.id}/messages/search/?` + p.toString(), { + headers: this.headers, + }) + .then((_) => _.json()) + .then((json: {messages: [messagejson][]; total_results: number}) => { + if (this.curSearch !== searchy) { + return; } - const html = message.buildhtml(undefined, true); - html.addEventListener("click", async () => { - try { - await message.channel.focus(message.id); - } catch (e) { - console.error(e); + //FIXME total_results shall be ignored as it's known to be bad, spacebar bug. + const messages = json.messages + .map(([m]) => { + const c = this.channelids.get(m.channel_id); + if (!c) return; + if (c.messages.get(m.id)) { + return c.messages.get(m.id); + } + return new Message(m, c, true); + }) + .filter((_) => _ !== undefined); + sideDiv.innerHTML = ""; + if (messages.length == 0 && page !== 0) { + maxpage = page - 1; + genPage(page - 1); + return; + } else if (messages.length !== 50) { + maxpage = page; + } + const sortBar = document.createElement("div"); + sortBar.classList.add("flexltr", "sortBar"); + + const newB = document.createElement("button"); + const old = document.createElement("button"); + [newB.textContent, old.textContent] = [I18n.search.new(), I18n.search.old()]; + old.onclick = () => { + p.set("sort_order", "asc"); + deleteMessages(); + genPage(0); + }; + newB.onclick = () => { + p.set("sort_order", "desc"); + deleteMessages(); + genPage(0); + }; + if (p.get("sort_order") === "asc") { + old.classList.add("selectedB"); + } else { + newB.classList.add("selectedB"); + } + + const spaceElm = document.createElement("div"); + spaceElm.classList.add("spaceElm"); + + sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, newB, old); + + sideDiv.append(sortBar); + + sideContainDiv.classList.add("searchDiv"); + let channel: Channel | undefined = undefined; + function deleteMessages() { + for (const elm of htmls) elm.remove(); + } + const htmls: HTMLElement[] = []; + for (const message of messages) { + if (channel !== message.channel) { + channel = message.channel; + const h3 = document.createElement("h3"); + h3.textContent = channel.name; + h3.classList.add("channelSTitle"); + sideDiv.append(h3); + htmls.push(h3); } - }); - sideDiv.append(html); - } - }); + const html = message.buildhtml(undefined, true); + html.addEventListener("click", async () => { + try { + await message.channel.focus(message.id); + } catch (e) { + console.error(e); + } + }); + sideDiv.append(html); + htmls.push(html); + } + if (messages.length === 0) { + const noMs = document.createElement("h3"); + noMs.textContent = I18n.search.nofind(); + sideDiv.append(noMs); + } + const bottombuttons = document.createElement("div"); + bottombuttons.classList.add("flexltr", "searchNavButtons"); + const next = document.createElement("button"); + if (page == maxpage) next.disabled = true; + next.onclick = () => { + deleteMessages(); + genPage(page + 1); + }; + const prev = document.createElement("button"); + prev.onclick = () => { + deleteMessages(); + genPage(page - 1); + }; + if (page == 0) prev.disabled = true; + [next.textContent, prev.textContent] = [I18n.search.next(), I18n.search.back()]; + bottombuttons.append(prev, next); + sideDiv.append(bottombuttons); + sideDiv.scrollTo({top: 0, behavior: "instant"}); + }); + }; + if (query === "") { + sideContainDiv.classList.remove("searchDiv"); + sideDiv.innerHTML = ""; + this.searching = false; + this.getSidePannel(); + return; + } + genPage(0); } keydown: (event: KeyboardEvent) => unknown = () => {}; diff --git a/src/webpage/style.css b/src/webpage/style.css index 9241e99..bad3bbe 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -57,16 +57,52 @@ body { display: flex; flex-direction: column; } +.searchNavButtons { + height: 0.3in; + flex-shrink: 0; + background: var(--secondary-bg); + top: 0px; + padding: 0.1in; + display: flex; + align-items: center; + margin-top: auto; + + button { + flex-shrink: 0; + margin-left: 0.1in; + } +} +.sortBar { + height: 0.3in; + flex-shrink: 0; + background: var(--secondary-bg); + position: sticky; + top: 0px; + padding: 0.1in; + display: flex; + align-items: center; + z-index: 1; + + button { + flex-shrink: 0; + margin-left: 0.1in; + } +} +.selectedB { + background: color-mix(in srgb, black, var(--button-bg) 60%); +} .pinnedMessages { position: absolute; background: var(--secondary-bg); width: 3.5in; padding: 8px; border-radius: 6px; - box-shadow: 1px 1px 10px black; + box-shadow: 1px 3px 10px var(--shadow); max-height: 60vh; overflow-y: auto; min-height: 1in; + z-index: 2; + b { width: 100%; height: 1in; @@ -129,6 +165,7 @@ body { .channelSTitle { margin-top: 0.2in; margin-bottom: 0; + margin-left: 10px; } p, h1, @@ -352,6 +389,14 @@ textarea { display: block; mask-size: cover !important; } +.svg-plainx { + mask: url(/icons/plainx.svg); + mask-size: contain !important; +} +.svg-search { + mask: url(/icons/search.svg); + mask-size: contain !important; +} .svg-spoiler { mask: url(/icons/spoiler.svg); } @@ -458,6 +503,16 @@ textarea { aspect-ratio: 1/1; flex-shrink: 0; } +#searchX { + width: 0.16in; + height: 0.16in; + position: absolute; + right: 13px; + top: 6px; +} +#searchX.svg-plainx{ + cursor:pointer; +} #pinnedM { width: 0.25in; height: 0.25in; @@ -891,7 +946,10 @@ span.instanceStatus { background: var(--channels-bg); user-select: none; } - +.searchMeta { + position: relative; + display: flex; +} .header { flex: none; height: 48px; @@ -1225,6 +1283,10 @@ span.instanceStatus { .searchBox:empty { width: 2in; } +.searchBox:empty::after { + content: var(--hint-text); + color: var(--primary-text-soft); +} .searchBox { white-space: nowrap; height: 0.075in; @@ -1739,6 +1801,7 @@ img.bigembedimg { /* Sidebar */ #sideContainDiv { + padding: 16px 8px; display: none; flex: none; width: 240px; @@ -1747,17 +1810,22 @@ img.bigembedimg { justify-content: space-between; } #sideDiv { - padding: 16px 8px; overflow-y: auto; box-sizing: border-box; + position: relative; + overflow: auto; + flex-grow: 1; } #page:has(#memberlisttoggle:checked) #sideContainDiv { display: flex; } #sideContainDiv.searchDiv { + padding: 0px; display: flex; width: 30vw; + .topMessage { + margin: 0px 10px; margin-top: 2px; margin-bottom: 10px; cursor: pointer; @@ -2033,6 +2101,7 @@ img.bigembedimg { height: 100%; width: 100%; background: color-mix(in srgb, var(--black) 75%, transparent); + z-index: 2; } .imgfit { max-height: 85svh; diff --git a/translations/en.json b/translations/en.json index 69fdec7..bfdc3cf 100644 --- a/translations/en.json +++ b/translations/en.json @@ -380,6 +380,15 @@ "mustTypePhrase": "To delete your account you must type the phrase", "manageInstance": "Manage Instance" }, + "search": { + "back": "Back", + "next": "Next", + "page": "Page $1", + "new": "New", + "old": "Old", + "search": "Search", + "nofind": "There seems to be no messages that match your search, maybe trying broadening your search to try and find what you want" + }, "manageInstance": { "stop": "Stop instance", "AreYouSureStop": "Are you sure you want to stop this instance?",