Monorepo for Tangled tangled.org

appview/pages: upload and render avatar

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

anirudh.fi afcdcfe8 4e728d11

verified
Changed files
+110 -8
appview
pages
+40
appview/pages/funcmap.go
··· 360 360 "fullAvatar": func(handle string) string { 361 361 return p.AvatarUrl(handle, "") 362 362 }, 363 + "profileAvatarUrl": func(profile *models.Profile, size string) string { 364 + return p.ProfileAvatarUrl(profile, size) 365 + }, 363 366 "langColor": enry.GetColor, 364 367 "layoutSide": func() string { 365 368 return "col-span-1 md:col-span-2 lg:col-span-3" ··· 412 415 sizeArg = fmt.Sprintf("size=%s", size) 413 416 } 414 417 return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg) 418 + } 419 + 420 + func (p *Pages) ProfileAvatarUrl(profile *models.Profile, size string) string { 421 + if profile != nil && profile.Avatar != "" { 422 + ident, err := p.resolver.ResolveIdent(context.Background(), profile.Did) 423 + if err == nil && ident.PDSEndpoint() != "" { 424 + blobUrl := fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", 425 + ident.PDSEndpoint(), 426 + profile.Did, 427 + profile.Avatar) 428 + 429 + handle := strings.TrimPrefix(profile.Did, "@") 430 + handle = p.resolveDid(handle) 431 + 432 + secret := p.avatar.SharedSecret 433 + h := hmac.New(sha256.New, []byte(secret)) 434 + h.Write([]byte(handle)) 435 + signature := hex.EncodeToString(h.Sum(nil)) 436 + 437 + sizeArg := "" 438 + if size != "" { 439 + sizeArg = fmt.Sprintf("&size=%s", size) 440 + } 441 + 442 + return fmt.Sprintf("%s/%s/%s?blob=%s%s", 443 + p.avatar.Host, 444 + signature, 445 + handle, 446 + url.QueryEscape(blobUrl), 447 + sizeArg) 448 + } 449 + } 450 + 451 + if profile != nil { 452 + return p.AvatarUrl(profile.Did, size) 453 + } 454 + return "" 415 455 } 416 456 417 457 func (p *Pages) icon(name string, classes []string) (template.HTML, error) {
+1 -1
appview/pages/templates/layouts/profilebase.html
··· 2 2 3 3 {{ define "extrameta" }} 4 4 {{ $handle := resolve .Card.UserDid }} 5 - {{ $avatarUrl := fullAvatar $handle }} 5 + {{ $avatarUrl := profileAvatarUrl .Card.Profile "" }} 6 6 <meta property="og:title" content="{{ $handle }}" /> 7 7 <meta property="og:type" content="profile" /> 8 8 <meta property="og:url" content="https://tangled.org/{{ $handle }}?tab={{ .Active }}" />
+44
appview/pages/templates/user/fragments/editAvatar.html
··· 1 + {{ define "user/fragments/editAvatar" }} 2 + <form 3 + hx-post="/profile/avatar" 4 + hx-encoding="multipart/form-data" 5 + hx-indicator="#spinner" 6 + hx-swap="none" 7 + class="flex flex-col gap-2"> 8 + <label for="avatar-file" class="uppercase p-0"> 9 + Upload avatar 10 + </label> 11 + <p class="text-sm text-gray-500 dark:text-gray-400">Select an image (PNG or JPEG, max 1MB)</p> 12 + <input 13 + type="file" 14 + id="avatar-file" 15 + name="avatar" 16 + accept="image/png,image/jpeg" 17 + required 18 + class="block w-full text-sm text-gray-500 dark:text-gray-400 19 + file:mr-4 file:py-2 file:px-4 20 + file:rounded file:border-0 21 + file:text-sm file:font-semibold 22 + file:bg-gray-100 file:text-gray-700 23 + dark:file:bg-gray-700 dark:file:text-gray-300 24 + hover:file:bg-gray-200 dark:hover:file:bg-gray-600" /> 25 + <div class="flex gap-2 pt-2"> 26 + <button 27 + id="cancel-avatar-btn" 28 + type="button" 29 + popovertarget="avatar-upload-modal" 30 + popovertargetaction="hide" 31 + class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 32 + {{ i "x" "size-4" }} 33 + cancel 34 + </button> 35 + <button type="submit" class="btn w-1/2 flex items-center"> 36 + <span class="inline-flex gap-2 items-center">{{ i "upload" "size-4" }} upload</span> 37 + <span id="spinner" class="group"> 38 + {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 39 + </span> 40 + </button> 41 + </div> 42 + <div id="avatar-error" class="text-red-500 dark:text-red-400"></div> 43 + </form> 44 + {{ end }}
+17 -3
appview/pages/templates/user/fragments/profileCard.html
··· 3 3 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 4 4 <div id="avatar" class="col-span-1 flex justify-center items-center"> 5 5 <div class="w-3/4 aspect-square relative"> 6 - <img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" /> 6 + <img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ profileAvatarUrl .Profile "" }}" /> 7 + {{ if eq .FollowStatus.String "IsSelf" }} 8 + <button 9 + class="absolute bottom-2 right-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" 10 + popovertarget="avatar-upload-modal" 11 + popovertargetaction="toggle" 12 + title="Upload avatar"> 13 + {{ i "camera" "w-4 h-4" }} 14 + </button> 15 + {{ end }} 7 16 </div> 17 + </div> 18 + <div 19 + id="avatar-upload-modal" 20 + popover 21 + class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50"> 22 + {{ template "user/fragments/editAvatar" . }} 8 23 </div> 9 24 <div class="col-span-2"> 10 25 <div class="flex items-center flex-row flex-nowrap gap-2"> ··· 36 51 {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 37 52 </div> 38 53 39 - <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 54 + <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 40 55 {{ if .Location }} 41 56 <div class="flex items-center gap-2"> 42 57 <span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span> ··· 111 126 </div> 112 127 {{ end }} 113 128 {{ end }} 114 -
+4 -2
appview/pages/templates/user/settings/emails.html
··· 62 62 hx-swap="none" 63 63 class="flex flex-col gap-2" 64 64 > 65 - <p class="uppercase p-0">ADD EMAIL</p> 65 + <label for="email-address" class="uppercase p-0"> 66 + add email 67 + </label> 66 68 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p> 67 69 <input 68 70 type="email" ··· 91 93 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div> 92 94 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div> 93 95 </form> 94 - {{ end }} 96 + {{ end }}
+4 -2
appview/pages/templates/user/settings/keys.html
··· 21 21 <div class="col-span-1 md:col-span-2"> 22 22 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2> 23 23 <p class="text-gray-500 dark:text-gray-400"> 24 - SSH public keys added here will be broadcasted to knots that you are a member of, 24 + SSH public keys added here will be broadcasted to knots that you are a member of, 25 25 allowing you to push to repositories there. 26 26 </p> 27 27 </div> ··· 63 63 hx-swap="none" 64 64 class="flex flex-col gap-2" 65 65 > 66 - <p class="uppercase p-0">ADD SSH KEY</p> 66 + <label for="key-name" class="uppercase p-0"> 67 + add ssh key 68 + </label> 67 69 <p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p> 68 70 <input 69 71 type="text"