import {Message} from "./message.js";
import {Contextmenu} from "./contextmenu.js";
import {Guild, makeInviteMenu} from "./guild.js";
import {Localuser} from "./localuser.js";
import {Permissions} from "./permissions.js";
import {Dialog, Float, Settings} from "./settings.js";
import {Role, RoleList} from "./role.js";
import {InfiniteScroller} from "./infiniteScroller.js";
import {SnowFlake} from "./snowflake.js";
import {
	channeljson,
	creatPollJSON,
	embedjson,
	filejson,
	memberjson,
	messageCreateJson,
	messagejson,
	mute_config,
	readyjson,
	startTypingjson,
	tagjson,
	threadMember,
	threadMetadata,
} from "./jsontypes.js";
import {MarkDown, saveCaretPosition} from "./markdown.js";
import {Member} from "./member.js";
import {Voice} from "./voice.js";
import {User, userVolMenu} from "./user.js";
import {I18n} from "./i18n.js";
import {createImg, safeImg} from "./utils/utils.js";
import {webhookMenu} from "./webhooks.js";
import {File} from "./file.js";
import {Sticker} from "./sticker.js";
import {Direct} from "./direct.js";
import {NotificationHandler} from "./notificationHandler.js";
import {Command} from "./interactions/commands.js";
import {Tag} from "./tag.js";
import {CDNParams} from "./utils/cdnParams.js";
import {TypeBox} from "./typeBox.js";
import {Hover} from "./hover.js";

class Channel extends SnowFlake {
	editing!: Message | null;
	type!: number;
	owner: Guild;
	headers: Localuser["headers"];
	name!: string;
	get shortName() {
		if (this.name.length > 50) return this.name.slice(0, 50) + "...";
		return this.name;
	}
	parentId?: string;
	parent?: Channel;
	children!: Channel[];
	readonly permissionOverwriteMap = new Map<string, Permissions>();
	topic!: string;
	nsfw!: boolean;
	position: number = 0;
	private lastreadmessageidint?: string;
	get lastreadmessageid() {
		return this.lastreadmessageidint;
	}
	async getOverwrites(): Promise<[User | Role | {id: string}, Permissions][]> {
		return await Promise.all(
			[...this.permissionOverwriteMap].map(async ([id, p]) => {
				const role = this.guild.roleids.get(id);
				if (role) return [role, p] as const;
				try {
					return [await this.localuser.getUser(id), p] as const;
				} catch {
					return [{id}, p] as const;
				}
			}),
		);
	}
	async getOverwritesOrder() {
		const overwrites = await this.getOverwrites();
		const users = overwrites.filter(([_]) => _ instanceof User);
		const roles = overwrites.filter(([_]) => _ instanceof Role) as [Role, Permissions][];
		const order = new Map<Role, number>();
		roles.forEach(([_]) => order.set(_, this.guild.roles.indexOf(_)));
		roles.sort(([a], [b]) => order.get(a)! - order.get(b)!);
		return [...users, ...roles];
	}
	set lastreadmessageid(id: string | undefined) {
		const cur = this.lastreadmessageidint;
		this.lastreadmessageidint = id;
		const m = this.messages.get(this.idToNext.get(cur as string) as string);
		if (m) {
			console.log(m);
			m.generateMessage();
		}
		const m2 = this.messages.get(this.idToNext.get(id as string) as string);
		if (m2) {
			m2.generateMessage();
		}
	}
	lastmessageid?: string;
	trueLastMessageid?: string;
	rate_limit_per_user: number = 0;
	mentions = 0;
	lastpin!: string;
	move_id?: string;
	typing!: number;
	message_notifications: number = 3;
	allthewayup!: boolean;
	static contextmenu = new Contextmenu<Channel, undefined>("channel menu");
	replyingto!: Message | null;
	infinite!: InfiniteScroller;
	readonly idToPrev: Map<string, string | undefined>;
	readonly idToNext: Map<string, string | undefined>;
	readonly messages: Map<string, Message>;
	voice?: Voice;
	bitrate: number = 128000;
	threadData?: threadMetadata;
	mute_config: mute_config | null = {selected_time_window: -1, end_time: 0};
	updateEvents = new Set<() => void>();
	fireEvents() {
		for (const thing of this.updateEvents) {
			thing();
		}
	}
	setLastMessageId(id: string) {
		this.lastmessageid = id;
		try {
			if (BigInt(id) > BigInt(this.trueLastMessageid || "0")) {
				this.trueLastMessageid = id;
			}
		} catch {}
	}
	handleUserOverrides(settings: {
		message_notifications: number;
		muted: boolean;
		mute_config: mute_config | null;
		channel_id: string;
	}) {
		this.message_notifications = settings.message_notifications;
		this.mute_config = settings.mute_config;
	}
	static setupcontextmenu() {
		this.contextmenu.addButton(
			() => I18n.channel.markRead(),
			function (this: Channel) {
				this.readbottom();
			},
		);

		//TODO invite icon
		this.contextmenu.addButton(
			() => I18n.channel.makeInvite(),
			function (this: Channel) {
				this.createInvite();
			},
			{
				visible: function () {
					return this.hasPermission("CREATE_INSTANT_INVITE") && this.type !== 4 && !this.isThread();
				},
				color: "blue",
			},
		);
		this.contextmenu.addSeperator();
		this.contextmenu.addButton(
			() => I18n.threads.leave(),
			function () {
				fetch(this.info.api + "/channels/" + this.id + "/thread-members/@me", {
					method: "DELETE",
					headers: this.headers,
				});
			},
			{
				visible: function () {
					return this.isThread() && !!this.member;
				},
			},
		);

		this.contextmenu.addButton(
			() => I18n.threads.join(),
			function () {
				fetch(this.info.api + "/channels/" + this.id + "/thread-members/@me", {
					method: "POST",
					headers: this.headers,
				});
			},
			{
				visible: function () {
					return this.isThread() && !this.member;
				},
			},
		);

		this.contextmenu.addButton(
			() => I18n.threads.editTags(),
			function () {
				this.editTags();
			},
			{
				visible: function () {
					return !!(this.isThread() && this.parent?.isForum() && this.parent.availableTags);
				},
			},
		);

		this.contextmenu.addSeperator();
		//TODO notifcations icon
		this.contextmenu.addButton(
			() => I18n.guild.notifications(),
			function () {
				this.setnotifcation();
			},
		);
		this.contextmenu.addButton(
			() => I18n.channel.mute(),
			function () {
				this.muteChannel();
			},
			{
				visible: function () {
					return !this.muted && this.type !== 4;
				},
			},
		);
		this.contextmenu.addButton(
			() => I18n.channel.unmute(),
			function () {
				this.unmuteChannel();
			},
			{
				visible: function () {
					return this.muted;
				},
			},
		);

		this.contextmenu.addButton(
			() => I18n.channel.settings(),
			function (this: Channel) {
				this.generateSettings();
			},
			{
				visible: function () {
					return this.hasPermission("MANAGE_CHANNELS") || this.owner_id === this.localuser.user.id;
				},
				icon: {
					css: "svg-settings",
				},
			},
		);

		this.contextmenu.addButton(
			function () {
				if (this.isThread()) {
					return I18n.channel.deleteThread();
				}
				if (this.type === 4) {
					return I18n.channel.deleteCat();
				}
				return I18n.channel.delete();
			},
			function (this: Channel) {
				this.deleteChannel();
			},
			{
				visible: function () {
					return this.isThread()
						? this.hasPermission("MANAGE_THREADS")
						: this.hasPermission("MANAGE_CHANNELS");
				},
				icon: {
					css: "svg-delete",
				},
				color: "red",
			},
		);

		this.contextmenu.addSeperator();
		this.contextmenu.addButton(
			I18n.channel.copyURL(),
			function (this: Channel) {
				navigator.clipboard.writeText(`${location.origin}/channels/${this.guild.id}/${this.id}`);
			},
			{
				visible: function () {
					return this.type !== 4;
				},
			},
		);
		//TODO copy ID icon
		this.contextmenu.addButton(
			function () {
				if (this.type == 4) {
					return I18n.channel.copyIdCat();
				}
				return I18n.channel.copyId();
			},
			function (this: Channel) {
				navigator.clipboard.writeText(this.id);
			},
		);
	}
	unmuteChannel() {
		const mute_config = {
			selected_time_window: -1,
			end_time: 0,
		};
		fetch(this.info.api + "/users/@me/guilds/" + this.guild.id + "/settings", {
			method: "PATCH",
			headers: this.headers,
			body: JSON.stringify({
				channel_overrides: {
					[this.id]: {
						message_notifications: this.mentions,
						muted: false,
						mute_config,
						channel_id: this.id,
					},
				},
			}),
		});
		this.mute_config = mute_config;
		this.html?.deref()?.classList.remove("muted");
		this.unreads();
		this.guild.unreads();
	}
	muteChannel() {
		const dio = new Dialog(I18n.channel.mute());
		const opt = dio.options;
		let time = 1800;
		opt.addSelect(
			I18n.muteDuration(),
			() => {},
			(["30m", "1h", "6h", "12h", "1d", "7d", "30d", "never"] as const).map((e) =>
				I18n.inviteOptions[e](),
			),
		).onchange = (e) => {
			time = [1800, 3600, 21600, 43200, 86400, 604800, 2592000, 1 << 30][e];
		};
		opt.addButtonInput("", I18n.submit(), () => {
			const mute_config = {
				selected_time_window: time,
				end_time: Math.floor(new Date(Date.now() + time * 1000).getTime()),
			};
			fetch(this.info.api + "/users/@me/guilds/" + this.guild.id + "/settings", {
				method: "PATCH",
				headers: this.headers,
				body: JSON.stringify({
					channel_overrides: {
						[this.id]: {
							message_notifications: this.mentions,
							muted: true,
							mute_config,
							channel_id: this.id,
						},
					},
				}),
			});
			this.mute_config = mute_config;
			this.html?.deref()?.classList.add("muted");
			dio.hide();
			this.unreads();
			this.guild.unreads();
		});
		dio.show();
	}
	get muted() {
		return !!this.mute_config && new Date(this.mute_config.end_time).getTime() > Date.now();
	}
	icon?: string;
	iconUrl() {
		return (
			`${this.info.cdn}/channel-icons/${this.id}/${this.icon}.png` +
			new CDNParams({expectedSize: 32})
		);
	}
	createInvite() {
		const div = document.createElement("div");
		div.classList.add("invitediv");
		const text = document.createElement("span");
		text.classList.add("ellipsis");
		div.append(text);
		let uses = 0;
		let expires = 1800;
		const copycontainer = document.createElement("div");
		copycontainer.classList.add("copycontainer");
		const copy = document.createElement("span");
		copy.classList.add("copybutton", "svgicon", "svg-copy");
		copycontainer.append(copy);
		copycontainer.onclick = (_) => {
			if (text.textContent) {
				navigator.clipboard.writeText(text.textContent);
			}
		};
		div.append(copycontainer);
		const update = () => {
			fetch(`${this.info.api}/channels/${this.id}/invites`, {
				method: "POST",
				headers: this.headers,
				body: JSON.stringify({
					flags: 0,
					target_type: null,
					target_user_id: null,
					max_age: expires + "",
					max_uses: uses,
					temporary: uses !== 0,
				}),
			})
				.then((_) => _.json())
				.then((json) => {
					const params = new URLSearchParams("");
					params.set("instance", this.localuser.instanceString());
					const encoded = params.toString();
					text.textContent = `${location.origin}/invite/${json.code}?${encoded}`;
				});
		};
		update();
		const inviteOptions = new Dialog("", {noSubmit: true});
		inviteOptions.options.addTitle(I18n.inviteOptions.title());
		inviteOptions.options.addText(I18n.invite.subtext(this.shortName, this.guild.properties.name));

		inviteOptions.options.addSelect(
			I18n.invite.expireAfter(),
			() => {},
			(["30m", "1h", "6h", "12h", "1d", "7d", "30d", "never"] as const).map((e) =>
				I18n.inviteOptions[e](),
			),
		).onchange = (e) => {
			expires = [1800, 3600, 21600, 43200, 86400, 604800, 2592000, 0][e];
			update();
		};

		const timeOptions = (["1", "5", "10", "25", "50", "100"] as const).map((e) =>
			I18n.inviteOptions.limit(e),
		);
		timeOptions.unshift(I18n.inviteOptions.noLimit());
		inviteOptions.options.addSelect(I18n.invite.expireAfter(), () => {}, timeOptions).onchange = (
			e,
		) => {
			uses = [0, 1, 5, 10, 25, 50, 100][e];
			update();
		};

		inviteOptions.options.addHTMLArea(div);
		inviteOptions.show();
	}
	generateSettings() {
		const settings = new Settings(I18n.channel.settingsFor(this.shortName));
		{
			const gensettings = settings.addButton(I18n.channel.settings());
			const form = gensettings.addForm("", () => {}, {
				fetchURL: this.info.api + "/channels/" + this.id,
				method: "PATCH",
				headers: this.headers,
				traditionalSubmit: true,
			});
			form.addTextInput(I18n.channel["name:"](), "name", {
				initText: this.name,
			});
			form.addMDInput(I18n.channel["topic:"](), "topic", {
				initText: this.topic,
			});
			form.addImageInput(I18n.channel.icon(), "icon", {
				initImg: this.icon ? this.iconUrl() : undefined,
				clear: true,
			});
			form.addCheckboxInput(I18n.channel["nsfw:"](), "nsfw", {
				initState: this.nsfw,
			});
			const times = [
				0,
				5,
				10,
				15,
				30,
				60,
				120,
				300,
				600,
				900,
				1800,
				60 * 60,
				60 * 60 * 2,
				60 * 60 * 6,
			];
			form.addSelect(
				I18n.channel.slowmode(),
				"rate_limit_per_user",
				["0s", "5s", "10s", "15s", "30s", "1m", "2m", "5m", "10m", "15m", "30m", "1h", "2h", "6h"],
				{
					defaultIndex: (times.findIndex((_) => _ === this.rate_limit_per_user) + 1 || 1) - 1,
				},
				times,
			);
			const arcTimes = [60, 60 * 24, 60 * 24 * 3, 60 * 24 * 7];
			form.addSelect(
				I18n.channel.hideThreads(),
				"default_auto_archive_duration",
				["1h", "24h", "3d", "7d"],
				{
					defaultIndex: arcTimes.findIndex((_) => _ === (this.defaultAutoArchiveDuration ?? 1440)),
				},
				arcTimes,
			);
			if (this.isForum()) {
				form.addText(I18n.forum.settings.editTags());
				const tags = document.createElement("div");
				tags.classList.add("forumTagSelect");
				const tagSet = new Set<string>();
				const makeTagHTML = (tag: Tag) => {
					tagSet.add(tag.id);
					let html = tag.makeHTML();
					html.onclick = () => {
						const d = new Dialog(I18n.forum.settings.editTag());
						const f = d.options.addForm(
							"",
							(_, obj) => {
								tag.update(obj as tagjson);
								const newhtml = tag.makeHTML();
								html.parentNode?.insertBefore(newhtml, html);
								html.remove();
								newhtml.onclick = html.onclick;
								html = newhtml;
								d.hide();
							},
							{
								fetchURL: this.info.api + "/channels/" + this.id + "/tags/" + tag.id,
								method: "PUT",
								headers: this.headers,
							},
						);
						f.addTextInput(I18n.forum.settings.tagName(), "name", {
							initText: tag.name,
						});
						f.addCheckboxInput(I18n.forum.settings.moderated(), "moderated", {
							initState: tag.moderated,
						});
						f.addButtonInput("", I18n.forum.settings.delTag(), async () => {
							const res = await fetch(this.info.api + "/channels/" + this.id + "/tags/" + tag.id, {
								method: "DELETE",
								headers: this.headers,
							});
							if (res.ok) {
								d.hide();
								html.remove();
							}
						});
						d.show();
					};
					tags.append(html);
				};
				for (const tag of this.availableTags) {
					makeTagHTML(tag);
				}
				form.addHTMLArea(tags);
				form.addButtonInput("", I18n.forum.settings.addTag(), () => {
					const d = new Dialog(I18n.forum.settings.editTag());
					const f = d.options.addForm(
						"",
						(json) => {
							const channel = json as channeljson;
							if (!channel.available_tags) return;
							const dontHave = channel.available_tags.filter((_) => !tagSet.has(_.id));
							for (const tagJson of dontHave) {
								makeTagHTML(new Tag(tagJson, this));
							}
							d.hide();
						},
						{
							fetchURL: this.info.api + "/channels/" + this.id + "/tags",
							method: "POST",
							headers: this.headers,
						},
					);
					f.addTextInput(I18n.forum.settings.tagName(), "name", {
						initText: "",
					});
					f.addCheckboxInput(I18n.forum.settings.moderated(), "moderated", {
						initState: false,
					});
					d.show();
				});
			}
			if (this.type !== 4 && !this.isThread() && !this.isForum()) {
				const options = ["voice", "text", "announcement"] as const;
				form.addSelect(
					"Type:",
					"type",
					options.map((e) => I18n.channel[e]()),
					{
						defaultIndex: options.indexOf(
							{0: "text", 2: "voice", 5: "announcement", 4: "category"}[this.type] as
								| "text"
								| "voice"
								| "announcement",
						),
					},
					options,
				);
				form.addPreprocessor((obj: any) => {
					obj.type = {text: 0, voice: 2, announcement: 5, category: 4}[obj.type as string];
				});
			}
		}
		if (!this.isThread()) {
			const s1 = settings.addButton(I18n.channel.permissions(), {optName: ""});

			(async () => {
				const list = await this.getOverwritesOrder();

				s1.options.push(
					new RoleList(list, this.guild, this.updateRolePermissions.bind(this), this),
				);
			})();
			const inviteMenu = settings.addButton(I18n.guild.invites());
			makeInviteMenu(inviteMenu, this.owner, this.info.api + `/channels/${this.id}/invites`);

			const webhooks = settings.addButton(I18n.webhooks.base());
			webhookMenu(this.guild, this.info.api + `/channels/${this.id}/webhooks`, webhooks, this.id);
		}

		settings.show();
	}

	setUpInfiniteScroller() {
		this.infinite = new InfiniteScroller(
			async (id: string, offset: number): Promise<string | undefined> => {
				if (offset === 1) {
					if (this.idToPrev.has(id)) {
						return this.idToPrev.get(id);
					} else {
						await this.grabBefore(id);
						return this.idToPrev.get(id);
					}
				} else {
					if (this.idToNext.has(id)) {
						return this.idToNext.get(id);
					} else if (this.lastmessage?.id !== id) {
						await this.grabAfter(id);
						return this.idToNext.get(id);
					} else {
					}
				}
				return undefined;
			},
			(id: string): HTMLElement => {
				//await new Promise(_=>{setTimeout(_,Math.random()*10)})
				const message = this.messages.get(id);
				try {
					if (message) {
						const html = this.fakeMessages.get(message);
						if (html) {
							return html;
						}
						return message.buildhtml();
					} else {
						console.error(id + " not found");
					}
				} catch (e) {
					console.error(e);
				}
				return document.createElement("div");
			},
			async (id: string) => {
				const message = this.messages.get(id);
				try {
					if (message) {
						const html = this.fakeMessages.get(message);
						if (html) {
							html.remove();
							return true;
						}
						message.deleteDiv();
						return true;
					}
				} catch (e) {
					console.error(e);
				} finally {
				}
				return false;
			},
			this.readbottom.bind(this),
		);
	}
	last_pin_timestamp?: string;
	member?: threadMember;

	memberCount?: number;
	messageCount?: number;
	totalMessageSent?: number;
	availableTags: Tag[] = [];
	flags!: number;
	defaultAutoArchiveDuration!: number;

	constructor(json: channeljson | -1, owner: Guild, id: string = json === -1 ? "" : json.id) {
		super(id);
		this.idToNext = owner.localuser.idToNext;
		this.idToPrev = owner.localuser.idToPrev;
		this.messages = owner.localuser.messages;

		this.owner = owner;
		this.headers = this.owner.headers;

		if (json === -1) {
			return;
		}
		this.localuser.channels.set(this.id, this);
		this.flags = json.flags || 0;
		this.memberCount = json.member_count;
		this.defaultAutoArchiveDuration = json.default_auto_archive_duration;
		this.appliedTags = json.applied_tags || [];
		this.availableTags = json.available_tags?.map((tag) => new Tag(tag, this)) || [];
		this.messageCount = json.message_count;
		this.totalMessageSent = json.total_message_sent;
		this.owner_id = json.owner_id;
		this.member = json.member;
		this.rate_limit_per_user = json.rate_limit_per_user || 0;
		this.editing;
		this.type = json.type;

		this.name = json.name;
		if (json.parent_id) {
			this.parentId = json.parent_id;
		}
		this.parent = undefined;
		this.children = [];
		this.icon = json.icon;
		this.permissionOverwriteMap.clear();
		(json.permission_overwrites ?? []).forEach((r) => {
			const p = new Permissions(r.allow, r.deny);
			this.permissionOverwriteMap.set(r.id, p);
			return r.id;
		});

		this.topic = json.topic;
		this.nsfw = json.nsfw;
		this.position = json.position;
		this.lastreadmessageid = undefined;
		if (json.last_message_id) {
			this.setLastMessageId(json.last_message_id);
		} else {
			this.lastmessageid = undefined;
		}
		if (this.type === 2 && this.localuser.voiceFactory) {
			this.voice = this.localuser.voiceFactory.makeVoice(this.guild.id, this.id, {
				bitrate: this.bitrate,
			});
			this.setUpVoice();
		}
		this.setUpInfiniteScroller();
		this.perminfo ??= {};
		this.threadData = json.thread_metadata;
		const read = this.localuser.unknownRead.get(this.id);
		if (read) {
			this.readStateInfo(read);
			this.localuser.unknownRead.delete(this.id);
		}
	}
	get perminfo() {
		return this.guild.perminfo.channels[this.id];
	}
	set perminfo(e) {
		this.guild.perminfo.channels[this.id] = e;
	}
	isAdmin() {
		return this.guild.member.isAdmin();
	}
	get guild() {
		return this.owner;
	}
	get localuser() {
		return this.guild.localuser;
	}
	get info() {
		return this.owner.info;
	}
	pinnedMessages?: Message[];
	async pinnedClick(rect: DOMRect) {
		const div = document.createElement("div");
		div.classList.add("flexttb", "pinnedMessages");
		div.style.top = rect.bottom + 20 + "px";
		div.style.right = window.innerWidth - rect.right + "px";
		document.body.append(div);
		Contextmenu.keepOnScreen(div);
		Contextmenu.declareMenu(div);
		this.last_pin_timestamp = this.lastpin;
		const l = (e: MouseEvent) => {
			if (e.target instanceof HTMLElement && div.contains(e.target)) {
				return;
			}
			div.remove();
			document.removeEventListener("click", l);
		};
		document.addEventListener("mouseup", l);
		if (!this.pinnedMessages) {
			const pinnedM = (await (
				await fetch(`${this.info.api}/channels/${this.id}/pins`, {headers: this.headers})
			).json()) as messagejson[];
			this.pinnedMessages = pinnedM.map((_) => {
				if (this.messages.has(_.id)) {
					return this.messages.get(_.id) as Message;
				} else {
					return new Message(_, this);
				}
			});
		}
		const pinnedM = document.getElementById("pinnedMDiv");
		if (pinnedM) {
			pinnedM.classList.remove("unreadPin");
		}
		if (this.pinnedMessages.length === 0) {
			const b = document.createElement("b");
			b.classList.add("noPins");
			b.textContent = I18n.noPins();
			div.append(b);
			return;
		}
		div.append(
			...this.pinnedMessages.map((_) => {
				const html = _.buildhtml(undefined, true);
				html.style.cursor = "pointer";
				html.onclick = async () => {
					div.remove();
					await this.focus(_.id);
				};
				Message.contextmenu.bindContextmenu(html, _);
				return html;
			}),
		);
	}
	readStateInfo(json: readyjson["d"]["read_state"]["entries"][0]) {
		const next = this.messages.get(this.idToNext.get(this.lastreadmessageid as string) as string);
		this.lastreadmessageid = isNaN(+json.last_message_id) ? "0" : json.last_message_id;
		this.mentions = json.mention_count;
		this.mentions ??= 0;
		this.lastpin = json.last_pin_timestamp;
		if (next) {
			next.generateMessage();
		}
	}
	get hasunreads(): boolean {
		return this.unreadState();
	}
	private unreadState(): boolean {
		if (this.muted) return false;
		if (!this.hasPermission("VIEW_CHANNEL")) {
			return false;
		}
		if (this.mentions) return true;
		let lastreadmessage = SnowFlake.stringToUnixTime(this.lastreadmessageid || "0");
		if (this.guild.member) {
			const joinedAt = new Date(this.guild.member.joined_at).getTime();
			if (!lastreadmessage || lastreadmessage < joinedAt) {
				lastreadmessage = joinedAt;
			}
		}
		const lastmessage = SnowFlake.stringToUnixTime(this.trueLastMessageid || "0");
		return !!lastmessage && (!lastreadmessage || lastmessage > lastreadmessage) && this.type !== 4;
	}

	hasPermission(name: string, member = this.guild.member): boolean {
		if (member.isAdmin()) {
			return true;
		}
		if (this.isThread() && this.parent) {
			return this.parent.hasPermission(name, member);
		}
		if (this.guild.member.commuicationDisabledLeft()) {
			const allowSet = new Set(["READ_MESSAGE_HISTORY", "VIEW_CHANNEL"]);
			if (!allowSet.has(name)) {
				return false;
			}
		}
		//if (this.id === "1499557059291885982") debugger;

		const premission = this.permissionOverwriteMap.get(member.id);
		if (premission) {
			const perm = premission.getPermission(name);
			if (perm) {
				return perm === 1;
			}
		}

		let deny = false;

		for (const role of member.roles) {
			const premission = this.permissionOverwriteMap.get(role.id);
			if (premission) {
				const perm = premission.getPermission(name);
				if (perm === 1) {
					return true;
				} else if (perm === -1) {
					deny = true;
				}
			}
		}
		if (deny) return false;

		for (const role of member.roles) {
			if (role.permissions.getPermission(name)) {
				return true;
			}
		}
		return false;
	}
	get canMessage(): boolean {
		//TODO get rid of this
		if (this.permissionOverwriteMap.size === 0 && this.hasPermission("MANAGE_CHANNELS")) {
			const role = this.guild.roles.find((_) => _.name === "@everyone");
			if (role) {
				this.addRoleToPerms(role);
			}
		}
		return this.isThread()
			? this.hasPermission("SEND_MESSAGES_IN_THREADS")
			: this.hasPermission("SEND_MESSAGES");
	}
	sortchildren() {
		this.children.sort((a, b) => {
			return a.position - b.position;
		});
	}
	resolveparent(_guild: Guild = this.owner) {
		const parentid = this.parentId;
		if (!parentid) return false;
		this.parent = this.localuser.channels.get(parentid);
		this.parent ??= undefined;
		if (this.parent !== undefined) {
			this.parent.children.push(this);
		}
		return this.parent !== undefined;
	}
	calculateReorder(position: number) {
		const build: {
			id: string;
			position: number | undefined;
			parent_id: string | undefined;
		}[] = [];
		for (const thing of this.children) {
			const thisthing: {
				id: string;
				position: number | undefined;
				parent_id: string | undefined;
			} = {id: thing.id, position: undefined, parent_id: undefined};

			if (thing.position != position) {
				thisthing.position = thing.position = position;
			}
			if (thing.move_id && thing.move_id !== thing.parentId) {
				thing.parentId = thing.move_id;
				thisthing.parent_id = thing.parent?.id;
				thing.move_id = undefined;
				//console.log(this.guild.channelids[thisthing.parent_id.id]);
			}
			if (thisthing.position || thisthing.parent_id) {
				build.push(thisthing);
			}
			position++;
		}
		return [build, position] as const;
	}
	static dragged: [Channel, HTMLDivElement] | [] = [];
	html: WeakRef<HTMLDivElement> | undefined;
	get visible() {
		return this.hasPermission("VIEW_CHANNEL");
	}
	voiceUsers = new WeakRef(document.createElement("div"));
	iconElm = new WeakRef(document.createElement("span") as HTMLSpanElement | safeImg);
	renderIcon() {
		let icon = this.iconElm.deref();
		if (this.icon && !this.localuser.perminfo.user.disableIcons) {
			if (icon instanceof HTMLImageElement) {
				icon.setSrcs(this.iconUrl());
			} else {
				const old = icon;
				const div = this.html?.deref();
				icon = createImg(this.iconUrl(), undefined, div, "icon");
				this.iconElm = new WeakRef(icon);
				if (old) {
					try {
						old.before(icon);
						old.remove();
					} catch {}
				}
			}
			icon.classList.add("space");
			return icon;
		} else if (!(icon instanceof HTMLSpanElement)) {
			const old = icon;
			icon = document.createElement("span");
			this.iconElm = new WeakRef(icon);
			if (old) {
				try {
					old.before(icon);
					old.remove();
				} catch {}
			}
		}
		icon.classList = "";
		if (this.type === 0) {
			if (this.guild.properties.rules_channel_id === this.id) {
				icon.classList.add("space", "svgicon", "svg-rules");
			} else {
				icon.classList.add("space", "svgicon", this.nsfw ? "svg-channelnsfw" : "svg-channel");
			}
		} else if (this.type === 2) {
			//
			icon.classList.add("space", "svgicon", this.nsfw ? "svg-voicensfw" : "svg-voice");
		} else if (this.type === 5) {
			//
			icon.classList.add("space", "svgicon", this.nsfw ? "svg-announcensfw" : "svg-announce");
		} else if (this.type === 15) {
			icon.classList.add("space", "svgicon", this.nsfw ? "svg-forumnsfw" : "svg-forum");
		} else {
			console.log(this.type);
		}
		return icon;
	}
	isThread() {
		return this.type === 10 || this.type === 11 || this.type === 12;
	}
	createguildHTML(admin = false): HTMLDivElement {
		const div = this.html?.deref() || document.createElement("div");
		div.innerHTML = "";

		if (this.muted) {
			div.classList.add("muted");
			setTimeout(
				() => {
					div.classList.remove("muted");
				},
				Math.min((this.mute_config?.end_time as number) - Date.now(), 2147483647),
			);
		}

		this.html = new WeakRef(div);
		if (!this.visible) {
			let quit = true;
			for (const thing of this.children) {
				if (thing.visible) {
					quit = false;
				}
			}
			if (quit) {
				return div;
			}
		}
		div.draggable = admin && !this.isThread();
		div.addEventListener("dragstart", (e) => {
			Channel.dragged = [this, div];
			e.stopImmediatePropagation();
		});
		div.addEventListener("dragend", () => {
			Channel.dragged = [];
		});
		const childrendiv = document.createElement("div");
		childrendiv.classList.add("channels");
		for (const channel of this.children.filter((_) => !_.isThread() || _.threadVis())) {
			childrendiv.appendChild(channel.createguildHTML(admin));
		}

		if (this.type === 4) {
			this.sortchildren();
			const caps = document.createElement("div");

			const decdiv = document.createElement("div");
			const decoration = document.createElement("span");
			decoration.classList.add("svgicon", "collapse-icon", "svg-category");
			decdiv.appendChild(decoration);

			const myhtml = document.createElement("p2");
			myhtml.classList.add("ellipsis");
			myhtml.textContent = this.shortName;
			this.nameSpan = new WeakRef(myhtml);
			decdiv.appendChild(myhtml);
			caps.appendChild(decdiv);

			if (this.guild.member.hasPermission("MANAGE_CHANNELS")) {
				const addchannel = document.createElement("span");
				addchannel.classList.add("addchannel", "svgicon", "svg-plus");
				caps.appendChild(addchannel);
				const h = new Hover(I18n.channel.addChannel(), {side: "right", weak: true});
				h.addEvent(addchannel);
				addchannel.onclick = (_) => {
					this.guild.createchannels(this.createChannel.bind(this));
				};
				this.coatDropDiv(decdiv, childrendiv);
			}
			div.appendChild(caps);
			caps.classList.add("flexltr", "capsflex");
			decdiv.classList.add("flexltr", "channeleffects");
			decdiv.classList.add("channel");

			Channel.contextmenu.bindContextmenu(decdiv, this, undefined);

			setTimeout((_: any) => {
				if (!this.perminfo.collapsed) {
					childrendiv.style.height = childrendiv.scrollHeight + "px";
				}
			}, 100);

			if (this.perminfo.collapsed) {
				decoration.classList.add("hiddencat");
				childrendiv.style.height = "0px";
			}
			const handleColapse = async (animate: boolean = true) => {
				if (this.perminfo.collapsed) {
					decoration.classList.add("hiddencat");
					childrendiv.style.height = childrendiv.scrollHeight + "px";
					await new Promise((res) => setTimeout(res, 0));
					childrendiv.style.height = "0px";
				} else {
					decoration.classList.remove("hiddencat");
					if (childrendiv.style.height === "0px" && animate) {
						childrendiv.style.height = childrendiv.scrollHeight + "px";
					} else {
						childrendiv.style.removeProperty("height");
					}
				}
			};
			const observer = new MutationObserver(handleColapse.bind(this, false));
			observer.observe(childrendiv, {childList: true, subtree: true});

			decdiv.onclick = () => {
				this.perminfo.collapsed = !this.perminfo.collapsed;
				handleColapse();
			};
		} else {
			childrendiv.classList.add("threads");
			div.classList.add("channel");
			this.unreads();
			Channel.contextmenu.bindContextmenu(div, this, undefined);
			if (admin && !this.isThread()) {
				this.coatDropDiv(div);
			}
			const button = document.createElement("button");
			button.classList.add("channelbutton");

			div.append(button);
			const myhtml = document.createElement("span");
			myhtml.classList.add("ellipsis");
			myhtml.textContent = this.shortName;
			this.nameSpan = new WeakRef(myhtml);
			const decoration = this.renderIcon();
			button.appendChild(decoration);
			button.appendChild(myhtml);
			button.onclick = (_) => {
				this.getHTML();
				const toggle = document.getElementById("maintoggle") as HTMLInputElement;
				toggle.checked = true;
			};
			if (this.type === 2) {
				const voiceUsers = document.createElement("div");
				div.append(voiceUsers);
				this.voiceUsers = new WeakRef(voiceUsers);
				this.updateVoiceUsers();
			}
		}
		div.appendChild(childrendiv);
		return div;
	}
	owner_id?: string;
	threadVis() {
		return (this.member && !this.threadData?.archived) || this.localuser.focusChannel === this;
	}
	async moveForDrag(x: number) {
		const mainarea = document.getElementById("mainarea");
		if (!mainarea) return;
		if (x === -1) {
			mainarea.style.removeProperty("left");
			mainarea.style.removeProperty("transition");
			return;
		}
		mainarea.style.left = x + "px";
		mainarea.style.transition = "left 0s";
	}
	async setUpVoice() {
		if (!this.voice) return;
		this.voice.onUserVol = (u) => {
			u.volume = this.localuser.getUserAudio(u.id) / 100;
		};
		this.voice.onMemberChange = async (memb, joined) => {
			console.log(memb, joined);
			if (typeof memb !== "string") {
				await Member.new(memb, this.guild);
			}

			const users = this.usersDiv.deref();
			if (users) {
				const user = await this.localuser.getUser(typeof memb === "string" ? memb : memb.id);
				if (joined) {
					this.makeUserBox(user, users);
				} else {
					this.destUserBox(user);
				}
			}

			this.updateVoiceUsers();
			if (this.voice === this.localuser.currentVoice && this.voice?.open) {
				this.localuser.play?.play("join", this.localuser.getNotiVolume());
			}
		};
		this.voice.onUserChange = (user, change) => {
			this.boxChange(user, change);
		};
		this.voice.onSpeakingChange = (id, speaking) => {
			const box = this.boxMap.get(id);
			if (box) {
				if (speaking) {
					box.classList.add("speaking");
				} else {
					box.classList.remove("speaking");
				}
			}
			const tray = this.voiceTray.get(id);
			console.log("tray! :3");
			if (tray && tray.parentElement) {
				const parent = tray.parentElement;
				const pfp = parent.children[0].getElementsByClassName("pfp")[0];
				if (!pfp) return;
				if (speaking) {
					pfp.classList.add("speaking");
				} else {
					pfp.classList.remove("speaking");
				}
			}
		};
	}
	voiceTray = new Map<string, HTMLDivElement>();
	async updateVoiceUsers() {
		const voiceUsers = this.voiceUsers.deref();
		if (!voiceUsers || !this.voice) return;
		console.warn(this.voice.userids);

		const html = (
			await Promise.all(
				this.voice.userids
					.entries()
					.toArray()
					.map(async (_) => {
						const user = await User.resolve(_[0], this.localuser);
						console.log(user);
						const member = await Member.resolveMember(user, this.guild);
						const array = [member, _[1]] as [Member, (typeof _)[1]];
						return array;
					}),
			)
		).flatMap(([member, obj]) => {
			if (!member) {
				console.warn("This is weird, member doesn't exist :P");
				return [];
			}
			const div = document.createElement("div");
			userVolMenu.bindContextmenu(div, this.localuser, member.id);
			div.classList.add("voiceuser", "flexltr");
			const span = document.createElement("span");
			span.textContent = member.name;
			member.subName(span);

			const tray = document.createElement("div");
			tray.classList.add("flexltr", "voiceTray");

			div.append(member.user.buildpfp(member), span, tray);
			member.user.bind(div, member.guild);

			this.voiceTray.set(member.id, tray);
			this.boxChange(member.id, obj);
			return div;
		});

		voiceUsers.innerHTML = "";
		voiceUsers.append(...html);
	}
	get myhtml() {
		if (this.html) {
			return this.html.deref();
		} else {
			return;
		}
	}
	readbottom() {
		if (!this.unreadState()) {
			this.guild.unreads();
			return;
		}
		this.mentions = 0;
		console.log(this.trueLastMessageid);
		fetch(this.info.api + "/channels/" + this.id + "/messages/" + this.trueLastMessageid + "/ack", {
			method: "POST",
			headers: this.headers,
			body: JSON.stringify({}),
		});
		const next = this.messages.get(this.idToNext.get(this.lastreadmessageid as string) as string);
		this.lastreadmessageid = this.trueLastMessageid;
		this.guild.unreads();
		this.unreads();
		if (next) {
			next.generateMessage();
		}
		this.infinite.toBottom();
	}
	static lastDragDiv = document.createElement("div");
	coatDropDiv(div: HTMLDivElement, container: HTMLElement | false = false) {
		div.style.position = "relative";
		div.addEventListener("dragenter", (event) => {
			console.log("enter");
			event.preventDefault();
		});
		const dragDiv = () => {
			if (Channel.lastDragDiv !== div) {
				Channel.lastDragDiv.classList.remove("dragTopView");
				Channel.lastDragDiv.classList.remove("dragBottomView");
			}
			Channel.lastDragDiv = div;
		};

		div.addEventListener("dragover", (event) => {
			dragDiv();
			const height = div.getBoundingClientRect().height;

			if (event.offsetY / height < 0.5) {
				div.classList.add("dragTopView");
				div.classList.remove("dragBottomView");
			} else {
				div.classList.remove("dragTopView");
				div.classList.add("dragBottomView");
			}
			event.preventDefault();
		});
		div.addEventListener("dragleave", () => {
			dragDiv();
			div.classList.remove("dragTopView");
			div.classList.remove("dragBottomView");
		});
		div.addEventListener("drop", (event) => {
			dragDiv();
			div.classList.remove("dragTopView");
			div.classList.remove("dragBottomView");
			const that = Channel.dragged[0];
			if (!that) return;
			event.preventDefault();
			const height = div.getBoundingClientRect().height;
			const before = event.offsetY / height < 0.5;
			if (container && that.type !== 4 && !before) {
				that.move_id = this.id;
				if (that.parent) {
					that.parent.children.splice(that.parent.children.indexOf(that), 1);
				}
				that.parent = this;
				container.prepend(Channel.dragged[1] as HTMLDivElement);
				this.children.unshift(that);
			} else {
				console.log(this, Channel.dragged);
				let thisy = this as Channel;
				if (that.type === 4) {
					console.log("check", this);
					if (this.parent) {
						thisy = this.parent;
						console.log("overwrite :3", thisy);
					}
				}
				that.move_id = thisy.parentId;
				if (that.parent) {
					that.parent.children.splice(that.parent.children.indexOf(that), 1);
				} else {
					thisy.guild.headchannels.splice(thisy.guild.headchannels.indexOf(that), 1);
				}
				that.parent = thisy.parent;
				if (that.parent) {
					const build: Channel[] = [];
					for (let i = 0; i < that.parent.children.length; i++) {
						build.push(that.parent.children[i]);
						if (that.parent.children[i] === thisy) {
							if (before) build.pop();
							build.push(that);
							if (before) build.push(thisy);
						}
					}
					that.parent.children = build;
					console.log(build);
				} else {
					const build: Channel[] = [];
					for (let i = 0; i < thisy.guild.headchannels.length; i++) {
						build.push(thisy.guild.headchannels[i]);
						if (thisy.guild.headchannels[i] === thisy) {
							if (before) build.pop();
							build.push(that);
							if (before) build.push(thisy);
						}
					}
					thisy.guild.headchannels = build;
				}
				if (Channel.dragged[1]) {
					if (this === thisy && this.type !== 4) {
						if (before) {
							div.before(Channel.dragged[1]);
						} else {
							div.after(Channel.dragged[1]);
						}
					} else {
						let tdiv = div.parentElement as HTMLDivElement;
						if (!tdiv) return;
						tdiv = tdiv.parentElement as HTMLDivElement;
						if (!tdiv) return;

						Channel.dragged[1].remove();
						if (before) {
							tdiv.before(Channel.dragged[1]);
						} else {
							tdiv.after(Channel.dragged[1]);
						}
					}
				}
			}
			this.guild.calculateReorder(that.id);
		});

		return div;
	}
	createChannel(name: string, type: number) {
		fetch(this.info.api + "/guilds/" + this.guild.id + "/channels", {
			method: "POST",
			headers: this.headers,
			body: JSON.stringify({
				name,
				type,
				parent_id: this.id,
				permission_overwrites: [],
			}),
		})
			.then((_) => _.json())
			.then((_) => this.guild.goToChannelDelay(_.id));
	}
	deleteChannel() {
		fetch(this.info.api + "/channels/" + this.id, {
			method: "DELETE",
			headers: this.headers,
		});
	}
	setReplying(message: Message) {
		if (this.replyingto?.div) {
			this.replyingto.div.classList.remove("replying");
		}
		this.replyingto = message;
		TypeBox.focus();
		if (!this.replyingto?.div) return;
		this.replyingto.div.classList.add("replying");
		this.makereplybox();
	}
	makereplybox() {
		TypeBox.updateReplying();
	}
	async getmessage(id: string): Promise<Message | undefined> {
		const message = this.messages.get(id);
		if (message) {
			return message;
		} else {
			const gety = await fetch(
				this.info.api + "/channels/" + this.id + "/messages?limit=1&around=" + id,
				{headers: this.headers},
			);
			const json = await gety.json();
			if (json.length === 0) {
				return undefined;
			}
			return new Message(json[0], this);
		}
	}
	async getMessages(id: string) {
		const m = await this.getmessage(id);
		if (!m) return;
		const waits: Promise<unknown>[] = [];
		let m1: string | undefined = m.id;
		for (let i = 0; i <= 50; i++) {
			if (!m1) {
				waits.push(this.grabBefore(id));
				break;
			}
			if ((this.idToNext.has(m1) && !this.idToNext.get(m1)) || this.lastmessage?.id === m1) break;
			m1 = this.idToNext.get(m1);
		}
		m1 = m.id;
		for (let i = 0; i <= 50; i++) {
			if (!m1) {
				waits.push(this.grabAfter(id));
				break;
			}
			if (this.idToPrev.has(m1) && !this.idToPrev.get(m1)) break;
			m1 = this.idToPrev.get(m1);
		}
		await Promise.all(waits);
		console.log(waits);
	}
	async focus(id: string, flash = true) {
		const prom = this.getMessages(id);

		if (await Promise.race([prom, new Promise((res) => setTimeout(() => res(true), 300))])) {
			const loading = document.getElementById("loadingdiv") as HTMLDivElement;
			Channel.regenLoadingMessages();
			loading.classList.add("loading");
			await prom;
			loading.classList.remove("loading");
		}

		if (this.localuser.focusChannel !== this) {
			await this.getHTML(true);
		}

		try {
			await this.infinite.focus(id, flash, true);
		} catch {}
	}
	editLast() {
		let message: Message | undefined = this.lastmessage;
		while (message && message.author !== this.localuser.user) {
			message = this.messages.get(this.idToPrev.get(message.id) as string);
		}
		if (message) {
			message.setEdit();
		}
	}
	static genid: number = 0;
	nsfwPannel() {
		const messages = document.getElementById("scrollWrap") as HTMLDivElement;
		const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer"));
		for (const thing of messageContainers) {
			thing.remove();
		}
		const elements = Array.from(messages.getElementsByClassName("scroller"));
		for (const elm of elements) {
			elm.remove();
			console.warn("rouge element detected and removed");
		}
		const div = document.getElementById("sideDiv") as HTMLDivElement;
		div.innerHTML = "";
		const float = new Float("");
		const options = float.options;
		//@ts-ignore weird hack, ik, but the user here does have that information
		//TODO make an extention of the user class with these aditional properties
		//TODO make a popup for `nsfw_allowed==null` to input age
		if (this.localuser.user.nsfw_allowed) {
			options.addTitle("This is a NSFW channel, do you wish to proceed?");
			const buttons = options.addOptions("", {ltr: true});
			buttons.addButtonInput("", "Yes", () => {
				this.perminfo.nsfwOk = true;
				this.localuser.focusChannel = undefined;
				this.getHTML();
			});
			buttons.addButtonInput("", "No", () => {
				window.history.back();
			});
		} else {
			options.addTitle("You are not allowed in this channel.");
		}
		const html = float.generateHTML();
		html.classList.add("messagecontainer");
		messages.append(html);
	}
	unreadPins() {
		if (!this.last_pin_timestamp && !this.lastpin) return false;
		return this.last_pin_timestamp !== this.lastpin;
	}
	boxMap = new Map<string, HTMLElement>();
	liveMap = new Map<string, HTMLElement>();
	destUserBox(user: User) {
		const box = this.boxMap.get(user.id);
		if (!box) return;
		box.remove();
		this.boxMap.delete(user.id);
		const live = this.liveMap.get(user.id);
		if (live) {
			live.remove();
			this.liveMap.delete(user.id);
		}
	}
	boxVid(id: string, elm: HTMLVideoElement) {
		//TODO make a loading screen thingy if the video isn't progressing in time yet
		const box = this.boxMap.get(id);
		if (!box) return;
		console.log("vid", elm);
		box.append(elm);
	}
	makeBig(box: HTMLElement) {
		const par = box.parentElement;
		if (!par) return;
		if (par.children[0] !== box || !box.classList.contains("bigBox")) {
			box.classList.add("bigBox");
			if (par.children[0] !== box) {
				par.children[0].classList.remove("bigBox");
			}
		} else {
			par.children[0].classList.remove("bigBox");
		}
		par.prepend(box);
	}
	decorateLive(id: string) {
		if (!this.voice) return;
		const box = this.liveMap.get(id);
		if (!box) return;
		box.innerHTML = "";
		const live = this.voice.getLive(id);
		const self = id === this.localuser.user.id;
		if (!this.voice.open) {
			const span = document.createElement("span");
			span.textContent = I18n.vc.joinForStream();
			box.append(span);
		} else if (live) {
			const leave = document.createElement("button");
			leave.classList.add("leave");
			leave.textContent = self ? I18n.vc.stopstream() : I18n.vc.leavestream();
			leave.onclick = (e) => {
				e.stopImmediatePropagation();
				if (self) {
					this.voice?.stopStream();
				} else {
					this.voice?.leaveLive(id);
				}
			};
			box.append(live, leave);
		} else if (!self) {
			const joinB = document.createElement("button");
			joinB.textContent = I18n.vc.joinstream();
			joinB.classList.add("joinb");
			box.append(joinB);
			joinB.onclick = () => {
				if (!this.voice) return;
				box.innerHTML = "";
				const span = document.createElement("span");
				span.textContent = I18n.vc.joiningStream();
				box.append(span);
				this.voice.joinLive(id);
			};
		}
	}
	purgeVid(id: string) {
		const box = this.boxMap.get(id);
		if (!box) return;
		const videos = Array.from(box.getElementsByTagName("video"));
		videos.forEach((_) => _.remove());
	}
	boxChange(id: string, change: {deaf: boolean; muted: boolean; video: boolean; live: boolean}) {
		const box = this.boxMap.get(id);

		if (!this.voice) return;
		if (box) {
			console.warn("purge:" + id);
			const vid = this.voice.videos.get(id);
			if (vid && change.video) {
				this.boxVid(id, vid);
			} else if (!change.video) {
				this.purgeVid(id);
			}
			Array.from(box.getElementsByClassName("statBub")).forEach((_) => _.remove());
			const statBub = document.createElement("div");
			statBub.classList.add("statBub");
			if (change.muted) {
				const span = document.createElement("span");
				span.classList.add("svg-micmute");
				statBub.append(span);
				box.append(statBub);
			} else if (change.video) {
				const span = document.createElement("span");
				span.classList.add("svg-video");
				statBub.append(span);
				box.append(statBub);
			}
		}

		const live = this.liveMap.get(id);
		if (live && !change.live) {
			live.remove();
			this.liveMap.delete(id);
		} else if (!live && change.live && box) {
			const livediv = document.createElement("div");
			this.liveMap.set(id, livediv);
			livediv.onclick = () => {
				this.makeBig(livediv);
			};
			box.parentElement?.prepend(livediv);
			this.decorateLive(id);
		}

		const tray = this.voiceTray.get(id);
		if (tray) {
			console.warn("tray build", tray, change);
			tray.innerHTML = "";
			if (change.muted) {
				const span = document.createElement("span");
				span.classList.add("svg-micmute");
				tray.append(span);
			}
			if (change.video) {
				const span = document.createElement("span");
				span.classList.add("svg-video");
				tray.append(span);
			}
		}
	}
	async makeUserBox(user: User, users: HTMLElement) {
		const memb = Member.resolveMember(user, this.guild);
		const box = document.createElement("div");
		userVolMenu.bindContextmenu(box, this.localuser, user.id);
		box.onclick = () => {
			this.makeBig(box);
		};
		this.boxMap.set(user.id, box);
		if (user.accent_color != undefined) {
			box.style.setProperty(
				"--accent_color",
				`#${user.accent_color.toString(16).padStart(6, "0")}`,
			);
		}
		memb.then((_) => {
			if (!_) return;
			if (_.accent_color !== undefined) {
				box.style.setProperty("--accent_color", `#${_.accent_color.toString(16).padStart(6, "0")}`);
			}
		});

		box.append(user.buildpfp(this.guild));

		const span = document.createElement("span");
		span.textContent = user.name;
		memb.then((_) => {
			if (!_) {
				user.subName(span);
				return;
			}
			_.subName(span);
			span.textContent = _.name;
		});
		span.classList.add("voiceUsername");
		box.append(span);
		users.append(box);
		if (!this.voice) return;
		const change = this.voice.userids.get(user.id);
		if (!change) return;
		this.boxChange(user.id, change);
	}
	usersDiv = new WeakRef(document.createElement("div"));
	async setUpVoiceArea() {
		if (!this.voice) throw new Error("voice not found?");
		const voiceArea = document.getElementById("voiceArea") as HTMLElement;
		const buttonRow = document.createElement("div");
		buttonRow.classList.add("flexltr", "buttonRow");
		const updateMicIcon = () => {
			mspan.classList.remove("svg-micmute", "svg-mic");
			mspan.classList.add(this.localuser.mute ? "svg-micmute" : "svg-mic");
		};

		const mute = document.createElement("div");
		const mspan = document.createElement("span");
		mute.append(mspan);
		updateMicIcon();
		this.localuser.updateOtherMic = updateMicIcon;
		mute.onclick = () => {
			this.localuser.mute = !this.localuser.mute;
			this.localuser.updateMic();
		};
		mute.classList.add("muteVoiceIcon");

		const muteOpt = document.createElement("div");
		muteOpt.classList.add("muteOptDiv");
		const mOptSpan = document.createElement("span");
		mOptSpan.classList.add("svg-category");
		muteOpt.append(mOptSpan);
		mute.append(muteOpt);
		muteOpt.onclick = async (e) => {
			e.preventDefault();
			e.stopImmediatePropagation();
			const menu = new Contextmenu<void, void>("");
			const mics = await this.localuser.getAudioDeviceList();
			for (const mic of mics) {
				menu.addButton(
					mic.label,
					() => {
						this.localuser.setNewDefualtDevice(mic.deviceId);
					},
					{
						icon: {
							css:
								mic.deviceId === this.localuser.getDefaultAudio() ? "svg-select" : "svg-noSelect",
						},
					},
				);
			}
			const box = muteOpt.getBoundingClientRect();
			menu.makemenu(box.left, box.top - 34 - window.innerHeight);
		};

		const updateCallIcon = () => {
			cspan.classList.remove("svg-call", "svg-hangup");
			cspan.classList.add(this.voice?.open ? "svg-hangup" : "svg-call");
		};
		const call = document.createElement("div");
		const cspan = document.createElement("span");
		call.append(cspan);
		updateCallIcon();
		call.onclick = async () => {
			if (this.voice?.userids.has(this.localuser.user.id)) {
				this.voice.leave();
			} else if (this.voice) {
				await this.localuser.joinVoice(this);
			}
			updateCallIcon();
		};
		call.classList.add("callVoiceIcon");

		const updateVideoIcon = () => {
			vspan.classList.remove("svg-video", "svg-novideo");
			vspan.classList.add(this.localuser.voiceFactory?.video ? "svg-video" : "svg-novideo");
		};
		const video = document.createElement("div");
		const vspan = document.createElement("span");
		video.append(vspan);
		updateVideoIcon();
		video.onclick = async () => {
			if (!this.voice) return;
			if (!this.voice.open) return;
			if (this.localuser.voiceFactory?.video) {
				this.voice.stopVideo();
			} else {
				const cam = await navigator.mediaDevices.getUserMedia({
					video: {
						advanced: [
							{
								aspectRatio: 1.75,
							},
						],
					},
				});
				if (!cam) return;
				this.voice.startVideo(cam);
			}
			updateVideoIcon();
		};
		video.classList.add("callVoiceIcon");

		const updateLiveIcon = () => {
			lspan.classList.remove("svg-stream", "svg-stopstream");
			lspan.classList.add(this.voice?.isLive() ? "svg-stopstream" : "svg-stream");
		};
		const live = document.createElement("div");
		const lspan = document.createElement("span");
		live.append(lspan);
		updateLiveIcon();
		live.onclick = async () => {
			if (!this.voice?.open) return;
			if (this.voice?.isLive()) {
				this.voice?.stopStream();
			} else {
				const stream = await navigator.mediaDevices.getDisplayMedia();
				const v = await this.voice?.createLive(stream);
				console.log(v);
			}
			updateLiveIcon();
		};
		live.classList.add("callVoiceIcon");

		const chat = document.createElement("div");
		const chatspan = document.createElement("span");
		chatspan.classList.add("svg-frmessage");
		chat.append(chatspan);
		updateLiveIcon();
		chat.onclick = async () => {
			this.voiceMode = this.voiceMode === "VoiceOnly" ? "ChatAndVoice" : "VoiceOnly";
			this.getHTML(true);
		};
		chat.classList.add("callVoiceIcon");

		buttonRow.append(mute, video, live, call, chat);

		const users = document.createElement("div");
		const mut = new MutationObserver(() => {
			const arr = Array.from(users.children);
			const box = arr.find((_) => _.classList.contains("bigBox"));
			if (box && arr[0] !== box) {
				users.prepend(box);
			}
		});
		mut.observe(users, {
			childList: true,
		});
		users.classList.add("voiceUsers");

		this.voice.userids.forEach(async (_, id) => {
			const user = await this.localuser.getUser(id);
			this.makeUserBox(user, users);
		});
		[...this.liveMap].forEach(([_, box]) => {
			users.prepend(box);
		});
		this.usersDiv = new WeakRef(users);

		voiceArea.append(users, buttonRow);
		this.voice.onVideo = (vid, id) => {
			this.localuser.regenVoiceIcons();
			console.warn("happened");
			this.boxVid(id, vid);
			updateVideoIcon();
		};
		this.voice.onGotStream = (_vid, id) => {
			this.localuser.regenVoiceIcons();
			updateLiveIcon();
			this.decorateLive(id);
		};
		this.voice.onconnect = () => {
			if (!this.voice) return;
			for (const [id] of this.liveMap) {
				this.decorateLive(id);
			}
		};
		this.voice.onLeaveStream = (id) => {
			this.decorateLive(id);
			updateLiveIcon();
			this.localuser.regenVoiceIcons();
		};

		this.voice.onLeave = () => {
			updateCallIcon();
			updateVideoIcon();
			for (const [id] of this.boxMap) {
				this.purgeVid(id);
			}
			if (!this.voice) return;
			for (const [id] of this.liveMap) {
				this.decorateLive(id);
			}
		};
	}
	curCommand?: Command;
	curWatch = () => {};
	async submitCommand() {
		if (!this.curCommand) return;
		if (await this.curCommand.submit(TypeBox.box, this)) {
			this.curCommand = undefined;
			TypeBox.markdown.boxEnabled = true;
			TypeBox.box.innerHTML = "";
			TypeBox.markdown.boxupdate();
			TypeBox.box.removeEventListener("keyup", this.curWatch);
		}
	}
	startCommand(command: Command) {
		this.curCommand = command;
		TypeBox.markdown.boxEnabled = false;
		const func = () => {
			const node = window.getSelection()?.focusNode;
			if (this.localuser.focusChannel === this) {
				const out = command.collect(TypeBox.box, this, node || undefined);
				if (!out) {
					TypeBox.markdown.boxEnabled = true;
					TypeBox.markdown.boxupdate();
					TypeBox.box.removeEventListener("keyup", func);
				}
			}
		};
		this.curWatch = func;
		TypeBox.box.addEventListener("keyup", func);
		command.render(TypeBox.box, this);
	}
	isForum() {
		return this.type === 15 || this.type === 16;
	}
	async renderThread(update = true) {
		let div = document.createElement("div");
		if (update) {
			const up = async () => {
				if (!document.contains(div)) {
					this.updateEvents.delete(up);
					return;
				}
				console.log(this.updateEvents);
				const c = await this.renderThread(false);
				if (!c) return;
				div.after(c);
				div.remove();
				div = c;
			};
			this.updateEvents.add(up);
		}
		div.classList.add("flexttb", "forumPostBody");
		div.onclick = (e) => {
			if (e.button === 0) this.getHTML();
		};
		Channel.contextmenu.bindContextmenu(div, this, undefined);
		const tags = document.createElement("div");
		tags.classList.add("flexltr", "tagDiv");
		for (const tagid of this.appliedTags) {
			const tag = this.parent?.availableTags.find((tag) => tag.id === tagid);
			if (!tag) continue;
			tags.append(tag.makeHTML());
		}
		div.append(tags);

		const title = document.createElement("h3");
		title.textContent = new MarkDown(this.shortName).makeHTML().textContent;
		div.append(title);

		const member =
			(await this.localuser.getMember(this.owner_id as string, this.guild.id)) ||
			(await User.resolve(this.owner_id as string, this.localuser));
		const message = this.localuser.messages.get(this.id);

		const name = document.createElement("span");
		name.classList.add("forumUsername");
		name.textContent = (member?.name || I18n.user.deleted()) + ": ";

		const messageContent = document.createElement("span");
		messageContent.classList.add("messageForumContent");
		if (message) messageContent.append(message.content.makeHTML());
		else messageContent.textContent = I18n.message.deleted();

		const mrow = document.createElement("div");
		mrow.classList.add("flexltr");
		mrow.append(name, messageContent);
		div.append(mrow);

		const micon = document.createElement("span");
		micon.classList.add("svg-frmessage", "svgicon");

		const mcount = document.createElement("span");
		mcount.textContent = this.messageCount + "";

		const msep = document.createElement("span");
		msep.classList.add("msep");

		const mtime = document.createElement("span");
		const updateTime = () => {
			mtime.textContent = MarkDown.relTime(
				new Date(
					this.lastmessageid ? SnowFlake.stringToUnixTime(this.lastmessageid) : this.getUnixTime(),
				),
				() => {
					if (!document.contains(mtime)) return;
					updateTime();
				},
			);
		};
		updateTime();

		const lrow = document.createElement("div");
		lrow.classList.add("forumMessageRow", "flexltr");

		lrow.append(micon, mcount, msep, mtime);
		div.append(lrow);

		return div;
	}
	appliedTags = [] as string[];
	forumFilters = {
		sortActive: true,
		recentFirst: true,
		tagMatchAll: false,
		tags: [] as string[],
	};
	hasFetchedForForum = false;
	editTags() {
		const d = new Dialog(I18n.threads.editTags());
		if (!this.appliedTags || !this.parent) return;
		let tagList = [...this.appliedTags];
		const patchList = () => {
			fetch(this.info.api + "/channels/" + this.id, {
				method: "PATCH",
				headers: this.headers,
				body: JSON.stringify({
					applied_tags: tagList,
				}),
			});
		};

		const tags = document.createElement("div");
		tags.classList.add("forumTagSelect");
		for (const tag of this.parent.availableTags.filter(
			(tag) => !tag.moderated || this.hasPermission("MANAGE_THREADS"),
		)) {
			const html = tag.makeHTML();
			html.onclick = () => {
				if (tagList.includes(tag.id)) {
					tagList = tagList.filter((id) => id !== tag.id);
					html.classList.remove("selected");
					patchList();
				} else {
					tagList.push(tag.id);
					tagList.sort();
					html.classList.add("selected");
					patchList();
				}
			};
			if (tagList.includes(tag.id)) html.classList.add("selected");
			tags.append(html);
		}
		const opt = d.options;
		opt.addHTMLArea(tags);
		d.show();
	}
	async fetchForum() {
		const arr = (await (
			await fetch(this.info.api + "/channels/" + this.id + "/post-data", {
				method: "POST",
				headers: this.headers,
				body: JSON.stringify({thread_ids: this.children.map(({id}) => id)}),
			})
		).json()) as {
			threads: Record<string, {first_message: null | messagejson; owner: null | memberjson}>;
		};
		for (const [id, {first_message, owner}] of Object.entries(arr.threads)) {
			const child = this.children.find(({id: cid}) => cid === id);
			if (!child) continue;
			if (owner) Member.new(owner, this.guild);
			if (first_message) {
				const m = this.localuser.messages.get(first_message.id);
				if (!m) this.localuser.messages.set(first_message.id, new Message(first_message, child));
			}
		}
		this.hasFetchedForForum = true;
		return;
	}

	umap = new Map<string, [number, string]>();
	search: [string, number, string][] = [];
	hasAllThreads = false;

	async fetchThreads(text: string, offset: number) {
		const filters = this.forumFilters;
		const uid = `${filters.recentFirst}-${filters.sortActive}-${filters.tagMatchAll}-${filters.tags.join("-")}-${text}`;
		const search = new URLSearchParams([
			["archived", "true"],
			["limit", "25"],
			["sort_by", filters.sortActive ? "last_message_time" : "creation_time"],
			["sort_order", filters.recentFirst ? "desc" : "asc"],
			["tag_setting", filters.tagMatchAll ? "match_all" : "match_some"],
			["offset", offset + ""],
			["tag", filters.tags.join(",")],
		]);
		const res = (await (
			await fetch(this.info.api + "/channels/" + this.id + "/threads/search?" + search, {
				headers: this.headers,
			})
		).json()) as {
			threads: channeljson[];
			members: memberjson[];
			messages: messagejson[];
			total_results: number;
			has_more: boolean;
		};
		for (const threadjson of res.threads) {
			if (this.localuser.channels.has(threadjson.id)) continue;
			const thread = new Channel(threadjson, this.guild);
			this.localuser.channels.set(threadjson.id, thread);
			thread.resolveparent();
		}
		for (const message of res.messages) {
			const thread = this.localuser.channels.get(message.channel_id);
			if (!thread) continue;
			const m = this.localuser.messages.get(message.id);
			if (!m) this.localuser.messages.set(message.id, new Message(message, thread));
		}
		for (const member of res.members) {
			Member.new(member, this.guild);
		}
		if (res.has_more) {
			const lastid = res.threads.at(-1)?.id as string;
			if (text) {
				const arr = this.search.find(([id]) => id === uid);
				if (arr) {
					arr[1] = offset + 25;
					arr[2] = lastid;
				} else {
					this.search.push([uid, offset + 25, lastid]);
					if (this.search.length > 200) {
						this.search.shift();
					}
				}
			} else {
				this.umap.set(uid, [offset + 25, lastid]);
			}
		} else {
			if (text) {
				const arr = this.search.find(([id]) => id === uid);
				if (arr) {
					arr[1] = offset + 25;
					arr[2] = "-1";
				} else {
					this.search.push([uid, offset + 25, "-1"]);
					if (this.search.length > 200) {
						this.search.shift();
					}
				}
			} else {
				this.umap.clear();
				this.search = [];
				this.hasAllThreads = true;
			}
		}
	}

	genCurSort(text: string, offset: number) {
		const filters = this.forumFilters;
		text = text.toLowerCase();
		let match = this.children.filter((thread) => thread.name.toLowerCase().includes(text));
		const tagSet = new Set(filters.tags);
		if (tagSet.size)
			match = match.filter((thread) => {
				const tags = new Set(thread.appliedTags);
				if (filters.tagMatchAll) {
					return tagSet.isSubsetOf(tags);
				} else {
					return tags.intersection(tagSet).size;
				}
			});
		match.sort((c1, c2) => {
			if (filters.sortActive) {
				return +(Number(c1.lastmessageid || c1.id) > Number(c2.lastmessageid || c2.id)) ^
					+filters.recentFirst
					? 1
					: -1;
			} else {
				return +(Number(c1.id) > Number(c2.id)) ^ +filters.recentFirst ? 1 : -1;
			}
		});

		return [match.slice(offset, offset + 25), !!match.at(offset + 25)] as const;
	}

	async findMatches(text: string, offset: number): Promise<readonly [Channel[], boolean]> {
		if (this.hasAllThreads) return this.genCurSort(text, offset);

		const filters = this.forumFilters;
		const uid = `${filters.recentFirst}-${filters.sortActive}-${filters.tagMatchAll}-${filters.tags.join("-")}-${text}`;
		let offset2 = 0;
		let lastid = "";
		if (text) {
			[, offset2, lastid] = this.search.find(([id]) => id === uid) || ["", 0, ""];
		} else {
			[offset2, lastid] = this.umap.get(uid) || [0, ""];
		}

		if (lastid) {
			const [list, more] = this.genCurSort(text, offset);
			for (const elm of list) {
				if (elm.id === lastid) {
					await this.fetchThreads(text, offset2);
					return this.genCurSort(text, offset);
				}
			}
			return [list, more];
		} else {
			await this.fetchThreads(text, offset2);
			return this.genCurSort(text, offset);
		}
	}

	async forumSearch(text: string, div: HTMLDivElement) {
		while (!this.hasFetchedForForum) await this.fetchForum();
		let offset = 0;
		const flipPage = async (by: number) => {
			offset += by;
			div.scrollTop = 0;
			const [match, more] = await this.findMatches(text, offset);

			await renderDiv(match, more);
		};

		const renderDiv = async (threads: Channel[], more: boolean) => {
			div.innerHTML = "";

			const sortRow = document.createElement("div");
			sortRow.classList.add("flexltr", "forumSortRow");
			div.append(sortRow);

			const cMenu = new Contextmenu<void, void>("sortAndStuff");
			const opts = I18n.forum.sortOptions;

			cMenu.addText(opts.sortby.title());
			cMenu.addButton(
				opts.sortby.recent(),
				() => {
					this.forumFilters.sortActive = true;
					flipPage(0);
				},
				{
					icon: {
						css: this.forumFilters.sortActive ? "svg-select" : "svg-noSelect",
					},
				},
			);
			cMenu.addButton(
				opts.sortby.posted(),
				() => {
					this.forumFilters.sortActive = false;
					flipPage(0);
				},
				{
					icon: {
						css: this.forumFilters.sortActive ? "svg-noSelect" : "svg-select",
					},
				},
			);
			cMenu.addSeperator();

			cMenu.addText(opts.sortOrder.title());
			cMenu.addButton(
				opts.sortOrder.recent(),
				() => {
					this.forumFilters.recentFirst = true;
					flipPage(0);
				},
				{
					icon: {
						css: this.forumFilters.recentFirst ? "svg-select" : "svg-noSelect",
					},
				},
			);

			cMenu.addButton(
				opts.sortOrder.old(),
				() => {
					this.forumFilters.recentFirst = false;
					flipPage(0);
				},
				{
					icon: {
						css: this.forumFilters.recentFirst ? "svg-noSelect" : "svg-select",
					},
				},
			);
			cMenu.addSeperator();

			cMenu.addText(opts.tagMatch.title());
			cMenu.addButton(
				opts.tagMatch.some(),
				() => {
					this.forumFilters.tagMatchAll = false;
					flipPage(0);
				},
				{
					icon: {css: this.forumFilters.tagMatchAll ? "svg-noSelect" : "svg-select"},
				},
			);
			cMenu.addButton(
				opts.tagMatch.all(),
				() => {
					this.forumFilters.tagMatchAll = true;
					flipPage(0);
				},
				{
					icon: {
						css: this.forumFilters.tagMatchAll ? "svg-select" : "svg-noSelect",
					},
				},
			);

			const sortOptionButton = document.createElement("button");
			sortOptionButton.textContent = opts.name();

			sortOptionButton.onclick = (e) => {
				if (e.button !== 0) return;
				e.preventDefault();
				e.stopImmediatePropagation();
				const box = sortOptionButton.getBoundingClientRect();
				cMenu.makemenu(box.left, box.bottom + 6);
			};

			const tags = document.createElement("div");
			tags.classList.add("forumTagSelect");

			const allMenu = new Contextmenu<void, void>("allTagMenu");

			for (const tag of this.availableTags) {
				const html = tag.makeHTML();
				if (this.forumFilters.tags.includes(tag.id)) html.classList.add("selected");
				html.onclick = () => {
					if (this.forumFilters.tags.includes(tag.id)) {
						this.forumFilters.tags = this.forumFilters.tags.filter((id) => id !== tag.id);
					} else {
						this.forumFilters.tags.push(tag.id);
						this.forumFilters.tags.sort();
					}
					flipPage(0);
				};
				tags.append(html);
				allMenu.addButton(
					tag.name,
					() => {
						if (this.forumFilters.tags.includes(tag.id)) {
							this.forumFilters.tags = this.forumFilters.tags.filter((id) => id !== tag.id);
						} else {
							this.forumFilters.tags.push(tag.id);
							this.forumFilters.tags.sort();
						}
						flipPage(0);
					},
					{
						icon: {css: this.forumFilters.tags.includes(tag.id) ? "svg-select" : "svg-noSelect"},
					},
				);
			}

			const allTags = document.createElement("button");
			allTags.classList.add("allTagButton");
			allTags.textContent = I18n.forum.allTags();

			allTags.onclick = (e) => {
				if (e.button !== 0) return;
				e.preventDefault();
				e.stopImmediatePropagation();
				const box = allTags.getBoundingClientRect();
				allMenu.makemenu(box.right - window.innerWidth, box.bottom + 6);
			};

			sortRow.append(sortOptionButton, tags, allTags);

			div.append(...(await Promise.all(threads.map((_) => _.renderThread()))));

			const buttonRow = document.createElement("div");
			buttonRow.classList.add("flexltr", "forumButtonRow");

			const next = document.createElement("button");
			next.textContent = I18n.forum.next();
			next.disabled = !more;
			if (more) {
				next.onclick = () => {
					flipPage(25);
				};
			}

			const back = document.createElement("button");
			back.textContent = I18n.forum.back();
			back.disabled = !offset;
			if (offset) {
				back.onclick = () => {
					flipPage(-25);
				};
			}

			buttonRow.append(back, next);
			div.append(buttonRow);
		};
		await flipPage(0);
	}
	renderForum() {
		(document.getElementById("typediv") as HTMLElement).style.visibility = "hidden";
		const container = document.createElement("div");

		container.classList.add("messagecontainer", "flexttb", "forumBody");
		(document.getElementById("scrollWrap") as HTMLElement).append(container);
		const superContainer = document.createElement("div");
		superContainer.classList.add("forumHead", "flexttb");
		const headContainer = document.createElement("div");
		superContainer.append(headContainer);
		headContainer.classList.add("flexltr");
		container.append(superContainer);

		const icon = document.createElement("span");
		icon.classList.add("svgicon", "svg-search", "forumIcon");
		headContainer.append(icon);

		const text = document.createElement("input");
		text.type = "text";
		text.classList.add("forumSearch");
		headContainer.append(text);
		text.placeholder = I18n.forum.creorsear();

		const post = document.createElement("button");
		post.textContent = I18n.forum.newPost();
		post.classList.add("newPostForumButton");
		headContainer.append(post);

		post.onclick = () => {
			const postF = async () => {
				if (this.flags & (1 << 4) && !tagList.length) {
					showError(I18n.forum.errors.tagsReq());
					return;
				}
				const content = md.rawString;
				if (!content) {
					showError(I18n.forum.errors.requireText());
					return;
				}
				const res = (await (
					await fetch(this.info.api + "/channels/" + this.id + "/threads", {
						method: "POST",
						headers: this.headers,
						body: JSON.stringify({
							name: text.value,
							applied_tags: tagList,
							message: {
								content,
							},
						}),
					})
				).json()) as channeljson;
				this.localuser.goToChannel(res.id);
			};
			post.onclick = postF;
			post.textContent = I18n.forum.post();
			const box = document.createElement("div");
			box.classList.add("messageEditContainer");
			const area = document.createElement("div");
			const sb = document.createElement("div");
			sb.style.position = "absolute";
			sb.style.width = "100%";
			const search = document.createElement("div");
			search.classList.add("searchOptions", "flexttb");
			area.classList.add("editMessage");
			try {
				area.contentEditable = "plaintext-only";
			} catch {
				area.contentEditable = "true";
			}
			const md = new MarkDown("", this, {keep: true});
			area.append(md.makeHTML());
			area.addEventListener("keyup", (event) => {
				if (this.localuser.keyup(event)) return;
			});
			area.addEventListener("keydown", (event) => {
				this.localuser.keydown(event);
			});
			md.giveBox(area, (str, pre) => {
				this.localuser.search(search, md, str, pre);
			});
			sb.append(search);
			box.append(sb, area);
			superContainer.append(box);
			setTimeout(() => {
				area.focus();
				const fun = saveCaretPosition(area, Infinity);
				if (fun) fun();
			});
			box.oncontextmenu = (e) => {
				e.stopImmediatePropagation();
			};

			let tagList: string[] = [];

			const tags = document.createElement("div");
			tags.classList.add("forumTagSelect");
			for (const tag of this.availableTags.filter(
				(tag) => !tag.moderated || this.hasPermission("MANAGE_THREADS"),
			)) {
				const html = tag.makeHTML();
				html.onclick = () => {
					if (tagList.includes(tag.id)) {
						tagList = tagList.filter((id) => id !== tag.id);
						html.classList.remove("selected");
					} else {
						tagList.push(tag.id);
						tagList.sort();
						html.classList.add("selected");
					}
				};
				tags.append(html);
			}
			superContainer.append(tags);
			function showError(text: string) {
				errorText.textContent = text;
				setTimeout(() => {
					errorText.textContent = "";
				}, 5000);
			}

			const errorText = document.createElement("span");
			errorText.classList.add("forumPostError");
			superContainer.append(errorText);
		};

		const theadList = document.createElement("div");
		theadList.classList.add("flexttb", "forumList");
		container.append(theadList);

		let curidsearch = 0;
		text.onkeyup = async () => {
			const thisid = ++curidsearch;
			await new Promise((res) => setTimeout(res, 120));
			if (thisid !== curidsearch) return;
			this.forumSearch(text.value, theadList);
		};
		this.forumSearch("", theadList);
	}
	async getHTML(addstate = true, getMessages: boolean | void = undefined, aroundMessage?: string) {
		if (!this.visible) {
			this.guild.loadChannel();
			return;
		}
		if (this.owner instanceof Direct) {
			this.owner.freindDiv?.classList.remove("viewChannel");
		}
		TypeBox.saveBox();
		if (!this.curCommand && !this.isForum()) {
			TypeBox.restoreBox(this);
		}
		TypeBox.markdown.boxEnabled = !this.curCommand;
		if (this.curCommand) {
			this.curCommand.render(TypeBox.box, this);
		}

		if (getMessages === undefined) {
			getMessages = this.type !== 2 || !this.localuser.voiceAllowed;
		}

		const messages = document.getElementById("scrollWrap") as HTMLDivElement;
		const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer"));
		for (const thing of messageContainers) {
			thing.remove();
		}
		const chatArea = document.getElementById("chatArea") as HTMLElement;

		const voiceArea = document.getElementById("voiceArea") as HTMLElement;
		voiceArea.innerHTML = "";
		if (getMessages && this.type !== 2) {
			chatArea.style.removeProperty("display");
		} else {
			if (this.voiceMode === "VoiceOnly") {
				chatArea.style.setProperty("display", "none");
			} else {
				chatArea.style.removeProperty("display");
				getMessages = true;
			}
			this.setUpVoiceArea();
			this.localuser.memberListUpdate();
		}

		const pinnedM = document.getElementById("pinnedMDiv");
		if (pinnedM) {
			if (this.unreadPins()) {
				pinnedM.classList.add("unreadPin");
			} else {
				pinnedM.classList.remove("unreadPin");
			}
		}
		if (addstate) {
			history.pushState(
				[this.guild.id, this.id, aroundMessage],
				"",
				"/channels/" + this.guild.id + "/" + this.id + (aroundMessage ? `/${aroundMessage}` : ""),
			);
		}
		this.localuser.pageTitle("#" + this.shortName);
		const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement;
		if (this.topic) {
			channelTopic.innerHTML = "";
			channelTopic.append(new MarkDown(this.topic, this).makeHTML());
			channelTopic.removeAttribute("hidden");
			channelTopic.onclick = () => {
				const d = new Dialog(this.name);
				d.options.addHTMLArea(new MarkDown(this.topic, this).makeHTML());
				d.show();
			};
		} else {
			channelTopic.setAttribute("hidden", "");
			channelTopic.onclick = () => {};
		}
		if (this.guild !== this.localuser.focusGuild) {
			this.guild.loadGuild();
		}

		if (this.localuser.focusChannel && this.localuser.focusChannel.myhtml) {
			this.localuser.focusChannel.myhtml.classList.remove("viewChannel");
		}
		if (this.myhtml) {
			this.myhtml.classList.add("viewChannel");
		}
		const id = ++Channel.genid;

		if (this.localuser.focusChannel && this.localuser.focusChannel !== this) {
			this.localuser.focusChannel.infinite.delete();

			if (this.localuser.focusChannel.isThread() && !this.localuser.focusChannel.member) {
				const prev = this.localuser.focusChannel;
				this.localuser.focusChannel = this;
				prev.parent?.createguildHTML();
			}
		} else if (this.localuser.focusChannel === this && !aroundMessage && !this.isForum()) {
			if (this.lastmessageid)
				this.infinite.focus(aroundMessage || this.lastmessageid, !!aroundMessage, true);
			return;
		}
		this.guild.prevchannel = this;
		this.guild.perminfo.prevchannel = this.id;
		this.localuser.focusChannel = this;

		if (this.isThread() && !this.member) {
			this.parent?.createguildHTML();
			if (this.myhtml) {
				this.myhtml.classList.add("viewChannel");
			}
		}

		if (
			this.nsfw && //@ts-ignore another hack
			(!this.perminfo.nsfwOk || !this.localuser.user.nsfw_allowed)
		) {
			this.nsfwPannel();
			return;
		}
		this.slowmode();
		this.localuser.getSidePannel();
		if (this.isForum()) {
			this.renderForum();
			return;
		}

		const prom = this.infinite.delete();
		if (getMessages) {
			const loading = document.getElementById("loadingdiv") as HTMLDivElement;
			Channel.regenLoadingMessages();
			loading.classList.add("loading");
		}
		this.rendertyping();

		TypeBox.changeWrite();

		try {
			if (getMessages) await this.putmessages();
		} catch (e) {
			if (e instanceof Error) {
				const d = new Dialog(e.message);
				d.show();
			}
			return;
		}

		await prom;
		if (id !== Channel.genid) {
			return;
		}
		this.makereplybox();

		if (getMessages) await this.buildmessages(aroundMessage);
		//loading.classList.remove("loading");
	}
	typingmap: Map<Member | User, number> = new Map();
	async typingStart(typing: startTypingjson): Promise<void> {
		const memb = typing.d.member
			? await Member.new(typing.d.member, this.guild)
			: await this.localuser.getUser(typing.d.user_id);
		if (!memb) return;
		this.typingmap.set(memb, Date.now());
		const user = memb instanceof User ? memb : memb.user;
		user.statusChange();
		setTimeout(() => {
			this.rendertyping();
			user.statusChange();
		}, 10000);
		if (memb.id === this.localuser.user.id) {
			console.log("you is typing");
			return;
		}
		console.log("user is typing and you should see it");
		this.rendertyping();
	}
	similar(str: string) {
		if (this.type === 4) return -1;
		const strl = Math.max(str.length, 1);
		if (this.name.includes(str)) {
			return strl / this.name.length;
		} else if (this.name.toLowerCase().includes(str.toLowerCase())) {
			return strl / this.name.length / 1.2;
		}
		return 0;
	}
	rendertyping(): void {
		const typingtext = document.getElementById("typing") as HTMLDivElement;
		let build = "";
		let showing = false;
		let i = 0;
		const curtime = Date.now() - 5000;
		for (const thing of this.typingmap.keys()) {
			const self = thing.id === this.localuser.user.id;
			if ((this.typingmap.get(thing) as number) > curtime) {
				if (!self) {
					if (i !== 0) {
						build += ", ";
					}
					i++;
					build += thing.name;
					showing = true;
				}
			} else {
				this.typingmap.delete(thing);
			}
		}
		build = I18n.typing(i + "", build);
		if (this.localuser.focusChannel === this) {
			if (showing) {
				typingtext.classList.remove("hidden");
				const typingtext2 = document.getElementById("typingtext") as HTMLDivElement;
				typingtext2.textContent = build;
			} else {
				typingtext.classList.add("hidden");
			}
		}
	}
	static regenLoadingMessages() {
		const loading = document.getElementById("loadingdiv") as HTMLDivElement;
		loading.innerHTML = "";
		for (let i = 0; i < 15; i++) {
			const div = document.createElement("div");
			div.classList.add("loadingmessage");
			if (Math.random() < 0.5) {
				const pfp = document.createElement("div");
				pfp.classList.add("loadingpfp");
				const username = document.createElement("div");
				username.style.width = Math.floor(Math.random() * 96 * 1.5 + 40) + "px";
				username.classList.add("loadingcontent");
				div.append(pfp, username);
			}
			const content = document.createElement("div");
			content.style.width = Math.floor(Math.random() * 96 * 3 + 40) + "px";
			content.style.height = Math.floor(Math.random() * 3 + 1) * 20 + "px";
			content.classList.add("loadingcontent");
			div.append(content);
			loading.append(div);
		}
	}
	lastmessage: Message | undefined;
	setnotifcation() {
		const optionsArr = ["all", "onlyMentions", "none", "default"] as const;
		const defualt = I18n.guild[optionsArr[this.guild.messageNotifications]]();
		const options = optionsArr.map((e) => I18n.guild[e](defualt));
		const notiselect = new Dialog("");
		const form = notiselect.options.addForm(
			"",
			(_, sent: any) => {
				notiselect.hide();
				console.log(sent);
				this.message_notifications = sent.channel_overrides[this.id].message_notifications;
			},
			{
				fetchURL: `${this.info.api}/users/@me/guilds/${this.guild.id}/settings/`,
				method: "PATCH",
				headers: this.headers,
			},
		);
		form.addSelect(
			I18n.guild.selectnoti(),
			"message_notifications",
			options,
			{
				radio: true,
				defaultIndex: this.message_notifications,
			},
			[0, 1, 2, 3],
		);

		form.addPreprocessor((e: any) => {
			const message_notifications = e.message_notifications;
			delete e.message_notifications;
			e.channel_overrides = {
				[this.id]: {
					message_notifications,
					muted: this.muted,
					mute_config: this.mute_config,
					channel_id: this.id,
				},
			};
		});
		notiselect.show();
	}
	async putmessages() {
		//TODO swap out with the WS op code
		if (this.allthewayup) {
			return;
		}
		if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) {
			return;
		}
		let response: messagejson[] | undefined = undefined;
		for (let i = 0; i < 5; i++) {
			try {
				const j = await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100", {
					headers: this.headers,
				});

				response = (await j.json()) as messagejson[];
				break;
			} catch {
				await new Promise((res) => setTimeout(res, 1000));
			}
		}
		if (!response) throw new Error(I18n.messageNotLoad());
		if (response.length !== 100) {
			this.allthewayup = true;
		}
		let prev: Message | undefined;
		for (const thing of response) {
			const message = new Message(thing, this);
			if (prev) {
				this.idToNext.set(message.id, prev.id);
				this.idToPrev.set(prev.id, message.id);
			} else {
				this.lastmessage = message;
				this.setLastMessageId(message.id);
			}
			prev = message;
		}
		if (!response.length) {
			this.lastmessageid = undefined;
			this.lastreadmessageid = undefined;
		}
		await this.slowmode();
	}
	delChannel(json: channeljson) {
		const build: Channel[] = [];
		for (const thing of this.children) {
			if (thing.id !== json.id) {
				build.push(thing);
			}
		}
		this.children = build;
	}
	afterProm?: Promise<void>;
	afterProms = new Map<string, () => void>();
	async grabAfter(id: string) {
		if (this.idToNext.has(id)) {
			return;
		}
		if (id === this.lastmessage?.id) {
			return;
		}
		if (this.afterProm) return new Promise<void>((res) => this.afterProms.set(id, res));
		let tempy: string | undefined = id;
		while (tempy && tempy.includes("fake")) {
			tempy = this.idToPrev.get(tempy);
		}
		if (!tempy) return;
		id = tempy;
		this.afterProm = new Promise(async (res) => {
			const messages = (await (
				await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100&after=" + id, {
					headers: this.headers,
				})
			).json()) as messagejson[];
			let i = 0;
			let previd: string = id;
			for (const response of messages) {
				let messager: Message;
				let willbreak = false;
				if (this.messages.has(response.id)) {
					messager = this.messages.get(response.id) as Message;
					willbreak = true;
				} else {
					messager = new Message(response, this);
				}
				this.idToPrev.set(messager.id, previd);
				this.idToNext.set(previd, messager.id);

				const res = this.afterProms.get(previd);
				if (res) {
					res();
					this.afterProms.delete(previd);
				}

				previd = messager.id;
				if (willbreak) {
					break;
				}
				i++;
			}
			if (i === 0) {
				this.idToNext.set(id, undefined);
			}
			{
				const res = this.afterProms.get(previd);
				if (res) {
					res();
					this.beforeProms.delete(previd);
				}
			}
			res();
			this.afterProm = undefined;
			if (this.afterProms.size !== 0) {
				const [id] = this.afterProms.entries().next().value as [string, () => void];
				this.grabAfter(id);
			}
		});
		return new Promise<void>((res) => this.afterProms.set(id, res));
	}
	async getArround(id: string) {
		if (!this.messages.has(id)) {
			await this.getmessage(id);
		} else {
			console.log("have " + id);
		}
		await Promise.all([this.grabBefore(id), this.grabAfter(id)]);
	}
	topid!: string;
	beforeProm?: Promise<void>;
	beforeProms = new Map<string, () => void>();
	async grabBefore(id: string) {
		if (this.beforeProm) return this.beforeProm;
		if (this.topid && id === this.topid) {
			return;
		}
		this.beforeProm = new Promise<void>(async (res) => {
			let tempy: string | undefined = id;
			while (tempy && tempy.includes("fake")) {
				tempy = this.idToPrev.get(tempy);
			}
			if (!tempy) {
				const res2 = this.beforeProms.get(id);
				res2?.();
				res();
				return;
			}
			id = tempy;
			const messages = (await (
				await fetch(
					this.info.api + "/channels/" + this.id + "/messages?before=" + id + "&limit=100",
					{
						headers: this.headers,
					},
				)
			).json()) as messagejson[];
			let previd = id;
			let i = 0;
			for (const response of messages) {
				let messager: Message;
				if (this.messages.has(response.id)) {
					messager = this.messages.get(response.id) as Message;
				} else {
					messager = new Message(response, this);
				}

				this.idToNext.set(messager.id, previd);
				this.idToPrev.set(previd, messager.id);

				const res = this.beforeProms.get(previd);
				if (res) {
					res();
					this.beforeProms.delete(previd);
				}

				previd = messager.id;

				if (i < 99) {
					this.topid = previd;
				}

				i++;
			}
			if (i < 100) {
				this.allthewayup = true;
				if (i === 0) {
					this.topid = id;
					this.idToPrev.set(id, undefined);
				}
			}
			{
				const res = this.beforeProms.get(previd);
				if (res) {
					res();
					this.beforeProms.delete(previd);
				}
			}
			this.beforeProm = undefined;
			res();
			if (this.beforeProms.size !== 0) {
				const [id] = this.beforeProms.entries().next().value as [string, () => void];
				this.grabBefore(id);
			}
		});
		return new Promise<void>((res) => this.beforeProms.set(id, res));
	}
	async buildmessages(id: string | void) {
		this.infinitefocus = false;
		await this.tryfocusinfinate(id, !!id);
	}
	infinitefocus = false;
	async tryfocusinfinate(id: string | void, flash = false) {
		const gid = ++Channel.genid;
		if (typeof id === "string" && !this.messages.has(id)) await this.getmessage(id);
		if (this.infinitefocus) return;
		this.infinitefocus = true;
		const messages = document.getElementById("scrollWrap") as HTMLDivElement;
		const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer"));
		for (const thing of messageContainers) {
			thing.remove();
		}
		const loading = document.getElementById("loadingdiv") as HTMLDivElement;
		const removetitle = document.getElementById("removetitle");
		//messages.innerHTML="";
		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) {
				const title = document.createElement("h2");
				title.id = "removetitle";
				title.textContent = I18n.noMessages();
				title.classList.add("titlespace", "messagecontainer");
				messages.append(title);
			}
			this.infinitefocus = false;
			loading.classList.remove("loading");
			return;
		} else if (removetitle) {
			removetitle.remove();
		}
		if (this.localuser.focusChannel !== this) {
			return;
		}
		const elements = Array.from(messages.getElementsByClassName("scroller"));
		for (const elm of elements) {
			elm.remove();
			console.warn("rouge element detected and removed");
		}
		const div = await this.infinite.getDiv(id, flash);
		if (gid !== Channel.genid) {
			return;
		}
		messages.append(div);
		/*
		await this.infinite.watchForChange().then(async (_) => {
			//await new Promise(resolve => setTimeout(resolve, 0));

			await this.infinite.focus(id, falsh); //if someone could figure out how to make this work correctly without this, that's be great :P


			this.infinite.focus(id, falsh, true);
		});
		*/
		await this.focus(id, flash);
		loading.classList.remove("loading");
		//this.infinite.focus(id.id,false);
	}
	private goBackIds(id: string, back: number, returnifnotexistant = true): string | undefined {
		while (back !== 0) {
			const nextid = this.idToPrev.get(id);
			if (nextid) {
				id = nextid;
				back--;
			} else {
				if (returnifnotexistant) {
					break;
				} else {
					return undefined;
				}
			}
		}
		return id;
	}
	private findClosest(id: string | undefined) {
		const mTime = (id: string) => {
			return this.messages.get(id)?.getTimeStamp() || -1;
		};
		if (!this.lastmessageid || !id) return;
		let flake: string | undefined = this.lastmessageid;
		const time = mTime(id);
		let flaketime = mTime(flake);
		while (flake && time < flaketime) {
			flake = this.idToPrev.get(flake);

			if (!flake) {
				return;
			}
			flaketime = mTime(flake);
		}
		return flake;
	}
	nameSpan = new WeakRef(document.createElement("span") as HTMLElement);
	updateChannel(json: channeljson) {
		this.type = json.type;
		if (json.flags !== undefined) this.flags = json.flags;
		this.name = json.name;
		this.owner_id = json.owner_id;
		this.icon = json.icon;
		this.defaultAutoArchiveDuration = json.default_auto_archive_duration;

		this.threadData = json.thread_metadata;
		this.rate_limit_per_user = json.rate_limit_per_user || 0;
		this.appliedTags = json.applied_tags || this.appliedTags;
		this.availableTags =
			json.available_tags?.map((tag) => new Tag(tag, this)) || this.availableTags;
		this.slowmode();

		const span = this.nameSpan.deref();
		if (span) span.textContent = this.name;
		const parent = this.localuser.channels.get(json.parent_id);
		if (parent) {
			this.parent = parent;
			this.parentId = parent.id;
		} else {
			this.parent = undefined;
			this.parentId = undefined;
		}

		(json.permission_overwrites ?? []).forEach((r) => {
			const p = new Permissions(r.allow, r.deny);
			this.permissionOverwriteMap.set(r.id, p);
		});

		this.croleUpdate();
		this.topic = json.topic;
		this.nsfw = json.nsfw;
		this.fireEvents();
		this.renderIcon();
	}
	croleUpdate: () => unknown = () => {};
	typingstart() {
		if (this.typing > Date.now()) {
			return;
		}
		this.typing = Date.now() + 6000;
		fetch(this.info.api + "/channels/" + this.id + "/typing", {
			method: "POST",
			headers: this.headers,
		});
	}
	get trueNotiValue() {
		const val = this.notification;
		if (val === "default") {
			switch (Number(this.guild.messageNotifications)) {
				case 0:
					return "all";
				case 1:
					return "mentions";
				case 2:
					return "none";
				default:
					return "mentions";
			}
		}

		return val;
	}
	get notification() {
		let notinumber: number | null = this.message_notifications;
		if (Number(notinumber) === 3) {
			notinumber = null;
		}
		notinumber ??= this.guild.messageNotifications;
		console.warn("info:", notinumber);
		switch (Number(notinumber)) {
			case 0:
				return "all";
			case 1:
				return "mentions";
			case 2:
				return "none";
			case 3:
			default:
				return "default";
		}
	}
	fakeMessages = new WeakMap<Message, HTMLElement>();
	nonceMap = new Map<string, string>();
	destroyFakeMessage(id: string) {
		const message = this.messages.get(id);
		if (!message) return;
		message.deleteEvent();

		const div = this.fakeMessages.get(message);
		div?.remove();
		this.fakeMessages.delete(message);
		this.messages.delete(id);

		for (const {url} of message.attachments) {
			try {
				URL.revokeObjectURL(url);
			} catch {}
		}
	}

	async makeFakeMessage(
		content: string,
		files: filejson[] = [],
		reply = undefined,
		sticker_ids: string[],
		nonce: string,
		embeds: embedjson[] = [],
	) {
		if (this.nonces.has(nonce)) return;
		const m = new Message(
			{
				author: this.localuser.user.tojson(),
				channel_id: this.id,
				guild_id: this.guild.id,
				id: "fake" + Math.random(),
				content: content.trim(),
				timestamp: new Date().toISOString(),
				edited_timestamp: null,
				mentions: [],
				mention_roles: [],
				mention_everyone: false,
				attachments: files,
				tts: false,
				embeds,
				reactions: [],
				nonce,
				type: 0,
				pinned: false,
				message_reference: reply,
				sticker_items: sticker_ids
					.map((_) => {
						return Sticker.getFromId(_, this.localuser);
					})
					.filter((_) => _ !== undefined),
			},
			this,
		);
		if (!this.lastmessageid) {
			this.topid = m.id;
		}
		this.nonceMap.set(nonce, m.id);
		const prev = this.lastmessage;
		const makeRecent = () => {
			if (this.lastmessageid) {
				this.idToNext.set(this.lastmessageid, m.id);
				this.idToPrev.set(m.id, this.lastmessageid);
			}
			this.lastmessage = m;
			this.setLastMessageId(m.id);
		};
		makeRecent();

		const html = m.buildhtml(prev, true);
		html.classList.add("messagediv", "loadingMessage");
		this.fakeMessages.set(m, html);
		let loadingP = document.createElement("span");

		const buttons = document.createElement("div");
		buttons.classList.add("flexltr");

		const retryB = document.createElement("button");
		retryB.textContent = I18n.message.retry();

		const dont = document.createElement("button");
		dont.textContent = I18n.message.delete();
		dont.onclick = (_) => {
			this.fakeMessages.delete(m);
			html.remove();
			for (const {url} of m.attachments) {
				URL.revokeObjectURL(url);
			}
		};
		dont.style.marginLeft = "4px";
		buttons.append(retryB, dont);

		if (this === this.localuser.focusChannel) {
			if (!this.infinitefocus) {
				await this.tryfocusinfinate();
			}
			await this.infinite.addedBottom();
			this.focus(m.id, false);
		}

		return {
			progress: (total: number, sofar: number) => {
				if (total < 20000 || sofar === total) {
					loadingP.remove();
					return;
				}
				html.append(loadingP);
				loadingP.textContent = File.filesizehuman(sofar) + " / " + File.filesizehuman(total);
			},
			failed: (retry: () => void) => {
				m.deleteEvent();
				makeRecent();
				loadingP.remove();
				html.append(buttons);
				retryB.onclick = () => {
					retry();
					html.classList.remove("erroredMessage");
					buttons.remove();
				};
				html.classList.add("erroredMessage");
			},
			void: () => {
				m.deleteEvent();
				html.remove();
			},
		};
	}
	async uploadFile(files: globalThis.File[]) {
		const urls = (await (
			await fetch(this.info.api + "/channels/" + this.id + "/attachments", {
				headers: this.headers,
				body: JSON.stringify({
					files: files.map((file, index) => {
						return {
							file_size: file.size,
							filename: file.name,
							id: index + "",
						};
					}),
				}),
				method: "POST",
			})
		).json()) as {
			attachments: {
				id: string;
				upload_url: string;
				upload_filename: string;
				original_content_type?: string;
			}[];
		};
		Promise.all(
			urls.attachments.map(async ({upload_url, id}) => {
				return await (
					await fetch(upload_url, {
						body: files[+id],
						method: "PUT",
					})
				).json();
			}),
		);
		return urls.attachments;
	}
	nonces = new Set<string>();
	lastSentMessage?: Message;
	canMessageRightNow() {
		if (this.guild.member) {
			const member = this.guild.member;
			if (member.commuicationDisabledLeft()) return false;
		}
		const t = this.lastSentMessage?.getTimeStamp();
		if (!t) return true;
		if (
			this.hasPermission("BYPASS_SLOWMODE") ||
			this.hasPermission("MANAGE_MESSAGES") ||
			this.hasPermission("MANAGE_CHANNELS")
		)
			return true;

		if (this.rate_limit_per_user === 0) return true;

		let canMessage = t + this.rate_limit_per_user * 1000;
		return canMessage <= Date.now();
	}
	async slowmode(bad = false) {
		const realbox = document.getElementById("realbox") as HTMLDivElement;
		Array.from(realbox.getElementsByClassName("slowmodeTimer")).forEach((_) => _.remove());
		if (this.guild.member) {
			const member = this.guild.member;
			const left = member.commuicationDisabledLeft();
			if (left) {
				const span = document.createElement("span");
				span.classList.add("slowmodeTimer");
				realbox.append(span);
				const canMessage = +(member.communication_disabled_until as Date);
				const tick = () => {
					const timeTill = canMessage - Date.now();
					if (timeTill <= 0 || !document.contains(span)) {
						if (document.contains(span)) span.remove();
						clearInterval(int);
						return;
					}
					span.textContent = I18n.channel.TimeOutCool(formatTime(timeTill));
				};
				tick();
				const int = setInterval(tick, 1000);
			}
		}
		function formatTime(timeTill: number) {
			let seconds = Math.round(timeTill / 1000);
			let minutes = Math.floor(seconds / 60);
			seconds -= minutes * 60;
			let hours = Math.floor(minutes / 60);
			minutes -= hours * 60;
			let build = "";
			build = seconds + "";
			if (minutes || hours) {
				build = minutes + ":" + build.padStart(2, "0");
			}
			if (hours) {
				build = hours + ":" + build.padStart(5, "0");
			}
			return build;
		}
		if (!this.rate_limit_per_user) return;
		if (
			this.hasPermission("BYPASS_SLOWMODE") ||
			this.hasPermission("MANAGE_MESSAGES") ||
			this.hasPermission("MANAGE_CHANNELS")
		)
			return;
		let m: Message | undefined = this.lastSentMessage || this.lastmessage;
		if (!this.lastSentMessage) {
			while (m) {
				if (m.author.id === this.localuser.user.id) {
					this.lastSentMessage = m;
					break;
				}
				m = this.messages.get(this.idToNext.get(m.id) as string);
			}
		}

		if (!m && bad) {
			const q = new URLSearchParams([
				["author_id", this.localuser.user.id],
				["limit", "1"],
			]);
			const {
				messages: [message],
			} = (await (
				await fetch(this.info.api + "/guilds/" + this.guild.id + "/messages/search/?" + q, {
					headers: this.headers,
				})
			).json()) as {messages: messagejson[]};
			m = new Message(message, this);
			this.lastSentMessage = m;
		}
		if (!m) return;
		const t = m.getTimeStamp();
		let canMessage = t + this.rate_limit_per_user * 1000;

		if (canMessage <= Date.now()) {
			realbox.classList.remove("cantSendMessage");
			return;
		} else {
			realbox.classList.add("cantSendMessage");
			const span = document.createElement("span");
			span.classList.add("slowmodeTimer");
			realbox.append(span);
			const tick = () => {
				const timeTill = canMessage - Date.now();
				if (timeTill <= 0 || !document.contains(span)) {
					if (document.contains(span)) span.remove();
					clearInterval(int);
					return;
				}
				span.textContent = I18n.channel.SlowmodeCool(formatTime(timeTill));
			};
			tick();
			const int = setInterval(tick, 1000);
		}
	}
	async sendMessage(
		content: string,
		{
			attachments = [],
			replyingto = null,
			embeds = [],
			sticker_ids = [],
			nonce = undefined,
			poll = undefined,
		}: {
			attachments?: Blob[];
			embeds?: embedjson[];
			replyingto?: Message | null;
			sticker_ids?: string[];
			nonce?: string;
			poll?: creatPollJSON;
		},
		onRes = (_e: "Ok" | "NotOk") => {},
	) {
		let ressy = (_e: "Ok" | "NotOk") => {};
		let resOnce = false;
		if (
			content.trim() === "" &&
			attachments.length === 0 &&
			embeds.length == 0 &&
			sticker_ids.length === 0 &&
			!poll
		) {
			return;
		}
		let replyjson: any;
		if (replyingto) {
			replyjson = {
				guild_id: replyingto.guild.id,
				channel_id: replyingto.channel.id,
				message_id: replyingto.id,
			};
		}

		let prom: Promise<void>;
		let res: XMLHttpRequest;
		let funcs:
			| undefined
			| {
					progress: (total: number, sofar: number) => void;
					failed: (restart: () => void) => void;
					void: () => void;
			  };
		const progress = (e: ProgressEvent<EventTarget>) => {
			funcs?.progress(e.total, e.loaded);
		};

		const fail = () => {
			console.warn("failed");
			funcs?.failed(() => {
				res.open("POST", this.info.api + "/channels/" + this.id + "/messages");
				res.setRequestHeader("Authorization", this.headers.Authorization);
				if (ctype) {
					res.setRequestHeader("Content-type", ctype);
				}
				res.send(rbody);
			});
		};

		const promiseHandler = (resolve: () => void) => {
			res.responseType = "json";
			res.onload = () => {
				if (res.status !== 200) {
					ressy("NotOk");
					onRes("NotOk");
					fail();
					const body = res.response as {code: number};
					if (body.code === 20016) {
						this.slowmode(true);
					}
					return;
				} else {
					ressy("Ok");
					onRes("Ok");
					if (!resOnce && res?.status) {
						resOnce = true;
					}
				}
				resolve();
			};
		};

		let rbody: string | FormData;
		let ctype: string | undefined;
		const maybeUpdate = () => {
			if ("updatePosition" in this && this.updatePosition instanceof Function) {
				console.log("here?");
				this.updatePosition(Date.now());
			}
		};
		maybeUpdate();
		ressy = async (e) => {
			if (e == "NotOk") {
				funcs?.void();
				return;
			}
		};
		if (attachments.length === 0) {
			const body = {
				content,
				nonce: nonce || Math.floor(Math.random() * 1000000000) + "",
				message_reference: undefined,
				sticker_ids,
				embeds,
				poll,
			};
			if (replyjson) {
				body.message_reference = replyjson;
			}
			res = new XMLHttpRequest();
			res.responseType = "json";
			res.upload.onprogress = progress;
			res.onerror = fail;
			prom = new Promise<void>(promiseHandler);
			res.open("POST", this.info.api + "/channels/" + this.id + "/messages");
			res.setRequestHeader("Content-type", (ctype = this.headers["Content-type"]));
			res.setRequestHeader("Authorization", this.headers.Authorization);
			funcs = await this.makeFakeMessage(
				content,
				[],
				body.message_reference,
				sticker_ids,
				body.nonce,
				embeds,
			);

			try {
				res.send((rbody = JSON.stringify(body)));
			} catch {
				fail();
			}
			/*
			res = fetch(this.info.api + "/channels/" + this.id + "/messages", {
				method: "POST",
				headers: this.headers,
				body: JSON.stringify(body),
			});
			*/
		} else {
			const formData = new FormData();
			const body = {
				content,
				nonce: nonce || Math.floor(Math.random() * 1000000000) + "",
				message_reference: undefined,
				sticker_ids,
				embeds,
				poll,
			};
			if (replyjson) {
				body.message_reference = replyjson;
			}
			formData.append("payload_json", JSON.stringify(body));
			for (const i in attachments) {
				formData.append("files[" + i + "]", attachments[i]);
			}

			res = new XMLHttpRequest();
			res.responseType = "json";
			res.upload.onprogress = progress;
			res.onerror = fail;
			prom = new Promise<void>(promiseHandler);
			res.open("POST", this.info.api + "/channels/" + this.id + "/messages", true);

			res.setRequestHeader("Authorization", this.headers.Authorization);

			funcs = await this.makeFakeMessage(
				content,
				attachments.map((_) => ({
					id: "string",
					filename: "",
					content_type: _.type,
					size: _.size,
					url: URL.createObjectURL(_),
				})),
				body.message_reference,
				sticker_ids,
				body.nonce,
			);
			try {
				res.send((rbody = formData));
			} catch {
				fail();
			}
			/*
			res = fetch(this.info.api + "/channels/" + this.id + "/messages", {
				method: "POST",
				body: formData,
				headers: {Authorization: this.headers.Authorization},
			});
			*/
		}

		return prom;
	}
	unreads() {
		if (!this.hasunreads) {
			if (this.myhtml) {
				this.myhtml.classList.remove("cunread", "mentioned");
			}
		} else {
			if (this.myhtml) {
				this.myhtml.classList.add("cunread");
			}
			if (this.mentions !== 0) {
				this.myhtml?.classList.add("mentioned");
			}
		}
	}
	async goToBottom() {
		if (this.lastmessageid) await this.focus(this.lastmessageid, false);
	}
	async messageCreate(messagep: messageCreateJson): Promise<void> {
		if (this.totalMessageSent !== undefined) this.totalMessageSent++;
		if (this.messageCount !== undefined) this.messageCount++;
		if (!this.hasPermission("VIEW_CHANNEL")) {
			return;
		}
		if (this.messages.get(messagep.d.id)) {
			console.error("Duped message?");
			return;
		}
		this.nonces.add(messagep.d.nonce);
		setTimeout(
			() => {
				this.nonces.delete(messagep.d.nonce);
			},
			1000 * 60 * 5,
		);

		if (!this.lastmessageid) {
			this.topid = messagep.d.id;
		}
		const messagez = new Message(messagep.d, this);
		Member.resolveMember(messagez.author, this.guild).then((_) => {
			this.typingmap.delete(_ as Member);
			this.rendertyping();
		});
		this.lastmessage = messagez;
		if (this.lastmessageid && this.lastmessageid !== messagez.id) {
			this.idToNext.set(this.lastmessageid, messagez.id);
			this.idToPrev.set(messagez.id, this.lastmessageid);
		} else {
			console.error("something bad happened");
		}
		if (
			(messagez.mentionsuser(this.localuser.user) || this.guild.id === "@me") &&
			messagez.author !== this.localuser.user
		) {
			this.mentions++;
		}
		this.setLastMessageId(messagez.id);

		if (this.infinite.atBottom()) {
			this.lastreadmessageid = messagez.id;
		}

		this.unreads();
		this.guild.unreads();
		if (this === this.localuser.focusChannel) {
			if (!this.infinitefocus) {
				await this.tryfocusinfinate();
			}
			await this.infinite.addedBottom();
		}

		if (messagez.author === this.localuser.user) {
			this.lastSentMessage = messagez;
			this.lastreadmessageid = messagez.id;
			this.slowmode();
			this.mentions = 0;
			this.unreads();
			this.guild.unreads();
			if (this == this.localuser.focusChannel) {
				setTimeout(() => this.goToBottom());
			}
		}
		this.fireEvents();

		if (messagez.author === this.localuser.user) {
			return;
		}
		if (this.localuser.focusGuild?.prevchannel === this && document.hasFocus()) {
			return;
		}

		this.notify(messagez);
	}
	notititle(message: Message): string {
		return message.author.username + " > " + this.guild.properties.name + " > " + this.name;
	}
	notify(message: Message, deep = 0) {
		if (this.muted) return;

		if (this.localuser.status === "dnd") return;

		if (this.guild.muted) {
			return;
		}
		if (this.trueNotiValue === "none") {
			return;
		} else if (this.notification === "mentions" && !message.mentionsuser(this.localuser.user)) {
			return;
		}
		if (message.author.relationshipType == 2) {
			return;
		}

		if (this.localuser.play) {
			this.localuser.playSound();
		} else {
			console.warn("no play 3:");
		}
		if ("Notification" in window && Notification.permission === "granted") {
			NotificationHandler.sendMessageNotification(message);
		} else if (Notification.permission !== "denied") {
			Notification.requestPermission().then(() => {
				if (deep === 3) {
					return;
				}
				this.notify(message, deep + 1);
			});
		}
	}
	voiceMode: "VoiceOnly" | "ChatAndVoice" = "VoiceOnly";
	async addRoleToPerms(role: Role | User) {
		await fetch(this.info.api + "/channels/" + this.id + "/permissions/" + role.id, {
			method: "PUT",
			headers: this.headers,
			body: JSON.stringify({
				allow: "0",
				deny: "0",
				id: role.id,
				type: role instanceof User ? 1 : 0,
			}),
		});
		const perm = new Permissions("0", "0");
		this.permissionOverwriteMap.set(role.id, perm);
	}
	async updateRolePermissions(id: string, perms: Permissions) {
		const permission = this.permissionOverwriteMap.get(id);
		if (permission) {
			permission.allow = perms.allow;
			permission.deny = perms.deny;
		} else {
			//this.permission_overwrites.set(id,perms);
		}
		await fetch(this.info.api + "/channels/" + this.id + "/permissions/" + id, {
			method: "PUT",
			headers: this.headers,
			body: JSON.stringify({
				allow: (perms.allow & ~Permissions.noOverwriteMask).toString(),
				deny: (perms.deny & ~Permissions.noOverwriteMask).toString(),
				id,
				type: this.localuser.userMap.get(id) ? 1 : 0,
			}),
		});
	}
}
Channel.setupcontextmenu();
export {Channel};
