Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reactions on main post #340

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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