@@ -3,8 +3,8 @@ import type { ReactionState } from "../api/client.js";
33import { trustedOrigin } from "../auth/origin.js" ;
44import { Authentication } from "../auth/controller.js" ;
55import { createAuthenticationStatus } from "../auth/status.js" ;
6- import { viewerSubjectStates } from "../protocol/github.js" ;
7- import type { ViewerSubjectState } from "../protocol/github.js" ;
6+ import { setReaction , viewerSubjectStates } from "../protocol/github.js" ;
7+ import type { Reaction , ViewerSubjectState } from "../protocol/github.js" ;
88
99const parameters = new URLSearchParams ( location . search ) ;
1010const site = parameters . get ( "site" ) ?? "feedback-cpp-social" ;
@@ -21,13 +21,14 @@ const client = new FeedbackClient({ apiOrigin, site });
2121const authentication = new Authentication ( { site, callbackOrigin : location . origin , service : client } ) ;
2222const status = required ( "status" ) ;
2323let replyTo : { key : string ; id : string } | undefined ;
24+ let cardViewerStates = new Map < string , ViewerSubjectState > ( ) ;
2425required ( "api-origin" ) . textContent = apiOrigin ;
2526const authenticationStatus = createAuthenticationStatus ( {
2627 mount : required ( "authentication-status" ) ,
2728 authentication,
2829 explanation : "Sign in is required to add comments or replies." ,
2930 onError : ( error ) => { status . textContent = error instanceof Error ? error . message : "Authentication failed." ; } ,
30- onChange : ( ) => { void render ( ) ; } ,
31+ onChange : ( ) => { updateAuthenticationUi ( ) ; void render ( ) ; } ,
3132} ) ;
3233required ( "comment-form" ) . addEventListener ( "submit" , ( event ) => {
3334 event . preventDefault ( ) ;
@@ -44,6 +45,7 @@ for (const key of keys) {
4445 option . textContent = key ;
4546 commentKey . append ( option ) ;
4647}
48+ updateAuthenticationUi ( ) ;
4749void render ( ) ;
4850
4951async function submitComment ( ) : Promise < void > {
@@ -53,6 +55,7 @@ async function submitComment(): Promise<void> {
5355 let token = authentication . token ( ) ;
5456 token ??= await authentication . authenticate ( ) ;
5557 authenticationStatus . refresh ( ) ;
58+ updateAuthenticationUi ( ) ;
5659 const key = replyTo ?. key ?? commentKey . value ;
5760 await client . addComment ( key , textarea . value , token , replyTo ?. id ) ;
5861 textarea . value = "" ;
@@ -70,12 +73,26 @@ async function render(): Promise<void> {
7073 try {
7174 let states = await client . reactions ( keys ) ;
7275 const token = authentication . token ( ) ;
73- if ( token !== null ) states = await client . syncViewerUpvotes ( keys , token ) ;
76+ if ( token !== null ) {
77+ states = await client . syncViewerUpvotes ( keys , token ) ;
78+ const ids = [ ...states . values ( ) ] . flatMap ( ( state ) => state . id ? [ state . id ] : [ ] ) ;
79+ cardViewerStates = new Map ( await viewerSubjectStates ( token , ids ) ) ;
80+ } else {
81+ cardViewerStates . clear ( ) ;
82+ }
83+ renderRankingCards ( states ) ;
7484 const existingKeys = keys . filter ( ( key ) => states . get ( key ) ?. id ) ;
75- const contents = await client . discussionContents ( existingKeys ) ;
7685 const root = required ( "thread" ) ;
7786 root . replaceChildren ( ) ;
78- await Promise . all ( keys . map ( async ( key ) => {
87+ let contents = new Map < string , { discussion : Record < string , unknown > } > ( ) ;
88+ let contentError : unknown ;
89+ try {
90+ contents = new Map ( await client . discussionContents ( existingKeys ) ) ;
91+ await addViewerState ( [ ...contents . values ( ) ] . map ( ( value ) => value . discussion ) ) ;
92+ } catch ( error ) {
93+ contentError = error ;
94+ }
95+ for ( const key of keys ) {
7996 const article = document . createElement ( "article" ) ;
8097 article . className = "discussion" ;
8198 root . append ( article ) ;
@@ -86,20 +103,77 @@ async function render(): Promise<void> {
86103 }
87104 try {
88105 const payload = contents . get ( key ) ;
89- if ( ! payload ) throw new Error ( "Missing discussion content" ) ;
90- await addViewerState ( payload . discussion ) ;
106+ if ( ! payload ) {
107+ throw contentError instanceof Error
108+ ? contentError
109+ : new Error ( "Missing discussion content" ) ;
110+ }
91111 renderDiscussion ( article , key , payload . discussion , state ) ;
92112 } catch ( error ) {
93113 appendText ( article , "h2" , key ) ;
94114 appendText ( article , "p" , error instanceof FeedbackError ? error . code : "Unable to load thread." ) ;
95115 }
96- } ) ) ;
97- status . textContent = "Ready." ;
116+ }
117+ status . textContent = contentError === undefined ? "Ready." : "Counters loaded; discussions unavailable .";
98118 } catch ( error ) {
99119 status . textContent = error instanceof FeedbackError ? error . code : "Unable to load discussions." ;
100120 }
101121}
102122
123+ function renderRankingCards ( states : ReadonlyMap < string , ReactionState > ) : void {
124+ const root = required ( "cards" ) ;
125+ root . replaceChildren ( ) ;
126+ for ( const key of keys ) {
127+ const state = states . get ( key ) ;
128+ const card = document . createElement ( "article" ) ;
129+ card . className = "ranking-card" ;
130+ appendText ( card , "h3" , key ) ;
131+ const button = actionButton (
132+ `${ state ?. viewerHasUpvoted === true ? "Remove upvote" : "Upvote" } · ${ String ( state ?. upvotes ?? 0 ) } ` ,
133+ ( ) => upvote ( key ) ,
134+ ) ;
135+ button . setAttribute ( "aria-pressed" , String ( state ?. viewerHasUpvoted === true ) ) ;
136+ card . append ( button ) ;
137+ const reactions = document . createElement ( "div" ) ;
138+ reactions . className = "reactions" ;
139+ for ( const [ name , count ] of Object . entries ( state ?. reactions ?? { } ) ) {
140+ const selected = state ?. id
141+ ? cardViewerStates . get ( state . id ) ?. reactions . has ( name as Reaction ) === true
142+ : false ;
143+ const reaction = actionButton (
144+ `${ name } : ${ String ( count ) } ` ,
145+ ( ) => react ( key , name as Reaction , selected ) ,
146+ ) ;
147+ reaction . className = "reaction-control" ;
148+ reaction . setAttribute ( "aria-pressed" , String ( selected ) ) ;
149+ reactions . append ( reaction ) ;
150+ }
151+ card . append ( reactions ) ;
152+ if ( ! state ?. id ) appendText ( card , "small" , "No discussion yet; all counters are zero." ) ;
153+ root . append ( card ) ;
154+ }
155+ }
156+
157+ async function react ( key : string , reaction : Reaction , selected : boolean ) : Promise < void > {
158+ try {
159+ const token = authentication . token ( ) ?? await authentication . authenticate ( ) ;
160+ authenticationStatus . refresh ( ) ;
161+ updateAuthenticationUi ( ) ;
162+ const state = ( await client . reactions ( [ key ] ) ) . get ( key ) ;
163+ if ( ! state ?. id ) throw new Error ( "Create the discussion with an upvote before reacting." ) ;
164+ await setReaction ( token , state . id , reaction , ! selected ) ;
165+ status . textContent = `Updated ${ reaction } on ${ key } .` ;
166+ await render ( ) ;
167+ } catch ( error ) {
168+ status . textContent = error instanceof Error ? error . message : "Unable to react." ;
169+ }
170+ }
171+
172+ function updateAuthenticationUi ( ) : void {
173+ const authenticated = authentication . token ( ) !== null ;
174+ required ( "comment-submit" ) . textContent = authenticated ? "Comment" : "Sign in and comment" ;
175+ }
176+
103177function renderDiscussion (
104178 root : HTMLElement ,
105179 key : string ,
@@ -241,7 +315,7 @@ function renderComment(key: string, comment: Record<string, unknown>, depth: num
241315 const tag = appendText (
242316 article ,
243317 "span" ,
244- `${ string ( value . content ) } : ${ String ( integer ( record ( value . users ) ?. totalCount ) ) } ` ,
318+ `${ string ( value . content ) } : ${ String ( integer ( record ( value . reactors ) ?. totalCount ) ) } ` ,
245319 "tag" ,
246320 ) ;
247321 if ( value . viewerHasReacted === true ) tag . classList . add ( "selected" ) ;
@@ -285,11 +359,11 @@ function isViewer(author: unknown): boolean {
285359 return viewerId !== undefined && string ( record ( author ) ?. id ) === viewerId ;
286360}
287361
288- async function addViewerState ( content : Record < string , unknown > ) : Promise < void > {
362+ async function addViewerState ( contents : readonly Record < string , unknown > [ ] ) : Promise < void > {
289363 const token = authentication . token ( ) ;
290364 if ( token === null ) return ;
291365 const subjects = new Map < string , Record < string , unknown > > ( ) ;
292- collectSubjects ( content , subjects ) ;
366+ for ( const content of contents ) collectSubjects ( content , subjects ) ;
293367 const states = await viewerSubjectStates ( token , [ ...subjects . keys ( ) ] ) ;
294368 for ( const [ id , state ] of states ) overlayViewerState ( subjects . get ( id ) , state ) ;
295369}
0 commit comments