gubes mirror. how does this work
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

my ass

leah c88bc587 08ad4a78

+198 -155
+93
core/buffer.ts
··· 1 + import { IrcMessage } from "./parser"; 2 + import type { Connection } from "./connection"; 3 + import { FetchHistoryParams } from "./history"; 4 + 5 + export abstract class ChatBuffer implements AsyncIterable<IrcMessage> { 6 + constructor(public name: string, public conn: Connection) { 7 + } 8 + 9 + [Symbol.asyncIterator] = this.history; 10 + 11 + /** iterate over the channel's history. 12 + * todo: improve documentation 13 + * @param [start_at="latest"] the timestamp to start fetching messages from. 14 + * @param [chunk_size=50] 15 + */ 16 + async* history(start_at: FetchHistoryParams[1] = "latest", chunk_size = 50) { 17 + let next_range = start_at; 18 + 19 + while (true) { 20 + const messages = await this.fetch_history_page(next_range, chunk_size); 21 + // console.log(messages.length, messages[0]) 22 + if (messages.length == 0) { 23 + console.log("exhausted history"); 24 + return; 25 + } 26 + 27 + messages.reverse(); 28 + 29 + for (const message of messages) { 30 + yield message; 31 + } 32 + 33 + next_range = { 34 + before: messages.at(-1)!.timestamp! 35 + } 36 + } 37 + } 38 + 39 + async fetch_history_page(range: FetchHistoryParams[1], chunk_size = 50) { 40 + if (!this.conn.fetch_history) throw new Error("No history fetcher registered") 41 + return this.conn.fetch_history( 42 + this.name, 43 + range, 44 + chunk_size 45 + ); 46 + } 47 + 48 + /** 49 + * Get messages pertaining to this channel. 50 + * @param callback Called on each message relevant to this channel 51 + * @returns A function to unsubscribe from the channel. 52 + * Call this after you're done with the subscription to prevent 53 + * memory leaks! 54 + */ 55 + subscribe(callback: (msg: IrcMessage) => void): () => void { 56 + const sub = this.conn.queue.subscribe(`${this.name} subscription`, (msg) => { 57 + const cmd = msg.command.toUpperCase(); 58 + if ( 59 + ( 60 + cmd == "PRIVMSG" || 61 + cmd == "NOTICE" || 62 + cmd == "JOIN" || 63 + cmd == "PART" || 64 + cmd == "TAGMSG" || 65 + cmd == "MODE" || 66 + cmd == "TOPIC" 67 + ) && msg.params?.[0] == this.name 68 + ) { 69 + callback(msg); 70 + } 71 + }); 72 + return () => sub.unsubscribe(); 73 + } 74 + 75 + /** 76 + * Send a message to the channel. 77 + * @param message The text content of the message 78 + * @returns The resulting message 79 + */ 80 + privmsg(message: string): IrcMessage { 81 + const msg = new IrcMessage({ 82 + command: "PRIVMSG", 83 + params: [this.name, message], 84 + timestamp: new Date(Date.now()), 85 + }); 86 + this.conn.send(msg.toString()); 87 + 88 + // include the source on the returned message to avoid special 89 + // handling for clients. 90 + msg.source = { nick: this.conn.nickname } 91 + return msg; 92 + } 93 + }
+2 -89
core/channel.ts
··· 2 2 import { IrcMessage } from "./parser"; 3 3 import { IrcChannelState, Numeric } from "./support"; 4 4 import { Mutex } from 'async-mutex'; 5 - import { signal, Signal, batch } from "@preact/signals-core"; 6 - import { FetchHistoryParams } from "./history"; 5 + import { batch, Signal, signal } from "@preact/signals-core"; 7 6 import { ArrayMatcher, Matcher, Wildcard } from "./queue"; 7 + import { ChatBuffer } from "./buffer"; 8 8 9 9 const member_prefixes = [ 10 10 "~", "&", "@", "%" 11 11 ]; 12 - 13 - export abstract class ChatBuffer implements AsyncIterable<IrcMessage> { 14 - constructor(public name: string, public conn: Connection) { } 15 - 16 - [Symbol.asyncIterator] = this.history; 17 - 18 - /** iterate over the channel's history. 19 - * todo: improve documentation 20 - * @param [start_at="latest"] the timestamp to start fetching messages from. 21 - * @param [chunk_size=50] 22 - */ 23 - async *history(start_at: FetchHistoryParams[1] = "latest", chunk_size = 50) { 24 - let next_range = start_at; 25 - 26 - while (true) { 27 - const messages = await this.fetch_history_page(next_range, chunk_size); 28 - // console.log(messages.length, messages[0]) 29 - if (messages.length == 0) { 30 - console.log("exhausted history"); 31 - return; 32 - }; 33 - 34 - for (const message of messages) { 35 - yield message; 36 - } 37 - 38 - next_range = { 39 - before: messages[0].timestamp! 40 - } 41 - } 42 - } 43 - 44 - async fetch_history_page(range: FetchHistoryParams[1], chunk_size = 50) { 45 - if (!this.conn.fetch_history) throw new Error("No history fetcher registered") 46 - return this.conn.fetch_history( 47 - this.name, 48 - range, 49 - chunk_size 50 - ); 51 - } 52 - 53 - /** 54 - * Get messages pertaining to this channel. 55 - * @param callback Called on each message relevant to this channel 56 - * @returns A function to unsubscribe from the channel. 57 - * Call this after you're done with the subscription to prevent 58 - * memory leaks! 59 - */ 60 - subscribe(callback: (msg: IrcMessage) => void): () => void { 61 - const sub = this.conn.queue.subscribe(`${this.name} subscription`, (msg) => { 62 - const cmd = msg.command.toUpperCase(); 63 - if ( 64 - ( 65 - cmd == "PRIVMSG" || 66 - cmd == "NOTICE" || 67 - cmd == "JOIN" || 68 - cmd == "PART" || 69 - cmd == "TAGMSG" || 70 - cmd == "MODE" || 71 - cmd == "TOPIC" 72 - ) && msg.params?.[0] == this.name 73 - ) { 74 - callback(msg); 75 - } 76 - }); 77 - return () => sub.unsubscribe(); 78 - } 79 - 80 - /** 81 - * Send a message to the channel. 82 - * @param message The text content of the message 83 - * @returns The resulting message 84 - */ 85 - privmsg(message: string): IrcMessage { 86 - const msg = new IrcMessage({ 87 - command: "PRIVMSG", 88 - params: [this.name, message], 89 - timestamp: new Date(Date.now()), 90 - }); 91 - this.conn.send(msg.toString()); 92 - 93 - // include the source on the returned message to avoid special 94 - // handling for clients. 95 - msg.source = { nick: this.conn.nickname } 96 - return msg; 97 - } 98 - } 99 12 100 13 export class IrcChannel extends ChatBuffer { 101 14 constructor(name: string, conn: Connection, initial_state = IrcChannelState.Pending) {
+2 -1
core/connection.ts
··· 1 1 import { Signal, signal } from "@preact/signals-core"; 2 2 import { nanoid } from "nanoid"; 3 - import { ChatBuffer, IrcChannel } from "./channel"; 3 + import { IrcChannel } from "./channel"; 4 4 import { MessageHandler, default_handler } from "./handler"; 5 5 import { FetchHistoryParams } from "./history"; 6 6 import { ISupport } from "./isupport"; ··· 9 9 import { SaslConfig } from "./sasl"; 10 10 import { IrcChannelState, Numeric, to_casemap_lowercase } from "./support"; 11 11 import list_channels from "./list"; 12 + import { ChatBuffer } from "./buffer"; 12 13 13 14 export interface ConnectionConfig { 14 15 /**
+9
core/direct.ts
··· 1 + import { ChatBuffer } from "./buffer"; 2 + 3 + /** 4 + * a ChatBuffer representing a one-to-one conversation. 5 + * i am not going to call this a query 6 + */ 7 + export class DirectMessage extends ChatBuffer { 8 + 9 + }
+1 -3
core/history.ts
··· 39 39 conn.send(`CHATHISTORY LATEST ${target} * ${limit}`); 40 40 } 41 41 42 - const msgs = await conn.collect_batch("chathistory", { mask: true, params: [target] }); 43 - 44 - return msgs; 42 + return await conn.collect_batch("chathistory", { mask: true, params: [target] }); 45 43 }
+3 -1
core/parser.ts
··· 6 6 command: Numeric | string, 7 7 params?: string[]; 8 8 timestamp?: Date; 9 + retriever?: string; 9 10 }; 10 11 11 12 export type MessageSource = { ··· 22 23 command: this.command, 23 24 params: this.params, 24 25 timestamp: this.timestamp, 26 + retriever: this.retriever, 25 27 } = opt); 26 28 } 27 29 ··· 30 32 command: Numeric | string; 31 33 params?: string[]; 32 34 timestamp?; 33 - retriver?: string; 35 + retriever?: string; 34 36 35 37 static parse(message: string) { 36 38 let state = message;
+1 -1
neo/src/bits/menu/list-menu.tsx
··· 12 12 onClick?: (e: MouseEvent & { currentTarget: HTMLButtonElement }) => void, 13 13 }> = ({ children, icon: Icon, destructive, onClick }) => { 14 14 return <li> 15 - <button type="dialog" class={`${destructive ? "danger" : ""}`} onClick={onClick}> 15 + <button class={`${destructive ? "danger" : ""}`} onClick={onClick}> 16 16 {Icon && <Icon aria-hidden />} {children} 17 17 </button> 18 18 </li>;
-1
neo/src/bits/sidebar/network-section.tsx
··· 5 5 import ReadMarkers from "@src/chat/read"; 6 6 import { FunctionalComponent } from "preact"; 7 7 import { Connection } from "tubes_core"; 8 - import { ChatBuffer } from "tubes_core/channel"; 9 8 import { ConnectionErrorCode, ConnectionState } from "tubes_core/connection"; 10 9 import { useLocation } from "wouter-preact"; 11 10 import { IconButton, TextButton } from "../buttons";
+1 -1
neo/src/buffer/input.tsx
··· 45 45 <div class="input"> 46 46 <input 47 47 type="text" 48 - label="Message Input" 48 + aria-label="Message Input" 49 49 placeholder="say something to #tubes" 50 50 value={input.value} 51 51 onInput={(e) => input.value = e.currentTarget.value}
+1 -1
neo/src/buffer/list-elements.tsx
··· 34 34 }); 35 35 36 36 if (process.env.NODE_ENV == "development") { 37 - date_string += ` (debug) retrieved by ${msg.retriver}`; 37 + date_string += ` (debug) retrieved by ${msg.retriever}`; 38 38 } 39 39 40 40 if (msg.is_action) {
-1
neo/src/buffer/squisher.tsx
··· 1 1 import ReadMarkers from "@src/chat/read"; 2 - import { ChatBuffer } from "tubes_core/channel"; 3 2 import { IrcMessage } from "tubes_core/parser"; 4 3 import { DateSeperator, Message, UnreadSeperator } from "./list-elements"; 5 4
+27 -25
neo/src/buffer/view.tsx
··· 2 2 import { Signal, useSignal } from "@preact/signals"; 3 3 import { execute_command } from "@src/chat/commands"; 4 4 import ReadMarkers, { ReadMarker } from "@src/chat/read"; 5 + import Storage from "@src/chat/storage"; 5 6 import { message_style } from "@src/support"; 6 7 import { FunctionalComponent, RefObject, createContext } from "preact"; 8 + import { HTMLProps } from "preact/compat"; 7 9 import { useEffect, useRef } from "preact/hooks"; 8 - import { ChatBuffer, IrcChannel } from "tubes_core/channel"; 10 + import { IrcChannel } from "tubes_core/channel"; 9 11 import { IrcMessage } from "tubes_core/parser"; 12 + import Trangle from "~icons/ph/triangle-fill"; 10 13 import MessageInput from "./input"; 14 + import { squish_messages } from "./squisher"; 11 15 12 - import { HTMLProps } from "preact/compat"; 13 - import Trangle from "~icons/ph/triangle-fill"; 14 - import { squish_messages } from "./squisher"; 15 - import Storage from "@src/chat/storage"; 16 + import MembersIcon from "~icons/ph/users"; 17 + import { IconButton } from "@src/bits/buttons"; 18 + import { ChatBuffer } from "tubes_core/buffer"; 16 19 17 20 async function load_msgs(buffer: ChatBuffer, limit = 100): Promise<IrcMessage[]> { 18 21 let count = 0; 19 22 const acc = []; 20 - const batch_size = Number(buffer.conn.isupport?.CHATHISTORY) || 50; 21 - for await (const msg of buffer.history("latest", batch_size)) { 23 + const chunk_size = Number(buffer.conn.isupport?.CHATHISTORY) || 50; 24 + for await (const msg of buffer.history("latest", chunk_size)) { 22 25 acc.push(IrcMessage.hydrate(msg)); 26 + console.log(msg); 23 27 if (count++ >= limit) break; 24 28 } 25 29 26 - return acc; 30 + // todo: this gets reversed twice because of the chunks. 31 + // can we make it not do that 32 + return acc.toReversed(); 27 33 } 28 34 29 35 export const BufferContext = createContext<ChatBuffer | null>(null); ··· 54 60 return () => { 55 61 marker.focused.value = false; 56 62 marker.last_read.value = msgs.value.findLast(x => x.command == "PRIVMSG" || x.command == "NOTICE"); 57 - 58 63 } 59 64 }, [buffer]); 60 65 ··· 91 96 is_scrolled={!marker.focused.value} 92 97 onSubmit={async (text, is_cmd) => { 93 98 if (is_cmd) { 94 - return await execute_command(text, buffer); 99 + return await execute_command({ 100 + input: text, buffer: buffer, 101 + reply(text) { 102 + const msg = buffer.privmsg(text); 103 + Storage.store_message(msg, buffer.conn); 104 + msgs.value = [...msgs.value, msg]; 105 + } 106 + }); 95 107 } 96 108 97 109 const msg = buffer.privmsg(text); ··· 143 155 <p> 144 156 {channel.$topic.value} 145 157 </p> 146 - <p>{channel.$members.value.length}</p> 158 + <MemberCount count={channel.$members.value.length} /> 147 159 </header> 160 + 161 + const MemberCount: FunctionalComponent<{ count: number }> = ({ count }) => { 162 + return <IconButton><MembersIcon />{count}</IconButton> 163 + } 148 164 149 165 const StartOfHistory: FunctionalComponent<{ buffer: ChatBuffer }> = ({ buffer }) => { 150 166 const chathistory = buffer.conn.capabilities.has("draft/chathistory"); ··· 170 186 </li> 171 187 })} 172 188 </ul> 173 - 174 - const LoadTrigger 175 - : FunctionalComponent<{ onIntersect: IntersectionObserverCallback, parent: RefObject<HTMLUListElement> }> 176 - = ({ onIntersect, parent }) => { 177 - const ref = useRef<HTMLDivElement>(null); 178 - useEffect(() => { 179 - const observer = new IntersectionObserver(onIntersect, { root: parent!.current }) 180 - observer.observe(ref.current!); 181 - 182 - return () => observer.disconnect(); 183 - }); 184 - 185 - return <div class="load-trigger" ref={ref} /> 186 - } 187 189 188 190 export default BufferView;
+18 -11
neo/src/chat/commands.ts
··· 1 - import { ChatBuffer, IrcChannel } from "tubes_core/channel"; 2 - import Storage from "./storage"; 1 + import { ChatBuffer } from "tubes_core/buffer"; 2 + import { IrcChannel } from "tubes_core/channel"; 3 3 4 4 export interface TubesCommand { 5 5 description: string, 6 - activate: (params: string, buffer: ChatBuffer) => Promise<void>, 6 + activate: (params: CommandParameters) => Promise<void>, 7 7 } 8 8 9 9 export const is_a_command = (input: string) => Boolean(input.match(/^\/[a-zA-Z]+/)); 10 10 11 - export function execute_command(input: string, buffer: ChatBuffer) { 11 + 12 + interface CommandParameters { 13 + input: string; 14 + buffer: ChatBuffer; 15 + 16 + reply(msg: string): void; 17 + } 18 + 19 + export function execute_command({ input, buffer, reply }: CommandParameters) { 12 20 if (is_a_command(input)) { 13 21 input = input.substring(1); 14 22 } ··· 20 28 const cmd = commands[cmd_name]; 21 29 22 30 if (!cmd) { 23 - throw new Error(`${cmd} is not a recognised command.`); 31 + throw new Error(`/${cmd_name} is not a recognised command.`); 24 32 } 25 33 26 - return cmd.activate(input, buffer); 34 + return cmd.activate({ input, buffer, reply }); 27 35 } 28 36 29 37 const commands = <Record<string, TubesCommand>>{ 30 38 "me": { 31 39 description: "", 32 - activate: async (text, buffer) => { 33 - const msg = buffer.privmsg(`\x01ACTION ${text}\x01`) 34 - Storage.store_message(msg, buffer.conn); 40 + activate: async ({ input, reply }) => { 41 + reply(`\x01ACTION ${input}\x01`); 35 42 }, 36 43 }, 37 44 "topic": { 38 45 description: "Set the channel's topic", 39 - activate: async (text, buffer) => { 46 + activate: async ({ input, buffer }) => { 40 47 console.log(buffer); 41 48 if (!(buffer instanceof IrcChannel)) { 42 49 throw new Error(`You can't set the topic on things that aren't channels.`); 43 50 } 44 51 45 - await buffer.set_topic(text); 52 + await buffer.set_topic(input); 46 53 } 47 54 } 48 55 };
-1
neo/src/chat/conns.ts
··· 1 1 import { computed, signal, type Signal } from "@preact/signals"; 2 2 import type { Connection } from "tubes_core"; 3 - import { ChatBuffer } from "tubes_core/channel"; 4 3 import type { ConnectionConfig } from "tubes_core/connection"; 5 4 import { Connection as WsConnection } from "tubes_core/ws"; 6 5 import { adapters } from "./adapters";
+7 -3
neo/src/chat/history.ts
··· 26 26 console.log(res.length, limit); 27 27 if (conn.debug) { 28 28 res = res.map(x => { 29 - x.retriver = "storage"; 29 + x.retriever = "storage"; 30 30 return x; 31 31 }); 32 32 } ··· 40 40 ); 41 41 if (conn.debug) { 42 42 more = more.map(x => { 43 - x.retriver = "chathistory"; 43 + x.retriever = "chathistory"; 44 44 return x; 45 45 }); 46 46 } 47 47 res = more.concat(res); 48 48 // if you start seeing message duplication, this is the place to look 49 49 for (const msg of res) { 50 - await Storage.store_message(msg, conn); 50 + try { 51 + await Storage.store_message(msg, conn); 52 + } catch (e) { 53 + console.log("failed to store chathistory message", e); 54 + } 51 55 } 52 56 } 53 57
+3 -3
neo/src/chat/storage.ts
··· 71 71 const range = IDBKeyRange.bound( 72 72 [0, target, conn.id, conn.adapter_id ?? ""], 73 73 [id, target, conn.id, conn.adapter_id ?? ""], 74 - false, true 74 + false, true, 75 75 ); 76 76 77 77 const cursor = await timestamps.openCursor(range, "prev"); 78 78 const msgs = []; 79 79 let count = 0; 80 - // this could get slow over time. 80 + // todo: this could get slow over time. 81 81 while (cursor && count < limit) { 82 82 const value = cursor.value; 83 83 if (!value) break; ··· 88 88 ) { 89 89 msgs.push(cursor.value); 90 90 count++; 91 - }; 91 + } 92 92 await cursor.continue(); 93 93 } 94 94
+4 -1
neo/src/css/buttons.css
··· 3 3 border: none; 4 4 padding: .15rem; 5 5 width: min-content; 6 - aspect-ratio: 1 / 1; 7 6 border-radius: 4px; 8 7 color: inherit; 9 8 cursor: pointer; 9 + 10 + display: flex; 11 + gap: .25rem; 12 + align-items: center; 10 13 11 14 &:hover { 12 15 background-color: var(--colour-grey-100);
+2 -2
neo/src/css/main.css
··· 123 123 124 124 --easing-subtle-out: cubic-bezier(0.165, 0.84, 0.44, 1); 125 125 126 - --font: 'Roboto Serif Variable', 'Roboto Serif', serif; 127 - font-family: var(--font); 126 + --font: 'Roboto Serif Variable', 'Roboto Serif'; 127 + font-family: var(--font), serif; 128 128 } 129 129 130 130 ::selection {
+21 -8
neo/src/css/messages.css
··· 92 92 margin-top: .6rem; 93 93 align-self: center; 94 94 font-size: 2rem; 95 - font-weight: 200; 95 + font-weight: 300; 96 + font-variation-settings: 'GRAD' -50; 96 97 color: var(--colour-grey-700); 97 98 user-select: none; 98 99 } ··· 384 385 385 386 padding: .725rem 1rem; 386 387 387 - align-items: baseline; 388 + align-items: center; 388 389 389 390 h1 { 390 391 font-size: .95rem; ··· 401 402 white-space: nowrap; 402 403 overflow: hidden; 403 404 text-overflow: ellipsis; 405 + align-self: baseline; 406 + } 407 + 408 + .icon-button { 409 + align-self: center; 410 + margin-left: auto; 411 + 412 + svg { 413 + width: 16px; 414 + height: 16px; 415 + } 404 416 } 405 417 } 406 418 ··· 529 541 color: var(--colour-grey-700); 530 542 531 543 .number { 532 - display: block; 544 + justify-self: end; 545 + 546 + display: flex; 547 + align-items: center; 548 + place-content: center; 549 + 533 550 height: 100%; 534 551 min-width: 1rem; 535 552 padding: 0 .15rem; 536 - display: flex; 537 - align-items: center; 538 - place-content: center; 539 - border-radius: 2px; 540 553 541 - justify-self: end; 542 554 font-variation-settings: 'GRAD' 100; 543 555 556 + border-radius: 2px; 544 557 background-color: var(--colour-accent-600); 545 558 color: white; 546 559 }
+3 -2
neo/vite.config.ts
··· 13 13 } 14 14 }, 15 15 build: { 16 - target: ["ESNext"] 17 - } 16 + target: ["ESNext"], 17 + }, 18 + base: './', 18 19 })