Skip to content

Commit

Permalink
Merge pull request #13 from RonLek/live-poll
Browse files Browse the repository at this point in the history
[NEW] Live poll: Multi-Question and Timed-Polls
  • Loading branch information
ramkumarkb authored Sep 14, 2021
2 parents 72ad9c5 + df03b72 commit 79c5e31
Show file tree
Hide file tree
Showing 17 changed files with 2,128 additions and 184 deletions.
264 changes: 223 additions & 41 deletions PollApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ import {
IRead,
} from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import { IAppInfo, RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata';
import { SettingType } from '@rocket.chat/apps-engine/definition/settings';
import {
IUIKitInteractionHandler,
UIKitBlockInteractionContext,
UIKitViewSubmitInteractionContext,
} from '@rocket.chat/apps-engine/definition/uikit';
import { addOptionModal } from './src/lib/addOptionModal';
import { createLivePollMessage } from './src/lib/createLivePollMessage';
import { createLivePollModal } from './src/lib/createLivePollModal';

import timeZones from './src/assets/timezones';
import { pollVisibility } from './src/definition';
import { createMixedVisibilityModal } from './src/lib/createMixedVisibilityModal';
import { createPollMessage } from './src/lib/createPollMessage';
import { createPollModal } from './src/lib/createPollModal';
import { finishPollMessage } from './src/lib/finishPollMessage';
import { nextPollMessage } from './src/lib/nextPollMessage';
import { updatePollMessage } from './src/lib/updatePollMessage';
import { votePoll } from './src/lib/votePoll';
import { PollCommand } from './src/PollCommand';
Expand Down Expand Up @@ -49,47 +53,149 @@ export class PollApp extends App implements IUIKitInteractionHandler {
additionalChoices?: string;
},
},
config?: {
mode?: string,
visibility?: string,
},
} = data.view as any;

if (!state) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
question: 'Error creating poll',
},
});
}

if (state.config && state.config.visibility !== pollVisibility.mixed) {
try {
await createPollMessage(data, read, modify, persistence, data.user.id);
} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
question: 'Error creating poll',
},
errors: err,
});
}
} else {
// Open mixed visibility modal
try {
const modal = await createMixedVisibilityModal({ question: state.poll.question, persistence, modify, data });
await modify.getUiController().openModalView(modal, context.getInteractionData(), data.user);

if (state.config && state.config.visibility !== pollVisibility.mixed) {
try {
await createPollMessage(data, read, modify, persistence, data.user.id);
} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
} else {
// Open mixed visibility modal
try {
const modal = await createMixedVisibilityModal({ question: state.poll.question, persistence, modify, data });
await modify.getUiController().openModalView(modal, context.getInteractionData(), data.user);

return {
success: true,
};

} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
return {
success: true,
};

} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
}

return {
success: true,
};
success: true,
};
} else if (/create-live-poll-modal/.test(id)) {
const { state }: {
state: {
poll: {
question: string,
[option: string]: string,
},
config?: {
mode?: string,
visibility?: string,
},
},
} = data.view as any;
if (!state) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
option: 'Error creating poll',
},
});
}
const association = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, data.view.id);
const [readData] = await read.getPersistenceReader().readByAssociation(association) as any;
const polls = readData.polls || [];
const pollIndex = +readData.pollIndex + 1;
const totalPolls = +readData.totalPolls;
// Prompt user to enter values for poll if left blank
try {

if (!state.poll || !state.poll.question || state.poll.question.trim() === '') {
throw { question: 'Please type your question here' };
}
if (!state.poll || !state.poll.ttv || isNaN(+state.poll.ttv)) {
throw { ttv: 'Please enter a valid time for the poll to end' };
}
if (!state.poll['option-0'] || state.poll['option-0'] === '') {
throw {
'option-0': 'Please provide one more option',
};
}
if (!state.poll['option-1'] || state.poll['option-1'] === '') {
throw {
'option-1': 'Please provide one more option',
};
}
} catch (err) {
this.getLogger().log(err);
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
polls.push(state);
readData.polls = polls;
readData.pollIndex = pollIndex;
readData.user = data.user;
readData.appId = data.appId;
readData.view = data.view;
readData.triggerId = data.triggerId;
await persistence.updateByAssociation(association, readData, true);
if (pollIndex === totalPolls) {
const pollId = `live-${Math.random().toString(36).slice(7)}`;
const livePollAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, pollId);
await persistence.createWithAssociation(readData, livePollAssociation);
try {
if (readData.save) {
const message = modify
.getCreator()
.startMessage()
.setSender(data.user)
.setText(`Live Poll has been saved with id ${pollId}. Use \`/poll live load ${pollId}\` to start.`)
.setUsernameAlias('Poll');

if (readData.room) {
message.setRoom(readData.room);
}
modify
.getNotifier()
.notifyUser(
data.user,
message.getMessage(),
);

} else {
await createLivePollMessage(data, read, modify, persistence, data.user.id, 0);
}
} catch (err) {
this.getLogger().log(err);
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
} else {

const modal = await createLivePollModal({id: data.view.id, question: '', persistence, modify, data, pollIndex, totalPolls});
return context.getInteractionResponder().updateModalViewResponse(modal);
}
} else if (/create-mixed-visibility-modal/.test(id)) {

const { state }: {
Expand All @@ -116,11 +222,7 @@ export class PollApp extends App implements IUIKitInteractionHandler {
viewId: data.view.id,
errors: err,
});
}

return {
success: true,
};
}
} else if (/add-option-modal/.test(id)) {
const { state }: {
state: {
Expand Down Expand Up @@ -180,11 +282,47 @@ export class PollApp extends App implements IUIKitInteractionHandler {
}

case 'addChoice': {
const modal = await createPollModal({ id: data.container.id, data, persistence, modify, options: parseInt(String(data.value), 10) });

let modal;
if (data.value && data.value.includes('live-')) {
modal = await createLivePollModal({
id: data.container.id,
data, persistence, modify,
options: parseInt(data.value.split('-')[1], 10),
pollIndex: parseInt(data.value.split('-')[2], 10),
totalPolls: parseInt(data.value.split('-')[3], 10),
});
} else {
modal = await createPollModal({ id: data.container.id, data, persistence, modify, options: parseInt(String(data.value), 10) });
}
return context.getInteractionResponder().updateModalViewResponse(modal);
}

case 'nextPoll': {
try {
const logger = this.getLogger();
await nextPollMessage({ data, read, persistence, modify, logger });
} catch (e) {

const { room } = context.getInteractionData();
const errorMessage = modify
.getCreator()
.startMessage()
.setSender(context.getInteractionData().user)
.setText(e.message)
.setUsernameAlias('Poll');

if (room) {
errorMessage.setRoom(room);
}
modify
.getNotifier()
.notifyUser(
context.getInteractionData().user,
errorMessage.getMessage(),
);
}
break;
}
case 'addUserChoice': {
const modal = await addOptionModal({ id: data.container.id, read, modify });

Expand Down Expand Up @@ -224,15 +362,59 @@ export class PollApp extends App implements IUIKitInteractionHandler {
}

public async initialize(configuration: IConfigurationExtend): Promise<void> {
await configuration.slashCommands.provideSlashCommand(new PollCommand());
configuration.scheduler.registerProcessors([
{
id: 'nextPoll',
processor: async (jobContext, read, modify, http, persis) => {
try {
const logger = this.getLogger();
await nextPollMessage({ data: jobContext, read, persistence: persis, modify, logger });

} catch (e) {
const { room } = jobContext.room;
const errorMessage = modify
.getCreator()
.startMessage()
.setSender(jobContext.user)
.setText(e.message)
.setUsernameAlias('Poll');

if (room) {
errorMessage.setRoom(room);
}
await modify
.getNotifier()
.notifyUser(
jobContext.user,
errorMessage.getMessage(),
);
}
},
},
]);
await configuration.slashCommands.provideSlashCommand(new PollCommand(this));
await configuration.settings.provideSetting({
id : 'use-user-name',
i18nLabel: 'Use name attribute to display voters, instead of username',
i18nDescription: 'When checked, display voters as full user names instead of username',
i18nLabel: 'use_user_name_label',
i18nDescription: 'use_user_name_description',
required: false,
type: SettingType.BOOLEAN,
public: true,
packageValue: false,
});
await configuration.settings.provideSetting({
id : 'timezone',
i18nLabel: 'timezone_label',
i18nDescription: 'timezone_description',
required: true,
type: SettingType.SELECT,
public: true,
packageValue: 'America/Danmarkshavn',
value: 'America/Danmarkshavn',
values: timeZones.timeZones.map((tz) => ({
i18nLabel: `${tz.value} (UTC ${tz.offset >= 0 ? '+ ' + tz.offset : '- ' + Math.abs(tz.offset)} )`,
key: tz.utc[0],
})),
});
}
}
33 changes: 31 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "c33fa1a6-68a7-491e-bf49-9d7b99671c48",
"version": "2.0.0",
"requiredApiVersion": "^1.12.0",
"requiredApiVersion": "^1.23.0",
"iconFile": "icon.png",
"author": {
"name": "Diego Sampaio",
Expand All @@ -14,5 +14,34 @@
"description": "A simple app to create polls on Rocket.Chat. Use the slash command: /poll [Question?]",
"implements": [
"IUIKitInteractionHandler"
],
"permissions": [
{
"name": "scheduler"
},
{
"name": "user.read"
},
{
"name": "server-setting.read"
},
{
"name": "room.read"
},
{
"name": "message.read"
},
{
"name": "message.write"
},
{
"name": "slashcommand"
},
{
"name": "persistence"
},
{
"name": "ui.interact"
}
]
}
}
8 changes: 6 additions & 2 deletions i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"cmd_description": "Create a simple poll",
"params_example": "Type your question or fill it up on the form."
"cmd_description": "Create a simple poll or a Live Poll",
"params_example": "Type your question or fill it up on the form. Optionally follow with live <number> or live save <number> or live load <id>.",
"use_user_name_label": "Use name attribute to display voters, instead of username",
"use_user_name_description": "When checked, display voters as full user names instead of username",
"timezone_label": "TimeZone",
"timezone_description": "Timezone to display Poll finish time in"
}
Loading

0 comments on commit 79c5e31

Please sign in to comment.