forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

better profile view; fix logout button

fix issue closing, indicate alpha etc

Changed files
+155 -68
appview
pages
templates
layouts
user
state
+1
appview/pages/pages.go
···
ProfileStats ProfileStats
FollowStatus db.FollowStatus
DidHandleMap map[string]string
+
AvatarUri string
}
type ProfileStats struct {
+2 -2
appview/pages/templates/layouts/topbar.html
···
<div class="container flex justify-between p-0">
<div id="left-items">
<a href="/" hx-boost="true" class="flex gap-2 font-semibold italic">
-
tangled
+
tangled<sub>alpha</sub>
</a>
</div>
<div id="right-items" class="flex gap-2">
{{ with .LoggedInUser }}
-
<a href="/repo/new"hx-boost="true">
+
<a href="/repo/new" hx-boost="true">
<i class="w-6 h-6" data-lucide="plus"></i>
</a>
{{ block "dropDown" . }} {{ end }}
+85 -61
appview/pages/templates/user/profile.html
···
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
{{ define "content" }}
+
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
+
<div class="lg:col-span-1">
+
{{ block "profileCard" . }} {{ end }}
+
</div>
-
<div class="flex">
-
<h1 class="pb-1 px-6">
-
{{ didOrHandle .UserDid .UserHandle }}
-
</h1>
-
{{ if ne .FollowStatus.String "IsSelf" }}
-
<button id="followBtn"
-
class="btn mt-2"
-
{{ if eq .FollowStatus.String "IsNotFollowing" }}
-
hx-post="/follow?subject={{.UserDid}}"
-
{{ else }}
-
hx-delete="/follow?subject={{.UserDid}}"
-
{{ end }}
-
hx-trigger="click"
-
hx-target="#followBtn"
-
hx-swap="outerHTML"
-
>
-
{{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
-
</button>
-
{{ end }}
+
<div class="lg:col-span-3">
+
{{ block "ownRepos" . }} {{ end }}
+
{{ block "collaboratingRepos" . }} {{ end }}
+
</div>
+
</div>
+
{{ end }}
+
+
{{ define "profileCard" }}
+
<div class="bg-white px-6 py-4 rounded drop-shadow-sm max-h-fit">
+
<div class="flex justify-center items-center">
+
{{ if .AvatarUri }}
+
<img class="w-1/2 lg:w-full rounded-full p-2" src="{{ .AvatarUri }}" />
+
{{ end }}
</div>
-
<div class="text-sm mb-4 px-6">
+
<p class="text-xl font-bold text-center">
+
{{ didOrHandle .UserDid .UserHandle }}
+
</p>
+
<div class="text-sm text-center">
<span>{{ .ProfileStats.Followers }} followers</span>
<div class="inline-block px-1 select-none after:content-['·']"></div>
<span>{{ .ProfileStats.Following }} following</span>
</div>
-
<p class="text-sm font-bold py-2 px-6">REPOS</p>
-
<div id="repos" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
-
{{ range .Repos }}
-
<div
-
id="repo-card"
-
class="py-4 px-6 drop-shadow-sm rounded bg-white"
-
>
-
<div id="repo-card-name" class="font-medium">
-
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"
-
>{{ .Name }}</a
+
+
{{ if ne .FollowStatus.String "IsSelf" }}
+
<button id="followBtn"
+
class="btn mt-2 w-full"
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}
+
hx-post="/follow?subject={{.UserDid}}"
+
{{ else }}
+
hx-delete="/follow?subject={{.UserDid}}"
+
{{ end }}
+
hx-trigger="click"
+
hx-target="#followBtn"
+
hx-swap="outerHTML"
>
-
</div>
-
<div
-
id="repo-knot-name"
-
class="text-gray-600 text-sm font-mono"
-
>
-
{{ .Knot }}
-
</div>
-
</div>
-
{{ else }}
-
<p class="px-6">This user does not have any repos yet.</p>
-
{{ end }}
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
+
</button>
+
{{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "ownRepos" }}
+
<p class="text-sm font-bold py-2 px-6">REPOS</p>
+
<div id="repos" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
+
{{ range .Repos }}
+
<div
+
id="repo-card"
+
class="py-4 px-6 drop-shadow-sm rounded bg-white"
+
>
+
<div id="repo-card-name" class="font-medium">
+
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"
+
>{{ .Name }}</a
+
>
+
</div>
+
<div
+
id="repo-knot-name"
+
class="text-gray-600 text-sm font-mono"
+
>
+
{{ .Knot }}
+
</div>
</div>
-
<p class="text-sm font-bold py-2 px-6">COLLABORATING ON</p>
-
<div id="collaborating" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
-
{{ range .CollaboratingRepos }}
-
<div
-
id="repo-card"
-
class="py-4 px-6 drop-shadow-sm rounded bg-white"
-
>
-
<div id="repo-card-name" class="font-medium">
-
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
-
{{ index $.DidHandleMap .Did }}/{{ .Name }}
-
</a>
-
</div>
-
<div
-
id="repo-knot-name"
-
class="text-gray-600 text-sm font-mono"
-
>
-
{{ .Knot }}
-
</div>
-
</div>
{{ else }}
-
<p class="px-6">This user is not collaborating.</p>
+
<p class="px-6">This user does not have any repos yet.</p>
{{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "collaboratingRepos" }}
+
<p class="text-sm font-bold py-2 px-6">COLLABORATING ON</p>
+
<div id="collaborating" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
+
{{ range .CollaboratingRepos }}
+
<div
+
id="repo-card"
+
class="py-4 px-6 drop-shadow-sm rounded bg-white"
+
>
+
<div id="repo-card-name" class="font-medium">
+
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
+
{{ index $.DidHandleMap .Did }}/{{ .Name }}
+
</a>
+
</div>
+
<div
+
id="repo-knot-name"
+
class="text-gray-600 text-sm font-mono"
+
>
+
{{ .Knot }}
+
</div>
</div>
+
{{ else }}
+
<p class="px-6">This user is not collaborating.</p>
+
{{ end }}
+
</div>
{{ end }}
+2 -2
appview/state/follow.go
···
w.Write([]byte(fmt.Sprintf(`
<button id="followBtn"
-
class="btn mt-2"
+
class="btn mt-2 w-full"
hx-delete="/follow?subject=%s"
hx-trigger="click"
hx-target="#followBtn"
···
w.Write([]byte(fmt.Sprintf(`
<button id="followBtn"
-
class="btn mt-2"
+
class="btn mt-2 w-full"
hx-post="/follow?subject=%s"
hx-trigger="click"
hx-target="#followBtn"
+12 -2
appview/state/repo.go
···
"math/rand/v2"
"net/http"
"path"
+
"slices"
"strconv"
"strings"
"time"
···
return
}
+
collaborators, err := f.Collaborators(r.Context(), s)
+
if err != nil {
+
log.Println("failed to fetch repo collaborators: %w", err)
+
}
+
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
+
return user.Did == collab.Did
+
})
+
isIssueOwner := user.Did == issue.OwnerDid
+
// TODO: make this more granular
-
if user.Did == f.OwnerDid() {
+
if isIssueOwner || isCollaborator {
closed := tangled.RepoIssueStateClosed
···
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
return
} else {
-
log.Println("user is not the owner of the repo")
+
log.Println("user is not permitted to close issue")
http.Error(w, "for biden", http.StatusUnauthorized)
return
}
+53 -1
appview/state/state.go
···
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
+
"encoding/json"
"fmt"
"log"
"log/slog"
···
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
s.auth.ClearSession(r, w)
-
s.pages.HxRedirect(w, "/")
+
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
}
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
···
followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
}
+
profileAvatarUri, err := GetAvatarUri(ident.DID.String())
+
if err != nil {
+
log.Println("failed to fetch bsky avatar", err)
+
}
+
s.pages.ProfilePage(w, pages.ProfilePageParams{
LoggedInUser: loggedInUser,
UserDid: ident.DID.String(),
···
},
FollowStatus: db.FollowStatus(followStatus),
DidHandleMap: didHandleMap,
+
AvatarUri: profileAvatarUri,
})
+
}
+
+
func GetAvatarUri(did string) (string, error) {
+
recordURL := fmt.Sprintf("https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", did)
+
+
recordResp, err := http.Get(recordURL)
+
if err != nil {
+
return "", err
+
}
+
defer recordResp.Body.Close()
+
+
if recordResp.StatusCode != http.StatusOK {
+
return "", fmt.Errorf("getRecord API returned status code %d", recordResp.StatusCode)
+
}
+
+
var profileResp map[string]any
+
if err := json.NewDecoder(recordResp.Body).Decode(&profileResp); err != nil {
+
return "", err
+
}
+
+
value, ok := profileResp["value"].(map[string]any)
+
if !ok {
+
log.Println(profileResp)
+
return "", fmt.Errorf("no value found for handle %s", did)
+
}
+
+
avatar, ok := value["avatar"].(map[string]any)
+
if !ok {
+
log.Println(profileResp)
+
return "", fmt.Errorf("no avatar found for handle %s", did)
+
}
+
+
blobRef, ok := avatar["ref"].(map[string]any)
+
if !ok {
+
log.Println(profileResp)
+
return "", fmt.Errorf("no ref found for handle %s", did)
+
}
+
+
link, ok := blobRef["$link"].(string)
+
if !ok {
+
log.Println(profileResp)
+
return "", fmt.Errorf("no link found for handle %s", did)
+
}
+
+
return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil
}
func (s *State) Router() http.Handler {