dev vouch dev on at. thats about it atvouch.dev
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

tweak styling

authored by

Luna and committed by tangled.org bf4d405d 40513c0b

+207 -67
+204 -65
frontend/src/App.tsx
··· 84 84 <div className="instructions"> 85 85 <p>instructions:</p> 86 86 <p> 87 - <strong>1. treat a vouch as an actual endorsement of someone's dev skills.</strong> 87 + <strong> 88 + 1. treat a vouch as an actual endorsement of someone's dev 89 + skills. 90 + </strong> 88 91 </p> 89 92 </div> 90 93 {/* <div className="notes"> ··· 126 129 </div> 127 130 {designOpen && <DesignDecisions />} 128 131 <footer className="footer"> 129 - <a href="https://tangled.org/l4.pm/atvouch" target="_blank" rel="noopener noreferrer"> 132 + <a 133 + href="https://tangled.org/l4.pm/atvouch" 134 + target="_blank" 135 + rel="noopener noreferrer" 136 + > 130 137 source 131 138 </a>{" "} 132 139 | powered by{" "} 133 - <a href="https://microcosm.blue" target="_blank" rel="noopener noreferrer"> 140 + <a 141 + href="https://microcosm.blue" 142 + target="_blank" 143 + rel="noopener noreferrer" 144 + > 134 145 microcosm 135 146 </a> 136 147 ,{" "} ··· 138 149 wisp.place 139 150 </a> 140 151 , and{" "} 141 - <a href="https://codeberg.org/mary-ext/atcute" target="_blank" rel="noopener noreferrer"> 152 + <a 153 + href="https://codeberg.org/mary-ext/atcute" 154 + target="_blank" 155 + rel="noopener noreferrer" 156 + > 142 157 atcute 143 - </a> 144 - {" "}| made by{" "} 145 - <a href="https://bsky.app/profile/l4.pm" target="_blank" rel="noopener noreferrer"> 158 + </a>{" "} 159 + | made by{" "} 160 + <a 161 + href="https://bsky.app/profile/l4.pm" 162 + target="_blank" 163 + rel="noopener noreferrer" 164 + > 146 165 luna 147 166 </a> 148 167 </footer> ··· 195 214 return ( 196 215 <div className="auth-warning"> 197 216 atvouch.dev needs to request all permissions due to{" "} 198 - <a href="https://github.com/bluesky-social/atproto/issues/4479" target="_blank" rel="noopener noreferrer">an issue</a>{" "} 217 + <a 218 + href="https://github.com/bluesky-social/atproto/issues/4479" 219 + target="_blank" 220 + rel="noopener noreferrer" 221 + > 222 + an issue 223 + </a>{" "} 199 224 in the Bluesky Reference PDS{" "} 200 225 <a 201 226 href="#" ··· 208 233 </a> 209 234 {expanded && ( 210 235 <p className="auth-warning-details"> 211 - my intent is to use only the XRPCs for the api.atvouch.dev service and never interact with 212 - the bsky lexicons, but <code>rpc?lxm=*&aud=did:web:api.atvouch.dev%23atvouch_appview</code> does not 213 - work due to the aforementioned issue. 214 - <br /><br /> 215 - I hope this can be fixed someday. help me bsky pbc, you're my only hope 236 + my intent is to use only the XRPCs for the api.atvouch.dev service and 237 + never interact with the bsky lexicons, but{" "} 238 + <code>rpc?lxm=*&aud=did:web:api.atvouch.dev%23atvouch_appview</code>{" "} 239 + does not work due to the aforementioned issue. 240 + <br /> 241 + <br />I hope this can be fixed someday. help me bsky pbc, you're my 242 + only hope 216 243 </p> 217 244 )} 218 245 </div> ··· 290 317 </div> 291 318 <div className="layout"> 292 319 <aside className="sidebar"> 293 - <h2>Your vouches {vouchesTotal > 0 && <span className="vouch-count">({vouchesTotal})</span>}</h2> 320 + <h2> 321 + Your vouches{" "} 322 + {vouchesTotal > 0 && ( 323 + <span className="vouch-count">({vouchesTotal})</span> 324 + )} 325 + </h2> 294 326 {vouchesError && <div className="error">{vouchesError}</div>} 295 327 {vouchesLoading ? ( 296 328 <p className="muted">Loading...</p> ··· 303 335 <li key={v.rkey}> 304 336 {v.valid ? ( 305 337 <> 306 - <a className="vouch-handle" href={`https://bsky.app/profile/${v.did}`} target="_blank" rel="noopener noreferrer">{v.handle ?? v.did}</a> 338 + <a 339 + className="vouch-handle" 340 + href={`https://bsky.app/profile/${v.did}`} 341 + target="_blank" 342 + rel="noopener noreferrer" 343 + > 344 + {v.handle ?? v.did} 345 + </a> 307 346 <span className="vouch-date"> 308 347 {new Date(v.createdAt).toISOString().slice(0, 10)} 309 348 </span> 310 349 </> 311 350 ) : ( 312 - <span className="vouch-handle vouch-invalid">INVALID</span> 351 + <span className="vouch-handle vouch-invalid"> 352 + INVALID 353 + </span> 313 354 )} 314 355 <button 315 356 className="vouch-delete" 316 357 title="Delete vouch" 317 358 onClick={async () => { 318 - if (!confirm(v.valid ? `Remove vouch for ${v.handle ?? v.did}?` : `Delete invalid record?`)) return; 359 + if ( 360 + !confirm( 361 + v.valid 362 + ? `Remove vouch for ${v.handle ?? v.did}?` 363 + : `Delete invalid record?`, 364 + ) 365 + ) 366 + return; 319 367 await deleteVouch(agent, v.rkey); 320 368 refreshVouches(); 321 369 }} ··· 390 438 disabled={submitting} 391 439 placeholder="handle to vouch for" 392 440 /> 393 - <button type="submit" disabled={submitting || !handle.trim() || handle.includes("@")}> 441 + <button 442 + type="submit" 443 + disabled={submitting || !handle.trim() || handle.includes("@")} 444 + > 394 445 {submitting ? "Creating..." : "Vouch"} 395 446 </button> 396 447 </div> 397 - {handle.includes("@") && <p className="field-hint">enter a handle, not a domain (e.g. alice.bsky.social)</p>} 448 + {handle.includes("@") && ( 449 + <p className="field-hint"> 450 + enter a handle, not a domain (e.g. alice.bsky.social) 451 + </p> 452 + )} 398 453 </form> 399 454 {error && <div className="error">{error}</div>} 400 455 {result && <pre className="success">{result}</pre>} ··· 438 493 disabled={loading} 439 494 placeholder="handle to check" 440 495 /> 441 - <button type="submit" disabled={loading || !handle.trim() || handle.includes("@")}> 496 + <button 497 + type="submit" 498 + disabled={loading || !handle.trim() || handle.includes("@")} 499 + > 442 500 {loading ? "Checking..." : "Check"} 443 501 </button> 444 502 </div> 445 - {handle.includes("@") && <p className="field-hint">enter a handle, not a domain (e.g. alice.bsky.social)</p>} 503 + {handle.includes("@") && ( 504 + <p className="field-hint"> 505 + enter a handle, not a domain (e.g. alice.bsky.social) 506 + </p> 507 + )} 446 508 </form> 447 509 {error && <div className="error">{error}</div>} 448 510 {result && ( ··· 471 533 const [loadingMore, setLoadingMore] = useState(false); 472 534 const [error, setError] = useState<string | null>(null); 473 535 474 - const resolveVouches = useCallback(async (vouches: { creatorDid: string }[]): Promise<RemoteVoucher[]> => { 475 - return Promise.all( 476 - vouches.map(async (v) => { 477 - let handle: string | null = null; 478 - try { 479 - handle = await resolveDidToHandle(v.creatorDid); 480 - } catch { 481 - // leave as null 482 - } 483 - return { did: v.creatorDid, handle }; 484 - }), 485 - ); 486 - }, []); 536 + const resolveVouches = useCallback( 537 + async (vouches: { creatorDid: string }[]): Promise<RemoteVoucher[]> => { 538 + return Promise.all( 539 + vouches.map(async (v) => { 540 + let handle: string | null = null; 541 + try { 542 + handle = await resolveDidToHandle(v.creatorDid); 543 + } catch { 544 + // leave as null 545 + } 546 + return { did: v.creatorDid, handle }; 547 + }), 548 + ); 549 + }, 550 + [], 551 + ); 487 552 488 553 useEffect(() => { 489 554 (async () => { ··· 494 559 let pageCursor: string | undefined; 495 560 let total = 0; 496 561 do { 497 - const result = await fetchRemoteVouchers(agent, { limit: 1000, cursor: pageCursor }); 562 + const result = await fetchRemoteVouchers(agent, { 563 + limit: 1000, 564 + cursor: pageCursor, 565 + }); 498 566 const resolved = await resolveVouches(result.vouches); 499 567 allVouches = [...allVouches, ...resolved]; 500 568 total = result.total; ··· 525 593 setLoadingMore(false); 526 594 }, [agent, cursor, resolveVouches]); 527 595 528 - const alreadyVouched = new Set(myVouches?.filter((v) => v.valid).map((v) => v.did) ?? []); 596 + const alreadyVouched = new Set( 597 + myVouches?.filter((v) => v.valid).map((v) => v.did) ?? [], 598 + ); 529 599 530 600 return ( 531 601 <aside className="sidebar sidebar-right"> 532 - <h2>Remote vouches {total > 0 && <span className="vouch-count">({total})</span>}</h2> 602 + <h2> 603 + Remote vouches{" "} 604 + {total > 0 && <span className="vouch-count">({total})</span>} 605 + </h2> 533 606 {error && <div className="error">{error}</div>} 534 607 {loading ? ( 535 608 <p className="muted">Loading...</p> ··· 577 650 const [adding, setAdding] = useState<string | null>(null); 578 651 const [removing, setRemoving] = useState<string | null>(null); 579 652 // maintainer handle map: did -> { handle, avatar } 580 - const [maintainerInfo, setMaintainerInfo] = useState<Record<string, { handle: string; avatar?: string }>>({}); 653 + const [maintainerInfo, setMaintainerInfo] = useState< 654 + Record<string, { handle: string; avatar?: string }> 655 + >({}); 581 656 582 657 const refresh = useCallback(async () => { 583 658 setLoading(true); ··· 600 675 // Try to get avatar from bsky public API 601 676 let avatar: string | undefined; 602 677 try { 603 - const resp = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`); 678 + const resp = await fetch( 679 + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`, 680 + ); 604 681 if (resp.ok) { 605 682 const profile = await resp.json(); 606 683 avatar = profile.avatar; ··· 651 728 setRemoving(null); 652 729 }; 653 730 654 - const handleAddMaintainer = async (membership: BotMembership, handle: string) => { 731 + const handleAddMaintainer = async ( 732 + membership: BotMembership, 733 + handle: string, 734 + ) => { 655 735 setError(null); 656 736 try { 657 737 const did = await resolveHandle(handle); ··· 659 739 setError(`${handle} is already a maintainer`); 660 740 return; 661 741 } 662 - await updateBotMembership( 663 - agent, 664 - membership.rkey, 665 - membership.repo, 666 - [...membership.maintainers, did], 667 - ); 742 + await updateBotMembership(agent, membership.rkey, membership.repo, [ 743 + ...membership.maintainers, 744 + did, 745 + ]); 668 746 await refresh(); 669 747 } catch (err) { 670 748 setError(String(err)); 671 749 } 672 750 }; 673 751 674 - const handleRemoveMaintainer = async (membership: BotMembership, did: string) => { 752 + const handleRemoveMaintainer = async ( 753 + membership: BotMembership, 754 + did: string, 755 + ) => { 675 756 setError(null); 676 757 try { 677 758 const updated = membership.maintainers.filter((d) => d !== did); ··· 679 760 setError("Cannot remove the last maintainer"); 680 761 return; 681 762 } 682 - await updateBotMembership(agent, membership.rkey, membership.repo, updated); 763 + await updateBotMembership( 764 + agent, 765 + membership.rkey, 766 + membership.repo, 767 + updated, 768 + ); 683 769 await refresh(); 684 770 } catch (err) { 685 771 setError(String(err)); ··· 688 774 689 775 return ( 690 776 <section className="bot-repos-section"> 691 - <h2>Repositories linked to atvouch bot</h2> 692 - <p className="bot-repos-desc"> 693 - the bot scrapes tangled repositories for the repos you add here 694 - </p> 777 + <h2>link your tangled repos to the atvouch tangled bot!</h2> 778 + <div className="bot-repos-desc"> 779 + <p>instructions</p> 780 + <p style={{ fontWeight: "bold", fontSize: "1.1em" }}> 781 + 1. link the tangled repositories you own to the atvouch bot 782 + </p> 783 + <p style={{ fontWeight: "bold", fontSize: "1.1em" }}> 784 + 2. add specific maintainers on the link 785 + </p> 786 + <p style={{ fontWeight: "bold", fontSize: "1.1em" }}> 787 + 3. upon a new pull request to that tangled repo, atvouch will comment 788 + with all vouch paths from the maintainers to the author of the repo 789 + </p> 790 + </div> 695 791 {error && <div className="error">{error}</div>} 696 792 {loading ? ( 697 793 <p className="muted">Loading...</p> ··· 706 802 <li key={repo.rkey} className="bot-repo-item"> 707 803 <div className="bot-repo-header"> 708 804 <div className="bot-repo-info"> 709 - <span className="bot-repo-name">{repo.name}</span> 805 + <span className="bot-repo-name"> 806 + {repo.name} 807 + {repo.source && ( 808 + <svg className="fork-icon" viewBox="0 0 16 16" width="14" height="14" fill="currentColor" title="Forked repository" aria-label="Fork"> 809 + <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path> 810 + </svg> 811 + )} 812 + </span> 710 813 {repo.description && ( 711 814 <span className="bot-repo-desc">{repo.description}</span> 712 815 )} ··· 717 820 disabled={removing === repo.rkey} 718 821 onClick={() => membership && handleRemove(membership)} 719 822 > 720 - {removing === repo.rkey ? "..." : "REMOVE"} 823 + {removing === repo.rkey ? "..." : "UNLINK"} 721 824 </button> 722 825 ) : ( 723 826 <button ··· 725 828 disabled={adding === repo.rkey} 726 829 onClick={() => handleAdd(repo)} 727 830 > 728 - {adding === repo.rkey ? "..." : "ADD"} 831 + {adding === repo.rkey ? "..." : "LINK"} 729 832 </button> 730 833 )} 731 834 </div> ··· 733 836 <MaintainersList 734 837 membership={membership} 735 838 maintainerInfo={maintainerInfo} 736 - onAddMaintainer={(handle) => handleAddMaintainer(membership, handle)} 737 - onRemoveMaintainer={(did) => handleRemoveMaintainer(membership, did)} 839 + onAddMaintainer={(handle) => 840 + handleAddMaintainer(membership, handle) 841 + } 842 + onRemoveMaintainer={(did) => 843 + handleRemoveMaintainer(membership, did) 844 + } 738 845 /> 739 846 )} 740 847 </li> ··· 760 867 const [showInput, setShowInput] = useState(false); 761 868 const [inputValue, setInputValue] = useState(""); 762 869 const [submitting, setSubmitting] = useState(false); 763 - const [suggestions, setSuggestions] = useState<{ did: string; handle: string; avatar?: string }[]>([]); 870 + const [suggestions, setSuggestions] = useState< 871 + { did: string; handle: string; avatar?: string }[] 872 + >([]); 764 873 const [activeIdx, setActiveIdx] = useState(-1); 765 874 766 875 useEffect(() => { ··· 770 879 } 771 880 const timer = setTimeout(async () => { 772 881 const results = await searchActorsTypeahead(inputValue); 773 - setSuggestions(results.map((a) => ({ did: a.did, handle: a.handle, avatar: a.avatar }))); 882 + setSuggestions( 883 + results.map((a) => ({ 884 + did: a.did, 885 + handle: a.handle, 886 + avatar: a.avatar, 887 + })), 888 + ); 774 889 setActiveIdx(-1); 775 890 }, 200); 776 891 return () => clearTimeout(timer); ··· 855 970 className={i === activeIdx ? "maintainer-dd-active" : ""} 856 971 onMouseDown={() => submit(s.handle)} 857 972 > 858 - {s.avatar && <img className="maintainer-dd-avatar" src={s.avatar} alt="" />} 973 + {s.avatar && ( 974 + <img 975 + className="maintainer-dd-avatar" 976 + src={s.avatar} 977 + alt="" 978 + /> 979 + )} 859 980 <span>{s.handle}</span> 860 981 </li> 861 982 ))} ··· 913 1034 explanation: 914 1035 "initially, there were buttons to let you vouch for your incoming vouches (i.e. mutual vouches). these were removed, " + 915 1036 "though, because the friction in vouching is very intentional from a design perspective: the intent is to make you " + 916 - "think more carefully about who you're willing to vouch for." 1037 + "think more carefully about who you're willing to vouch for.", 917 1038 }, 918 1039 { 919 1040 id: "separate-maintainer-list", 920 - title: "(5) atvouch tangled bot has a separate maintainer list than tangled", 1041 + title: 1042 + "(5) atvouch tangled bot has a separate maintainer list than tangled", 921 1043 explanation: 922 1044 "this is intentional because people may want to have different policy for who can write to a repo vs what to consider " + 923 1045 "for vouching. i could in some future prefill with the sh.tangled.repo.collaborator to kickstart the membership record " + 924 - "but i don't want to deal with that yet." 1046 + "but i don't want to deal with that yet.", 925 1047 }, 926 1048 ]; 927 1049 ··· 948 1070 handle: string; 949 1071 }) { 950 1072 if (result.directVouch) { 951 - return <pre className="success">you -&gt; <a href={`https://bsky.app/profile/${result.targetDID}`} target="_blank" rel="noopener noreferrer">{handle}</a></pre>; 1073 + return ( 1074 + <pre className="success"> 1075 + you -&gt;{" "} 1076 + <a 1077 + href={`https://bsky.app/profile/${result.targetDID}`} 1078 + target="_blank" 1079 + rel="noopener noreferrer" 1080 + > 1081 + {handle} 1082 + </a> 1083 + </pre> 1084 + ); 952 1085 } 953 1086 954 1087 if (result.paths.length === 0) { ··· 967 1100 return ( 968 1101 <span key={did}> 969 1102 {j > 0 && " -> "} 970 - <a href={`https://bsky.app/profile/${did}`} target="_blank" rel="noopener noreferrer">{label}</a> 1103 + <a 1104 + href={`https://bsky.app/profile/${did}`} 1105 + target="_blank" 1106 + rel="noopener noreferrer" 1107 + > 1108 + {label} 1109 + </a> 971 1110 </span> 972 1111 ); 973 1112 })}
+3 -2
frontend/src/api.ts
··· 228 228 rkey: string; 229 229 name: string; 230 230 description?: string; 231 + source?: string; 231 232 } 232 233 233 234 export async function listTangledRepos(agent: OAuthUserAgent): Promise<TangledRepo[]> { ··· 242 243 }; 243 244 if (cursor) params.cursor = cursor; 244 245 const resp = await rpc.get("com.atproto.repo.listRecords", { params } as any); 245 - const data = resp.data as unknown as { records: { uri: string; value: { name: string; description?: string } }[]; cursor?: string }; 246 + const data = resp.data as unknown as { records: { uri: string; value: { name: string; description?: string; source?: string } }[]; cursor?: string }; 246 247 for (const rec of data.records) { 247 248 const rkey = rec.uri.split("/").pop()!; 248 - all.push({ uri: rec.uri, rkey, name: rec.value.name, description: rec.value.description }); 249 + all.push({ uri: rec.uri, rkey, name: rec.value.name, description: rec.value.description, source: rec.value.source }); 249 250 } 250 251 cursor = data.cursor; 251 252 } while (cursor);