Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ chrome
dist
node_modules
yarn-error.log
.idea
125 changes: 125 additions & 0 deletions src/post-reaction-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
createIssue as createGitHubIssue,
Issue,
loadIssueByNumber,
ReactionID,
Reactions,
reactionTypes,
toggleReaction,
User
} from './github';
import { EmptyReactions, reactionEmoji, reactionNames } from './reactions';
import { pageAttributes as page } from './page-attributes';

export class PostReactionComponent {
public readonly element: HTMLElement;
private readonly countAnchor: HTMLAnchorElement;
private readonly reactionListContainer: HTMLFormElement;
private reactions: Reactions = new EmptyReactions();
private reactionsCount: number = 0;
private issueURL: string = '';

constructor(
private user: User | null,
private issue: Issue | null,
private createIssueCallback: (issue: Issue) => Promise<null>
) {
this.element = document.createElement('section');
this.element.classList.add('post-reactions');
this.element.innerHTML = `
<header>
<a class="text-link" target="_blank"></a>
</header>
<form class="post-reaction-list BtnGroup" action="javascript:">
</form>`;
this.countAnchor = this.element.querySelector('header a') as HTMLAnchorElement;
this.reactionListContainer = this.element.querySelector('.post-reaction-list') as HTMLFormElement;
this.setIssue(this.issue)
this.render();
}

public setIssue(issue: Issue | null) {
this.issue = issue;
if (issue) {
this.reactions = issue.reactions;
this.reactionsCount = issue.reactions.total_count;
this.issueURL = issue.html_url;
this.render();
}
}

private setupSubmitHandler() {
const buttons = this.reactionListContainer.querySelectorAll('button');

function toggleButtons(disabled: boolean) {
buttons.forEach(b => b.disabled = disabled);
}

const handler = async (event: Event) => {
event.preventDefault();

const button = event.target as HTMLButtonElement;
toggleButtons(true);
const id = button.value as ReactionID;
const issueExists = !!this.issue;

if (!issueExists) {
const newIssue = await createGitHubIssue(
page.issueTerm as string,
page.url,
page.title,
page.description || '',
page.label
);
const issue = await loadIssueByNumber(newIssue.number);
this.issue = issue;
this.reactions = issue.reactions;
await this.createIssueCallback(issue);
}

const url = this.reactions.url;
const {deleted} = await toggleReaction(url, id);
const delta = deleted ? -1 : 1;
this.reactions[id] += delta;
this.reactions.total_count += delta;
this.issue!.reactions = this.reactions;
toggleButtons(false);
this.setIssue(this.issue);
}

buttons.forEach(b => b.addEventListener('click', handler, true))
}

private getSubmitButtons(): string {
function buttonFor(url: string, reaction: ReactionID, disabled: boolean, count: number): string {
return `
<button
type="submit"
action="javascript:"
formaction="${url}"
class="btn BtnGroup-item btn-outline post-reaction-button"
value="${reaction}"
aria-label="Toggle ${reactionNames[reaction]} reaction"
reaction-count="${count}"
${disabled ? 'disabled' : ''}>
${reactionEmoji[reaction]}
</button>`;
}

const issueLocked = this.issue ? this.issue.locked : false;
return reactionTypes
.map(id => buttonFor(this.reactions.url, id, !this.user || issueLocked, this.reactions[id]))
.join('')
}

private render() {
if (this.issueURL !== '') {
this.countAnchor.href = this.issueURL;
} else {
this.countAnchor.removeAttribute('href');
}
this.countAnchor.textContent = `${this.reactionsCount} Reaction${this.reactionsCount === 1 ? '' : 's'}`;
this.reactionListContainer.innerHTML = this.getSubmitButtons();
this.setupSubmitHandler();
}
}
16 changes: 15 additions & 1 deletion src/reactions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toggleReaction, ReactionID, reactionTypes } from './github';
import { ReactionID, Reactions, reactionTypes, toggleReaction } from './github';
import { getLoginUrl } from './oauth';
import { pageAttributes } from './page-attributes';
import { scheduleMeasure } from './measure';
Expand All @@ -25,6 +25,20 @@ export const reactionEmoji: Record<ReactionID, string> = {
'eyes': '👀'
};

export class EmptyReactions implements Reactions {
'+1' = 0;
'-1' = 0;
confused = 0;
eyes = 0;
heart = 0;
hooray = 0;
laugh = 0;
rocket = 0;
// tslint:disable-next-line:variable-name
total_count = 0;
url = '';
}

export function getReactionHtml(url: string, reaction: ReactionID, disabled: boolean, count: number) {
return `
<button
Expand Down
23 changes: 23 additions & 0 deletions src/stylesheets/post-reactions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.post-reactions {
margin: $spacer-3 0;
padding: 0 $spacer-1;
display: flex;
flex-direction: column;
align-items: center;

.post-reaction-list {
margin: $spacer-2 0;

& > .post-reaction-button {
font-weight: normal;
padding: $spacer-2 $spacer-3;
border-radius: 0 !important;

&::after {
display: inline-block;
margin-left: 2px;
content: attr(reaction-count);
}
}
}
}
1 change: 1 addition & 0 deletions src/stylesheets/utterances.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@import "@primer/css/forms/form-control";
@import "@primer/css/popover/index";
@import "./util";
@import "./post-reactions";
@import "./timeline";
@import "./timeline-comment";
@import "./permalink-code";
Expand Down
2 changes: 1 addition & 1 deletion src/timeline-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class TimelineComponent {
private user: User | null,
private issue: Issue | null
) {
this.element = document.createElement('main');
this.element = document.createElement('section');
this.element.classList.add('timeline');
this.element.innerHTML = `
<h1 class="timeline-header">
Expand Down
36 changes: 24 additions & 12 deletions src/utterances.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { pageAttributes as page } from './page-attributes';
import {
createIssue as createGitHubIssue,
Issue,
setRepoContext,
loadIssueByTerm,
loadIssueByNumber,
IssueComment,
loadCommentsPage,
loadIssueByNumber,
loadIssueByTerm,
loadUser,
postComment,
createIssue,
PAGE_SIZE,
IssueComment
postComment,
setRepoContext
} from './github';
import { TimelineComponent } from './timeline-component';
import { NewCommentComponent } from './new-comment-component';
import { startMeasuring, scheduleMeasure } from './measure';
import { scheduleMeasure, startMeasuring } from './measure';
import { loadTheme } from './theme';
import { getRepoConfig } from './repo-config';
import { loadToken } from './oauth';
import { enableReactions } from './reactions';
import { PostReactionComponent } from './post-reaction-component';

setRepoContext(page);

Expand All @@ -29,6 +30,9 @@ function loadIssue(): Promise<Issue | null> {
}

async function bootstrap() {
const main = document.createElement('main');
document.body.appendChild(main);

await loadToken();
// tslint:disable-next-line:prefer-const
let [issue, user] = await Promise.all([
Expand All @@ -40,7 +44,13 @@ async function bootstrap() {
startMeasuring(page.origin);

const timeline = new TimelineComponent(user, issue);
document.body.appendChild(timeline.element);
const createIssueCallback = async (newIssue: Issue) => {
issue = newIssue;
timeline.setIssue(issue);
}
const postReactionComponent = new PostReactionComponent(user, issue, createIssueCallback);
main.appendChild(postReactionComponent.element);
main.appendChild(timeline.element);

if (issue && issue.comments > 0) {
renderComments(issue, timeline);
Expand All @@ -57,14 +67,16 @@ async function bootstrap() {
const submit = async (markdown: string) => {
await assertOrigin();
if (!issue) {
issue = await createIssue(
const newIssue = await createGitHubIssue(
page.issueTerm as string,
page.url,
page.title,
page.description || '',
page.label
);
issue = await loadIssueByNumber(newIssue.number);
timeline.setIssue(issue);
postReactionComponent.setIssue(issue);
}
const comment = await postComment(issue.number, markdown);
timeline.insertComment(comment, true);
Expand Down Expand Up @@ -136,8 +148,8 @@ async function renderComments(issue: Issue, timeline: TimelineComponent) {
}

export async function assertOrigin() {
const { origins } = await getRepoConfig();
const { origin, owner, repo } = page;
const {origins} = await getRepoConfig();
const {origin, owner, repo} = page;
if (origins.indexOf(origin) !== -1) {
return;
}
Expand All @@ -151,7 +163,7 @@ export async function assertOrigin() {
</a>
to include <code>${origin}</code> in the list of origins.<br/><br/>
Suggested configuration:<br/>
<pre><code>${JSON.stringify({ origins: [origin] }, null, 2)}</code></pre>
<pre><code>${JSON.stringify({origins: [origin]}, null, 2)}</code></pre>
</div>`);
scheduleMeasure();
throw new Error('Origin not permitted.');
Expand Down