diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index a328aa2..c32a6ab 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -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); diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index 62e7de8..ac8aceb 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -187,6 +187,21 @@ class TextInput implements OptionsElement { } } 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 { readonly onSubmit!: (str: string) => void; @@ -412,7 +436,134 @@ export class ColorInput implements OptionsElement { this.onSubmit(this.colorContent); } } +interface searchRes { + value: string; + name: string; +} +class AsyncMultiSelect implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: string[]) => void; + select!: WeakRef; + searchFunc: (term: string, cur: string[]) => Promise | 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 { 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 { 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 { 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, }); diff --git a/src/webpage/snowflake.ts b/src/webpage/snowflake.ts index 2a2af43..f1ee276 100644 --- a/src/webpage/snowflake.ts +++ b/src/webpage/snowflake.ts @@ -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}; diff --git a/src/webpage/style.css b/src/webpage/style.css index 323c7df..e0e7883 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -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; + } +} diff --git a/translations/en.json b/translations/en.json index 5931658..f6138e7 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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": {