feat(profile): up to four links and forgejo style identicons
8 files changed, +61 −7
- flow_test.go modified +17 −0
- static/style.css modified +2 −0
- store.go modified +28 −2
- templates/org.html modified +1 −1
- templates/orgsettings.html modified +1 −1
- templates/settings.html modified +1 −1
- templates/user.html modified +1 −1
- web.go modified +10 −1
flow_test.go modified
| @@ -478,3 +478,20 @@ func TestNotifications(t *testing.T) { | ||
| 478 | 478 | t.Errorf("private repository leaked through notifications: %+v", n) |
| 479 | 479 | } |
| 480 | 480 | } |
| 481 | + | |
| 482 | +func TestProfileLinks(t *testing.T) { | |
| 483 | + s, h := testServer(t) | |
| 484 | + alice, _ := s.store.CreateUser("alice", "correct horse") | |
| 485 | + c, _ := s.store.CreateSession(alice.ID) | |
| 486 | + form := url.Values{"display": {"Alice"}, "website": {"example.org", "", "https://a.example/x", "http://b.example", "c.example", "d.example"}} | |
| 487 | + if w := post(h, "/settings/profile", form, c); w.Code != http.StatusSeeOther { | |
| 488 | + t.Fatalf("saving profile gave %d", w.Code) | |
| 489 | + } | |
| 490 | + links := s.store.Profile(alice.ID).Links() | |
| 491 | + if len(links) != 4 || links[0] != "https://example.org" || links[2] != "http://b.example" { | |
| 492 | + t.Errorf("links stored as %q", links) | |
| 493 | + } | |
| 494 | + if body := get(h, "/~alice", "").Body.String(); strings.Count(body, `rel="nofollow me"`) != 4 { | |
| 495 | + t.Error("profile does not show four links") | |
| 496 | + } | |
| 497 | +} | |
static/style.css modified
| @@ -500,3 +500,5 @@ nav.site .bell .badge{position:absolute;top:-.35rem;left:.85rem;min-width:1rem;p | ||
| 500 | 500 | .inbox .title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 501 | 501 | .inbox .who{font-size:.82rem;white-space:nowrap} |
| 502 | 502 | @media(max-width:700px){.inbox li a{grid-template-columns:1.1rem minmax(0,1fr)}.inbox .who{grid-column:2}} |
| 503 | +fieldset.links{border:0;padding:0;margin:0 0 .8rem;display:flex;flex-direction:column;gap:.35rem;min-width:0} | |
| 504 | +fieldset.links legend{padding:0;margin-bottom:.25rem} | |
store.go modified
| @@ -696,10 +696,36 @@ func (s *Store) Profile(user int64) Profile { | ||
| 696 | 696 | return p |
| 697 | 697 | } |
| 698 | 698 | |
| 699 | +const maxLinks = 4 | |
| 700 | + | |
| 701 | +func (p Profile) Links() []string { | |
| 702 | + if p.Website == "" { | |
| 703 | + return nil | |
| 704 | + } | |
| 705 | + return strings.Split(p.Website, "\n") | |
| 706 | +} | |
| 707 | + | |
| 708 | +func (p Profile) LinkSlots() []string { | |
| 709 | + links := p.Links() | |
| 710 | + for len(links) < maxLinks { | |
| 711 | + links = append(links, "") | |
| 712 | + } | |
| 713 | + return links | |
| 714 | +} | |
| 715 | + | |
| 699 | 716 | func (s *Store) UpdateProfile(user int64, p Profile) error { |
| 700 | - if p.Website != "" && !strings.HasPrefix(p.Website, "https://") && !strings.HasPrefix(p.Website, "http://") { | |
| 701 | - p.Website = "https://" + p.Website | |
| 717 | + var links []string | |
| 718 | + for _, l := range strings.Split(p.Website, "\n") { | |
| 719 | + l = strings.TrimSpace(l) | |
| 720 | + if l == "" || len(links) == maxLinks { | |
| 721 | + continue | |
| 722 | + } | |
| 723 | + if !strings.HasPrefix(l, "https://") && !strings.HasPrefix(l, "http://") { | |
| 724 | + l = "https://" + l | |
| 725 | + } | |
| 726 | + links = append(links, l) | |
| 702 | 727 | } |
| 728 | + p.Website = strings.Join(links, "\n") | |
| 703 | 729 | _, err := s.db.Exec(`update users set display = ?, bio = ?, location = ?, workplace = ?, website = ? where id = ?`, p.Display, p.Bio, p.Location, p.Workplace, p.Website, user) |
| 704 | 730 | return err |
| 705 | 731 | } |
templates/org.html modified
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | {{with .Data.Bio}}<div class="bio readme">{{.}}</div>{{end}} |
| 10 | 10 | <ul class="facts"> |
| 11 | 11 | {{if $p.Location}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 15s5-4.6 5-8.5A5 5 0 0 0 3 6.5C3 10.4 8 15 8 15z"/><circle cx="8" cy="6.5" r="1.8"/></svg>{{$p.Location}}</li>{{end}} |
| 12 | -{{if $p.Website}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.8 9.2a3 3 0 0 0 4.2 0l2.2-2.2a3 3 0 0 0-4.2-4.2l-.9.9"/><path d="M9.2 6.8a3 3 0 0 0-4.2 0L2.8 9a3 3 0 0 0 4.2 4.2l.9-.9"/></svg><a href="{{$p.Website}}" rel="nofollow me">{{bare $p.Website}}</a></li>{{end}} | |
| 12 | +{{range $p.Links}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.8 9.2a3 3 0 0 0 4.2 0l2.2-2.2a3 3 0 0 0-4.2-4.2l-.9.9"/><path d="M9.2 6.8a3 3 0 0 0-4.2 0L2.8 9a3 3 0 0 0 4.2 4.2l.9-.9"/></svg><a href="{{.}}" rel="nofollow me">{{bare .}}</a></li>{{end}} | |
| 13 | 13 | <li title="{{date $o.Created}}"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M5.5 1.5v3M10.5 1.5v3"/></svg>Since {{$o.Created.Format "January 2006"}}</li> |
| 14 | 14 | </ul> |
| 15 | 15 | </div> |
templates/orgsettings.html modified
| @@ -23,7 +23,7 @@ | ||
| 23 | 23 | </div> |
| 24 | 24 | <div class="pair"> |
| 25 | 25 | <label>Location <input name="location" value="{{$p.Location}}" maxlength="100"></label> |
| 26 | -<label>URL <input name="website" value="{{$p.Website}}" maxlength="200" placeholder="https://"></label> | |
| 26 | +<fieldset class="links"><legend>URLs</legend>{{range $i, $l := $p.LinkSlots}}<input name="website" value="{{$l}}" maxlength="200" placeholder="https://" aria-label="Link">{{end}}</fieldset> | |
| 27 | 27 | </div> |
| 28 | 28 | <label>Bio <textarea name="bio" rows="5" maxlength="4000" placeholder="Markdown supported, paste or drop images to attach them" data-md>{{$p.Bio}}</textarea></label> |
| 29 | 29 | <div class="actions"><button class="btn primary">Save changes ▸</button></div> |
templates/settings.html modified
| @@ -34,7 +34,7 @@ | ||
| 34 | 34 | <label>Location <input name="location" value="{{$p.Location}}" maxlength="100"></label> |
| 35 | 35 | <label>Workplace <input name="workplace" value="{{$p.Workplace}}" maxlength="100"></label> |
| 36 | 36 | </div> |
| 37 | -<label>URL <input name="website" value="{{$p.Website}}" maxlength="200" placeholder="https://"></label> | |
| 37 | +<fieldset class="links"><legend>URLs</legend>{{range $i, $l := $p.LinkSlots}}<input name="website" value="{{$l}}" maxlength="200" placeholder="https://" aria-label="Link">{{end}}</fieldset> | |
| 38 | 38 | <label>Bio <textarea name="bio" rows="6" maxlength="4000" placeholder="Markdown supported, paste or drop images to attach them" data-md>{{$p.Bio}}</textarea></label> |
| 39 | 39 | <div class="actions"><button class="btn primary">Save changes ▸</button></div> |
| 40 | 40 | </form> |
templates/user.html modified
| @@ -13,7 +13,7 @@ | ||
| 13 | 13 | <ul class="facts"> |
| 14 | 14 | {{if $p.Location}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 15s5-4.6 5-8.5A5 5 0 0 0 3 6.5C3 10.4 8 15 8 15z"/><circle cx="8" cy="6.5" r="1.8"/></svg>{{$p.Location}}</li>{{end}} |
| 15 | 15 | {{if $p.Workplace}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="5" width="12" height="9" rx="1.5"/><path d="M6 5V3.5A1 1 0 0 1 7 2.5h2a1 1 0 0 1 1 1V5"/></svg>{{with .Data.Workplace}}<a href="/~{{.}}">{{$p.Workplace}}</a>{{else}}{{$p.Workplace}}{{end}}</li>{{end}} |
| 16 | -{{if $p.Website}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.8 9.2a3 3 0 0 0 4.2 0l2.2-2.2a3 3 0 0 0-4.2-4.2l-.9.9"/><path d="M9.2 6.8a3 3 0 0 0-4.2 0L2.8 9a3 3 0 0 0 4.2 4.2l.9-.9"/></svg><a href="{{$p.Website}}" rel="nofollow me">{{bare $p.Website}}</a></li>{{end}} | |
| 16 | +{{range $p.Links}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.8 9.2a3 3 0 0 0 4.2 0l2.2-2.2a3 3 0 0 0-4.2-4.2l-.9.9"/><path d="M9.2 6.8a3 3 0 0 0-4.2 0L2.8 9a3 3 0 0 0 4.2 4.2l.9-.9"/></svg><a href="{{.}}" rel="nofollow me">{{bare .}}</a></li>{{end}} | |
| 17 | 17 | <li title="{{date .Data.Owner.Created}}"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M5.5 1.5v3M10.5 1.5v3"/></svg>Joined {{.Data.Owner.Created.Format "January 2006"}}</li> |
| 18 | 18 | </ul> |
| 19 | 19 | {{with .Data.Orgs}}<div class="orglist"><h4>Organisations</h4><div class="orgicons">{{range .}}<a href="/~{{.Name}}" title="{{with .Profile.Display}}{{.}}{{else}}~{{.Name}}{{end}}" aria-label="~{{.Name}}"><img src="/~{{.Name}}/avatar?v={{.Profile.Avatar}}" alt=""></a>{{end}}</div></div>{{end}} |
web.go modified
| @@ -716,6 +716,15 @@ func (s *Server) profile(w http.ResponseWriter, r *http.Request) { | ||
| 716 | 716 | if u == nil { |
| 717 | 717 | return |
| 718 | 718 | } |
| 719 | + links := func(vals []string) string { | |
| 720 | + var out []string | |
| 721 | + for _, v := range vals { | |
| 722 | + if v = strings.TrimSpace(v); v != "" && len(v) <= 200 && !strings.ContainsAny(v, "\r\n") { | |
| 723 | + out = append(out, v) | |
| 724 | + } | |
| 725 | + } | |
| 726 | + return strings.Join(out, "\n") | |
| 727 | + } | |
| 719 | 728 | field := func(name string, max int) string { |
| 720 | 729 | v := strings.TrimSpace(r.FormValue(name)) |
| 721 | 730 | if len(v) > max { |
| @@ -723,7 +732,7 @@ func (s *Server) profile(w http.ResponseWriter, r *http.Request) { | ||
| 723 | 732 | } |
| 724 | 733 | return v |
| 725 | 734 | } |
| 726 | - p := Profile{Display: field("display", 100), Bio: field("bio", 4000), Location: field("location", 100), Workplace: field("workplace", 100), Website: field("website", 200)} | |
| 735 | + p := Profile{Display: field("display", 100), Bio: field("bio", 4000), Location: field("location", 100), Workplace: field("workplace", 100), Website: links(r.Form["website"])} | |
| 727 | 736 | if err := s.store.UpdateProfile(u.ID, p); err != nil { |
| 728 | 737 | http.Redirect(w, r, back+"?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) |
| 729 | 738 | return |