···4040documentation at the moment, so if you get stuck and/or confused please do ping
4141me!
42424343-> [!NOTE]
4444-> This is a purely ideological project. Any changes that appear technically motivated will be rejected.
4343+> [!NOTE]\
4444+> This is a purely ideological project. Any changes that appear technically
4545+> motivated will be rejected.
45464647[codeberg]: https://codeberg.org/smethwick/tubes
4748[issues]: https://codeberg.org/smethwick/tubes/issues
48494950## licence
50515151-MPL 2.0, except tubes_core (inside lib/), which is ISC
5252+MPL 2.0, except tubes_core (inside core/), which is ISC
+1-1
core/adapter.ts
···11-import { Signal } from "@preact/signals";
11+import { Signal } from "@preact/signals-core";
22import { Connection, ConnectionConfig } from "./connection";
33import { IrcProtocol } from "./support";
44
+35-13
core/channel.ts
···11import type { Connection } from "./connection";
22import { IrcMessage } from "./parser";
33-import { IrcChannelState } from "./support";
33+import { IrcChannelState, Numeric } from "./support";
44import { Mutex } from 'async-mutex';
55-import { signal, Signal, batch } from "@preact/signals";
55+import { signal, Signal, batch } from "@preact/signals-core";
66import { FetchHistoryParams } from "./history";
77+import { ArrayMatcher, Matcher, Wildcard } from "./queue";
7889const member_prefixes = [
910 "~", "&", "@", "%"
···49505051 // set the range for the next iteration
5152 next_range = { after: oldest };
5252-5353+5354 page.reverse();
5455 } else {
5556 // as above, but inverted
···130131 this.$state.value = initial_state;
131132132133 this.join_promise = new Promise((resolve, reject) => {
133133- this.join_resolve = resolve;
134134- this.join_reject = reject;
134134+ this.#join_resolve = resolve;
135135+ this.#join_reject = reject;
135136136137 setTimeout(() => {
137138 if (this.$state.value == IrcChannelState.Pending) {
138139 this.$state.value = IrcChannelState.Failed;
139140 this.$error.value = "Timed out";
140140- this.join_reject("Timed out");
141141+ this.#join_reject("Timed out");
141142 }
142143 }, 30000);
143144 });
···149150150151 $members: Signal<string[]> = signal([]);
151152 $member_names: Signal<string[]> = signal([]);
152152- member_mutex = new Mutex();
153153+ #member_mutex = new Mutex();
153154154155 join_promise: Promise<IrcChannel>;
155156156156- private join_resolve!: (value: IrcChannel) => void;
157157- private join_reject!: (reason?: any) => void;
157157+ #join_resolve!: (value: IrcChannel) => void;
158158+ #join_reject!: (reason?: any) => void;
158159159160 finish_join() {
160161 this.$state.value = IrcChannelState.Joined;
161161- this.join_resolve(this);
162162+ this.#join_resolve(this);
162163 }
163164164165 handle_join(message: IrcMessage) {
165166 const nick = message.source!.nick;
166166- this.member_mutex.runExclusive(() => {
167167+ this.#member_mutex.runExclusive(() => {
167168 this.$members.value = [...this.$members.value, nick];
168169 })
169170 }
170171171172 handle_part(message: IrcMessage) {
172173 const nick = message.source!.nick;
173173- this.member_mutex.runExclusive(() => {
174174+ this.#member_mutex.runExclusive(() => {
174175 this.$members.value = this.$members.value.filter(o => o != nick);
175176 });
176177 }
···197198 return o;
198199 });
199200200200- this.member_mutex.runExclusive(() => {
201201+ this.#member_mutex.runExclusive(() => {
201202 if (this.#has_all_names) {
202203 this.$members.value = [];
203204 }
···229230 this.$members.value = [];
230231 this.$state.value = IrcChannelState.Parted;
231232 })
233233+ }
234234+235235+ /**
236236+ * Set the channel's topic.
237237+ * @param text The new topic for the channel
238238+ * @returns A promise that resolves when the server acknowledges the change,
239239+ * or rejects if the server isn't happy with it.
240240+ */
241241+ set_topic(text: string) {
242242+ this.conn.send(`TOPIC ${this.name} :${text}`);
243243+ return this.conn.expect(
244244+ `changing topic for ${this.name}`,
245245+ new Matcher("TOPIC", this.name),
246246+ // rejection cases
247247+ new ArrayMatcher(
248248+ new Matcher(Numeric.ERR_CHANOPRIVSNEEDED, Wildcard.Any, this.name),
249249+ new Matcher(Numeric.ERR_NOTONCHANNEL, Wildcard.Any, this.name),
250250+ new Matcher(Numeric.ERR_NOSUCHCHANNEL, Wildcard.Any, this.name),
251251+ new Matcher(Numeric.ERR_NEEDMOREPARAMS, Wildcard.Any, this.name),
252252+ )
253253+ )
232254 }
233255}
234256
+3-2
core/connection.ts
···11-import { Signal, signal } from "@preact/signals";
11+import { Signal, signal } from "@preact/signals-core";
22import { nanoid } from "nanoid";
33import { ChatBuffer, IrcChannel } from "./channel";
44import { MessageHandler, default_handler } from "./handler";
···104104 SaslFailed,
105105 SaslTooLong,
106106 SaslAborted,
107107+ Banned,
107108}
108109109110const ping_interval = 30000;
···335336336337 if (!supported_chan_prefixes.includes(channel.charAt(0))) {
337338 throw new Error(
338338- `Channels on this network must start with one of these: ` +
339339+ `Channels on this network must start with one of these symbols: ` +
339340 supported_chan_prefixes.join(", ")
340341 );
341342 }
+8-1
core/handler.ts
···11-import { batch } from "@preact/signals";
11+import { batch } from "@preact/signals-core";
22import { IrcChannel } from "./channel";
33import { Connection, ConnectionErrorCode, ConnectionState } from "./connection";
44import { parse_isupport } from "./isupport";
···1515 connection.$error.value = [ConnectionErrorCode.NickTaken, message];
1616 }
17171818+ break;
1919+ }
2020+2121+ case Numeric.ERR_YOUREBANNEDCREEP: {
2222+ connection.disconnect(ConnectionState.Failed);
2323+ connection.$error.value = [ConnectionErrorCode.Banned, message];
2424+1825 break;
1926 }
2027
···11-import { Signal, signal } from "@preact/signals";
11+import { Signal, signal } from "@preact/signals-core";
22import { Adapter } from "../adapter";
33import { ConnectionConfig, ConnectionParameters } from "../connection";
44import { IrcProtocol } from "../support";
+3-3
core/soju/connection.ts
···11import WebSocket from "isomorphic-ws";
22import { Connection } from "..";
33+import { collect_caps, negotiate_caps } from "../capabilities";
34import { ConnectionErrorCode, ConnectionParameters, ConnectionState } from "../connection";
55+import { collect_motd } from "../motd";
46import { IrcMessage } from "../parser";
55-import { SojuAdapter } from "./adapter";
67import { Deferred } from "../queue";
77-import { collect_caps, negotiate_caps } from "../capabilities";
88import { Sasl } from "../sasl";
99import { Numeric } from "../support";
1010-import { collect_motd } from "../motd";
1010+import { SojuAdapter } from "./adapter";
11111212export class SojuConnection extends Connection {
1313 constructor(
···11+import "@css/join-channel.css";
22+import { useSignal } from "@preact/signals";
33+import { PrimaryButton } from "@src/bits/buttons";
44+import { ErrorMessage } from "@src/bits/errors";
55+import FormField from "@src/bits/form/form-field";
66+import { FunctionalComponent } from "preact"
77+import { Connection } from "tubes_core"
88+import Connections, { connection_base } from "@src/chat/conns";
99+import { useLocation } from "wouter-preact";
1010+1111+const JoinChannelPage: FunctionalComponent<{ conn: Connection }> = ({ conn }) => {
1212+ const error = useSignal<string | undefined>();
1313+ const [, set_location] = useLocation();
1414+1515+ return <article class="join-channel">
1616+ <section>
1717+ <h2>Join a Channel</h2>
1818+ <p class="body-small">
1919+ Conversations in Tubes happen in seperate channels.
2020+ Channels tend to center around a specific topic or project
2121+ they're associated with.
2222+ </p>
2323+2424+ <form onSubmit={(e) => {
2525+ error.value = undefined;
2626+ e.preventDefault();
2727+ const data = new FormData(e.currentTarget);
2828+ const channel = data.get("channel");
2929+ if (!channel || typeof channel != "string") {
3030+ return;
3131+ }
3232+3333+ try {
3434+ conn.join_channel(channel);
3535+ Connections.add_autojoin(channel, conn);
3636+ set_location(`${connection_base(conn)}/channel/${channel}`)
3737+ } catch (e) {
3838+ if (e instanceof Error) {
3939+ error.value = e.message;
4040+ }
4141+ }
4242+ }}>
4343+ <FormField
4444+ label="Channel Name"
4545+ flavour_text="the name of the channel you want to join. these usually start with a '#'"
4646+ >
4747+ <input required type="text" name="channel" placeholder="e.g., #tubes" />
4848+ </FormField>
4949+5050+ {error.value && <ErrorMessage>{error}</ErrorMessage>}
5151+5252+ <PrimaryButton style="margin-left: auto;">join</PrimaryButton>
5353+ </form>
5454+ </section>
5555+ <section>
5656+ <h2>Browse Channels</h2>
5757+ <p class="body-small">
5858+ Get a big ol list of every channel on this network.
5959+ </p>
6060+ </section>
6161+ </article>
6262+}
6363+6464+export default JoinChannelPage;
+30-8
neo/src/settings/index.tsx
···44import { FunctionalComponent } from "preact";
55import { HTMLProps, TargetedEvent } from "preact/compat";
66import { useEffect, useRef } from "preact/hooks";
77+import Meta from "../../../meta.json";
7889// helper component that fires an event when a radio button in a fieldset is selected
910const FieldSetRadioButtonEventGroupThing
···51525253 <h2>bouncers</h2>
5354 <div class="panel">
5454- <p class="intro">
5555- bouncers
5555+ <p class="intro body-small">
5656+ a bouncer is an external service that keeps a record of message
5757+ history while tubes is closed, among other things.
5858+ <br />
5959+ depending on what networks you're connecting to and your
6060+ personal value system this might or might not be desirable.
5661 </p>
57625863 <div>
···133138 </div>
134139 <h2>networks</h2>
135140 <div class="panel">
136136- networks
141141+ <p class="intro body-small">take a cold hard look at the networks you're connected to.</p>
137142 </div>
138143 <h2>notifications</h2>
139144 <div class="panel">
140140- notifications
145145+ <p class="intro body-small">get a wide variety of dings and buzzes when things happen.</p>
141146 </div>
142147 <h2>accessibility</h2>
143148 <div class="panel">
144144- accessibility
149149+ <p class="intro body-small">make tubes work a little bit better for you.</p>
150150+145151 </div>
146146- <h2>other</h2>
152152+ <h2>bonus</h2>
147153 <div class="panel">
148148- other
154154+ <p class="intro body-small">additional buttons for adventurous folk.</p>
149155 </div>
150156 <h2>about</h2>
151157 <div class="panel">
152152- about
158158+ <article class="blurb body-small">
159159+ <b>Tubes {Meta.version}</b>
160160+ <p>
161161+ Tubes is developed in front of a live studio audience by
162162+ <a href="https://leah.pronounmail.com" target="_blank">
163163+ Leah Clark
164164+ </a>.
165165+ </p>
166166+ <p>
167167+ Tubes is nice and
168168+ <a href="https://codeberg.org/smethwick/tubes" target="_blank">
169169+ open source
170170+ </a>,
171171+ so please feel free to come along and contribute some code
172172+ if you feel so inclined!
173173+ </p>
174174+ </article>
153175 </div>
154176 </article>
155177}