+7
appview/db/db.go
+7
appview/db/db.go
···
1173
1173
return err
1174
1174
})
1175
1175
1176
+
runMigration(conn, logger, "add-default-knot-profile", func(tx *sql.Tx) error {
1177
+
_, err := tx.Exec(`
1178
+
alter table profile add column default_knot text;
1179
+
`)
1180
+
return err
1181
+
})
1182
+
1176
1183
return &DB{
1177
1184
db,
1178
1185
logger,
+11
-4
appview/db/profile.go
+11
-4
appview/db/profile.go
···
138
138
description,
139
139
include_bluesky,
140
140
location,
141
-
pronouns
141
+
pronouns,
142
+
default_knot
142
143
)
143
-
values (?, ?, ?, ?, ?)`,
144
+
values (?, ?, ?, ?, ?, ?)`,
144
145
profile.Did,
145
146
profile.Description,
146
147
includeBskyValue,
147
148
profile.Location,
148
149
profile.Pronouns,
150
+
profile.DefaultKnot,
149
151
)
150
152
151
153
if err != nil {
···
324
326
func GetProfile(e Execer, did string) (*models.Profile, error) {
325
327
var profile models.Profile
326
328
var pronouns sql.Null[string]
329
+
var defaultKnot sql.Null[string]
327
330
328
331
profile.Did = did
329
332
330
333
includeBluesky := 0
331
334
332
335
err := e.QueryRow(
333
-
`select description, include_bluesky, location, pronouns from profile where did = ?`,
336
+
`select description, include_bluesky, location, pronouns, default_knot from profile where did = ?`,
334
337
did,
335
-
).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns)
338
+
).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns, &defaultKnot)
336
339
if err == sql.ErrNoRows {
337
340
profile := models.Profile{}
338
341
profile.Did = did
···
349
352
350
353
if pronouns.Valid {
351
354
profile.Pronouns = pronouns.V
355
+
}
356
+
357
+
if defaultKnot.Valid {
358
+
profile.DefaultKnot = defaultKnot.V
352
359
}
353
360
354
361
rows, err := e.Query(`select link from profile_links where did = ?`, did)
+10
appview/knots/knots.go
+10
appview/knots/knots.go
···
81
81
return
82
82
}
83
83
84
+
defaultKnot := ""
85
+
profile, err := db.GetProfile(k.Db, user.Did)
86
+
if err != nil {
87
+
k.Logger.Warn("gettings user profile to get default knot", "error", err)
88
+
}
89
+
if profile != nil {
90
+
defaultKnot = profile.DefaultKnot
91
+
}
92
+
84
93
k.Pages.Knots(w, pages.KnotsParams{
85
94
LoggedInUser: user,
86
95
Registrations: registrations,
87
96
Tabs: knotsTabs,
88
97
Tab: "knots",
98
+
DefaultKnot: defaultKnot,
89
99
})
90
100
}
91
101
+1
appview/models/profile.go
+1
appview/models/profile.go
+3
appview/pages/pages.go
+3
appview/pages/pages.go
···
419
419
Registrations []models.Registration
420
420
Tabs []map[string]any
421
421
Tab string
422
+
DefaultKnot string
422
423
}
423
424
424
425
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
484
485
type NewRepoParams struct {
485
486
LoggedInUser *oauth.User
486
487
Knots []string
488
+
DefaultKnot string
487
489
}
488
490
489
491
func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
···
494
496
LoggedInUser *oauth.User
495
497
Knots []string
496
498
RepoInfo repoinfo.RepoInfo
499
+
DefaultKnot string
497
500
}
498
501
499
502
func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
+24
appview/pages/templates/knots/index.html
+24
appview/pages/templates/knots/index.html
···
31
31
<div class="flex flex-col gap-6">
32
32
{{ block "list" . }} {{ end }}
33
33
{{ block "register" . }} {{ end }}
34
+
{{ block "default-knot" . }} {{ end }}
34
35
</div>
35
36
</section>
36
37
{{ end }}
···
59
60
{{ end }}
60
61
</div>
61
62
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
63
+
</section>
64
+
{{ end }}
65
+
66
+
{{ define "default-knot" }}
67
+
<section class="rounded w-full flex flex-col gap-2">
68
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">default knot</h2>
69
+
<select id="default-knot" name="default-knot"
70
+
class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
71
+
hx-post="/profile/default-knot"
72
+
hx-swap="none"
73
+
name="default-knot">
74
+
<option value="" >
75
+
Choose a default Knot
76
+
</option>
77
+
<option value="knot1.tangled.sh" {{if eq $.DefaultKnot "knot1.tangled.sh"}}selected{{end}} >
78
+
knot1.tangled.sh
79
+
</option>
80
+
{{ range $registration := .Registrations }}
81
+
<option value="{{ .Domain }}" class="py-1" {{if eq $.DefaultKnot .Domain}}selected{{end}}>
82
+
{{ .Domain }}
83
+
</option>
84
+
{{ end }}
85
+
</select>
62
86
</section>
63
87
{{ end }}
64
88
+4
appview/pages/templates/layouts/base.html
+4
appview/pages/templates/layouts/base.html
···
11
11
<script defer src="/static/htmx-ext-ws.min.js"></script>
12
12
<script defer src="/static/actor-typeahead.js" type="module"></script>
13
13
14
+
<link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/>
15
+
<link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/>
16
+
<link rel="apple-touch-icon" href="/static/logos/dolly.png"/>
17
+
14
18
<!-- preconnect to image cdn -->
15
19
<link rel="preconnect" href="https://avatar.tangled.sh" />
16
20
<link rel="preconnect" href="https://camo.tangled.sh" />
+3
-1
appview/pages/templates/repo/fork.html
+3
-1
appview/pages/templates/repo/fork.html
···
25
25
value="{{ . }}"
26
26
class="mr-2"
27
27
id="domain-{{ . }}"
28
-
{{if eq (len $.Knots) 1}}checked{{end}}
28
+
{{if eq (len $.Knots) 1}}checked
29
+
{{else if eq $.DefaultKnot . }}checked
30
+
{{end}}
29
31
/>
30
32
<label for="domain-{{ . }}" class="dark:text-white">{{ . }}</label>
31
33
</div>
+3
-1
appview/pages/templates/repo/new.html
+3
-1
appview/pages/templates/repo/new.html
···
155
155
class="mr-2"
156
156
id="domain-{{ . }}"
157
157
required
158
-
{{if eq (len $.Knots) 1}}checked{{end}}
158
+
{{if eq (len $.Knots) 1}}checked
159
+
{{else if eq $.DefaultKnot . }}checked
160
+
{{end}}
159
161
/>
160
162
<label for="domain-{{ . }}" class="dark:text-white lowercase">{{ . }}</label>
161
163
</div>
+10
appview/repo/repo.go
+10
appview/repo/repo.go
···
1004
1004
return
1005
1005
}
1006
1006
1007
+
defaultKnot := ""
1008
+
profile, err := db.GetProfile(rp.db, user.Did)
1009
+
if err != nil {
1010
+
rp.logger.Warn("gettings user profile to get default knot", "error", err)
1011
+
}
1012
+
if profile != nil {
1013
+
defaultKnot = profile.DefaultKnot
1014
+
}
1015
+
1007
1016
rp.pages.ForkRepo(w, pages.ForkRepoParams{
1008
1017
LoggedInUser: user,
1009
1018
Knots: knots,
1010
1019
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
1020
+
DefaultKnot: defaultKnot,
1011
1021
})
1012
1022
1013
1023
case http.MethodPost:
+29
appview/state/manifest.go
+29
appview/state/manifest.go
···
1
+
package state
2
+
3
+
import (
4
+
"encoding/json"
5
+
"net/http"
6
+
)
7
+
8
+
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
9
+
// https://www.w3.org/TR/appmanifest/
10
+
var manifestData = map[string]any{
11
+
"name": "tangled",
12
+
"description": "tightly-knit social coding.",
13
+
"icons": []map[string]string{
14
+
{
15
+
"src": "/static/logos/dolly.svg",
16
+
"sizes": "144x144",
17
+
},
18
+
},
19
+
"start_url": "/",
20
+
"id": "https://tangled.org",
21
+
"display": "standalone",
22
+
"background_color": "#111827",
23
+
"theme_color": "#111827",
24
+
}
25
+
26
+
func (p *State) WebAppManifest(w http.ResponseWriter, r *http.Request) {
27
+
w.Header().Set("Content-Type", "application/manifest+json")
28
+
json.NewEncoder(w).Encode(manifestData)
29
+
}
+32
appview/state/profile.go
+32
appview/state/profile.go
···
616
616
s.updateProfile(profile, w, r)
617
617
}
618
618
619
+
func (s *State) UpdateProfileDefaultKnot(w http.ResponseWriter, r *http.Request) {
620
+
err := r.ParseForm()
621
+
if err != nil {
622
+
log.Println("invalid profile update form", err)
623
+
return
624
+
}
625
+
user := s.oauth.GetUser(r)
626
+
627
+
profile, err := db.GetProfile(s.db, user.Did)
628
+
if err != nil {
629
+
log.Printf("getting profile data for %s: %s", user.Did, err)
630
+
}
631
+
632
+
if profile == nil {
633
+
return
634
+
}
635
+
636
+
profile.DefaultKnot = r.Form.Get("default-knot")
637
+
638
+
tx, err := s.db.BeginTx(r.Context(), nil)
639
+
if err != nil {
640
+
log.Println("failed to start transaction", err)
641
+
return
642
+
}
643
+
644
+
err = db.UpsertProfile(tx, profile)
645
+
if err != nil {
646
+
log.Println("failed to update profile", err)
647
+
return
648
+
}
649
+
}
650
+
619
651
func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) {
620
652
user := s.oauth.GetUser(r)
621
653
tx, err := s.db.BeginTx(r.Context(), nil)
+2
-3
appview/state/router.go
+2
-3
appview/state/router.go
···
32
32
s.pages,
33
33
)
34
34
35
-
router.Get("/favicon.svg", s.Favicon)
36
-
router.Get("/favicon.ico", s.Favicon)
37
-
router.Get("/pwa-manifest.json", s.PWAManifest)
35
+
router.Get("/pwa-manifest.json", s.WebAppManifest)
38
36
router.Get("/robots.txt", s.RobotsTxt)
39
37
40
38
userRouter := s.UserRouter(&middleware)
···
164
162
r.Get("/edit-pins", s.EditPinsFragment)
165
163
r.Post("/bio", s.UpdateProfileBio)
166
164
r.Post("/pins", s.UpdateProfilePins)
165
+
r.Post("/default-knot", s.UpdateProfileDefaultKnot)
167
166
})
168
167
169
168
r.Mount("/settings", s.SettingsRouter())
+10
-36
appview/state/state.go
+10
-36
appview/state/state.go
···
202
202
return s.db.Close()
203
203
}
204
204
205
-
func (s *State) Favicon(w http.ResponseWriter, r *http.Request) {
206
-
w.Header().Set("Content-Type", "image/svg+xml")
207
-
w.Header().Set("Cache-Control", "public, max-age=31536000") // one year
208
-
w.Header().Set("ETag", `"favicon-svg-v1"`)
209
-
210
-
if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` {
211
-
w.WriteHeader(http.StatusNotModified)
212
-
return
213
-
}
214
-
215
-
s.pages.Favicon(w)
216
-
}
217
-
218
205
func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) {
219
206
w.Header().Set("Content-Type", "text/plain")
220
207
w.Header().Set("Cache-Control", "public, max-age=86400") // one day
···
223
210
Allow: /
224
211
`
225
212
w.Write([]byte(robotsTxt))
226
-
}
227
-
228
-
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
229
-
const manifestJson = `{
230
-
"name": "tangled",
231
-
"description": "tightly-knit social coding.",
232
-
"icons": [
233
-
{
234
-
"src": "/favicon.svg",
235
-
"sizes": "144x144"
236
-
}
237
-
],
238
-
"start_url": "/",
239
-
"id": "org.tangled",
240
-
241
-
"display": "standalone",
242
-
"background_color": "#111827",
243
-
"theme_color": "#111827"
244
-
}`
245
-
246
-
func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) {
247
-
w.Header().Set("Content-Type", "application/json")
248
-
w.Write([]byte(manifestJson))
249
213
}
250
214
251
215
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
···
454
418
return
455
419
}
456
420
421
+
defaultKnot := ""
422
+
profile, err := db.GetProfile(s.db, user.Did)
423
+
if err != nil {
424
+
s.logger.Warn("gettings user profile to get default knot", "error", err)
425
+
}
426
+
if profile != nil {
427
+
defaultKnot = profile.DefaultKnot
428
+
}
429
+
457
430
s.pages.NewRepo(w, pages.NewRepoParams{
458
431
LoggedInUser: user,
459
432
Knots: knots,
433
+
DefaultKnot: defaultKnot,
460
434
})
461
435
462
436
case http.MethodPost:
+1
-1
cmd/dolly/main.go
+1
-1
cmd/dolly/main.go
+6
docs/logo.html
+6
docs/logo.html
+2
docs/template.html
+2
docs/template.html
···
74
74
${ x.svg() }
75
75
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
76
76
</button>
77
+
${ logo.html() }
77
78
${ search.html() }
78
79
${ table-of-contents:toc.html() }
79
80
</div>
···
88
89
class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen
89
90
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
90
91
p-4 z-50 overflow-y-auto">
92
+
${ logo.html() }
91
93
${ search.html() }
92
94
<div class="flex-1">
93
95
$if(toc-title)$
+4
nix/pkgs/docs.nix
+4
nix/pkgs/docs.nix
···
5
5
inter-fonts-src,
6
6
ibm-plex-mono-src,
7
7
lucide-src,
8
+
dolly,
8
9
src,
9
10
}:
10
11
runCommandLocal "docs" {} ''
···
17
18
18
19
# icons
19
20
cp -rf ${lucide-src}/*.svg working/
21
+
22
+
# logo
23
+
${dolly}/bin/dolly -output working/dolly.svg -color currentColor
20
24
21
25
# content - chunked
22
26
${pandoc}/bin/pandoc ${src}/docs/DOCS.md \