Browse Source

Poll creator:

Non-anonymous mode support;
Multiple choice mode support;
Quiz mode support;
Fix revote rights;
master
morethanwords 4 years ago
parent
commit
8d9a8b6261
  1. 10
      src/components/chat/contextMenu.ts
  2. 17
      src/components/checkbox.ts
  3. 137
      src/components/popupCreatePoll.ts
  4. 18
      src/components/radioField.ts
  5. 19
      src/lib/appManagers/appMessagesManager.ts
  6. 14
      src/lib/appManagers/appPollsManager.ts
  7. 32
      src/scss/partials/popups/_createPoll.scss
  8. 126
      src/scss/style.scss

10
src/components/chat/contextMenu.ts

@ -2,7 +2,7 @@ import appChatsManager from "../../lib/appManagers/appChatsManager";
import appImManager from "../../lib/appManagers/appImManager"; import appImManager from "../../lib/appManagers/appImManager";
import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appMessagesManager from "../../lib/appManagers/appMessagesManager";
import appPeersManager from "../../lib/appManagers/appPeersManager"; import appPeersManager from "../../lib/appManagers/appPeersManager";
import appPollsManager from "../../lib/appManagers/appPollsManager"; import appPollsManager, { Poll } from "../../lib/appManagers/appPollsManager";
import $rootScope from "../../lib/rootScope"; import $rootScope from "../../lib/rootScope";
import { findUpClassName } from "../../lib/utils"; import { findUpClassName } from "../../lib/utils";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
@ -67,7 +67,7 @@ export class ChatContextMenu {
icon: 'edit', icon: 'edit',
text: 'Edit', text: 'Edit',
onClick: this.onEditClick, onClick: this.onEditClick,
verify: (peerID: number, msgID: number) => appMessagesManager.canEditMessage(msgID) verify: (peerID: number, msgID: number) => appMessagesManager.canEditMessage(msgID, 'text')
}, { }, {
icon: 'copy', icon: 'copy',
text: 'Copy', text: 'Copy',
@ -84,8 +84,8 @@ export class ChatContextMenu {
onClick: this.onRetractVote, onClick: this.onRetractVote,
verify: (peerID: number, msgID) => { verify: (peerID: number, msgID) => {
const message = appMessagesManager.getMessage(msgID); const message = appMessagesManager.getMessage(msgID);
const poll = message.media?.poll; const poll = message.media?.poll as Poll;
return poll && !poll.pFlags.closed; return poll && poll.chosenIndexes.length && !poll.pFlags.closed && !poll.pFlags.quiz;
} }
}, { }, {
icon: 'lock', icon: 'lock',
@ -94,7 +94,7 @@ export class ChatContextMenu {
verify: (peerID: number, msgID) => { verify: (peerID: number, msgID) => {
const message = appMessagesManager.getMessage(msgID); const message = appMessagesManager.getMessage(msgID);
const poll = message.media?.poll; const poll = message.media?.poll;
return appMessagesManager.canEditMessage(msgID) && message.fromID == $rootScope.myID && message.fwd_from === undefined && poll && !poll.pFlags.closed; return appMessagesManager.canEditMessage(msgID, 'poll') && poll && !poll.pFlags.closed;
} }
}, { }, {
icon: 'forward', icon: 'forward',

17
src/components/checkbox.ts

@ -0,0 +1,17 @@
const CheckboxField = (text: string, name: string) => {
const label = document.createElement('label');
label.classList.add('checkbox-field');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = 'input-' + name;
const span = document.createElement('span');
span.innerText = text;
label.append(input, span);
return {label, input, span};
};
export default CheckboxField;

137
src/components/popupCreatePoll.ts

@ -1,7 +1,11 @@
import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager";
import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager"; import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager";
import $rootScope from "../lib/rootScope"; import $rootScope from "../lib/rootScope";
import { findUpTag, whichChild } from "../lib/utils";
import CheckboxField from "./checkbox";
import { PopupElement } from "./popup"; import { PopupElement } from "./popup";
import RadioField from "./radioField";
import Scrollable from "./scrollable"; import Scrollable from "./scrollable";
import { toast } from "./toast"; import { toast } from "./toast";
@ -23,6 +27,13 @@ export default class PopupCreatePoll extends PopupElement {
private scrollable: Scrollable; private scrollable: Scrollable;
private tempID = 0; private tempID = 0;
private anonymousCheckboxField: ReturnType<typeof CheckboxField>;
private multipleCheckboxField: PopupCreatePoll['anonymousCheckboxField'];
private quizCheckboxField: PopupCreatePoll['anonymousCheckboxField'];
private correctAnswers: Uint8Array[];
private quizSolutionInput: HTMLInputElement;
constructor() { constructor() {
super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'CREATE', body: true}); super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'CREATE', body: true});
@ -38,11 +49,71 @@ export default class PopupCreatePoll extends PopupElement {
d.classList.add('caption'); d.classList.add('caption');
d.innerText = 'Options'; d.innerText = 'Options';
this.questions = document.createElement('div'); this.questions = document.createElement('form');
this.questions.classList.add('poll-create-questions'); this.questions.classList.add('poll-create-questions');
const dd = document.createElement('div');
dd.classList.add('poll-create-settings');
const settingsCaption = document.createElement('div');
settingsCaption.classList.add('caption');
settingsCaption.innerText = 'Settings';
const peerID = $rootScope.selectedPeerID;
if(!appPeersManager.isBroadcast(peerID)) {
this.anonymousCheckboxField = CheckboxField('Anonymous Voting', 'anonymous');
this.anonymousCheckboxField.input.checked = true;
dd.append(this.anonymousCheckboxField.label);
}
this.multipleCheckboxField = CheckboxField('Multiple Answers', 'multiple');
this.quizCheckboxField = CheckboxField('Quiz Mode', 'quiz');
this.multipleCheckboxField.input.addEventListener('change', () => {
const checked = this.multipleCheckboxField.input.checked;
this.quizCheckboxField.input.toggleAttribute('disabled', checked);
});
this.quizCheckboxField.input.addEventListener('change', () => {
const checked = this.quizCheckboxField.input.checked;
(Array.from(this.questions.children) as HTMLElement[]).map(el => {
el.classList.toggle('radio-field', checked);
});
quizElements.forEach(el => el.classList.toggle('hide', !checked));
this.multipleCheckboxField.input.toggleAttribute('disabled', checked);
});
dd.append(this.multipleCheckboxField.label, this.quizCheckboxField.label);
const quizElements: HTMLElement[] = [];
const quizSolutionCaption = document.createElement('div');
quizSolutionCaption.classList.add('caption');
quizSolutionCaption.innerText = 'Explanation';
const quizHr = document.createElement('hr');
const quizSolutionContainer = document.createElement('div');
quizSolutionContainer.classList.add('poll-create-questions');
const quizSolutionField = InputField('Add a Comment (Optional)', 'Add a Comment (Optional)', 'solution');
this.quizSolutionInput = quizSolutionField.firstElementChild as HTMLInputElement;
const quizSolutionSubtitle = document.createElement('div');
quizSolutionSubtitle.classList.add('subtitle');
quizSolutionSubtitle.innerText = 'Users will see this comment after choosing a wrong answer, good for educational purposes.';
quizSolutionContainer.append(quizSolutionField, quizSolutionSubtitle);
quizElements.push(quizHr, quizSolutionCaption, quizSolutionContainer);
quizElements.forEach(el => el.classList.add('hide'));
this.body.parentElement.insertBefore(hr, this.body); this.body.parentElement.insertBefore(hr, this.body);
this.body.append(d, this.questions); this.body.append(d, this.questions, document.createElement('hr'), settingsCaption, dd, ...quizElements);
this.confirmBtn.addEventListener('click', this.onSubmitClick); this.confirmBtn.addEventListener('click', this.onSubmitClick);
@ -54,17 +125,22 @@ export default class PopupCreatePoll extends PopupElement {
const question = this.questionInput.value; const question = this.questionInput.value;
if(!question.trim()) { if(!question.trim()) {
toast('Please enter a question'); toast('Please enter a question.');
return;
}
if(this.quizCheckboxField.input.checked && !this.correctAnswers?.length) {
toast('Please choose the correct answer.');
return; return;
} }
const answers = Array.from(this.questions.children).map((el, idx) => { const answers = Array.from(this.questions.children).map((el, idx) => {
const input = (el.firstElementChild as HTMLInputElement); const input = el.querySelector('input[type="text"]') as HTMLInputElement;
return input.value; return input.value;
}).filter(v => !!v.trim()); }).filter(v => !!v.trim());
if(answers.length < 2) { if(answers.length < 2) {
toast('Please enter at least two options'); toast('Please enter at least two options.');
return; return;
} }
@ -74,8 +150,23 @@ export default class PopupCreatePoll extends PopupElement {
//const randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)]; //const randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];
//const randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString(); //const randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString();
const pFlags: Poll['pFlags'] = {};
if(this.anonymousCheckboxField && !this.anonymousCheckboxField.input.checked) {
pFlags.public_voters = true;
}
if(this.multipleCheckboxField.input.checked) {
pFlags.multiple_choice = true;
}
if(this.quizCheckboxField.input.checked) {
pFlags.quiz = true;
}
const poll: Poll = { const poll: Poll = {
_: 'poll', _: 'poll',
pFlags,
question, question,
answers: answers.map((value, idx) => { answers: answers.map((value, idx) => {
return { return {
@ -88,17 +179,22 @@ export default class PopupCreatePoll extends PopupElement {
}; };
//poll.id = randomIDS; //poll.id = randomIDS;
appMessagesManager.sendOther($rootScope.selectedPeerID, appPollsManager.getInputMediaPoll(poll)); const inputMediaPoll = appPollsManager.getInputMediaPoll(poll, this.correctAnswers, this.quizSolutionInput ? this.quizSolutionInput.value : undefined);
appMessagesManager.sendOther($rootScope.selectedPeerID, inputMediaPoll);
}; };
onInput = (e: Event) => { onInput = (e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const radioLabel = findUpTag(target, 'LABEL');
if(target.value.length) { if(target.value.length) {
target.parentElement.classList.add('is-filled'); target.parentElement.classList.add('is-filled');
radioLabel.classList.remove('hidden-widget');
radioLabel.firstElementChild.removeAttribute('disabled');
} }
const isLast = !target.parentElement.nextElementSibling; const isLast = !radioLabel.nextElementSibling;
if(isLast && target.value.length && this.questions.childElementCount < 10) { if(isLast && target.value.length && this.questions.childElementCount < 10) {
this.appendMoreField(); this.appendMoreField();
} }
@ -106,27 +202,44 @@ export default class PopupCreatePoll extends PopupElement {
onDeleteClick = (e: MouseEvent) => { onDeleteClick = (e: MouseEvent) => {
const target = e.target as HTMLSpanElement; const target = e.target as HTMLSpanElement;
target.parentElement.remove(); findUpTag(target, 'LABEL').remove();
Array.from(this.questions.children).forEach((el, idx) => { Array.from(this.questions.children).forEach((el, idx) => {
const label = el.firstElementChild.nextElementSibling as HTMLLabelElement; const label = el.querySelector('label') as HTMLLabelElement;
label.innerText = 'Option ' + (idx + 1); label.innerText = 'Option ' + (idx + 1);
}); });
}; };
private appendMoreField() { private appendMoreField() {
const tempID = this.tempID++;
const idx = this.questions.childElementCount + 1; const idx = this.questions.childElementCount + 1;
const questionField = InputField('Add an Option', 'Option ' + idx, 'question-' + this.tempID++); const questionField = InputField('Add an Option', 'Option ' + idx, 'question-' + tempID);
(questionField.firstElementChild as HTMLInputElement).addEventListener('input', this.onInput); (questionField.firstElementChild as HTMLInputElement).addEventListener('input', this.onInput);
const radioField = RadioField('', 'question');
radioField.main.append(questionField);
radioField.label.classList.add('hidden-widget');
radioField.input.disabled = true;
if(!this.quizCheckboxField.input.checked) {
radioField.label.classList.remove('radio-field');
}
radioField.input.addEventListener('change', () => {
const checked = radioField.input.checked;
if(checked) {
const idx = whichChild(radioField.label);
this.correctAnswers = [new Uint8Array([idx])];
}
});
const deleteBtn = document.createElement('span'); const deleteBtn = document.createElement('span');
deleteBtn.classList.add('btn-icon', 'tgico-close'); deleteBtn.classList.add('btn-icon', 'tgico-close');
questionField.append(deleteBtn); questionField.append(deleteBtn);
deleteBtn.addEventListener('click', this.onDeleteClick, {once: true}); deleteBtn.addEventListener('click', this.onDeleteClick, {once: true});
this.questions.append(questionField); this.questions.append(radioField.label);
this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true); this.scrollable.scrollIntoView(this.questions.lastElementChild as HTMLElement, true);
//this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true);
} }
} }

18
src/components/radioField.ts

@ -0,0 +1,18 @@
const RadioField = (text: string, name: string) => {
const label = document.createElement('label');
label.classList.add('radio-field');
const input = document.createElement('input');
input.type = 'radio';
input.id = input.name = 'input-radio-' + name;
const main = document.createElement('div');
main.classList.add('radio-field-main');
main.innerText = text;
label.append(input, main);
return {label, input, main};
};
export default RadioField;

19
src/lib/appManagers/appMessagesManager.ts

@ -667,9 +667,9 @@ export class AppMessagesManager {
noWebPage: true, noWebPage: true,
newMedia: any newMedia: any
}> = {}) { }> = {}) {
if(!this.canEditMessage(messageID)) { /* if(!this.canEditMessage(messageID)) {
return Promise.reject({type: 'MESSAGE_EDIT_FORBIDDEN'}); return Promise.reject({type: 'MESSAGE_EDIT_FORBIDDEN'});
} } */
if(messageID < 0) { if(messageID < 0) {
if(this.tempFinalizeCallbacks[messageID] === undefined) { if(this.tempFinalizeCallbacks[messageID] === undefined) {
@ -2397,8 +2397,6 @@ export class AppMessagesManager {
apiMessage.totalEntities = RichTextProcessor.mergeEntities(myEntities, apiEntities, !apiMessage.pending); apiMessage.totalEntities = RichTextProcessor.mergeEntities(myEntities, apiEntities, !apiMessage.pending);
} }
apiMessage.canBeEdited = this.canMessageBeEdited(apiMessage);
if(!options.isEdited) { if(!options.isEdited) {
this.messagesStorage[mid] = apiMessage; this.messagesStorage[mid] = apiMessage;
(this.messagesStorageByPeerID[peerID] ?? (this.messagesStorageByPeerID[peerID] = {}))[mid] = apiMessage; (this.messagesStorageByPeerID[peerID] ?? (this.messagesStorageByPeerID[peerID] = {}))[mid] = apiMessage;
@ -2588,15 +2586,18 @@ export class AppMessagesManager {
} }
} }
public canMessageBeEdited(message: any) { public canMessageBeEdited(message: any, kind: 'text' | 'poll') {
const goodMedias = [ const goodMedias = [
'messageMediaPhoto', 'messageMediaPhoto',
'messageMediaDocument', 'messageMediaDocument',
'messageMediaWebPage', 'messageMediaWebPage',
'messageMediaPending', 'messageMediaPending'
'messageMediaPoll'
]; ];
if(kind == 'poll') {
goodMedias.push('messageMediaPoll');
}
if(message._ != 'message' || if(message._ != 'message' ||
message.deleted || message.deleted ||
message.fwd_from || message.fwd_from ||
@ -2615,13 +2616,13 @@ export class AppMessagesManager {
return true; return true;
} }
public canEditMessage(messageID: number) { public canEditMessage(messageID: number, kind: 'text' | 'poll' = 'text') {
if(!this.messagesStorage[messageID]) { if(!this.messagesStorage[messageID]) {
return false; return false;
} }
const message = this.messagesStorage[messageID]; const message = this.messagesStorage[messageID];
if(!message || !message.canBeEdited) { if(!message || !this.canMessageBeEdited(message, kind)) {
return false; return false;
} }

14
src/lib/appManagers/appPollsManager.ts

@ -1,3 +1,4 @@
import { InputMedia } from "../../layer";
import { logger, LogLevels } from "../logger"; import { logger, LogLevels } from "../logger";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";
import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config"; import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config";
@ -140,10 +141,19 @@ class AppPollsManager {
}; };
} }
public getInputMediaPoll(poll: Poll) { public getInputMediaPoll(poll: Poll, correctAnswers?: Uint8Array[], solution?: string): InputMedia.inputMediaPoll {
let solution_entities: any[];
if(solution) {
solution_entities = [];
solution = RichTextProcessor.parseMarkdown(solution, solution_entities);
}
return { return {
_: 'inputMediaPoll', _: 'inputMediaPoll',
poll poll,
correct_answers: correctAnswers,
solution,
solution_entities
}; };
} }

32
src/scss/partials/popups/_createPoll.scss

@ -8,6 +8,17 @@
} }
} }
.radio-field {
margin: 0;
}
.hidden-widget, .radio-field:first-child:last-child {
.btn-icon {
pointer-events: none;
opacity: 0 !important;
}
}
.input-field { .input-field {
margin-top: 25px; margin-top: 25px;
.btn-icon { .btn-icon {
@ -19,13 +30,6 @@
opacity: 1; opacity: 1;
transition: opacity .2s ease; transition: opacity .2s ease;
} }
&:not(.is-filled), &:first-child:last-child {
.btn-icon {
pointer-events: none;
opacity: 0;
}
}
/* &:last-child:not(:nth-child(10)) { /* &:last-child:not(:nth-child(10)) {
.btn-icon { .btn-icon {
display: none; display: none;
@ -36,14 +40,24 @@
.caption { .caption {
color: #707579; color: #707579;
font-weight: 500; font-weight: 500;
padding: 16px 24px 0; padding: 1rem 1.5rem 0;
} }
.poll-create-questions { .poll-create-questions {
padding: 0px 20px 32.5px; padding: 0px 1.25rem 2.03125rem;
}
.poll-create-settings {
padding: 0 .5rem .5rem;
} }
hr { hr {
border-bottom: 1px solid #edeff1; border-bottom: 1px solid #edeff1;
} }
.subtitle {
margin-top: .875rem;
font-size: .875rem;
line-height: 1.2;
}
} }

126
src/scss/style.scss

@ -72,6 +72,7 @@ $floating-left-sidebar: 925px;
} }
:root { :root {
--z-below: -1;
--color-gray: #c4c9cc; --color-gray: #c4c9cc;
--color-gray-hover: rgba(112, 117, 121, .08); --color-gray-hover: rgba(112, 117, 121, .08);
--layer-transition: .2s ease-in-out; --layer-transition: .2s ease-in-out;
@ -675,7 +676,7 @@ hr {
margin: 1.25rem 0; margin: 1.25rem 0;
display: block; display: block;
text-align: left; text-align: left;
padding: 0 18px; padding: 0 1.125rem;
/* font-weight: 500; */ /* font-weight: 500; */
position: relative; position: relative;
@ -684,23 +685,138 @@ hr {
} }
} }
[type="checkbox"] { .radio-field {
display: block;
position: relative;
padding-left: 3.5rem;
text-align: left;
margin: 1.25rem 0;
line-height: 1.5rem;
cursor: pointer;
&.hidden-widget {
cursor: default;
.radio-field-main {
&::before, &::after {
visibility: hidden;
}
}
}
> input {
&:checked {
& ~ .radio-field-main {
&::before {
border-color: $button-primary-background;
}
&::after {
opacity: 1;
}
}
}
}
.radio-field-main {
&::before, &::after {
content: '';
display: block;
position: absolute; position: absolute;
left: .25rem;
top: 50%;
width: 1.25rem;
height: 1.25rem;
transform: translateY(-50%);
}
&::before {
border: 2px solid #8d969c;
border-radius: 50%;
background-color: white;
opacity: 1;
transition: border-color .1s ease, opacity .1s ease;
}
&::after {
left: .5625rem;
width: .625rem;
height: .625rem;
border-radius: 50%;
background: $button-primary-background;
opacity: 0; opacity: 0;
pointer-events: none; transition: opacity .1s ease;
-webkit-box-sizing: border-box; }
/* .label {
display: block;
word-break: break-word;
}
.subLabel {
display: block;
font-size: 0.875rem;
line-height: 1rem;
color: var(--color-text-secondary);
} */
}
}
[type="checkbox"], [type="radio"] {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
opacity: 0;
z-index: var(--z-below);
position: absolute;
}
[type="checkbox"] {
& + span { & + span {
position: relative; position: relative;
padding-left: calc(18px + 2.25rem); padding-left: 3.5rem;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
height: 25px; height: 25px;
line-height: 25px; line-height: 25px;
user-select: none; user-select: none;
transition: .2s opacity;
}
&:not(:checked) + span:before {
width: 0;
height: 0;
border: 2px solid transparent;
left: 6px;
top: 10px;
transform: rotateZ(45deg);
transform-origin: 100% 100%;
}
&:checked + span:before {
top: 4px;
left: -1px;
width: 8px;
height: 14px;
border-top: 2px solid transparent;
border-left: 2px solid transparent;
border-right: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotateZ(45deg);
transform-origin: 100% 100%;
}
&:not(:checked) + span:after {
background-color: transparent;
border-color: #8d969c;
}
&:checked + span:after {
background-color: $button-primary-background;
}
}
[type="checkbox"] {
& + span {
&:before, &:after { &:before, &:after {
content: ''; content: '';
left: 0; left: 0;

Loading…
Cancel
Save