Monorepo for Tangled tangled.org

appview/{pages,db}: show follow/unfollow button on the timeline

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by anirudh.fi and committed by Tangled 04314f77 4202ffcc

Changed files
+59 -35
appview
db
pages
templates
timeline
fragments
user
fragments
state
+18 -7
appview/db/timeline.go
··· 18 // optional: populate only if event is Follow 19 *Profile 20 *FollowStats 21 } 22 23 // TODO: this gathers heterogenous events from different sources and aggregates 24 // them in code; if we did this entirely in sql, we could order and limit and paginate easily 25 - func MakeTimeline(e Execer, limit int) ([]TimelineEvent, error) { 26 var events []TimelineEvent 27 28 repos, err := getTimelineRepos(e, limit) ··· 35 return nil, err 36 } 37 38 - follows, err := getTimelineFollows(e, limit) 39 if err != nil { 40 return nil, err 41 } ··· 129 return events, nil 130 } 131 132 - func getTimelineFollows(e Execer, limit int) ([]TimelineEvent, error) { 133 follows, err := GetFollows(e, limit) 134 if err != nil { 135 return nil, err ··· 159 profile, _ := profiles[f.SubjectDid] 160 followStatMap, _ := followStatMap[f.SubjectDid] 161 162 events = append(events, TimelineEvent{ 163 - Follow: &f, 164 - Profile: profile, 165 - FollowStats: &followStatMap, 166 - EventAt: f.FollowedAt, 167 }) 168 } 169
··· 18 // optional: populate only if event is Follow 19 *Profile 20 *FollowStats 21 + *FollowStatus 22 + 23 + // optional: populate only if event is Repo 24 + IsStarred bool 25 + StarCount int64 26 } 27 28 // TODO: this gathers heterogenous events from different sources and aggregates 29 // them in code; if we did this entirely in sql, we could order and limit and paginate easily 30 + func MakeTimeline(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) { 31 var events []TimelineEvent 32 33 repos, err := getTimelineRepos(e, limit) ··· 40 return nil, err 41 } 42 43 + follows, err := getTimelineFollows(e, limit, loggedInUserDid) 44 if err != nil { 45 return nil, err 46 } ··· 134 return events, nil 135 } 136 137 + func getTimelineFollows(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) { 138 follows, err := GetFollows(e, limit) 139 if err != nil { 140 return nil, err ··· 164 profile, _ := profiles[f.SubjectDid] 165 followStatMap, _ := followStatMap[f.SubjectDid] 166 167 + followStatus := IsNotFollowing 168 + if followStatuses != nil { 169 + followStatus = followStatuses[f.SubjectDid] 170 + } 171 + 172 events = append(events, TimelineEvent{ 173 + Follow: &f, 174 + Profile: profile, 175 + FollowStats: &followStatMap, 176 + FollowStatus: &followStatus, 177 + EventAt: f.FollowedAt, 178 }) 179 } 180
+33 -24
appview/pages/templates/timeline/fragments/timeline.html
··· 17 {{ else if .Star }} 18 {{ template "timeline/fragments/starEvent" (list $ .Star) }} 19 {{ else if .Follow }} 20 - {{ template "timeline/fragments/followEvent" (list $ .Follow .Profile .FollowStats) }} 21 {{ end }} 22 </div> 23 {{ end }} ··· 77 78 {{ define "timeline/fragments/followEvent" }} 79 {{ $root := index . 0 }} 80 - {{ $follow := index . 1 }} 81 - {{ $profile := index . 2 }} 82 - {{ $stat := index . 3 }} 83 84 {{ $userHandle := resolve $follow.UserDid }} 85 {{ $subjectHandle := resolve $follow.SubjectDid }} ··· 89 {{ template "user/fragments/picHandleLink" $subjectHandle }} 90 <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span> 91 </div> 92 - <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4"> 93 - <div class="flex-shrink-0 max-h-full w-24 h-24"> 94 - <img alt="" class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" /> 95 - </div> 96 97 - <div class="flex-1 min-h-0 justify-around flex flex-col"> 98 - <a href="/{{ $subjectHandle }}"> 99 - <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span> 100 - </a> 101 - {{ with $profile }} 102 - {{ with .Description }} 103 - <p class="text-sm pb-2 md:pb-2">{{.}}</p> 104 {{ end }} 105 - {{ end }} 106 - {{ with $stat }} 107 - <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 108 - <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 109 - <span id="followers"><a href="/{{ $subjectHandle }}?tab=followers">{{ .Followers }} followers</a></span> 110 - <span class="select-none after:content-['·']"></span> 111 - <span id="following"><a href="/{{ $subjectHandle }}?tab=following">{{ .Following }} following</a></span> 112 - </div> 113 - {{ end }} 114 </div> 115 </div> 116 {{ end }}
··· 17 {{ else if .Star }} 18 {{ template "timeline/fragments/starEvent" (list $ .Star) }} 19 {{ else if .Follow }} 20 + {{ template "timeline/fragments/followEvent" (list $ .) }} 21 {{ end }} 22 </div> 23 {{ end }} ··· 77 78 {{ define "timeline/fragments/followEvent" }} 79 {{ $root := index . 0 }} 80 + {{ $event := index . 1 }} 81 + {{ $follow := $event.Follow }} 82 + {{ $profile := $event.Profile }} 83 + {{ $stat := $event.FollowStats }} 84 85 {{ $userHandle := resolve $follow.UserDid }} 86 {{ $subjectHandle := resolve $follow.SubjectDid }} ··· 90 {{ template "user/fragments/picHandleLink" $subjectHandle }} 91 <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span> 92 </div> 93 + <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col md:flex-row md:items-center gap-4"> 94 + <div class="flex items-center gap-4 flex-1"> 95 + <div class="flex-shrink-0 max-h-full w-24 h-24"> 96 + <img alt="" class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" /> 97 + </div> 98 99 + <div class="flex-1 min-h-0 justify-around flex flex-col"> 100 + <a href="/{{ $subjectHandle }}"> 101 + <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span> 102 + </a> 103 + {{ with $profile }} 104 + {{ with .Description }} 105 + <p class="text-sm pb-2 md:pb-2">{{.}}</p> 106 + {{ end }} 107 + {{ end }} 108 + {{ with $stat }} 109 + <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 110 + <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 111 + <span id="followers"><a href="/{{ $subjectHandle }}?tab=followers">{{ .Followers }} followers</a></span> 112 + <span class="select-none after:content-['·']"></span> 113 + <span id="following"><a href="/{{ $subjectHandle }}?tab=following">{{ .Following }} following</a></span> 114 + </div> 115 {{ end }} 116 + </div> 117 </div> 118 + 119 + {{ if and $root.LoggedInUser (ne $event.FollowStatus.String "IsSelf") }} 120 + <div class="flex-shrink-0 w-fit ml-auto"> 121 + {{ template "user/fragments/follow" (dict "UserDid" $follow.SubjectDid "FollowStatus" $event.FollowStatus) }} 122 + </div> 123 + {{ end }} 124 </div> 125 {{ end }}
+2 -2
appview/pages/templates/user/fragments/follow.html
··· 1 {{ define "user/fragments/follow" }} 2 <button id="{{ normalizeForHtmlId .UserDid }}" 3 - class="btn mt-2 w-full flex gap-2 items-center group" 4 5 {{ if eq .FollowStatus.String "IsNotFollowing" }} 6 hx-post="/follow?subject={{.UserDid}}" ··· 12 hx-target="#{{ normalizeForHtmlId .UserDid }}" 13 hx-swap="outerHTML" 14 > 15 - {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }} 16 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 17 </button> 18 {{ end }}
··· 1 {{ define "user/fragments/follow" }} 2 <button id="{{ normalizeForHtmlId .UserDid }}" 3 + class="btn mt-2 flex gap-2 items-center group" 4 5 {{ if eq .FollowStatus.String "IsNotFollowing" }} 6 hx-post="/follow?subject={{.UserDid}}" ··· 12 hx-target="#{{ normalizeForHtmlId .UserDid }}" 13 hx-swap="outerHTML" 14 > 15 + {{ if eq .FollowStatus.String "IsNotFollowing" }}{{ i "user-round-plus" "w-4 h-4" }} follow{{ else }}{{ i "user-round-minus" "w-4 h-4" }} unfollow{{ end }} 16 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 17 </button> 18 {{ end }}
+6 -2
appview/state/state.go
··· 211 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 212 user := s.oauth.GetUser(r) 213 214 - timeline, err := db.MakeTimeline(s.db, 50) 215 if err != nil { 216 log.Println(err) 217 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") ··· 266 } 267 268 func (s *State) Home(w http.ResponseWriter, r *http.Request) { 269 - timeline, err := db.MakeTimeline(s.db, 5) 270 if err != nil { 271 log.Println(err) 272 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
··· 211 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 212 user := s.oauth.GetUser(r) 213 214 + var userDid string 215 + if user != nil { 216 + userDid = user.Did 217 + } 218 + timeline, err := db.MakeTimeline(s.db, 50, userDid) 219 if err != nil { 220 log.Println(err) 221 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") ··· 270 } 271 272 func (s *State) Home(w http.ResponseWriter, r *http.Request) { 273 + timeline, err := db.MakeTimeline(s.db, 5, "") 274 if err != nil { 275 log.Println(err) 276 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")