···11+import { IrcMessage } from "./parser";
22+import type { Connection } from "./connection";
33+import { FetchHistoryParams } from "./history";
44+55+export abstract class ChatBuffer implements AsyncIterable<IrcMessage> {
66+ constructor(public name: string, public conn: Connection) {
77+ }
88+99+ [Symbol.asyncIterator] = this.history;
1010+1111+ /** iterate over the channel's history.
1212+ * todo: improve documentation
1313+ * @param [start_at="latest"] the timestamp to start fetching messages from.
1414+ * @param [chunk_size=50]
1515+ */
1616+ async* history(start_at: FetchHistoryParams[1] = "latest", chunk_size = 50) {
1717+ let next_range = start_at;
1818+1919+ while (true) {
2020+ const messages = await this.fetch_history_page(next_range, chunk_size);
2121+ // console.log(messages.length, messages[0])
2222+ if (messages.length == 0) {
2323+ console.log("exhausted history");
2424+ return;
2525+ }
2626+2727+ messages.reverse();
2828+2929+ for (const message of messages) {
3030+ yield message;
3131+ }
3232+3333+ next_range = {
3434+ before: messages.at(-1)!.timestamp!
3535+ }
3636+ }
3737+ }
3838+3939+ async fetch_history_page(range: FetchHistoryParams[1], chunk_size = 50) {
4040+ if (!this.conn.fetch_history) throw new Error("No history fetcher registered")
4141+ return this.conn.fetch_history(
4242+ this.name,
4343+ range,
4444+ chunk_size
4545+ );
4646+ }
4747+4848+ /**
4949+ * Get messages pertaining to this channel.
5050+ * @param callback Called on each message relevant to this channel
5151+ * @returns A function to unsubscribe from the channel.
5252+ * Call this after you're done with the subscription to prevent
5353+ * memory leaks!
5454+ */
5555+ subscribe(callback: (msg: IrcMessage) => void): () => void {
5656+ const sub = this.conn.queue.subscribe(`${this.name} subscription`, (msg) => {
5757+ const cmd = msg.command.toUpperCase();
5858+ if (
5959+ (
6060+ cmd == "PRIVMSG" ||
6161+ cmd == "NOTICE" ||
6262+ cmd == "JOIN" ||
6363+ cmd == "PART" ||
6464+ cmd == "TAGMSG" ||
6565+ cmd == "MODE" ||
6666+ cmd == "TOPIC"
6767+ ) && msg.params?.[0] == this.name
6868+ ) {
6969+ callback(msg);
7070+ }
7171+ });
7272+ return () => sub.unsubscribe();
7373+ }
7474+7575+ /**
7676+ * Send a message to the channel.
7777+ * @param message The text content of the message
7878+ * @returns The resulting message
7979+ */
8080+ privmsg(message: string): IrcMessage {
8181+ const msg = new IrcMessage({
8282+ command: "PRIVMSG",
8383+ params: [this.name, message],
8484+ timestamp: new Date(Date.now()),
8585+ });
8686+ this.conn.send(msg.toString());
8787+8888+ // include the source on the returned message to avoid special
8989+ // handling for clients.
9090+ msg.source = { nick: this.conn.nickname }
9191+ return msg;
9292+ }
9393+}
+2-89
core/channel.ts
···22import { IrcMessage } from "./parser";
33import { IrcChannelState, Numeric } from "./support";
44import { Mutex } from 'async-mutex';
55-import { signal, Signal, batch } from "@preact/signals-core";
66-import { FetchHistoryParams } from "./history";
55+import { batch, Signal, signal } from "@preact/signals-core";
76import { ArrayMatcher, Matcher, Wildcard } from "./queue";
77+import { ChatBuffer } from "./buffer";
8899const member_prefixes = [
1010 "~", "&", "@", "%"
1111];
1212-1313-export abstract class ChatBuffer implements AsyncIterable<IrcMessage> {
1414- constructor(public name: string, public conn: Connection) { }
1515-1616- [Symbol.asyncIterator] = this.history;
1717-1818- /** iterate over the channel's history.
1919- * todo: improve documentation
2020- * @param [start_at="latest"] the timestamp to start fetching messages from.
2121- * @param [chunk_size=50]
2222- */
2323- async *history(start_at: FetchHistoryParams[1] = "latest", chunk_size = 50) {
2424- let next_range = start_at;
2525-2626- while (true) {
2727- const messages = await this.fetch_history_page(next_range, chunk_size);
2828- // console.log(messages.length, messages[0])
2929- if (messages.length == 0) {
3030- console.log("exhausted history");
3131- return;
3232- };
3333-3434- for (const message of messages) {
3535- yield message;
3636- }
3737-3838- next_range = {
3939- before: messages[0].timestamp!
4040- }
4141- }
4242- }
4343-4444- async fetch_history_page(range: FetchHistoryParams[1], chunk_size = 50) {
4545- if (!this.conn.fetch_history) throw new Error("No history fetcher registered")
4646- return this.conn.fetch_history(
4747- this.name,
4848- range,
4949- chunk_size
5050- );
5151- }
5252-5353- /**
5454- * Get messages pertaining to this channel.
5555- * @param callback Called on each message relevant to this channel
5656- * @returns A function to unsubscribe from the channel.
5757- * Call this after you're done with the subscription to prevent
5858- * memory leaks!
5959- */
6060- subscribe(callback: (msg: IrcMessage) => void): () => void {
6161- const sub = this.conn.queue.subscribe(`${this.name} subscription`, (msg) => {
6262- const cmd = msg.command.toUpperCase();
6363- if (
6464- (
6565- cmd == "PRIVMSG" ||
6666- cmd == "NOTICE" ||
6767- cmd == "JOIN" ||
6868- cmd == "PART" ||
6969- cmd == "TAGMSG" ||
7070- cmd == "MODE" ||
7171- cmd == "TOPIC"
7272- ) && msg.params?.[0] == this.name
7373- ) {
7474- callback(msg);
7575- }
7676- });
7777- return () => sub.unsubscribe();
7878- }
7979-8080- /**
8181- * Send a message to the channel.
8282- * @param message The text content of the message
8383- * @returns The resulting message
8484- */
8585- privmsg(message: string): IrcMessage {
8686- const msg = new IrcMessage({
8787- command: "PRIVMSG",
8888- params: [this.name, message],
8989- timestamp: new Date(Date.now()),
9090- });
9191- this.conn.send(msg.toString());
9292-9393- // include the source on the returned message to avoid special
9494- // handling for clients.
9595- msg.source = { nick: this.conn.nickname }
9696- return msg;
9797- }
9898-}
991210013export class IrcChannel extends ChatBuffer {
10114 constructor(name: string, conn: Connection, initial_state = IrcChannelState.Pending) {
+2-1
core/connection.ts
···11import { Signal, signal } from "@preact/signals-core";
22import { nanoid } from "nanoid";
33-import { ChatBuffer, IrcChannel } from "./channel";
33+import { IrcChannel } from "./channel";
44import { MessageHandler, default_handler } from "./handler";
55import { FetchHistoryParams } from "./history";
66import { ISupport } from "./isupport";
···99import { SaslConfig } from "./sasl";
1010import { IrcChannelState, Numeric, to_casemap_lowercase } from "./support";
1111import list_channels from "./list";
1212+import { ChatBuffer } from "./buffer";
12131314export interface ConnectionConfig {
1415 /**
+9
core/direct.ts
···11+import { ChatBuffer } from "./buffer";
22+33+/**
44+ * a ChatBuffer representing a one-to-one conversation.
55+ * i am not going to call this a query
66+ */
77+export class DirectMessage extends ChatBuffer {
88+99+}
···3434 });
35353636 if (process.env.NODE_ENV == "development") {
3737- date_string += ` (debug) retrieved by ${msg.retriver}`;
3737+ date_string += ` (debug) retrieved by ${msg.retriever}`;
3838 }
39394040 if (msg.is_action) {
-1
neo/src/buffer/squisher.tsx
···11import ReadMarkers from "@src/chat/read";
22-import { ChatBuffer } from "tubes_core/channel";
32import { IrcMessage } from "tubes_core/parser";
43import { DateSeperator, Message, UnreadSeperator } from "./list-elements";
54
+27-25
neo/src/buffer/view.tsx
···22import { Signal, useSignal } from "@preact/signals";
33import { execute_command } from "@src/chat/commands";
44import ReadMarkers, { ReadMarker } from "@src/chat/read";
55+import Storage from "@src/chat/storage";
56import { message_style } from "@src/support";
67import { FunctionalComponent, RefObject, createContext } from "preact";
88+import { HTMLProps } from "preact/compat";
79import { useEffect, useRef } from "preact/hooks";
88-import { ChatBuffer, IrcChannel } from "tubes_core/channel";
1010+import { IrcChannel } from "tubes_core/channel";
911import { IrcMessage } from "tubes_core/parser";
1212+import Trangle from "~icons/ph/triangle-fill";
1013import MessageInput from "./input";
1414+import { squish_messages } from "./squisher";
11151212-import { HTMLProps } from "preact/compat";
1313-import Trangle from "~icons/ph/triangle-fill";
1414-import { squish_messages } from "./squisher";
1515-import Storage from "@src/chat/storage";
1616+import MembersIcon from "~icons/ph/users";
1717+import { IconButton } from "@src/bits/buttons";
1818+import { ChatBuffer } from "tubes_core/buffer";
16191720async function load_msgs(buffer: ChatBuffer, limit = 100): Promise<IrcMessage[]> {
1821 let count = 0;
1922 const acc = [];
2020- const batch_size = Number(buffer.conn.isupport?.CHATHISTORY) || 50;
2121- for await (const msg of buffer.history("latest", batch_size)) {
2323+ const chunk_size = Number(buffer.conn.isupport?.CHATHISTORY) || 50;
2424+ for await (const msg of buffer.history("latest", chunk_size)) {
2225 acc.push(IrcMessage.hydrate(msg));
2626+ console.log(msg);
2327 if (count++ >= limit) break;
2428 }
25292626- return acc;
3030+ // todo: this gets reversed twice because of the chunks.
3131+ // can we make it not do that
3232+ return acc.toReversed();
2733}
28342935export const BufferContext = createContext<ChatBuffer | null>(null);
···5460 return () => {
5561 marker.focused.value = false;
5662 marker.last_read.value = msgs.value.findLast(x => x.command == "PRIVMSG" || x.command == "NOTICE");
5757-5863 }
5964 }, [buffer]);
6065···9196 is_scrolled={!marker.focused.value}
9297 onSubmit={async (text, is_cmd) => {
9398 if (is_cmd) {
9494- return await execute_command(text, buffer);
9999+ return await execute_command({
100100+ input: text, buffer: buffer,
101101+ reply(text) {
102102+ const msg = buffer.privmsg(text);
103103+ Storage.store_message(msg, buffer.conn);
104104+ msgs.value = [...msgs.value, msg];
105105+ }
106106+ });
95107 }
9610897109 const msg = buffer.privmsg(text);
···143155 <p>
144156 {channel.$topic.value}
145157 </p>
146146- <p>{channel.$members.value.length}</p>
158158+ <MemberCount count={channel.$members.value.length} />
147159 </header>
160160+161161+const MemberCount: FunctionalComponent<{ count: number }> = ({ count }) => {
162162+ return <IconButton><MembersIcon />{count}</IconButton>
163163+}
148164149165const StartOfHistory: FunctionalComponent<{ buffer: ChatBuffer }> = ({ buffer }) => {
150166 const chathistory = buffer.conn.capabilities.has("draft/chathistory");
···170186 </li>
171187 })}
172188 </ul>
173173-174174-const LoadTrigger
175175- : FunctionalComponent<{ onIntersect: IntersectionObserverCallback, parent: RefObject<HTMLUListElement> }>
176176- = ({ onIntersect, parent }) => {
177177- const ref = useRef<HTMLDivElement>(null);
178178- useEffect(() => {
179179- const observer = new IntersectionObserver(onIntersect, { root: parent!.current })
180180- observer.observe(ref.current!);
181181-182182- return () => observer.disconnect();
183183- });
184184-185185- return <div class="load-trigger" ref={ref} />
186186- }
187189188190export default BufferView;
+18-11
neo/src/chat/commands.ts
···11-import { ChatBuffer, IrcChannel } from "tubes_core/channel";
22-import Storage from "./storage";
11+import { ChatBuffer } from "tubes_core/buffer";
22+import { IrcChannel } from "tubes_core/channel";
3344export interface TubesCommand {
55 description: string,
66- activate: (params: string, buffer: ChatBuffer) => Promise<void>,
66+ activate: (params: CommandParameters) => Promise<void>,
77}
8899export const is_a_command = (input: string) => Boolean(input.match(/^\/[a-zA-Z]+/));
10101111-export function execute_command(input: string, buffer: ChatBuffer) {
1111+1212+interface CommandParameters {
1313+ input: string;
1414+ buffer: ChatBuffer;
1515+1616+ reply(msg: string): void;
1717+}
1818+1919+export function execute_command({ input, buffer, reply }: CommandParameters) {
1220 if (is_a_command(input)) {
1321 input = input.substring(1);
1422 }
···2028 const cmd = commands[cmd_name];
21292230 if (!cmd) {
2323- throw new Error(`${cmd} is not a recognised command.`);
3131+ throw new Error(`/${cmd_name} is not a recognised command.`);
2432 }
25332626- return cmd.activate(input, buffer);
3434+ return cmd.activate({ input, buffer, reply });
2735}
28362937const commands = <Record<string, TubesCommand>>{
3038 "me": {
3139 description: "",
3232- activate: async (text, buffer) => {
3333- const msg = buffer.privmsg(`\x01ACTION ${text}\x01`)
3434- Storage.store_message(msg, buffer.conn);
4040+ activate: async ({ input, reply }) => {
4141+ reply(`\x01ACTION ${input}\x01`);
3542 },
3643 },
3744 "topic": {
3845 description: "Set the channel's topic",
3939- activate: async (text, buffer) => {
4646+ activate: async ({ input, buffer }) => {
4047 console.log(buffer);
4148 if (!(buffer instanceof IrcChannel)) {
4249 throw new Error(`You can't set the topic on things that aren't channels.`);
4350 }
44514545- await buffer.set_topic(text);
5252+ await buffer.set_topic(input);
4653 }
4754 }
4855};
-1
neo/src/chat/conns.ts
···11import { computed, signal, type Signal } from "@preact/signals";
22import type { Connection } from "tubes_core";
33-import { ChatBuffer } from "tubes_core/channel";
43import type { ConnectionConfig } from "tubes_core/connection";
54import { Connection as WsConnection } from "tubes_core/ws";
65import { adapters } from "./adapters";
+7-3
neo/src/chat/history.ts
···2626 console.log(res.length, limit);
2727 if (conn.debug) {
2828 res = res.map(x => {
2929- x.retriver = "storage";
2929+ x.retriever = "storage";
3030 return x;
3131 });
3232 }
···4040 );
4141 if (conn.debug) {
4242 more = more.map(x => {
4343- x.retriver = "chathistory";
4343+ x.retriever = "chathistory";
4444 return x;
4545 });
4646 }
4747 res = more.concat(res);
4848 // if you start seeing message duplication, this is the place to look
4949 for (const msg of res) {
5050- await Storage.store_message(msg, conn);
5050+ try {
5151+ await Storage.store_message(msg, conn);
5252+ } catch (e) {
5353+ console.log("failed to store chathistory message", e);
5454+ }
5155 }
5256 }
5357
+3-3
neo/src/chat/storage.ts
···7171 const range = IDBKeyRange.bound(
7272 [0, target, conn.id, conn.adapter_id ?? ""],
7373 [id, target, conn.id, conn.adapter_id ?? ""],
7474- false, true
7474+ false, true,
7575 );
76767777 const cursor = await timestamps.openCursor(range, "prev");
7878 const msgs = [];
7979 let count = 0;
8080- // this could get slow over time.
8080+ // todo: this could get slow over time.
8181 while (cursor && count < limit) {
8282 const value = cursor.value;
8383 if (!value) break;
···8888 ) {
8989 msgs.push(cursor.value);
9090 count++;
9191- };
9191+ }
9292 await cursor.continue();
9393 }
9494