Skip to content

Commit 9c7d7ac

Browse files
window manager, dom selector, quote maker
1 parent aeba6e7 commit 9c7d7ac

10 files changed

Lines changed: 1051 additions & 99 deletions

File tree

src/components/Dialog.jsx

Lines changed: 192 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,176 @@
1-
import { Show, onMount, onCleanup } from "solid-js";
1+
import { Show, onMount, onCleanup, createSignal, children, createEffect } from "solid-js";
22
import { HiOutlineXMark } from "solid-icons/hi";
3+
import { render } from "solid-js/web";
4+
5+
const MIN_WIDTH = 320;
6+
const MIN_HEIGHT = 200;
7+
const SNAP_DISTANCE = 24;
8+
9+
// Module-level counter shared across all Dialog instances
10+
let topZIndex = 100;
311

412
export default function Dialog(props) {
13+
let dialog;
14+
15+
const [zIndex, setZIndex] = createSignal(topZIndex);
16+
17+
const bringToFront = () => {
18+
topZIndex += 1;
19+
setZIndex(topZIndex);
20+
};
21+
22+
createEffect(() => {
23+
if (!props.open || !dialog) return;
24+
25+
const header = dialog.querySelector(".dialog_header");
26+
if (!header) return;
27+
28+
if (header.querySelector(".dialog_close")) return;
29+
30+
const button = document.createElement("c");
31+
button.className = "dialog_close";
32+
button.setAttribute("aria-label", "Close dialog");
33+
34+
const dispose = render(
35+
() => <HiOutlineXMark size={24} />,
36+
button
37+
);
38+
39+
button.onclick = () => props.onClose?.();
40+
41+
header.appendChild(button);
42+
43+
onCleanup(() => {
44+
dispose();
45+
button.remove();
46+
});
47+
});
48+
49+
// Bring to front whenever the dialog opens
50+
createEffect(() => {
51+
if (props.open) bringToFront();
52+
});
53+
54+
let dragging = false;
55+
let resizing = false;
56+
let resizeDir = "";
57+
58+
const [rect, setRect] = createSignal({
59+
x: window.innerWidth / 2 - 500,
60+
y: window.innerHeight / 2 - 400,
61+
width: 1000,
62+
height: 800,
63+
});
64+
565
const handleKeyDown = (e) => {
6-
if (e.key === "Escape") {
7-
props.onClose?.();
8-
}
66+
if (e.key === "Escape") props.onClose?.();
67+
};
68+
69+
const beginDrag = (e) => {
70+
bringToFront();
71+
72+
const header = e.target.closest(".dialog_header");
73+
if (!header) return;
74+
75+
dragging = true;
76+
77+
const startX = e.clientX;
78+
const startY = e.clientY;
79+
const start = rect();
80+
81+
const move = (ev) => {
82+
if (!dragging) return;
83+
84+
setRect({
85+
...start,
86+
x: start.x + ev.clientX - startX,
87+
y: start.y + ev.clientY - startY,
88+
});
89+
};
90+
91+
const up = () => {
92+
dragging = false;
93+
94+
const r = rect();
95+
96+
if (r.x <= SNAP_DISTANCE) {
97+
setRect({
98+
x: 0,
99+
y: 0,
100+
width: window.innerWidth / 2,
101+
height: window.innerHeight,
102+
});
103+
} else if (
104+
r.x + r.width >=
105+
window.innerWidth - SNAP_DISTANCE
106+
) {
107+
setRect({
108+
x: window.innerWidth / 2,
109+
y: 0,
110+
width: window.innerWidth / 2,
111+
height: window.innerHeight,
112+
});
113+
} else if (r.y <= SNAP_DISTANCE) {
114+
setRect({
115+
x: 0,
116+
y: 0,
117+
width: window.innerWidth,
118+
height: window.innerHeight / 2,
119+
});
120+
}
121+
122+
window.removeEventListener("pointermove", move);
123+
window.removeEventListener("pointerup", up);
124+
};
125+
126+
window.addEventListener("pointermove", move);
127+
window.addEventListener("pointerup", up);
128+
};
129+
130+
const beginResize = (e, dir) => {
131+
e.stopPropagation();
132+
bringToFront();
133+
134+
resizing = true;
135+
resizeDir = dir;
136+
137+
const start = rect();
138+
const sx = e.clientX;
139+
const sy = e.clientY;
140+
141+
const move = (ev) => {
142+
let { x, y, width, height } = start;
143+
144+
const dx = ev.clientX - sx;
145+
const dy = ev.clientY - sy;
146+
147+
if (resizeDir.includes("e"))
148+
width = Math.max(MIN_WIDTH, start.width + dx);
149+
150+
if (resizeDir.includes("s"))
151+
height = Math.max(MIN_HEIGHT, start.height + dy);
152+
153+
if (resizeDir.includes("w")) {
154+
width = Math.max(MIN_WIDTH, start.width - dx);
155+
x = start.x + (start.width - width);
156+
}
157+
158+
if (resizeDir.includes("n")) {
159+
height = Math.max(MIN_HEIGHT, start.height - dy);
160+
y = start.y + (start.height - height);
161+
}
162+
163+
setRect({ x, y, width, height });
164+
};
165+
166+
const up = () => {
167+
resizing = false;
168+
window.removeEventListener("pointermove", move);
169+
window.removeEventListener("pointerup", up);
170+
};
171+
172+
window.addEventListener("pointermove", move);
173+
window.addEventListener("pointerup", up);
9174
};
10175

11176
onMount(() => {
@@ -16,26 +181,39 @@ export default function Dialog(props) {
16181
document.removeEventListener("keydown", handleKeyDown);
17182
});
18183

184+
const resolvedChildren = children(() => props.children);
185+
19186
return (
20187
<Show when={props.open}>
21188
<div
22189
class="dialog_overlay"
23-
onClick={() => props.onClose?.()}
190+
style={{ "z-index": zIndex() }}
24191
>
25192
<div
193+
ref={dialog}
26194
class="dialog"
195+
onPointerDown={(e) => {
196+
bringToFront();
197+
beginDrag(e);
198+
}}
27199
onClick={(e) => e.stopPropagation()}
200+
style={{
201+
left: `${rect().x}px`,
202+
top: `${rect().y}px`,
203+
width: `${rect().width}px`,
204+
height: `${rect().height}px`,
205+
}}
28206
>
29-
<button
30-
class="dialog_close"
31-
type="button"
32-
aria-label="Close dialog"
33-
onClick={() => props.onClose?.()}
34-
>
35-
<HiOutlineXMark size={24} />
36-
</button>
37-
38207
{props.children}
208+
209+
<div class="resize n" onPointerDown={(e) => beginResize(e, "n")} />
210+
<div class="resize s" onPointerDown={(e) => beginResize(e, "s")} />
211+
<div class="resize e" onPointerDown={(e) => beginResize(e, "e")} />
212+
<div class="resize w" onPointerDown={(e) => beginResize(e, "w")} />
213+
<div class="resize ne" onPointerDown={(e) => beginResize(e, "ne")} />
214+
<div class="resize nw" onPointerDown={(e) => beginResize(e, "nw")} />
215+
<div class="resize se" onPointerDown={(e) => beginResize(e, "se")} />
216+
<div class="resize sw" onPointerDown={(e) => beginResize(e, "sw")} />
39217
</div>
40218
</div>
41219
</Show>

src/components/compose/MessageComposer.jsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ import { HiOutlineXMark, HiOutlinePlus, HiOutlinePencil, HiOutlineArrowUpOnSquar
66
import { fetchRoturValidator } from "../../core/server_connection";
77
import Typing from "./Typing";
88

9+
export async function fileFromDataURI(dataURI, filename = `image-${Date.now()}.png`) {
10+
const res = await fetch(dataURI);
11+
const blob = await res.blob();
12+
return new File([blob], filename, {
13+
type: blob.type
14+
});
15+
}
16+
17+
export async function addAttachment(fileOrDataURI) {
18+
const file =
19+
typeof fileOrDataURI === "string"
20+
? await fileFromDataURI(fileOrDataURI)
21+
: fileOrDataURI;
22+
23+
queueAttachment(file);
24+
}
25+
926
export default function MessageComposer(props) {
1027
let textarea;
1128
let fileInput;
@@ -122,10 +139,8 @@ export default function MessageComposer(props) {
122139
}
123140

124141
async function handleFiles(e) {
125-
const files = [...e.target.files];
126-
127-
for (const file of files) {
128-
queueAttachment(file);
142+
for (const file of e.target.files) {
143+
await addAttachment(file);
129144
}
130145

131146
e.target.value = "";
@@ -403,21 +418,19 @@ export default function MessageComposer(props) {
403418
rows={1}
404419
placeholder={`Message #${props.channel}`}
405420
class="fill"
406-
onPaste={async (e) => {
421+
onPaste={async e => {
407422
const items = [...(e.clipboardData?.items || [])];
408423

409-
const imageItems = items.filter(
410-
item => item.kind === "file" &&
424+
for (const item of items) {
425+
if (
426+
item.kind === "file" &&
411427
item.type.startsWith("image/")
412-
);
413-
414-
if (imageItems.length === 0) return;
415-
416-
e.preventDefault();
417-
418-
for (const item of imageItems) {
419-
const file = item.getAsFile();
420-
if (file) queueAttachment(file);
428+
) {
429+
const file = item.getAsFile();
430+
if (file) {
431+
await addAttachment(file);
432+
}
433+
}
421434
}
422435
}}
423436
onInput={(e) => {

src/components/serverSidebar/Settings.jsx

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -195,30 +195,37 @@ export default function SettingsPage() {
195195
tabs.find((tab) => tab.id === activeTab());
196196

197197
return (
198-
<div class="fill x" style={{ "height": "100%" }}>
199-
<nav class="y" style={{ "gap": ".3em", "padding": ".5em", "background-color": "var(--bg-two)", "min-width": "200px" }}>
200-
<For each={tabs}>
201-
{(tab) => {
202-
const Icon = tab.icon;
203-
204-
return (
205-
<button
206-
type="button"
207-
onClick={() => setActiveTab(tab.id)}
208-
class={`icon_button text ${activeTab() === tab.id ? "active" : ""
209-
}`}
210-
>
211-
<Icon class="tab-icon" />
212-
<span>{tab.title}</span>
213-
</button>
214-
);
215-
}}
216-
</For>
217-
</nav>
218-
219-
<main class="fill settings_content" style={{ "padding": "0 2em" }}>
220-
<Dynamic component={currentTab()?.component} />
221-
</main>
222-
</div>
198+
<>
199+
<div className="dialog_header">
200+
<div className="x" style={{gap: ".3em", "align-items": "center"}}>
201+
<HiOutlineCog6Tooth />
202+
<span>Settings</span>
203+
</div>
204+
</div>
205+
<div class="fill x" style={{ "height": "100%" }}>
206+
<nav class="y" style={{ "gap": ".3em", "padding": ".5em", "background-color": "var(--bg-two)", "min-width": "200px" }}>
207+
<For each={tabs}>
208+
{(tab) => {
209+
const Icon = tab.icon;
210+
211+
return (
212+
<button
213+
type="button"
214+
onClick={() => setActiveTab(tab.id)}
215+
class={`icon_button text ${activeTab() === tab.id ? "active" : ""
216+
}`}
217+
>
218+
<Icon class="tab-icon" />
219+
<span>{tab.title}</span>
220+
</button>
221+
);
222+
}}
223+
</For>
224+
</nav>
225+
226+
<main class="fill settings_content" style={{ "padding": "0 2em" }}>
227+
<Dynamic component={currentTab()?.component} />
228+
</main>
229+
</div></>
223230
);
224231
}

0 commit comments

Comments
 (0)