gubes mirror. how does this work
1
fork

Configure Feed

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

overengineered dialogs

leah f553a923 c88bc587

+173 -91
+11
core/handler.ts
··· 4 4 import { parse_isupport } from "./isupport"; 5 5 import { IrcMessage } from "./parser"; 6 6 import { IrcChannelState, Numeric } from "./support"; 7 + import { DirectMessage } from "./direct"; 7 8 8 9 export type MessageHandler = (message: IrcMessage, connection: Connection) => Promise<void>; 9 10 ··· 103 104 : null; 104 105 }); 105 106 107 + break; 108 + } 109 + 110 + case "PRIVMSG": { 111 + if (message.source && message.params?.[0] == connection.nickname) { 112 + const nick = message.source?.nick; 113 + if (connection.$buffers.value.find(x => x.name == nick)) break; 114 + const buf = new DirectMessage(nick, connection); 115 + connection.$buffers.value = [...connection.$buffers.value, buf]; 116 + } 106 117 break; 107 118 } 108 119
+9 -4
core/history.ts
··· 1 1 import { Connection } from "./index"; 2 2 import { IrcMessage } from "./parser"; 3 + import { ChatBuffer } from "./buffer"; 3 4 4 5 export type FetchHistoryParams 5 6 = [ 6 - target: string, 7 - range: { before: Date, } 7 + target: string, 8 + range: { before: Date, } 8 9 | { after: Date, } 9 10 | { during: [Date, Date] } 10 11 | "latest", 11 - limit: number 12 - ] 12 + limit: number 13 + ] 13 14 14 15 /** 15 16 * History fetchers fetch history from locations. ··· 41 42 42 43 return await conn.collect_batch("chathistory", { mask: true, params: [target] }); 43 44 } 45 + 46 + export async function chathistory_targets(conn: Connection): Promise<ChatBuffer[]> { 47 + return [] 48 + }
+2
core/ws/connection.ts
··· 99 99 this.$buffers.value = channels; 100 100 } 101 101 102 + 103 + 102 104 // do this /after/ getting the motd to avoid it being missing 103 105 // when the connection opens. 104 106 // this feels incorrect, but who gives a shit
+53 -34
neo/src/bits/dialog.tsx
··· 1 1 import "@css/dialog.css"; 2 2 3 - import { useSignal } from "@preact/signals"; 3 + import { effect, useSignal } from "@preact/signals"; 4 4 import { reduced_motion } from "@src/support"; 5 5 import { spring, timeline } from "motion"; 6 - import { useLayoutEffect, useRef } from "preact/hooks"; 6 + import { useId } from "preact/hooks"; 7 + import { createRef, render } from "preact"; 7 8 8 - export type DialogControls = { open: () => void, close: (mode?: CloseReason) => void } 9 + export type DialogControls = { open: () => void, close: (mode?: CloseReason) => void } 9 10 export type DialogInnards<T = {}> = (props: DialogControls & T) => any; 10 11 11 12 export enum CloseReason { ··· 18 19 props?: T, 19 20 ) => { 20 21 const is_open = useSignal(false); 21 - const dialog_ref = useRef<HTMLDialogElement>(null); 22 22 23 - const open = () => is_open.value = true; 23 + const open = (ev?: MouseEvent) => { 24 + ev?.stopPropagation(); 25 + is_open.value = true; 26 + }; 27 + 28 + const id = useId(); 29 + const dialog_ref = createRef<HTMLDialogElement>(); 30 + 24 31 const close = (mode?: CloseReason) => { 25 32 if (!dialog_ref.current) return; 26 33 ··· 28 35 ? trans_out 29 36 : trans_out_cancel 30 37 31 - !reduced_motion() 32 - ? transition(dialog_ref.current) 33 - .finished 34 - .then(() => is_open.value = false) 35 - : is_open.value = false; 38 + !reduced_motion() 39 + ? transition(dialog_ref.current) 40 + .finished 41 + .then(() => dialog_ref.current?.close()) 42 + : dialog_ref.current?.close(); 36 43 }; 37 44 38 - const component = (diag_props?: any) => is_open.value 39 - ? <> 40 - <div class="dialog-scrim" aria-hidden /> 41 - <dialog ref={dialog_ref} class="fancy-dialog" {...(diag_props ?? {})}> 42 - {/* @ts-ignore probably fine */} 43 - <Content open={open} close={close} {...(props ?? {})} /> 44 - </dialog> 45 - </> 46 - : <></> 45 + const on_outside_click = () => close(CloseReason.Cancel); 46 + 47 + const Component = (diag_props?: any) => <dialog 48 + ref={dialog_ref} 49 + class="fancy-dialog" 50 + {...(diag_props ?? {})} 51 + > 52 + <div 53 + class="innards" 54 + // don't close when click inside the thing 55 + onClick={e => e.stopPropagation()} 56 + > 57 + {/* @ts-ignore probably fine */} 58 + <Content open={open} close={close} {...(props ?? {})} /> 59 + </div> 60 + <div class="dialog-scrim" aria-hidden/> 61 + </dialog> 47 62 48 - useLayoutEffect(() => { 49 - if (!dialog_ref.current) return; 50 - if (is_open.value == true) { 51 - dialog_ref.current.showModal(); 52 - dialog_ref.current.addEventListener("close", (e) => { 53 - e.preventDefault(); 54 - close() 55 - }); 63 + effect(() => { 64 + if (is_open.value) { 65 + const elem = document.createElement("div"); 66 + elem.id = id; 67 + const main = document.getElementById("app-main")!; 68 + render(<Component/>, main.appendChild(elem)); 69 + 70 + dialog_ref.current?.showModal(); 71 + dialog_ref.current?.addEventListener("close", () => is_open.value = false); 72 + dialog_ref.current?.addEventListener("click", on_outside_click); 56 73 57 - !reduced_motion() && trans_in(dialog_ref.current); 74 + !reduced_motion() && trans_in(dialog_ref.current!); 75 + } else { 76 + const elem = document.getElementById(id); 77 + elem?.removeEventListener("click", on_outside_click); 78 + elem?.remove(); 58 79 } 59 - }, [is_open.value]); 60 - 80 + }); 61 81 62 82 return { 63 83 open, 64 84 close, 65 85 is_open, 66 - Component: component, 67 86 } 68 87 } 69 88 70 89 const dialog_spring = spring({ stiffness: 750, damping: 45 }); 71 90 72 91 const trans_in = (dialog: HTMLDialogElement) => timeline([ 73 - [dialog, { opacity: [0, 1], scale: [0.5, 1] }], 92 + [dialog.children[0], { opacity: [0, 1], scale: [0.5, 1] }], 74 93 [".dialog-scrim", { opacity: [0, 1] }, { at: 0 }], 75 94 ], { defaultOptions: { easing: dialog_spring } }); 76 95 77 96 const trans_out_cancel = (dialog: HTMLDialogElement) => timeline([ 78 - [dialog, { opacity: [1, 0, null], scale: [1, 0.75] }, { at: 0 }], 97 + [dialog.children[0], { opacity: [1, 0, null], scale: [1, 0.75] }, { at: 0 }], 79 98 [".dialog-scrim", { opacity: [1, 0] }, { at: 0 }], 80 99 ], { defaultOptions: { easing: dialog_spring } }); 81 100 82 101 const trans_out = (dialog: HTMLDialogElement) => timeline([ 83 - [dialog, { opacity: [1, 0, null], scale: [1, 1.25] }], 102 + [dialog.children[0], { opacity: [1, 0, null], scale: [1, 1.25] }], 84 103 [".dialog-scrim", { opacity: [1, 0] }, { at: 0 }], 85 104 ], { defaultOptions: { easing: dialog_spring } });
+1 -2
neo/src/bits/sidebar/add-network.tsx
··· 16 16 const dialog = create_dialog(AddNetworkDialog); 17 17 18 18 return <> 19 - <dialog.Component style="max-width: 36rem; width: auto;" /> 20 19 <TextButton class="add-network" onClick={() => { dialog.open() }}> 21 20 <Plus width="15px" height="15px" /> 22 21 Add Network ··· 40 39 } 41 40 42 41 const error = signal(""); 43 - return <MiniNavigator transitions> 42 + return <MiniNavigator transitions style="padding: .75rem; width: 36rem;"> 44 43 {controls => <> 45 44 <hgroup style="margin-bottom: 1rem"> 46 45 <h2 class="heading">Add a Network</h2>
+49 -26
neo/src/bits/sidebar/network-section.tsx
··· 14 14 import { create_popover } from "../popover"; 15 15 import { SidebarLink } from "./sidebar-item"; 16 16 import SidebarSection from "./sidebar-section"; 17 + import { IrcChannel } from "tubes_core/channel.ts"; 18 + import { DirectMessage } from "tubes_core/direct.ts"; 17 19 18 20 import ArchiveIcon from "~icons/ph/archive"; 19 21 import ConnectionFailedIcon from "~icons/ph/diamond-fill"; ··· 29 31 import ConnectIcon from "~icons/ph/plugs-connected"; 30 32 import JoinIcon from "~icons/ph/plus"; 31 33 import LoginIcon from "~icons/ph/sign-in"; 34 + import { ChatBuffer } from "tubes_core/buffer"; 32 35 33 36 export const NetworkSection: FunctionalComponent<{ conn: Connection; }> = ({ conn }) => { 34 37 const errored = conn.$state.value == ConnectionState.Failed; ··· 44 47 const icon = useComputed(() => { 45 48 switch (conn.$state.value) { 46 49 case ConnectionState.Disconnected: 47 - case ConnectionState.Disconnecting: return <DisconnectedIcon title="Disconnected" />; 50 + case ConnectionState.Disconnecting: 51 + return <DisconnectedIcon title="Disconnected"/>; 48 52 49 53 case ConnectionState.Connecting: 50 - case ConnectionState.Registering: return <ConnectingIcon title="Connecting" />; 54 + case ConnectionState.Registering: 55 + return <ConnectingIcon title="Connecting"/>; 51 56 52 - case ConnectionState.Failed: return <ConnectionFailedIcon title="Failed to Connect" />; 57 + case ConnectionState.Failed: 58 + return <ConnectionFailedIcon title="Failed to Connect"/>; 53 59 54 - case ConnectionState.Connected: return <></>; 60 + case ConnectionState.Connected: 61 + return <></>; 55 62 } 56 63 }); 57 64 ··· 91 98 92 99 93 100 {conn.supports.sasl() && <ListMenuItem 94 - onClick={() => { }} 101 + onClick={() => { 102 + }} 95 103 icon={LoginIcon} 96 104 > 97 105 Log In 98 106 </ListMenuItem>} 99 107 100 108 {conn.supports.registration() && <ListMenuItem 101 - onClick={() => { }} 109 + onClick={() => { 110 + }} 102 111 icon={RegisterIcon} 103 112 > 104 113 Register 105 114 </ListMenuItem>} 106 115 107 116 <ListMenuItem 108 - onClick={() => { details_diag.open() }} 117 + onClick={() => { 118 + details_diag.open() 119 + }} 109 120 icon={InfoIcon} 110 121 > 111 122 Details 112 123 </ListMenuItem> 113 124 114 125 <ListMenuItem 115 - onClick={() => { }} 126 + onClick={() => { 127 + }} 116 128 icon={ConfIcon} 117 129 > 118 130 Configure ··· 121 133 <ListMenuItem destructive icon={ArchiveIcon}>Archive</ListMenuItem> 122 134 </ListMenu>); 123 135 136 + const channels = useComputed(() => 137 + conn.$buffers.value.filter(x => x instanceof IrcChannel) 138 + ) 139 + const dms = useComputed(() => 140 + conn.$buffers.value.filter(x => x instanceof DirectMessage) 141 + ) 142 + 124 143 return <SidebarSection> 125 144 {/* da header */} 126 145 <header class="sidebar-section-header"> ··· 142 161 title="Connect" 143 162 onClick={() => conn.connect()} 144 163 > 145 - <ConnectIcon aria-hidden /> 164 + <ConnectIcon aria-hidden/> 146 165 </IconButton> 147 166 : <IconButton 148 167 title="Connection Info" ··· 151 170 details_diag.open() 152 171 }} 153 172 > 154 - <InfoIcon aria-hidden /> 173 + <InfoIcon aria-hidden/> 155 174 </IconButton> 156 175 } 157 176 <IconButton ··· 161 180 }} 162 181 title="Configure" 163 182 > 164 - <ConfIcon aria-hidden /> 183 + <ConfIcon aria-hidden/> 165 184 </IconButton> 166 - <IconButton title="More Stuff" onClick={(e) => menu.open(e)}><EtcIcon aria-hidden /></IconButton> 185 + <IconButton title="More Stuff" onClick={(e) => menu.open(e)}><EtcIcon aria-hidden/></IconButton> 167 186 168 - <menu.Popover /> 187 + <menu.Popover/> 169 188 </header> 170 189 171 190 {/* error message if there is one */} 172 - {errored && <div class="connection-error"><ConnErrorMessage conn={conn} /></div>} 173 - 191 + {errored && <div class="connection-error"><ConnErrorMessage conn={conn}/></div>} 192 + 174 193 {conn.$recovering.value && "Recovering..."} 175 194 176 195 {/* list of buffers */} 177 196 <ul class="sidebar-list"> 178 197 {/* buffers */} 179 - {conn.$buffers.value.map(x => <BufferListItem buffer={x} />)} 180 - 198 + {dms.value.length != 0 && <h3>Channels</h3>} 199 + {channels.value.map(x => <BufferListItem buffer={x}/>)} 181 200 <SidebarLink to={`${connection_base(conn)}/join`}> 182 - Join Channel <JoinIcon /> 201 + Join Channel <JoinIcon/> 183 202 </SidebarLink> 203 + {dms.value.length != 0 && <h3>Direct Messages</h3>} 204 + {dms.value.map(x => <BufferListItem buffer={x}/>)} 184 205 </ul> 185 - 186 - <details_diag.Component style="max-width: 90vw; width: auto; padding: 0;" /> 187 206 </SidebarSection>; 188 207 }; 189 208 ··· 200 219 201 220 const ConnErrorMessage: FunctionalComponent<{ conn: Connection }> = ({ conn }) => { 202 221 switch (conn.$error.value?.[0]) { 203 - case ConnectionErrorCode.NickTaken: return <NickTaken conn={conn} /> 204 - case ConnectionErrorCode.SocketError: return <SocketError conn={conn} /> 205 - case ConnectionErrorCode.Banned: return <Banned conn={conn} /> 222 + case ConnectionErrorCode.NickTaken: 223 + return <NickTaken conn={conn}/> 224 + case ConnectionErrorCode.SocketError: 225 + return <SocketError conn={conn}/> 226 + case ConnectionErrorCode.Banned: 227 + return <Banned conn={conn}/> 206 228 // case ConnectionError.SaslFailed: 207 229 // case ConnectionError.SaslTooLong: 208 230 // case ConnectionError.SaslAborted: 209 - default: return <></> 231 + default: 232 + return <></> 210 233 } 211 234 } 212 235 ··· 236 259 <div class="buttons"> 237 260 <TextButton onClick={() => conn.connect()}>Try Again</TextButton> 238 261 <TextButton onClick={update.open}>Update Nickname</TextButton> 239 - <update.Popover /> 262 + <update.Popover/> 240 263 </div> 241 264 </>; 242 265 } ··· 245 268 return <> 246 269 <h3>Couldn't Connect</h3> 247 270 <p class="body-small low-emphasis"> 248 - Tubes could not connect to <i>{conn.config.url}</i>. <br /> 271 + Tubes could not connect to <i>{conn.config.url}</i>. <br/> 249 272 Check if you're connected to the internet, the URL is correct 250 273 and the network isn't experiencing any downtime. 251 274 </p>
-16
neo/src/css/debug.css
··· 26 26 } 27 27 } 28 28 29 - div.sep { 30 - display: flex; 31 - white-space: nowrap; 32 - align-items: center; 33 - gap: 1rem; 34 - letter-spacing: .5rem; 35 - user-select: none; 36 - 37 - &::before, &::after { 38 - content: ''; 39 - width: 100%; 40 - height: 1px; 41 - background-color: var(--colour-grey-200); 42 - } 43 - } 44 - 45 29 h2 { 46 30 font-size: 6vw; 47 31 font-weight: 200;
+24 -6
neo/src/css/dialog.css
··· 1 1 dialog.fancy-dialog { 2 - position: relative; 2 + position: fixed; 3 3 z-index: 10; 4 4 border: none; 5 - border-radius: 6px; 6 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 7 - padding: .75rem; 8 - max-width: auto; 9 - 5 + background-color: transparent; 6 + width: 100%; 7 + height: 100%; 8 + padding: 1rem; 9 + 10 10 &::backdrop { 11 11 display: none; 12 12 } 13 + } 14 + 15 + dialog.fancy-dialog > .innards { 16 + position: fixed; 17 + 18 + left: 0; 19 + right: 0; 20 + top: 0; 21 + bottom: 0; 22 + width: max-content; 23 + height: max-content; 24 + 25 + margin: auto; 26 + z-index: 10; 27 + 28 + border-radius: 6px; 29 + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 30 + background-color: #fff; 13 31 } 14 32 15 33 .dialog-scrim {
+9 -2
neo/src/css/settings.css
··· 2 2 padding-bottom: 2rem; 3 3 container-type: inline-size; 4 4 5 + display: grid; 6 + grid-template-columns: 0 minmax(1fr, 56rem) 0; 7 + gap: 0 .5rem; 8 + 5 9 h1 { 6 10 text-align: center; 7 11 margin-top: .4em; 8 12 margin-bottom: .5em; 9 13 color: var(--colour-grey-900); 14 + grid-column: 1 / span 3; 10 15 } 11 16 12 17 h2 { ··· 20 25 user-select: none; 21 26 z-index: -1; 22 27 letter-spacing: -0.025em; 28 + grid-column: 1 / span 3; 23 29 } 24 30 25 31 div.sep { ··· 36 42 height: 1px; 37 43 background-color: var(--colour-grey-200); 38 44 } 45 + 46 + grid-column: 1 / span 3; 39 47 } 40 48 41 49 div.panel { 50 + grid-column: 2; 42 51 position: relative; 43 52 z-index: 1; 44 53 max-width: 56rem; 45 54 width: 100%; 46 - margin: auto; 47 - margin-bottom: 2rem; 48 55 background-color: white; 49 56 border-radius: 6px; 50 57 padding: .5rem;
+14
neo/src/css/sidebar.css
··· 140 140 display: flex; 141 141 flex-direction: column; 142 142 gap: .15rem; 143 + 144 + h3 { 145 + font-size: .7rem; 146 + font-weight: 500; 147 + margin: 0 .75rem; 148 + margin-bottom: .1rem; 149 + margin-top: .5rem; 150 + font-variation-settings: 'GRAD' 100; 151 + color: var(--colour-grey-600); 152 + 153 + &:first-of-type { 154 + margin-top: .25rem; 155 + } 156 + } 143 157 } 144 158 145 159 li.sidebar-item {
+1 -1
neo/src/main.tsx
··· 21 21 const css = build_accent_css(accent.value); 22 22 23 23 return <Router hook={useHashLocation}> 24 - <main class="app" style={css}> 24 + <main class="app" id="app-main" style={css}> 25 25 <Sidebar /> 26 26 <Switch> 27 27 <Route path="/">