Monorepo for Tangled tangled.org

appview/pages: upload and render avatar #894

merged opened by anirudh.fi targeting master from icy/tolqpt
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo.pull/3m7znwnzq6k22
+110 -8
Diff #0
+40
appview/pages/funcmap.go
··· 360 "fullAvatar": func(handle string) string { 361 return p.AvatarUrl(handle, "") 362 }, 363 "langColor": enry.GetColor, 364 "layoutSide": func() string { 365 return "col-span-1 md:col-span-2 lg:col-span-3" ··· 414 return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg) 415 } 416 417 func (p *Pages) icon(name string, classes []string) (template.HTML, error) { 418 iconPath := filepath.Join("static", "icons", name) 419
··· 360 "fullAvatar": func(handle string) string { 361 return p.AvatarUrl(handle, "") 362 }, 363 + "profileAvatarUrl": func(profile *models.Profile, size string) string { 364 + return p.ProfileAvatarUrl(profile, size) 365 + }, 366 "langColor": enry.GetColor, 367 "layoutSide": func() string { 368 return "col-span-1 md:col-span-2 lg:col-span-3" ··· 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 "" 455 + } 456 + 457 func (p *Pages) icon(name string, classes []string) (template.HTML, error) { 458 iconPath := filepath.Join("static", "icons", name) 459
+1 -1
appview/pages/templates/layouts/profilebase.html
··· 2 3 {{ define "extrameta" }} 4 {{ $handle := resolve .Card.UserDid }} 5 - {{ $avatarUrl := fullAvatar $handle }} 6 <meta property="og:title" content="{{ $handle }}" /> 7 <meta property="og:type" content="profile" /> 8 <meta property="og:url" content="https://tangled.org/{{ $handle }}?tab={{ .Active }}" />
··· 2 3 {{ define "extrameta" }} 4 {{ $handle := resolve .Card.UserDid }} 5 + {{ $avatarUrl := profileAvatarUrl .Card.Profile "" }} 6 <meta property="og:title" content="{{ $handle }}" /> 7 <meta property="og:type" content="profile" /> 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 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 4 <div id="avatar" class="col-span-1 flex justify-center items-center"> 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 }}" /> 7 </div> 8 </div> 9 <div class="col-span-2"> 10 <div class="flex items-center flex-row flex-nowrap gap-2"> 11 <p title="{{ $userIdent }}" ··· 36 {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 37 </div> 38 39 - <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 40 {{ if .Location }} 41 <div class="flex items-center gap-2"> 42 <span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span> ··· 111 </div> 112 {{ end }} 113 {{ end }} 114 -
··· 3 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 4 <div id="avatar" class="col-span-1 flex justify-center items-center"> 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="{{ 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 }} 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" . }} 23 + </div> 24 <div class="col-span-2"> 25 <div class="flex items-center flex-row flex-nowrap gap-2"> 26 <p title="{{ $userIdent }}" ··· 51 {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 52 </div> 53 54 + <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 55 {{ if .Location }} 56 <div class="flex items-center gap-2"> 57 <span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span> ··· 126 </div> 127 {{ end }} 128 {{ end }}
+4 -2
appview/pages/templates/user/settings/emails.html
··· 62 hx-swap="none" 63 class="flex flex-col gap-2" 64 > 65 - <p class="uppercase p-0">ADD EMAIL</p> 66 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p> 67 <input 68 type="email" ··· 91 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div> 92 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div> 93 </form> 94 - {{ end }}
··· 62 hx-swap="none" 63 class="flex flex-col gap-2" 64 > 65 + <label for="email-address" class="uppercase p-0"> 66 + add email 67 + </label> 68 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p> 69 <input 70 type="email" ··· 93 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div> 94 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div> 95 </form> 96 + {{ end }}
+4 -2
appview/pages/templates/user/settings/keys.html
··· 21 <div class="col-span-1 md:col-span-2"> 22 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2> 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, 25 allowing you to push to repositories there. 26 </p> 27 </div> ··· 63 hx-swap="none" 64 class="flex flex-col gap-2" 65 > 66 - <p class="uppercase p-0">ADD SSH KEY</p> 67 <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 <input 69 type="text"
··· 21 <div class="col-span-1 md:col-span-2"> 22 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2> 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, 25 allowing you to push to repositories there. 26 </p> 27 </div> ··· 63 hx-swap="none" 64 class="flex flex-col gap-2" 65 > 66 + <label for="key-name" class="uppercase p-0"> 67 + add ssh key 68 + </label> 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> 70 <input 71 type="text"

History

7 rounds 2 comments
sign up or login to add to the discussion
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
expand 2 comments
  • profileAvatarUrl is only used where models.Profile is available, is the uploaded profile picture not rendered across the board? (the previous PR in the stack implies otherwise, in which case, why do we need any special logic for avatar URLs on the appview?)
  • this resolves handle again, we already have a resolved identity in ident
  • there is repetition between ProfileAvatarUrl and AvatarUrl (the signature calculation logic is duplicated)

profileAvatarUrl is only used where models.Profile is available, is the uploaded profile picture not rendered across the board? (the previous PR in the stack implies otherwise, in which case, why do we need any special logic for avatar URLs on the appview?)

it exists to construct the blob url to to fetch from the pds. the avatar service merely checks if a blob url has been provided (if not, fallback to bsky). i could clean this up though, i didn't particularly like it myself.

anirudh.fi submitted #0
1 commit
expand
appview/pages: upload and render avatar
expand 0 comments