search filtering

This commit is contained in:
MathMan05
2026-05-20 15:45:46 -05:00
parent b2c75957e1
commit ea00aa6a83
5 changed files with 386 additions and 5 deletions
+145 -3
View File
@@ -47,6 +47,7 @@ import {getDeveloperSettings, setDeveloperSettings} from "./utils/storage/devSet
import {getLocalSettings, ServiceWorkerModeValues} from "./utils/storage/localSettings.js";
import {PromiseLock} from "./utils/promiseLock.js";
import {CDNParams} from "./utils/cdnParams.js";
import {SnowFlake} from "./snowflake.js";
type traceObj = {
micros: number;
calls?: (string | traceObj)[];
@@ -4359,7 +4360,7 @@ class Localuser {
MDFindChannel(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
const maybe: [number, Channel][] = [];
if (this.lookingguild && this.lookingguild.id !== "@me") {
for (const channel of this.lookingguild.channels) {
for (const channel of this.lookingguild.channels.filter((_) => _.visible)) {
const confidence = channel.similar(name);
if (confidence > 0) {
maybe.push([confidence, channel]);
@@ -4539,13 +4540,19 @@ class Localuser {
const sideDiv = document.getElementById("sideDiv");
const sideContainDiv = document.getElementById("sideContainDiv");
if (!sideDiv || !sideContainDiv) return;
let authorIds = [] as string[];
let mentionIds = [] as string[];
let channels = [] as Channel[];
const genPage = (page: number) => {
p.set("offset", page * 50 + "");
const guildSearch = this.lookingguild?.id !== "@me";
fetch(
this.info.api +
`${guildSearch ? `/guilds/${this.lookingguild?.id}` : `/channels/${this.channelfocus?.id}`}/messages/search/?` +
p.toString(),
p.toString() +
(authorIds.length ? authorIds.map((_) => `&author_id=${_}`).join("") : "") +
(mentionIds.length ? mentionIds.map((_) => `&mentions=${_}`).join("") : "") +
(channels.length ? channels.map((_) => `&channel_id=${_.id}`).join("") : ""),
{
headers: this.headers,
},
@@ -4577,6 +4584,141 @@ class Localuser {
const sortBar = document.createElement("div");
sortBar.classList.add("flexltr", "sortBar");
const settingsB = document.createElement("button");
settingsB.textContent = I18n.search.settings();
settingsB.onclick = () => {
const d = new Dialog(I18n.search.settings());
const opt = d.options;
const b = p.get("max_id");
const before = opt.addDateInput(I18n.search.before(), () => {}, {
initText: b ? new Date(SnowFlake.stringToUnixTime(b)) : undefined,
});
before.onchange = (_) => {
if (before.dateValue) p.set("max_id", SnowFlake.DateToID(before.dateValue));
else p.delete("max_id");
console.log([...p.entries()], before.dateValue);
};
const a = p.get("min_id");
const after = opt.addDateInput(I18n.search.after(), () => {}, {
initText: a ? new Date(SnowFlake.stringToUnixTime(a)) : undefined,
});
after.onchange = (_) => {
if (after.dateValue) p.set("min_id", SnowFlake.DateToID(after.dateValue));
else p.delete("min_id");
console.log([...p.entries()], after.dateValue);
};
opt.addCheckboxInput(I18n.search.includensfw(), () => {}, {
initState: p.get("include_nsfw") !== "false",
}).onchange = (s) => {
if (s) p.delete("include_nsfw");
else p.set("include_nsfw", "false");
};
const userSearch = async (name: string, ids: string[]) => {
const g = this.lookingguild;
if (!g) return [];
g.searchMembers(8, name);
const members = [] as [User | Member, number][];
if (g.id === "@me") {
const dirrect = this.channelfocus as Group;
for (const user of dirrect.users) {
const rank = user.compare(name);
if (rank > 0) {
members.push([user, rank]);
}
}
} else {
for (const member of g.members) {
const rank = member.compare(name);
if (rank > 0) {
members.push([member, rank]);
}
}
}
const idSet = new Set(ids);
members.sort((a, b) => b[1] - a[1]);
return members
.filter((_) => !idSet.has(_[0].id))
.slice(0, 5)
.map(([_]) => {
return {value: _.id, name: _.name};
});
};
opt.addAsyncMultiSelect(I18n.search.authors(), () => {}, userSearch, {
defaultValues: authorIds
.map((id) => {
const g = this.lookingguild;
if (!g || g.id === "@me") {
return this.userMap.get(id);
} else {
return [...g.members].find(({id: d}) => d === id);
}
})
.filter((_) => _ !== undefined)
.map((_) => ({value: _.id, name: _.name})),
}).onchange = (values: string[]) => {
authorIds = values;
};
opt.addAsyncMultiSelect(I18n.search.mentions(), () => {}, userSearch, {
defaultValues: mentionIds
.map((id) => {
const g = this.lookingguild;
if (!g || g.id === "@me") {
return this.userMap.get(id);
} else {
return [...g.members].find(({id: d}) => d === id);
}
})
.filter((_) => _ !== undefined)
.map((_) => ({value: _.id, name: _.name})),
}).onchange = (values: string[]) => {
mentionIds = values;
};
if (this.lookingguild?.id !== "@me")
opt.addAsyncMultiSelect(
I18n.search.channels(),
() => {},
async (name, ids) => {
const g = this.lookingguild;
if (!g) return [];
const c = g.channels.filter((_) => _.visible);
const maybe: [number, Channel][] = [];
for (const channel of c) {
const confidence = channel.similar(name);
if (confidence > 0) {
maybe.push([confidence, channel]);
}
}
maybe.sort((a, b) => b[0] - a[0]);
const idSet = new Set(ids);
return maybe
.filter((_) => !idSet.has(_[1].id))
.slice(0, 5)
.map(([_r, _]) => {
return {value: _.id, name: _.name};
});
},
{
defaultValues: channels.map((_) => ({name: _.name, value: _.id})),
},
).onchange = (values: string[]) => {
const g = this.lookingguild;
if (!g) return;
channels = values.map((id) => g.getChannel(id)).filter((_) => _ !== undefined);
};
d.onhide = () => {
genPage(0);
};
d.show();
};
const newB = document.createElement("button");
const old = document.createElement("button");
[newB.textContent, old.textContent] = [I18n.search.new(), I18n.search.old()];
@@ -4599,7 +4741,7 @@ class Localuser {
const spaceElm = document.createElement("div");
spaceElm.classList.add("spaceElm");
sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, newB, old);
sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, settingsB, newB, old);
sideDiv.append(sortBar);
+172 -1
View File
@@ -187,6 +187,21 @@ class TextInput implements OptionsElement<string> {
}
}
class DateInput extends TextInput {
dateValue: Date | null;
constructor(
label: string,
onSubmit: (str: string) => void,
owner: Options,
{initText = "" as string | Date} = {},
) {
let initDate: DateInput["dateValue"] = null;
if (initText instanceof Date) {
initDate = initText;
initText = "";
}
super(label, onSubmit, owner, {initText});
this.dateValue = initDate;
}
generateHTML(): HTMLDivElement {
const div = document.createElement("div");
const span = document.createElement("span");
@@ -195,11 +210,20 @@ class DateInput extends TextInput {
const input = document.createElement("input");
input.value = this.value;
input.type = "date";
if (this.dateValue) input.valueAsDate = this.dateValue;
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
return div;
}
onChange() {
const input = this.input.deref();
if (input) {
const value = input.valueAsDate;
this.dateValue = value;
}
super.onChange();
}
}
class SettingsMDText implements OptionsElement<void> {
readonly onSubmit!: (str: string) => void;
@@ -412,7 +436,134 @@ export class ColorInput implements OptionsElement<string> {
this.onSubmit(this.colorContent);
}
}
interface searchRes {
value: string;
name: string;
}
class AsyncMultiSelect implements OptionsElement<string[]> {
readonly label: string;
readonly owner: Options;
readonly onSubmit: (str: string[]) => void;
select!: WeakRef<HTMLSelectElement>;
searchFunc: (term: string, cur: string[]) => Promise<searchRes[]> | searchRes[];
value = [] as string[];
nmap = [] as string[];
constructor(
label: string,
onSubmit: (str: string[]) => void,
selections: AsyncMultiSelect["searchFunc"],
owner: Options,
{defaultValues = []}: {defaultValues: searchRes[]} = {defaultValues: []},
) {
this.label = label;
this.value = defaultValues.map((_) => _.value);
this.nmap = defaultValues.map((_) => _.name);
this.owner = owner;
this.onSubmit = onSubmit;
this.searchFunc = selections;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const s = document.createElement("div");
s.classList.add("amsCont");
const genArea = () => {
s.textContent = "";
let i = 0;
for (const name of this.nmap) {
const si = i++;
const elm = document.createElement("div");
elm.classList.add("amsElm", "flexltr");
const x = document.createElement("span");
x.classList.add("svgicon", "svg-plainx");
x.onclick = () => {
this.value.splice(si, 1);
this.nmap.splice(si, 1);
genArea();
this.onchange(this.value);
};
const nspan = document.createElement("span");
nspan.textContent = name;
elm.append(nspan, x);
s.append(elm);
}
const addbg = document.createElement("div");
addbg.classList.add("addbg");
const add = document.createElement("span");
add.classList.add("svgicon", "svg-plus");
addbg.append(add);
s.append(addbg);
add.onclick = (e) => {
this.popUpSearchBox(e.x, e.y, () => {
genArea();
this.onchange(this.value);
});
};
};
genArea();
div.append(s);
return div;
}
async popUpSearchBox(x: number, y: number, done: () => void) {
const searchBox = document.createElement("div");
searchBox.style.top = y + "px";
searchBox.style.left = x + "px";
searchBox.classList.add("amsBox");
const input = document.createElement("input");
input.type = "text";
const reses = document.createElement("div");
reses.classList.add("flexttb", "reses");
searchBox.append(input, reses);
let opts = [] as searchRes[];
const search = async () => {
const res = await this.searchFunc(input.value || "", this.value);
reses.textContent = "";
opts = res;
for (const opt of opts) {
const span = document.createElement("span");
span.textContent = opt.name;
span.onmousedown = () => {
removeAni(searchBox);
this.value.push(opt.value);
this.nmap.push(opt.name);
done();
};
reses.append(span);
}
input.onblur = async () => {
removeAni(searchBox);
};
};
input.onkeyup = (e) => {
if (e.key === "Enter" && opts[0]) {
removeAni(searchBox);
this.value.push(opts[0].value);
this.nmap.push(opts[0].name);
done();
} else {
search();
}
};
search();
document.body.append(searchBox);
input.focus();
}
submit() {
this.onSubmit(this.value);
}
onchange: (str: string[]) => void = (_) => {};
watchForChange(func: (str: string[]) => void) {
this.onchange = func;
}
}
class SelectInput implements OptionsElement<number> {
readonly label: string;
readonly owner: Options;
@@ -869,6 +1020,7 @@ class Dialog {
this.float = new Float(name, {ltr, noSubmit});
this.above = goAbove;
}
onhide = () => {};
show(hideOnClick = true) {
const background = document.createElement("div");
background.classList.add("background");
@@ -883,6 +1035,7 @@ class Dialog {
background.onclick = (_) => {
if (hideOnClick && _.target === background) {
removeAni(background);
this.onhide();
}
};
background.tabIndex = 0;
@@ -890,6 +1043,7 @@ class Dialog {
background.onkeydown = (e) => {
if (e.key === "Escape" && hideOnClick) {
removeAni(background);
this.onhide();
}
};
return center;
@@ -1177,6 +1331,19 @@ class Options implements OptionsElement<void> {
this.generate(select);
return select;
}
addAsyncMultiSelect(
label: string,
onSubmit: (str: string[]) => void,
selections: AsyncMultiSelect["searchFunc"],
{defaultValues = [] as searchRes[]} = {},
) {
const select = new AsyncMultiSelect(label, onSubmit, selections, this, {
defaultValues,
});
this.options.push(select);
this.generate(select);
return select;
}
addImageInput(
label: string,
onSubmit: (files: FileList | null) => void,
@@ -1193,7 +1360,11 @@ class Options implements OptionsElement<void> {
this.generate(FI);
return FI;
}
addDateInput(label: string, onSubmit: (str: string) => void, {initText = ""} = {}) {
addDateInput(
label: string,
onSubmit: (str: string | undefined) => void,
{initText = "" as string | Date} = {},
) {
const textInput = new DateInput(label, onSubmit, this, {
initText,
});
+3
View File
@@ -13,5 +13,8 @@ abstract class SnowFlake {
throw new Error(`The ID is corrupted, it's ${str} when it should be some number.`);
}
}
static DateToID(date: Date) {
return ((BigInt(+date) - 1420070400000n) << 22n).toString();
}
}
export {SnowFlake};
+58
View File
@@ -183,6 +183,42 @@ body {
min-height: 0;
display: flex;
}
.amsCont {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
.addbg {
background: #0000006e;
padding: 4px;
border-radius: 6px;
height: 16px;
width: 16px;
cursor: pointer;
}
.svg-plus {
width: 16px;
height: 16px;
}
}
.amsElm {
height: 18px;
background: #00000061;
border-radius: 4px;
margin: 4px;
padding: 4px;
width: fit-content;
cursor: pointer;
align-items: center;
.svg-plainx {
margin-left: 10px;
width: 14px;
height: 14px;
cursor: pointer;
}
}
.sessionDiv {
padding: 8px;
background: #00000040;
@@ -221,6 +257,15 @@ body {
display: flex;
flex-direction: column;
}
.reses {
span {
background: #0000007a;
margin: 3px;
padding: 3px;
border-radius: 4px;
cursor: pointer;
}
}
.actConnectionDiv {
background: #0000005c;
padding: 6px;
@@ -5051,3 +5096,16 @@ img.error::after {
max-height: 400px;
border-radius: 3px;
}
.amsBox {
position: absolute;
z-index: 10000;
padding: 5px;
background: var(--primary-bg);
box-shadow: 0px 0px 3px 1px black;
border-radius: 6px;
&.removeElm {
animation-duration: 0.2s;
animation-name: context-fade-out;
animation-timing-function: ease-out;
}
}
+8 -1
View File
@@ -825,7 +825,14 @@
"nofind": "There seems to be no messages that match your search, maybe trying broadening your search to try and find what you want",
"old": "Old",
"page": "Page $1",
"search": "Search"
"search": "Search",
"settings":"Search Settings",
"before":"Before:",
"after":"After:",
"includensfw":"Include NSFW:",
"authors":"Authors:",
"mentions":"Mentions:",
"channels":"Channels:"
},
"searchGifs": "Search $1",
"settings": {