better search

This commit is contained in:
MathMan05 2025-04-10 12:10:34 -05:00
parent 01e267846e
commit 83dc0ebb71
11 changed files with 321 additions and 70 deletions

View file

@ -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<Member, number> = 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");

View file

@ -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");

View file

@ -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();

View file

@ -0,0 +1 @@
<svg viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg"><path fill="#c9c9c9" stroke="red" stroke-linecap="round" stroke-width="32.2" d="M16 164 164 16M16 16l148 148"/></svg>

After

Width:  |  Height:  |  Size: 179 B

View file

@ -0,0 +1 @@
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg"><circle style="fill:none;stroke:#fe0000;stroke-width:80;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" cx="-178" cy="177.4" r="136" transform="scale(-1 1)"/><path style="fill:none;stroke:#fe0000;stroke-width:80;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m274 280 200 192"/></svg>

After

Width:  |  Height:  |  Size: 389 B

View file

@ -78,7 +78,10 @@
<div id="pinnedMDiv">
<span class="svgicon svg-pin" id="pinnedM"></span>
</div>
<div contenteditable="true" class="searchBox" id="searchBox"></div>
<div class="searchMeta">
<div contenteditable="true" class="searchBox" id="searchBox"></div>
<span id="searchX" class="svgicon svg-search" contenteditable="false"></span>
</div>
<label for="memberlisttoggle" id="memberlisttoggleicon">
<span class="svgicon svg-friends"></span>
</label>

View file

@ -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) => {

View file

@ -242,7 +242,18 @@ class InfiniteScroller {
}
}
async watchForChange(): Promise<boolean> {
async watchForChange(stop = false): Promise<boolean> {
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<void> {
async focus(id: string, flash = true, sec = false): Promise<void> {
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<boolean>((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");
}
}

View file

@ -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<void> {
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 = () => {};

View file

@ -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;

View file

@ -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?",