+18
appview/notify/merged_notifier.go
+18
appview/notify/merged_notifier.go
···
66
66
notifier.UpdateProfile(ctx, profile)
67
67
}
68
68
}
69
+
70
+
func (m *mergedNotifier) NewString(ctx context.Context, string *db.String) {
71
+
for _, notifier := range m.notifiers {
72
+
notifier.NewString(ctx, string)
73
+
}
74
+
}
75
+
76
+
func (m *mergedNotifier) EditString(ctx context.Context, string *db.String) {
77
+
for _, notifier := range m.notifiers {
78
+
notifier.EditString(ctx, string)
79
+
}
80
+
}
81
+
82
+
func (m *mergedNotifier) DeleteString(ctx context.Context, did, rkey string) {
83
+
for _, notifier := range m.notifiers {
84
+
notifier.DeleteString(ctx, did, rkey)
85
+
}
86
+
}
+8
appview/notify/notifier.go
+8
appview/notify/notifier.go
···
21
21
NewPullComment(ctx context.Context, comment *db.PullComment)
22
22
23
23
UpdateProfile(ctx context.Context, profile *db.Profile)
24
+
25
+
NewString(ctx context.Context, s *db.String)
26
+
EditString(ctx context.Context, s *db.String)
27
+
DeleteString(ctx context.Context, did, rkey string)
24
28
}
25
29
26
30
// BaseNotifier is a listener that does nothing
···
42
46
func (m *BaseNotifier) NewPullComment(ctx context.Context, comment *db.PullComment) {}
43
47
44
48
func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *db.Profile) {}
49
+
50
+
func (m *BaseNotifier) NewString(ctx context.Context, s *db.String) {}
51
+
func (m *BaseNotifier) EditString(ctx context.Context, s *db.String) {}
52
+
func (m *BaseNotifier) DeleteString(ctx context.Context, did, rkey string) {}
+90
appview/pages/templates/fragments/multiline-select.html
+90
appview/pages/templates/fragments/multiline-select.html
···
1
+
{{ define "fragments/multiline-select" }}
2
+
<script>
3
+
function highlight(scroll = false) {
4
+
document.querySelectorAll(".hl").forEach(el => {
5
+
el.classList.remove("hl");
6
+
});
7
+
8
+
const hash = window.location.hash;
9
+
if (!hash || !hash.startsWith("#L")) {
10
+
return;
11
+
}
12
+
13
+
const rangeStr = hash.substring(2);
14
+
const parts = rangeStr.split("-");
15
+
let startLine, endLine;
16
+
17
+
if (parts.length === 2) {
18
+
startLine = parseInt(parts[0], 10);
19
+
endLine = parseInt(parts[1], 10);
20
+
} else {
21
+
startLine = parseInt(parts[0], 10);
22
+
endLine = startLine;
23
+
}
24
+
25
+
if (isNaN(startLine) || isNaN(endLine)) {
26
+
console.log("nan");
27
+
console.log(startLine);
28
+
console.log(endLine);
29
+
return;
30
+
}
31
+
32
+
let target = null;
33
+
34
+
for (let i = startLine; i<= endLine; i++) {
35
+
const idEl = document.getElementById(`L${i}`);
36
+
if (idEl) {
37
+
const el = idEl.closest(".line");
38
+
if (el) {
39
+
el.classList.add("hl");
40
+
target = el;
41
+
}
42
+
}
43
+
}
44
+
45
+
if (scroll && target) {
46
+
target.scrollIntoView({
47
+
behavior: "smooth",
48
+
block: "center",
49
+
});
50
+
}
51
+
}
52
+
53
+
document.addEventListener("DOMContentLoaded", () => {
54
+
console.log("DOMContentLoaded");
55
+
highlight(true);
56
+
});
57
+
window.addEventListener("hashchange", () => {
58
+
console.log("hashchange");
59
+
highlight();
60
+
});
61
+
window.addEventListener("popstate", () => {
62
+
console.log("popstate");
63
+
highlight();
64
+
});
65
+
66
+
const lineNumbers = document.querySelectorAll('a[href^="#L"');
67
+
let startLine = null;
68
+
69
+
lineNumbers.forEach(el => {
70
+
el.addEventListener("click", (event) => {
71
+
event.preventDefault();
72
+
const currentLine = parseInt(el.href.split("#L")[1]);
73
+
74
+
if (event.shiftKey && startLine !== null) {
75
+
const endLine = currentLine;
76
+
const min = Math.min(startLine, endLine);
77
+
const max = Math.max(startLine, endLine);
78
+
const newHash = `#L${min}-${max}`;
79
+
history.pushState(null, '', newHash);
80
+
} else {
81
+
const newHash = `#L${currentLine}`;
82
+
history.pushState(null, '', newHash);
83
+
startLine = currentLine;
84
+
}
85
+
86
+
highlight();
87
+
});
88
+
});
89
+
</script>
90
+
{{ end }}
+7
-2
appview/pages/templates/layouts/profilebase.html
+7
-2
appview/pages/templates/layouts/profilebase.html
···
9
9
10
10
{{ define "content" }}
11
11
{{ template "profileTabs" . }}
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded w-full dark:text-white drop-shadow-sm">
12
+
<section class="bg-white dark:bg-gray-800 px-2 py-6 md:p-6 rounded w-full dark:text-white drop-shadow-sm">
13
13
<div class="grid grid-cols-1 md:grid-cols-11 gap-4">
14
-
<div class="md:col-span-3 order-1 md:order-1">
14
+
{{ $style := "hidden md:block md:col-span-3" }}
15
+
{{ if eq $.Active "overview" }}
16
+
{{ $style = "md:col-span-3" }}
17
+
{{ end }}
18
+
<div class="{{ $style }} order-1 order-1">
15
19
<div class="flex flex-col gap-4">
16
20
{{ template "user/fragments/profileCard" .Card }}
17
21
{{ block "punchcard" .Card.Punchcard }} {{ end }}
18
22
</div>
19
23
</div>
24
+
20
25
{{ block "profileContent" . }} {{ end }}
21
26
</div>
22
27
</section>
+1
appview/pages/templates/repo/blob.html
+1
appview/pages/templates/repo/blob.html
+6
-1
appview/pages/templates/repo/fragments/shortTimeAgo.html
+6
-1
appview/pages/templates/repo/fragments/shortTimeAgo.html
···
1
1
{{ define "repo/fragments/shortTimeAgo" }}
2
-
{{ template "repo/fragments/timeWrapper" (dict "Time" . "Content" (print (. | shortRelTimeFmt) " ago")) }}
2
+
{{ $formatted := shortRelTimeFmt . }}
3
+
{{ $content := printf "%s ago" $formatted }}
4
+
{{ if eq $formatted "now" }}
5
+
{{ $content = "now" }}
6
+
{{ end }}
7
+
{{ template "repo/fragments/timeWrapper" (dict "Time" . "Content" $content) }}
3
8
{{ end }}
4
9
+3
-2
appview/pages/templates/strings/string.html
+3
-2
appview/pages/templates/strings/string.html
···
23
23
hx-boost="true"
24
24
href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit">
25
25
{{ i "pencil" "size-4" }}
26
-
<span class="hidden md:inline">edit</span>
26
+
<span class="hidden md:inline">edit</span>
27
27
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
28
28
</a>
29
29
<button
···
34
34
hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?"
35
35
>
36
36
{{ i "trash-2" "size-4" }}
37
-
<span class="hidden md:inline">delete</span>
37
+
<span class="hidden md:inline">delete</span>
38
38
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
39
39
</button>
40
40
</div>
···
80
80
<div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div>
81
81
{{ end }}
82
82
</div>
83
+
{{ template "fragments/multiline-select" }}
83
84
</section>
84
85
{{ end }}
+33
appview/posthog/notifier.go
+33
appview/posthog/notifier.go
···
129
129
log.Println("failed to enqueue posthog event:", err)
130
130
}
131
131
}
132
+
133
+
func (n *posthogNotifier) DeleteString(ctx context.Context, did, rkey string) {
134
+
err := n.client.Enqueue(posthog.Capture{
135
+
DistinctId: did,
136
+
Event: "delete_string",
137
+
Properties: posthog.Properties{"rkey": rkey},
138
+
})
139
+
if err != nil {
140
+
log.Println("failed to enqueue posthog event:", err)
141
+
}
142
+
}
143
+
144
+
func (n *posthogNotifier) EditString(ctx context.Context, string *db.String) {
145
+
err := n.client.Enqueue(posthog.Capture{
146
+
DistinctId: string.Did.String(),
147
+
Event: "edit_string",
148
+
Properties: posthog.Properties{"rkey": string.Rkey},
149
+
})
150
+
if err != nil {
151
+
log.Println("failed to enqueue posthog event:", err)
152
+
}
153
+
}
154
+
155
+
func (n *posthogNotifier) CreateString(ctx context.Context, string *db.String) {
156
+
err := n.client.Enqueue(posthog.Capture{
157
+
DistinctId: string.Did.String(),
158
+
Event: "create_string",
159
+
Properties: posthog.Properties{"rkey": string.Rkey},
160
+
})
161
+
if err != nil {
162
+
log.Println("failed to enqueue posthog event:", err)
163
+
}
164
+
}
+8
appview/strings/strings.go
+8
appview/strings/strings.go
···
12
12
"tangled.sh/tangled.sh/core/appview/config"
13
13
"tangled.sh/tangled.sh/core/appview/db"
14
14
"tangled.sh/tangled.sh/core/appview/middleware"
15
+
"tangled.sh/tangled.sh/core/appview/notify"
15
16
"tangled.sh/tangled.sh/core/appview/oauth"
16
17
"tangled.sh/tangled.sh/core/appview/pages"
17
18
"tangled.sh/tangled.sh/core/appview/pages/markup"
···
36
37
IdResolver *idresolver.Resolver
37
38
Logger *slog.Logger
38
39
Knotstream *eventconsumer.Consumer
40
+
Notifier notify.Notifier
39
41
}
40
42
41
43
func (s *Strings) Router(mw *middleware.Middleware) http.Handler {
···
284
286
return
285
287
}
286
288
289
+
s.Notifier.EditString(r.Context(), &entry)
290
+
287
291
// if that went okay, redir to the string
288
292
s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+entry.Rkey)
289
293
}
···
358
362
return
359
363
}
360
364
365
+
s.Notifier.NewString(r.Context(), &string)
366
+
361
367
// successful
362
368
s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+string.Rkey)
363
369
}
···
399
405
fail("Failed to delete string.", err)
400
406
return
401
407
}
408
+
409
+
s.Notifier.DeleteString(r.Context(), user.Did, rkey)
402
410
403
411
s.Pages.HxRedirect(w, "/strings/"+user.Handle)
404
412
}
+44
contrib/Tiltfile
+44
contrib/Tiltfile
···
1
+
common_env = {
2
+
"TANGLED_VM_SPINDLE_OWNER": os.getenv("TANGLED_VM_SPINDLE_OWNER", default=""),
3
+
"TANGLED_VM_KNOT_OWNER": os.getenv("TANGLED_VM_KNOT_OWNER", default=""),
4
+
"TANGLED_DB_PATH": os.getenv("TANGLED_DB_PATH", default="dev.db"),
5
+
"TANGLED_DEV": os.getenv("TANGLED_DEV", default="true"),
6
+
}
7
+
8
+
nix_globs = ["nix/**", "flake.nix", "flake.lock"]
9
+
10
+
local_resource(
11
+
name="appview",
12
+
serve_cmd="nix run .#watch-appview",
13
+
serve_dir="..",
14
+
deps=nix_globs,
15
+
env=common_env,
16
+
allow_parallel=True,
17
+
)
18
+
19
+
local_resource(
20
+
name="tailwind",
21
+
serve_cmd="nix run .#watch-tailwind",
22
+
serve_dir="..",
23
+
deps=nix_globs,
24
+
env=common_env,
25
+
allow_parallel=True,
26
+
)
27
+
28
+
local_resource(
29
+
name="redis",
30
+
serve_cmd="redis-server",
31
+
serve_dir="..",
32
+
deps=nix_globs,
33
+
env=common_env,
34
+
allow_parallel=True,
35
+
)
36
+
37
+
local_resource(
38
+
name="vm",
39
+
serve_cmd="nix run --impure .#vm",
40
+
serve_dir="..",
41
+
deps=nix_globs,
42
+
env=common_env,
43
+
allow_parallel=True,
44
+
)
+2
-1
flake.nix
+2
-1
flake.nix
···
151
151
nativeBuildInputs = [
152
152
pkgs.go
153
153
pkgs.air
154
+
pkgs.tilt
154
155
pkgs.gopls
155
156
pkgs.httpie
156
157
pkgs.litecli
···
187
188
tailwind-watcher =
188
189
pkgs.writeShellScriptBin "run"
189
190
''
190
-
${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css
191
+
${pkgs.tailwindcss}/bin/tailwindcss --watch=always -i input.css -o ./appview/pages/static/tw.css
191
192
'';
192
193
in {
193
194
fmt = {
+2
-5
input.css
+2
-5
input.css
···
228
228
}
229
229
/* LineHighlight */
230
230
.chroma .hl {
231
-
background-color: #bcc0cc;
231
+
@apply bg-amber-400/30 dark:bg-amber-500/20;
232
232
}
233
+
233
234
/* LineNumbersTable */
234
235
.chroma .lnt {
235
236
white-space: pre;
···
864
865
text-decoration: underline;
865
866
}
866
867
}
867
-
868
-
.chroma .line:has(.ln:target) {
869
-
@apply bg-amber-400/30 dark:bg-amber-500/20;
870
-
}