mirror of
https://github.com/MathMan05/Fermi.git
synced 2026-08-28 02:54:08 +00:00
search filtering
This commit is contained in:
+145
-3
@@ -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
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user