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 #1
+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" ··· 414 417 return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg) 415 418 } 416 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 + 417 457 func (p *Pages) icon(name string, classes []string) (template.HTML, error) { 418 458 iconPath := filepath.Join("static", "icons", name) 419 459
+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> 8 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> 9 24 <div class="col-span-2"> 10 25 <div class="flex items-center flex-row flex-nowrap gap-2"> 11 26 <p title="{{ $userIdent }}" ··· 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"

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.

1 commit
expand
appview/pages: upload and render avatar
expand 0 comments