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

Added Unlock feature #46

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions src/Actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Poll } from "./Poll";
import { WebClient, WebAPICallResult, ChatPostMessageArguments } from "@slack/web-api";
import { KnownBlock } from "@slack/types";
import { Request, Response } from "express";
import {Poll} from "./Poll";
import {ChatPostMessageArguments, WebAPICallResult, WebClient} from "@slack/web-api";
import {KnownBlock} from "@slack/types";
import {Request, Response} from "express";
import * as Sentry from "@sentry/node";

const errorMsg = "An error occurred; please contact the administrators for assistance.";
Expand Down Expand Up @@ -36,7 +36,12 @@ export class Actions {
const poll = new Poll(payload.message.blocks);
poll.vote(payload.actions[0].text.text, payload.user.id);
payload.message.blocks = poll.getBlocks();
payload.message.text = "Vote changed!";
// Sends user message if their vote was changed or blocked due to the poll being locked
const vote_text = poll.getLockedStatus() ? "You cannot vote after the poll has been locked!" : "Vote changed!";
this.wc.chat.postEphemeral({
channel: payload.channel.id,
text: vote_text, user: payload.user.id
});
// We respond with the new payload
res(payload.message);
// In case it is being slow users will see this message
Expand All @@ -59,6 +64,9 @@ export class Actions {
case "lock":
this.onLockSelected(payload, poll);
break;
case "unlock":
this.onUnlockSelected(payload, poll);
break;
case "delete":
this.onDeleteSelected(payload, poll);
break;
Expand Down Expand Up @@ -123,6 +131,16 @@ export class Actions {
}
}

private onUnlockSelected(payload: any, poll: Poll): void {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

payload.message.text = "Poll unlocked!";
if (Actions.isPollAuthor(payload, poll)) {
poll.unlockpoll();
payload.message.blocks = poll.getBlocks();
} else {
this.postEphemeralOnlyAuthor("unlock", "poll", payload.channel.id, payload.user.id);
}
}

private onDeleteSelected(payload: any, poll: Poll): void {
if (Actions.isPollAuthor(payload, poll)) {
payload.message.text = "This poll has been deleted.";
Expand Down
108 changes: 63 additions & 45 deletions src/Poll.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import {
KnownBlock, SectionBlock, ContextBlock, Button, ActionsBlock, StaticSelect, PlainTextElement, MrkdwnElement
ActionsBlock,
Button,
ContextBlock,
KnownBlock,
MrkdwnElement,
PlainTextElement,
SectionBlock,
StaticSelect
} from "@slack/types";
import { PollHelpers } from "./PollHelpers";
import {PollHelpers} from "./PollHelpers";
import * as Sentry from "@sentry/node";

export class Poll {
Expand All @@ -11,6 +18,23 @@ export class Poll {
private checkIfMsgContains(value: string): boolean {
return this.getTitleFromMsg().includes(value);
}

private message: KnownBlock[] = [];
private multiple = false;
private anonymous = false;
private isLocked = false;

constructor(message: KnownBlock[]) {
this.message = message;
// Since its databaseless the way we know if it is anonymous or multiple is by parsing the title
this.multiple = this.checkIfMsgContains("(Multiple Answers)");
this.anonymous = this.checkIfMsgContains("(Anonymous)");
// If there's no buttons then the poll is locked
if (this.message.length - 1 !== this.getDividerId()) {
this.isLocked = ((this.message[this.getDividerId() + 1] as SectionBlock).text as MrkdwnElement).text === ":lock:";
}
}

static slashCreate(author: string, parameters: string[]): Poll {
if (process.env.SENTRY_DSN) {
Sentry.configureScope(scope => {
Expand All @@ -34,23 +58,9 @@ export class Poll {

const titleBlock = PollHelpers.buildSectionBlock(mrkdwnValue);
message.push(titleBlock, PollHelpers.buildContextBlock(`Asked by: ${author}`));
const actionBlocks: ActionsBlock[] = [{ type: "actions", elements: [] }];
let actionBlockCount = 0;
// Construct all the buttons
const start = titleBlock.text!.text === parameters[0] ? 1 : 2;
for (let i = start; i < parameters.length; i++) {
if (i % 5 === 0) {
const newActionBlock: ActionsBlock = { type: "actions", elements: [] };
actionBlocks.push(newActionBlock);
actionBlockCount++;
}
// Remove special characters, should be able to remove this once slack figures itself out
parameters[i] = parameters[i].replace("&amp;", "+").replace("&gt;", "greater than ")
.replace("&lt;", "less than ");
// We set value to empty string so that it is always defined
const button: Button = { type: "button", value: " ", text: PollHelpers.buildTextElem(parameters[i]) };
actionBlocks[actionBlockCount].elements.push(button);
}
const actionBlocks = PollHelpers.buildVoteOptions(parameters, start);
// The various poll options
const selection: StaticSelect = {
type: "static_select",
Expand All @@ -70,19 +80,6 @@ export class Poll {
return new Poll(message);
}

private message: KnownBlock[] = [];
private multiple = false;
private anonymous = false;
private isLocked = false;
constructor(message: KnownBlock[]) {
this.message = message;
// Since its databaseless the way we know if it is anonymous or multiple is by parsing the title
this.multiple = this.checkIfMsgContains("(Multiple Answers)");
this.anonymous = this.checkIfMsgContains("(Anonymous)");
// If there's no buttons then the poll is locked
this.isLocked = this.message[3].type === "divider";
}

public getBlocks(): KnownBlock[] {
return this.message;
}
Expand All @@ -100,19 +97,8 @@ export class Poll {
return { votes, userIdIndex: votes.indexOf(userId) };
}

public resetVote(userId: string): void {
this.processButtons(this.message.length, button => {
const { votes, userIdIndex } = this.getVotesAndUserIndex(button, userId);
if (userIdIndex === -1) return false;
votes.splice(userIdIndex, 1);
button.value = votes.join(",");
// Optimization: why search the rest if we know they only have one vote?
return !this.multiple;
});
this.generateVoteResults();
}

public vote(buttonText: string, userId: string): void {
if (this.isLocked) return;
this.processButtons(this.message.length, button => {
const { votes, userIdIndex } = this.getVotesAndUserIndex(button, userId);
if (!this.multiple && userIdIndex > -1 && button.text.text !== buttonText) {
Expand All @@ -126,11 +112,42 @@ export class Poll {
this.generateVoteResults();
}


public resetVote(userId: string): void {
this.processButtons(this.message.length, button => {
const {votes, userIdIndex} = this.getVotesAndUserIndex(button, userId);
if (userIdIndex === -1) return false;
votes.splice(userIdIndex, 1);
button.value = votes.join(",");
// Optimization: why search the rest if we know they only have one vote?
return !this.multiple;
});
this.generateVoteResults();
}

public lockPoll(): void {
if (this.isLocked) return;
this.isLocked = true;
this.generateVoteResults();
this.message = this.message.slice(0, 2).concat(this.message.slice(this.getDividerId() - 1));
this.message = this.message.slice(0, this.getDividerId() - 1).concat(this.getDynamicSelect()).concat(this.message.slice(this.getDividerId()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identical blocks of code found in 2 locations. Consider refactoring.

}

public unlockpoll(): void {
this.isLocked = false;
this.message = this.message.slice(0, this.getDividerId() - 1).concat(this.getDynamicSelect()).concat({type: "divider"}).concat(this.message.slice(this.getDividerId() + 2));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identical blocks of code found in 2 locations. Consider refactoring.

}

private getDynamicSelect(): ActionsBlock[] {
const selection: StaticSelect = {
type: "static_select",
placeholder: PollHelpers.buildTextElem("Poll Options"),
options: [
PollHelpers.buildSelectOption("Reset your vote", "reset"),
this.isLocked ? PollHelpers.buildSelectOption(":unlock: Unlock poll", "unlock") : PollHelpers.buildSelectOption(":lock: Lock poll", "lock"),
PollHelpers.buildSelectOption("Move to bottom", "bottom"),
PollHelpers.buildSelectOption("Delete poll", "delete")
]
};
return [{type: "actions", elements: [selection]}];
}

// Creates the message that will be sent to the poll author with the final results
Expand All @@ -141,6 +158,7 @@ export class Poll {
].concat(results);
}


private processButtons(loopEnd: number, buttonCallback: (b: Button) => boolean): void {
for (let i = 2; i < loopEnd; i++) {
if (this.message[i].type !== "actions") continue;
Expand Down Expand Up @@ -179,7 +197,7 @@ export class Poll {
}
// const sections = Object.keys(votes).map(key => this.buildVoteTally(overrideAnon, votes, key));
if (this.isLocked) sections.unshift(PollHelpers.buildSectionBlock(":lock:"));
return sections;
return sections.filter(f => f !== null);
}

private generateVoteResults(): void {
Expand Down
34 changes: 27 additions & 7 deletions src/PollHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
import {
SectionBlock, ContextBlock, PlainTextElement, Option
} from "@slack/types";
import {ActionsBlock, ContextBlock, Option, PlainTextElement, SectionBlock} from "@slack/types";

export class PollHelpers {
public static appendIfMatching(optionArray: string[], keyword: string, appendText: string): string {
return optionArray[0].toLowerCase() === keyword || optionArray[1].toLowerCase() === keyword ? appendText : "";
}

public static buildSectionBlock(mrkdwnValue: string): SectionBlock {
return { type: "section", text: { type: "mrkdwn", text: mrkdwnValue } };
return {type: "section", text: {type: "mrkdwn", text: mrkdwnValue}};
}

public static buildContextBlock(mrkdwnValue: string): ContextBlock {
return { type: "context", elements: [ { type: "mrkdwn", text: mrkdwnValue } ] };
return {type: "context", elements: [{type: "mrkdwn", text: mrkdwnValue}]};
}

public static buildSelectOption(text: string, value: string): Option {
return { text: this.buildTextElem(text), value: value };
return {text: this.buildTextElem(text), value: value};
}

public static buildTextElem(text: string): PlainTextElement {
return { type: "plain_text", text, emoji: true };
return {type: "plain_text", text, emoji: true};
}

public static buildVoteOptions(parameters: string [], start: number): ActionsBlock[] {
const actionBlocks: ActionsBlock[] = [{type: "actions", elements: []}];
let actionBlockCount = 0;
// Construct all the buttons
for (let i = start; i < parameters.length; i++) {
if (i % 5 === 0 && i != 0) {
actionBlocks.push({type: "actions", elements: []});
actionBlockCount++;
}
// Remove special characters, should be able to remove this once slack figures itself out
parameters[i] = parameters[i].replace("&amp;", "+").replace("&gt;", "greater than ")
.replace("&lt;", "less than ");
// We set value to empty string so that it is always defined
actionBlocks[actionBlockCount].elements.push({
type: "button",
value: " ",
text: PollHelpers.buildTextElem(parameters[i])
});
}
return actionBlocks;
}
}