diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index 5bc9469..6d639c3 100644 --- a/harbour-fernschreiber.pro +++ b/harbour-fernschreiber.pro @@ -39,6 +39,7 @@ DISTFILES += qml/harbour-fernschreiber.qml \ qml/components/ImagePreview.qml \ qml/components/InReplyToRow.qml \ qml/components/LocationPreview.qml \ + qml/components/PollPreview.qml \ qml/components/StickerPicker.qml \ qml/components/PhotoTextsListItem.qml \ qml/components/WebPagePreview.qml \ @@ -60,6 +61,8 @@ DISTFILES += qml/harbour-fernschreiber.qml \ qml/pages/InitializationPage.qml \ qml/pages/OverviewPage.qml \ qml/pages/AboutPage.qml \ + qml/pages/PollCreationPage.qml \ + qml/pages/PollResultsPage.qml \ qml/pages/SettingsPage.qml \ qml/pages/VideoPage.qml \ rpm/harbour-fernschreiber.changes.in \ diff --git a/qml/components/PollPreview.qml b/qml/components/PollPreview.qml new file mode 100644 index 0000000..11aa8cc --- /dev/null +++ b/qml/components/PollPreview.qml @@ -0,0 +1,293 @@ +/* + Copyright (C) 2020 Sebastian J. Wolf and other contributors + + This file is part of Fernschreiber. + + Fernschreiber is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Fernschreiber is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Fernschreiber. If not, see . +*/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import QtGraphicalEffects 1.0 + +import "../js/functions.js" as Functions +import "../js/twemoji.js" as Emoji + +Item { + id: pollMessageComponent + + property string chatId + property var message:({}) + property bool isOwnMessage + + property string messageId: message.id + property bool canEdit: message.can_be_edited + property var pollData: message.content.poll + property var chosenPollData:({}) + property var chosenIndexes: [] + property bool hasAnswered: { + return pollData.options.filter(function(option){ + return option.is_chosen + }).length > 0; + } + property bool canAnswer: !hasAnswered && !pollData.is_closed + property bool isQuiz: pollData.type['@type'] === "pollTypeQuiz" + property Item messageItem + height: pollColumn.height + opacity: 0 + Behavior on opacity { FadeAnimator {} } + function handleChoose(index) { + if(!pollData.type.allow_multiple_answers) { + chosenIndexes = [index]; + sendResponse(); + return; + } + var indexes = chosenIndexes; + var found = indexes.indexOf(index); + if(found > -1) { // uncheck + indexes.splice(found, 1); + } else { + indexes.push(index) + } + chosenIndexes = indexes; + } + function resetChosen() { + chosenIndexes = []; + sendResponse(); + } + function sendResponse() { + tdLibWrapper.setPollAnswer(chatId, messageId, chosenIndexes); + } + + Column { + id: pollColumn + width: parent.width + spacing: Theme.paddingSmall + Label { + font.pixelSize: Theme.fontSizeSmall + width: parent.width + visible: text !== "" + text: Emoji.emojify(pollData.question, Theme.fontSizeSmall) + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + color: pollMessageComponent.isOwnMessage ? Theme.highlightColor : Theme.primaryColor + } + + Label { + font.pixelSize: Theme.fontSizeTiny + width: parent.width + visible: text !== "" + text: pollData.is_closed ? qsTr("Final Result:") : (pollData.type.allow_multiple_answers ? qsTr("Multiple Answers are allowed.") : "") + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + color: pollMessageComponent.isOwnMessage ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + + Item { + visible: !pollMessageComponent.canAnswer + width: parent.width + height: Theme.paddingSmall + } + + Component { + id: canAnswerDelegate + TextSwitch { + id: optionDelegate + width: pollMessageComponent.width + automaticCheck: false + // emojify does not work here :/ + text: modelData.text + checked: pollMessageComponent.chosenIndexes.indexOf(index) > -1 + onClicked: { + pollMessageComponent.handleChoose(index); + } + } + } + Component { + id: resultDelegate + Item { + id: optionDelegate + width: pollMessageComponent.width + height: displayOptionLabel.height + displayOptionStatistics.height + + Rectangle { + id: displayOptionChosenMarker + height: parent.height + width: Theme.horizontalPageMargin/2 + color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) + visible: modelData.is_chosen + x: -width + } + OpacityRampEffect { + sourceItem: displayOptionChosenMarker + direction: OpacityRamp.LeftToRight + } + Column { + id: iconsColumn + width: pollMessageComponent.isQuiz ?Theme.iconSizeSmall + Theme.paddingMedium : Theme.paddingMedium + + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + + Icon { + highlighted: pollMessageComponent.isOwnMessage + property bool isRight: pollMessageComponent.isQuiz && pollData.type.correct_option_id === index + source: "image://theme/icon-s-accept" + visible: isRight + } + } + + Label { + id: displayOptionLabel + text: Emoji.emojify(modelData.text, Theme.fontSizeMedium) + + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + anchors { + left: iconsColumn.right + top: parent.top + right: parent.right + } + color: pollMessageComponent.isOwnMessage ? Theme.highlightColor : Theme.primaryColor + } + Item { + id: displayOptionStatistics + height: optionVoterPercentage.height + optionVoterPercentageBar.height + anchors { + top: displayOptionLabel.bottom + left: iconsColumn.right + right: parent.right + } + + Label { + id: optionVoterPercentage + font.pixelSize: Theme.fontSizeTiny + text: qsTr("%L1\%", "% of votes for option").arg(modelData.vote_percentage) + horizontalAlignment: Text.AlignRight + anchors { + right: parent.right + left: parent.horizontalCenter + leftMargin: Theme.paddingSmall + } + color: pollMessageComponent.isOwnMessage ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + Rectangle { + id: optionVoterPercentageBar + height: Theme.paddingSmall + width: parent.width + + color: Theme.rgba(pollMessageComponent.isOwnMessage ? Theme.secondaryHighlightColor : Theme.secondaryColor, 0.3) + radius: height/2 + anchors { + left: parent.left + bottom: parent.bottom + } + + Rectangle { + height: parent.height + color: pollMessageComponent.isOwnMessage ? Theme.highlightColor : Theme.primaryColor + radius: height/2 + width: parent.width * modelData.vote_percentage * 0.01 + } + } + } + } + } + + Repeater { + model: pollData.options + delegate: pollMessageComponent.canAnswer ? canAnswerDelegate : resultDelegate + } + + Row { + layoutDirection: Qt.RightToLeft + width: parent.width + spacing: Theme.paddingMedium + Behavior on height { NumberAnimation {}} + + + Label { + id: totalVoterCount + font.pixelSize: Theme.fontSizeTiny + anchors.verticalCenter: parent.verticalCenter + text: qsTr("%L1 vote(s) total", "number of total votes", pollData.total_voter_count).arg(pollData.total_voter_count) + width: contentWidth + height: contentHeight + horizontalAlignment: Text.AlignRight + color: pollMessageComponent.isOwnMessage ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + + Row { + spacing: Theme.paddingSmall + width: parent.width - totalVoterCount.width - parent.spacing + IconButton { + visible: !pollData.is_closed && pollMessageComponent.chosenIndexes.length > 0 && pollData.type.allow_multiple_answers && !pollMessageComponent.hasAnswered + opacity: visible ? 1.0 : 0.0 + Behavior on opacity { NumberAnimation {}} + icon.source: "image://theme/icon-m-send" + onClicked: { + pollMessageComponent.sendResponse() + } + } + + + IconButton { + visible: !pollMessageComponent.canAnswer && !pollData.is_anonymous && pollData.total_voter_count > 0 + icon.source: "image://theme/icon-m-media-artists" + onClicked: { + pageStack.push(Qt.resolvedUrl("../pages/PollResultsPage.qml"), { chatId:chatId, message:pollMessageComponent.message}); + } + Icon { + opacity: 0.8 + source: "image://theme/icon-s-maybe" + anchors { + right: parent.right + top: parent.top + } + } + } + } + } + + } + Component { + id: closePollMenuItemComponent + MenuItem { + text: qsTr("Close Poll") + onClicked: { + tdLibWrapper.stopPoll(pollMessageComponent.chatId, pollMessageComponent.messageId); + } + } + } + Component { + id: resetAnswerMenuItemComponent + MenuItem { + text: qsTr("Reset Answer") + onClicked: { + pollMessageComponent.resetChosen() + } + } + } + + Component.onCompleted: { + opacity = 1; + if(messageItem && messageItem.menu ) { // workaround to add menu entries + if(!pollData.is_closed && pollMessageComponent.canEdit) { + closePollMenuItemComponent.createObject(messageItem.menu._contentColumn); + } + if(!pollData.is_closed && !pollMessageComponent.isQuiz && pollMessageComponent.hasAnswered) { + resetAnswerMenuItemComponent.createObject(messageItem.menu._contentColumn); + } + } + } +} diff --git a/qml/js/functions.js b/qml/js/functions.js index 771d836..b62ffb9 100644 --- a/qml/js/functions.js +++ b/qml/js/functions.js @@ -97,6 +97,18 @@ function getMessageText(message, simple, myself) { if (message.content['@type'] === 'messageChatChangeTitle') { return myself ? qsTr("changed the chat title to %1", "myself").arg(message.content.title) : qsTr("changed the chat title to %1").arg(message.content.title); } + if (message.content['@type'] === 'messagePoll') { + if(message.content.poll.type['@type'] === "pollTypeQuiz") { + if(message.content.poll.is_anonymous) { + return myself ? qsTr("sent an anonymous quiz", "myself") : qsTr("sent an anonymous quiz"); + } + return myself ? qsTr("sent a quiz", "myself") : qsTr("sent a quiz"); + } + if(message.content.poll.is_anonymous) { + return myself ? qsTr("sent an anonymous poll", "myself") : qsTr("sent an anonymous poll"); + } + return myself ? qsTr("sent a poll", "myself") : qsTr("sent a poll"); + } return qsTr("Unsupported message: %1").arg(message.content['@type'].substring(7)); } diff --git a/qml/pages/ChatPage.qml b/qml/pages/ChatPage.qml index 6350048..56c39da 100644 --- a/qml/pages/ChatPage.qml +++ b/qml/pages/ChatPage.qml @@ -220,7 +220,6 @@ Page { tdLibWrapper.openChat(chatInformation.id); break; case PageStatus.Active: - console.log("CHAT opendirectly?", chatPage.isInitialized) if (!chatPage.isInitialized) { chatModel.initialize(chatInformation); chatPage.isInitialized = true; @@ -537,6 +536,7 @@ Page { property bool containsAudio: (( display.content['@type'] === "messageVoiceNote" ) || ( display.content['@type'] === "messageAudio" )); property bool containsDocument: ( display.content['@type'] === "messageDocument" ) property bool containsLocation: ( display.content['@type'] === "messageLocation" || ( display.content['@type'] === "messageVenue" )) + property bool containsPoll: display.content['@type'] === "messagePoll" menu: ContextMenu { MenuItem { @@ -594,6 +594,7 @@ Page { audioPreviewLoader.active = messageListItem.containsAudio; documentPreviewLoader.active = messageListItem.containsDocument; locationPreviewLoader.active = messageListItem.containsLocation; + pollPreviewLoader.active = messageListItem.containsPoll; forwardedInformationLoader.active = messageListItem.isForwarded; } } @@ -921,6 +922,25 @@ Page { } } + Loader { + id: pollPreviewLoader + active: false + asynchronous: true + width: parent.width +// height: messageListItem.containsLocation ? (item ? item.height : (parent.width * 2 / 3)) : 0 + sourceComponent: Component { + id: pollPreviewComponent + PollPreview { + id: messageLocationPreview + width: parent.width + chatId: chatInformation.id + isOwnMessage: messageListItem.isOwnMessage + message: display + messageItem: messageListItem + } + } + } + Timer { id: messageDateUpdater interval: 60000 @@ -1160,6 +1180,14 @@ Page { } } } + IconButton { + visible: !chatPage.isPrivateChat + icon.source: "image://theme/icon-m-question" + onClicked: { + pageStack.push(Qt.resolvedUrl("../pages/PollCreationPage.qml"), { "chatId" : chatInformation.id, groupName: chatInformation.title}); + attachmentOptionsRow.visible = false; + } + } } Row { diff --git a/qml/pages/OverviewPage.qml b/qml/pages/OverviewPage.qml index f3533f5..cda297f 100644 --- a/qml/pages/OverviewPage.qml +++ b/qml/pages/OverviewPage.qml @@ -154,7 +154,6 @@ Page { } onChatReceived: { if(chat["@extra"] === "openDirectly") { - console.log("ON CHAT RECEIVED", JSON.stringify(chat, null, 2)); if (status !== PageStatus.Active) { pageStack.pop(pageStack.find( function(page){ return(page._depth === 0)} ), PageStackAction.Immediate); } diff --git a/qml/pages/PollCreationPage.qml b/qml/pages/PollCreationPage.qml new file mode 100644 index 0000000..4b00a5e --- /dev/null +++ b/qml/pages/PollCreationPage.qml @@ -0,0 +1,353 @@ +/* + Copyright (C) 2020 Sebastian J. Wolf and other contributors + + This file is part of Fernschreiber. + + Fernschreiber is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Fernschreiber is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Fernschreiber. If not, see . +*/ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import QtMultimedia 5.0 +import "../components" +import "../js/functions.js" as Functions +import "../js/twemoji.js" as Emoji + +Dialog { + id: pollCreationPage + allowedOrientations: Orientation.All + property string groupName + // poll request data start + property string chatId + property alias pollQuestion: questionTextArea.text + property ListModel options: ListModel { + ListElement { + text: "" + } + } + property alias anonymous: anonymousSwitch.checked + property int correctOption: -1 + property alias quiz: quizSwitch.checked + property alias multiple: multipleSwitch.checked + property string replyToMessageId: "0" + // poll request data end + + canAccept: validationErrors.length === 0 + onDone: { + } + onAcceptPendingChanged: { + if(acceptPending) { + + validate(); + + if(validationErrors.length > 0) { + validationErrorsVisible = true; + contentFlickable.scrollToTop(); + } + } + } + + property var validationErrorsVisible: false + property var validationErrors:[""] + + function validate() { + var errors = []; + if(pollQuestion.length === 0) { + errors.push(qsTr("You have to enter a question.")); + } else if(pollQuestion.length > 255) { + errors.push(qsTr("The question has to be shorter than 256 characters.")); + } + + if(options.count < 2 || options.count > 10) { + errors.push(qsTr("A poll requires 2-10 answers.")); + } else { + for(var i = 0; i < options.count; i += 1) { + var len = options.get(i).text.length + if(len < 1 || len > 100) { + errors.push(qsTr("All answers have to contain 1-100 characters.")); + break; + } + } + } + if(quiz && (correctOption < 0 || correctOption > options.count - 1)) { + errors.push(qsTr("To send a quiz, you have to specify the right answer.")); + } + if(errors.length === 0) { + validationErrorsVisible = false; + } + + validationErrors = errors; + } + function createNewOption() { + if(options.count < 10) { + pollCreationPage.options.append({text:""}); + focusLastOptionTimer.start(); + } + } + + signal focusOption(int focusIndex) + DialogHeader { + id: header + dialog: pollCreationPage + title: qsTr("Create a Poll", "Dialog Header") + } + Label { + id: subHeaderLabel + anchors { + verticalCenter: header.bottom + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + + color: Theme.secondaryHighlightColor + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: qsTr("in %1", "After dialog header… Create a Poll in [group name]").arg(Emoji.emojify(pollCreationPage.groupName, font.pixelSize)) + font.pixelSize: Theme.fontSizeSmall + } + + SilicaFlickable { + id: contentFlickable + clip: true + anchors { + top: subHeaderLabel.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + + contentHeight: contentColumn.height + + Column { + id: contentColumn + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + Item { + id: errorItem + width: parent.width - Theme.horizontalPageMargin * 2 + x: Theme.horizontalPageMargin + property bool shown: pollCreationPage.validationErrorsVisible && pollCreationPage.validationErrors.length > 0 + property int visibleHeight: errorContentColumn.height + height: pollCreationPage.validationErrorsVisible ? visibleHeight : 0 + clip: true; + opacity: pollCreationPage.validationErrorsVisible ? 1.0 : 0.0 + Behavior on opacity { PropertyAnimation {duration: 500; easing.type: Easing.InOutCubic}} + Behavior on height { PropertyAnimation {duration: 200; easing.type: Easing.InOutCubic}} + Rectangle { + color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) + anchors.fill: parent + radius: Theme.paddingLarge + IconButton { + icon.source: "image://theme/icon-m-close" + anchors { + right: parent.right + top: parent.top + } + onClicked: { + pollCreationPage.validationErrorsVisible = false + } + } + } + + + Column { + id: errorContentColumn + width: parent.width - Theme.paddingLarge * 2 - Theme.itemSizeSmall + spacing: Theme.paddingMedium + padding: Theme.paddingLarge + Repeater { + model: pollCreationPage.validationErrors + delegate: Label { + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + width: errorContentColumn.width + text: modelData + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + leftPadding: Theme.iconSizeSmall + Theme.paddingSmall + Icon { + highlighted: true + source: "image://theme/icon-s-high-importance" + y: Theme.paddingSmall / 2 + } + } + } + + } + } + + TextArea { + id: questionTextArea + width: parent.width + placeholderText: qsTr("Enter your question here") + property int charactersLeft: 255 - text.length + color: charactersLeft < 0 ? Theme.errorColor : Theme.highlightColor + label: qsTr("Question (%n1 characters left)", "", charactersLeft).arg(charactersLeft) + wrapMode: TextEdit.Wrap + onFocusChanged: { + validate(); + } + } + SectionHeader { + topPadding: 0 + text: qsTr("Answers", "Section header") + } + + Column { + id: optionsListView + width: parent.width - Theme.horizontalPageMargin * 2 + x: Theme.horizontalPageMargin + add: Transition { + NumberAnimation { properties: "opacity"; from: 0; to: 1; duration: 200; easing.type: Easing.InOutCubic } + NumberAnimation { properties: "height"; from: 0; to: ViewTransition.item.childrenRect.height; duration: 200; easing.type: Easing.InOutCubic } + } + move: Transition { + NumberAnimation { properties: "y"; duration: 200; easing.type: Easing.InOutCubic } + } + Behavior on height { PropertyAnimation {duration: 200; easing.type: Easing.InOutCubic}} + Repeater { + model: pollCreationPage.options + + delegate: Row { + width: parent.width + BackgroundItem { + id: answerCorrectBackgroundItem + width: enabled ? Theme.itemSizeSmall : 0 + contentItem.radius: height/2 + height: Theme.itemSizeSmall + property bool checked: pollCreationPage.correctOption === index + enabled: pollCreationPage.quiz + opacity: enabled ? (checked ? 1.0 : 0.5) : 0.0 + Behavior on opacity { PropertyAnimation {duration: 500; easing.type: Easing.InOutCubic}} + Behavior on width { PropertyAnimation {duration: 500; easing.type: Easing.InOutCubic}} + Icon { + source: "image://theme/icon-m-accept" + anchors.centerIn: parent + } + onClicked: { + pollCreationPage.correctOption = index + validate(); + } + + } + + TextField { + id: answerTextArea + textMargin: Theme.paddingSmall + width: answerCorrectBackgroundItem.enabled ? parent.width - Theme.itemSizeSmall * 2 : parent.width - Theme.itemSizeSmall + Behavior on width { PropertyAnimation {duration: 500; easing.type: Easing.InOutCubic}} + text: model.text + onTextChanged: { + pollCreationPage.options.setProperty(index, "text", text) + pollCreationPage.validate() + } + placeholderText: qsTr("Enter an answer here") + property int charactersLeft: 100 - text.length + color: charactersLeft < 0 ? Theme.errorColor : Theme.highlightColor + label: qsTr("Answer (%n1 characters left)", "", charactersLeft).arg(charactersLeft) + property bool hasNextOption: index < pollCreationPage.options.count - 1 + EnterKey.onClicked: { + if(hasNextOption) { + pollCreationPage.focusOption(index + 1); + } else if(pollCreationPage.options.count < 10) { + pollCreationPage.createNewOption(); + } else { + focus = false; + } + } + EnterKey.iconSource: hasNextOption ? "image://theme/icon-m-enter-next" : (pollCreationPage.options.count < 10 ? "image://theme/icon-m-add" : "image://theme/icon-m-enter-close") + + onFocusChanged: { + validate(); + } + } + Connections { + target: pollCreationPage + onFocusOption: { + if(focusIndex === index) answerTextArea.forceActiveFocus() + } + } + + IconButton { + icon.source: "image://theme/icon-m-remove" + onClicked: { + pollCreationPage.options.remove(index) + + validate(); + } + } + } + } + } + ButtonLayout { + Button { + enabled: pollCreationPage.options.count < 10 + text: qsTr("Add an answer") + onClicked: { + pollCreationPage.createNewOption(); + validate(); + } + } + } + Timer { + id: focusLastOptionTimer + interval: 20 + onTriggered: { + pollCreationPage.focusOption(pollCreationPage.options.count - 1); + } + } + + SectionHeader { + text: qsTr("Poll Options", "Section header") + } + TextSwitch { + id: anonymousSwitch + text: qsTr("Anonymous answers") + } + TextSwitch { + id: multipleSwitch + text: qsTr("Multiple answers allowed") + onCheckedChanged: { + if(checked) { + quizSwitch.checked = false + } + } + } + TextSwitch { + id: quizSwitch + text: qsTr("Quiz Mode") + onCheckedChanged: { + if(checked) { + multipleSwitch.checked = false + } + validate(); + } + description: qsTr("Quizzes have one correct answer. Participants can't revoke their responses.") + } + } + } + + onAccepted: { + var optionsArr = []; + for(var i = 0; i < options.count; i += 1) { + optionsArr.push(options.get(i).text); + } + + tdLibWrapper.sendPollMessage(chatId, pollQuestion, optionsArr, anonymous, quiz ? correctOption : -1, multiple, "0"); + + } + + +} diff --git a/qml/pages/PollResultsPage.qml b/qml/pages/PollResultsPage.qml new file mode 100644 index 0000000..764754c --- /dev/null +++ b/qml/pages/PollResultsPage.qml @@ -0,0 +1,328 @@ +/* + Copyright (C) 2020 Sebastian J. Wolf and other contributors + + This file is part of Fernschreiber. + + Fernschreiber is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Fernschreiber is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Fernschreiber. If not, see . +*/ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import QtMultimedia 5.0 +import "../components" +import "../js/functions.js" as Functions +import "../js/twemoji.js" as Emoji + +Page { + id: pollResultsPage + allowedOrientations: Orientation.All + property string chatId; + property var message: ({}); + + property string messageId: message.id; + + property var pollData: message.content.poll + + property var userInformation: tdLibWrapper.getUserInformation(message.sender_user_id) + + property bool isQuiz: pollData.type['@type'] === "pollTypeQuiz" + + property bool hasAnswered: { + return pollData.options.filter(function(option){ + return option.is_chosen + }).length > 0; + } + + property bool canAnswer: !hasAnswered && !pollData.is_closed + onCanAnswerChanged: { + if(canAnswer) { // vote removed from another client? + pageStack.pop() + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: pageHeader.height + contentColumn.height + + PageHeader { + id: pageHeader + title: pollResultsPage.isQuiz ? qsTr("Quiz Results") : qsTr("Poll Results") + description: qsTr("%L1 vote(s) total", "number of total votes", pollData.total_voter_count).arg(pollData.total_voter_count) + leftMargin: headerPictureThumbnail.width + Theme.paddingLarge + Theme.horizontalPageMargin + ProfileThumbnail { + id: headerPictureThumbnail + photoData: (typeof pollResultsPage.userInformation.profile_photo !== "undefined") ? pollResultsPage.userInformation.profile_photo.small : "" + replacementStringHint: Emoji.emojify(Functions.getUserName(pollResultsPage.userInformation), font.pixelSize) + width: visible ? Theme.itemSizeSmall : 0 + height: visible ? Theme.itemSizeSmall : 0 + anchors { + verticalCenter: pageHeader.verticalCenter + left: parent.left + leftMargin: Theme.horizontalPageMargin + } + } + } + Column { + id: contentColumn + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + right: parent.right + rightMargin: Theme.horizontalPageMargin + top: pageHeader.bottom + } + SectionHeader { + x: 0 + text: qsTr("Question", "section header") + } +// Label { +// width: parent.width +// font.pixelSize: Theme.fontSizeTiny +// wrapMode: Text.Wrap +// color: Theme.secondaryHighlightColor +// text: JSON.stringify(pollData, null, 2) +// } +// Label { +// width: parent.width +// font.pixelSize: Theme.fontSizeTiny +// wrapMode: Text.Wrap +// color: Theme.secondaryHighlightColor +// text: JSON.stringify(userInformation, null, 2) +// } + Label { + width: parent.width + wrapMode: Text.Wrap + color: Theme.secondaryHighlightColor + text: Emoji.emojify(pollData.question, font.pixelSize) + } + + Column { + id: resultsColumn + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + SectionHeader { + x: 0 + text: qsTr("Results", "section header") + } + Repeater { + model: pollData.options + delegate: Item { + id: optionDelegate + width: resultsColumn.width + height: displayOptionLabel.height + displayOptionStatistics.height + displayOptionUsers.height + Theme.paddingLarge + property ListModel users: ListModel {} + property string usersResponseIdentifierString: "pollResults."+pollResultsPage.chatId+"."+pollResultsPage.messageId+"."+index + function loadUsers() { + if(users.count < modelData.voter_count) { + tdLibWrapper.getPollVoters(pollResultsPage.chatId, pollResultsPage.messageId, index, 50, users.length, usersResponseIdentifierString) + } + } + Component.onCompleted: { +// loadUsers() + loadUsersTimer.start() + } + Timer { + id: loadUsersTimer + interval: index * 80 + onTriggered: { + optionDelegate.loadUsers(); + } + } + + Connections { + target: tdLibWrapper + onUsersReceived: { + if(extra === optionDelegate.usersResponseIdentifierString) { + for(var i = 0; i < userIds.length; i += 1) { + optionDelegate.users.append({id: userIds[i], user:tdLibWrapper.getUserInformation(userIds[i])}); + console.log("APPEND USER", JSON.stringify({id: userIds[i], user:tdLibWrapper.getUserInformation(userIds[i])})); + } + loadUsersTimer.start(); + } + } + } + Rectangle { + id: displayOptionChosenMarker + height: parent.height + width: Theme.horizontalPageMargin/2 + color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) + visible: modelData.is_chosen + x: -width + } + OpacityRampEffect { + sourceItem: displayOptionChosenMarker + direction: OpacityRamp.LeftToRight + } + Column { + id: iconsColumn + width: pollResultsPage.isQuiz ?Theme.iconSizeSmall + Theme.paddingMedium : Theme.paddingMedium + height: displayOptionLabel.height + displayOptionStatistics.height + anchors { + left: parent.left +// verticalCenter: parent.verticalCenter + } + + Icon { + highlighted: true + property bool isRight: pollResultsPage.isQuiz && pollData.type.correct_option_id === index + source: "image://theme/icon-s-accept" + visible: isRight + anchors.verticalCenter: parent.verticalCenter + } + } + + Label { + id: displayOptionLabel + text: Emoji.emojify(modelData.text, Theme.fontSizeMedium) + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + anchors { + left: iconsColumn.right + top: parent.top + right: parent.right + } + color: Theme.highlightColor + } + Item { + id: displayOptionStatistics + height: optionVoterPercentage.height + optionVoterPercentageBar.height + anchors { + top: displayOptionLabel.bottom + left: iconsColumn.right + right: parent.right + } + + Label { + id: optionVoterCount + font.pixelSize: Theme.fontSizeTiny + text: modelData.is_chosen ? qsTr("%L1 vote(s) including yours", "number of votes for option", modelData.voter_count).arg(modelData.voter_count) : qsTr("%L1 vote(s)", "number of votes for option", modelData.voter_count).arg(modelData.voter_count) + anchors { + left: parent.left + right: parent.horizontalCenter + rightMargin: Theme.paddingSmall + } + color: Theme.secondaryHighlightColor + } + Label { + id: optionVoterPercentage + font.pixelSize: Theme.fontSizeTiny + text: qsTr("%L1\%", "% of votes for option").arg(modelData.vote_percentage) + horizontalAlignment: Text.AlignRight + anchors { + right: parent.right + left: parent.horizontalCenter + leftMargin: Theme.paddingSmall + } + color: Theme.secondaryHighlightColor + } + Rectangle { + id: optionVoterPercentageBar + height: Theme.paddingSmall + width: parent.width + + color: Theme.rgba(Theme.secondaryHighlightColor, 0.3) + radius: height/2 + anchors { + left: parent.left + bottom: parent.bottom + } + + Rectangle { + height: parent.height + color: Theme.highlightColor + radius: height/2 + width: parent.width * modelData.vote_percentage * 0.01 + } + } + } + + + // users voted for this: + Flow { + id: displayOptionUsers + anchors.top: displayOptionStatistics.bottom + width: parent.width + visible: optionDelegate.users.count > 0 + topPadding: Theme.paddingLarge + spacing: Theme.paddingMedium + leftPadding: iconsColumn.width + property int itemHeight: Theme.itemSizeExtraSmall / 2 + Item { + height: displayOptionUsers.itemHeight + width: chosenByUserText.width + Label { + id: chosenByUserText + font.pixelSize: Theme.fontSizeTiny + text: qsTr("Chosen by:", "This answer has been chosen by the following users") + width: contentWidth + anchors.centerIn: parent + color: Theme.secondaryHighlightColor + } + } + Repeater { + model: optionDelegate.users + delegate: + Item { + id: chosenByUserItem + width: chosenByUserPictureThumbnail.width + chosenByUserLabel.width + Theme.paddingSmall + height: displayOptionUsers.itemHeight + + ProfileThumbnail { + id: chosenByUserPictureThumbnail + photoData: (typeof model.user.profile_photo !== "undefined") ? model.user.profile_photo.small : "" + replacementStringHint: chosenByUserLabel.text + width: visible ? parent.height : 0 + height: width + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + } + + Label { + id: chosenByUserLabel + font.pixelSize: Theme.fontSizeSmall + text: Emoji.emojify(Functions.getUserName(model.user), font.pixelSize) + width: contentWidth + height: contentHeight + color: Theme.highlightColor + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + } + } + + + } + } + + } + } + + } + + } + } + + Connections { + target: tdLibWrapper + onMessageContentUpdated: { + if(chatId === pollResultsPage.chatId && messageId === pollResultsPage.messageId) { + pollResultsPage.pollData = newContent.poll; + } + } + } + +} diff --git a/src/fernschreiberutils.cpp b/src/fernschreiberutils.cpp index b52122e..99c05e6 100644 --- a/src/fernschreiberutils.cpp +++ b/src/fernschreiberutils.cpp @@ -54,5 +54,11 @@ QString FernschreiberUtils::getMessageShortText(const QVariantMap &messageConten if (contentType == "messageChatChangeTitle") { return myself ? tr("changed the chat title", "myself") : tr("changed the chat title"); } + if (contentType == "messagePoll") { + if(messageContent.value("poll").toMap().value("type").toMap().value("@type").toString() == "pollTypeQuiz") { + return myself ? tr("sent a quiz", "myself") : tr("sent a quiz"); + } + return myself ? tr("sent a poll", "myself") : tr("sent a poll"); + } return tr("Unsupported message: %1").arg(contentType.mid(7)); } diff --git a/src/tdlibreceiver.cpp b/src/tdlibreceiver.cpp index cb80fe1..134e023 100644 --- a/src/tdlibreceiver.cpp +++ b/src/tdlibreceiver.cpp @@ -116,6 +116,7 @@ TDLibReceiver::TDLibReceiver(void *tdLibClient, QObject *parent) : QThread(paren handlers.insert("userProfilePhotos", &TDLibReceiver::processUserProfilePhotos); handlers.insert("updateChatPermissions", &TDLibReceiver::processUpdateChatPermissions); handlers.insert("updateChatTitle", &TDLibReceiver::processUpdateChatTitle); + handlers.insert("users", &TDLibReceiver::processUsers); } void TDLibReceiver::setActive(const bool &active) @@ -494,3 +495,9 @@ void TDLibReceiver::processUpdateChatTitle(const QVariantMap &receivedInformatio LOG("Received UpdateChatTitle"); emit chatTitleUpdated(receivedInformation.value("chat_id").toString(), receivedInformation.value("title").toString()); } + +void TDLibReceiver::processUsers(const QVariantMap &receivedInformation) +{ + LOG("Received Users"); + emit usersReceived(receivedInformation.value(EXTRA).toString(), receivedInformation.value("user_ids").toList(), receivedInformation.value("total_count").toInt()); +} diff --git a/src/tdlibreceiver.h b/src/tdlibreceiver.h index a8eb27a..663ce87 100644 --- a/src/tdlibreceiver.h +++ b/src/tdlibreceiver.h @@ -80,6 +80,7 @@ signals: void userProfilePhotos(const QString &extra, const QVariantList &photos, const int &totalPhotos); void chatPermissionsUpdated(const QString &chatId, const QVariantMap &chatPermissions); void chatTitleUpdated(const QString &chatId, const QString &title); + void usersReceived(const QString &extra, const QVariantList &userIds, const int &totalUsers); private: typedef void (TDLibReceiver::*Handler)(const QVariantMap &); @@ -134,6 +135,7 @@ private: void processUserProfilePhotos(const QVariantMap &receivedInformation); void processUpdateChatPermissions(const QVariantMap &receivedInformation); void processUpdateChatTitle(const QVariantMap &receivedInformation); + void processUsers(const QVariantMap &receivedInformation); }; #endif // TDLIBRECEIVER_H diff --git a/src/tdlibwrapper.cpp b/src/tdlibwrapper.cpp index c1d6ffc..eb3dec1 100644 --- a/src/tdlibwrapper.cpp +++ b/src/tdlibwrapper.cpp @@ -104,8 +104,8 @@ TDLibWrapper::TDLibWrapper(QObject *parent) : QObject(parent) connect(this->tdLibReceiver, SIGNAL(userProfilePhotos(QString, QVariantList, int)), this, SLOT(handleUserProfilePhotos(QString, QVariantList, int))); connect(this->tdLibReceiver, SIGNAL(userProfilePhotos(QString, QVariantList, int)), this, SLOT(handleUserProfilePhotos(QString, QVariantList, int))); connect(this->tdLibReceiver, SIGNAL(chatPermissionsUpdated(QString, QVariantMap)), this, SLOT(handleChatPermissionsUpdated(QString, QVariantMap))); - connect(this->tdLibReceiver, SIGNAL(chatTitleUpdated(QString, QString)), this, SLOT(handleChatTitleUpdated(QString, QString))); + connect(this->tdLibReceiver, SIGNAL(usersReceived(QString, QVariantList, int)), this, SLOT(handleUsersReceived(QString, QVariantList, int))); connect(&emojiSearchWorker, SIGNAL(searchCompleted(QString, QVariantList)), this, SLOT(handleEmojiSearchCompleted(QString, QVariantList))); @@ -384,6 +384,36 @@ void TDLibWrapper::sendStickerMessage(const QString &chatId, const QString &file this->sendRequest(requestObject); } +void TDLibWrapper::sendPollMessage(const QString &chatId, const QString &question, const QVariantList &options, const bool &anonymous, const int &correctOption, const bool &multiple, const QString &replyToMessageId) +{ + LOG("Sending poll message" << chatId << question << replyToMessageId); + QVariantMap requestObject; + requestObject.insert(_TYPE, "sendMessage"); + requestObject.insert("chat_id", chatId); + if (replyToMessageId != "0") { + requestObject.insert("reply_to_message_id", replyToMessageId); + } + QVariantMap inputMessageContent; + inputMessageContent.insert(_TYPE, "inputMessagePoll"); + + QVariantMap pollType; + if(correctOption > -1) { + pollType.insert(_TYPE, "pollTypeQuiz"); + pollType.insert("correct_option_id", correctOption); + } else { + pollType.insert(_TYPE, "pollTypeRegular"); + pollType.insert("allow_multiple_answers", multiple); + } + + inputMessageContent.insert("type", pollType); + inputMessageContent.insert("question", question); + inputMessageContent.insert("options", options); + inputMessageContent.insert("is_anonymous", anonymous); + + requestObject.insert("input_message_content", inputMessageContent); + this->sendRequest(requestObject); +} + void TDLibWrapper::getMessage(const QString &chatId, const QString &messageId) { LOG("Retrieving message" << chatId << messageId); @@ -623,6 +653,41 @@ void TDLibWrapper::toggleSupergroupIsAllHistoryAvailable(const QString &groupId, this->sendRequest(requestObject); } +void TDLibWrapper::setPollAnswer(const QString &chatId, const qlonglong &messageId, QVariantList optionIds) +{ + LOG("Setting Poll Answer"); + QVariantMap requestObject; + requestObject.insert(_TYPE, "setPollAnswer"); + requestObject.insert("chat_id", chatId); + requestObject.insert("message_id", messageId); + requestObject.insert("option_ids", optionIds); + this->sendRequest(requestObject); +} + +void TDLibWrapper::stopPoll(const QString &chatId, const qlonglong &messageId) +{ + LOG("Stopping Poll"); + QVariantMap requestObject; + requestObject.insert(_TYPE, "stopPoll"); + requestObject.insert("chat_id", chatId); + requestObject.insert("message_id", messageId); + this->sendRequest(requestObject); +} + +void TDLibWrapper::getPollVoters(const QString &chatId, const qlonglong &messageId, const int &optionId, const int &limit, const int &offset, const QString &extra) +{ + LOG("Retrieving Poll Voters"); + QVariantMap requestObject; + requestObject.insert(_TYPE, "getPollVoters"); + requestObject.insert(_EXTRA, extra); + requestObject.insert("chat_id", chatId); + requestObject.insert("message_id", messageId); + requestObject.insert("option_id", optionId); + requestObject.insert("offset", offset); + requestObject.insert("limit", limit); //max 50 + this->sendRequest(requestObject); +} + void TDLibWrapper::searchEmoji(const QString &queryString) { LOG("Searching emoji" << queryString); @@ -1049,16 +1114,21 @@ void TDLibWrapper::handleUserProfilePhotos(const QString &extra, const QVariantL emit this->userProfilePhotosReceived(extra, photos, totalPhotos); } -void TDLibWrapper::handleChatPermissionsUpdated(const QString &chatId, const QVariantMap permissions) +void TDLibWrapper::handleChatPermissionsUpdated(const QString &chatId, const QVariantMap &permissions) { emit this->chatPermissionsUpdated(chatId, permissions); } -void TDLibWrapper::handleChatTitleUpdated(const QString &chatId, const QString title) +void TDLibWrapper::handleChatTitleUpdated(const QString &chatId, const QString &title) { emit this->chatTitleUpdated(chatId, title); } +void TDLibWrapper::handleUsersReceived(const QString &extra, const QVariantList &userIds, const int &totalUsers) +{ + emit this->usersReceived(extra, userIds, totalUsers); +} + void TDLibWrapper::setInitialParameters() { LOG("Sending initial parameters to TD Lib"); diff --git a/src/tdlibwrapper.h b/src/tdlibwrapper.h index d440b04..5603ba3 100644 --- a/src/tdlibwrapper.h +++ b/src/tdlibwrapper.h @@ -121,6 +121,7 @@ public: Q_INVOKABLE void sendVideoMessage(const QString &chatId, const QString &filePath, const QString &message, const QString &replyToMessageId = "0"); Q_INVOKABLE void sendDocumentMessage(const QString &chatId, const QString &filePath, const QString &message, const QString &replyToMessageId = "0"); Q_INVOKABLE void sendStickerMessage(const QString &chatId, const QString &fileId, const QString &replyToMessageId = "0"); + Q_INVOKABLE void sendPollMessage(const QString &chatId, const QString &question, const QVariantList &options, const bool &anonymous, const int &correctOption, const bool &multiple, const QString &replyToMessageId = "0"); Q_INVOKABLE void getMessage(const QString &chatId, const QString &messageId); Q_INVOKABLE void setOptionInteger(const QString &optionName, const int &optionValue); Q_INVOKABLE void setChatNotificationSettings(const QString &chatId, const QVariantMap ¬ificationSettings); @@ -142,6 +143,9 @@ public: Q_INVOKABLE void setChatTitle(const QString &chatId, const QString &title); Q_INVOKABLE void setBio(const QString &bio); Q_INVOKABLE void toggleSupergroupIsAllHistoryAvailable(const QString &groupId, const bool &isAllHistoryAvailable); + Q_INVOKABLE void setPollAnswer(const QString &chatId, const qlonglong &messageId, QVariantList optionIds); + Q_INVOKABLE void stopPoll(const QString &chatId, const qlonglong &messageId); + Q_INVOKABLE void getPollVoters(const QString &chatId, const qlonglong &messageId, const int &optionId, const int &limit, const int &offset, const QString &extra); // Others (candidates for extraction ;)) Q_INVOKABLE void searchEmoji(const QString &queryString); @@ -198,6 +202,7 @@ signals: void userProfilePhotosReceived(const QString &extra, const QVariantList &photos, const int &totalPhotos); void chatPermissionsUpdated(const QString &chatId, const QVariantMap &permissions); void chatTitleUpdated(const QString &chatId, const QString &title); + void usersReceived(const QString &extra, const QVariantList &userIds, const int &totalUsers); public slots: void handleVersionDetected(const QString &version); @@ -243,8 +248,11 @@ public slots: void handleSupergroupFullInfo(const QString &groupId, const QVariantMap &groupFullInfo); void handleSupergroupFullInfoUpdated(const QString &groupId, const QVariantMap &groupFullInfo); void handleUserProfilePhotos(const QString &extra, const QVariantList &photos, const int &totalPhotos); - void handleChatPermissionsUpdated(const QString &chatId, const QVariantMap permissions); - void handleChatTitleUpdated(const QString &chatId, const QString title); + void handleChatPermissionsUpdated(const QString &chatId, const QVariantMap &permissions); + void handleChatTitleUpdated(const QString &chatId, const QString &title); + void handleUsersReceived(const QString &extra, const QVariantList &userIds, const int &totalUsers); + + private: void setInitialParameters(); diff --git a/translations/harbour-fernschreiber-de.ts b/translations/harbour-fernschreiber-de.ts index 93e44cf..eaae40c 100644 --- a/translations/harbour-fernschreiber-de.ts +++ b/translations/harbour-fernschreiber-de.ts @@ -542,6 +542,24 @@ changed the chat title hat den Chattitel geändert + + sent a poll + myself + haben eine Umfrage geschickt + + + sent a poll + hat eine Umfrage geschickt + + + sent a quiz + myself + haben ein Quiz geschickt + + + sent a quiz + hat ein Quiz geschickt + ImagePage @@ -685,6 +703,178 @@ Sie haben noch keine Chats. + + PollCreationPage + + All answers have to contain 1-100 characters. + Alle Antworten müssen 1-100 Zeichen beinhalten. + + + To send a quiz, you have to specify the right answer. + Um ein Quiz zu senden, müssen Sie die richtige Antwort auswählen. + + + You have to enter a question. + Sie müssen eine Frage eingeben. + + + The question has to be shorter than 256 characters. + Die Frage muss kürzer als 256 Zeichen sein. + + + A poll requires 2-10 answers. + Eine Umfrage benötigt 2-10 Antworten. + + + Create a Poll + Dialog Header + Erstellen Sie eine Umfrage + + + in %1 + After dialog header… Create a Poll in [group name] + in %1 + + + Enter your question here + Geben Sie Ihre Frage ein + + + Question (%n1 characters left) + + Frage (%n1 Zeichen übrig) + Frage (%n1 Zeichen übrig) + + + + Answers + Section header + Antworten + + + Enter an answer here + Geben Sie eine Antwort ein + + + Answer (%n1 characters left) + + Antwort (%n1 Zeichen übrig) + Antwort (%n1 Zeichen übrig) + + + + Add an answer + Antwort hinzufügen + + + Poll Options + Section header + Umfrageoptionen + + + Anonymous answers + Anonyme Antworten + + + Multiple answers allowed + Mehrere Antworten erlaubt + + + Quiz Mode + Quizmodus + + + Quizzes have one correct answer. Participants can't revoke their responses. + Quizze haben eine korrekte Antwort. Teilnehmer können ihre Antwort nicht zurückziehen. + + + + PollPreview + + %L1% + % of votes for option + %L1% + + + Final Result: + Endergebnis: + + + Multiple Answers are allowed. + Mehrfachauswahl ist erlaubt. + + + %L1 vote(s) total + number of total votes + + %L1 Stimme insgesamt + %L1 Stimmen insgesamt + + + + Close Poll + Umfrage beenden + + + Reset Answer + Antwort zurückziehen + + + + PollResultsPage + + Quiz Results + Quizergebnis + + + Poll Results + Umfrageergebnis + + + %L1 vote(s) total + number of total votes + + %L1 Stimme insgesamt + %L1 Stimmen insgesamt + + + + Question + section header + Frage + + + Results + section header + Ergebnis + + + %L1 vote(s) + number of votes for option + + %L1 Antwort + %L1 Antworten + + + + %L1% + % of votes for option + %L1% + + + Chosen by: + This answer has been chosen by the following users + Gewählt von: + + + %L1 vote(s) including yours + number of votes for option + + %L1 Antwort inklusive Ihrer + %L1 Antworten inklusive Ihrer + + + SettingsPage @@ -964,5 +1154,41 @@ changed the chat title to %1 hat den Chattitel zu %1 geändert + + sent a poll + myself + haben eine Umfrage gesendet + + + sent a poll + hat eine Umfrage gesendet + + + sent an anonymous quiz + myself + haben ein anonymes Quiz gesendet + + + sent an anonymous quiz + hat ein anonymes Quiz gesendet + + + sent a quiz + myself + haben ein Quiz gesendet + + + sent a quiz + hat ein Quiz gesendet + + + sent an anonymous poll + myself + haben eine anonyme Umfrage gesendet + + + sent an anonymous poll + hat eine anonyme Umfrage gesendet + diff --git a/translations/harbour-fernschreiber-es.ts b/translations/harbour-fernschreiber-es.ts index c85a3da..69add33 100644 --- a/translations/harbour-fernschreiber-es.ts +++ b/translations/harbour-fernschreiber-es.ts @@ -542,6 +542,24 @@ changed the chat title + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,172 @@ No hay todavía ninguna charla . + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + SettingsPage @@ -964,5 +1148,41 @@ changed the chat title to %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + + diff --git a/translations/harbour-fernschreiber-fi.ts b/translations/harbour-fernschreiber-fi.ts index 6645016..4b0ac72 100644 --- a/translations/harbour-fernschreiber-fi.ts +++ b/translations/harbour-fernschreiber-fi.ts @@ -542,6 +542,24 @@ changed the chat title + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,178 @@ Sinulla ei ole vielä keskusteluja + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + + SettingsPage @@ -964,5 +1154,41 @@ changed the chat title to %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + + diff --git a/translations/harbour-fernschreiber-hu.ts b/translations/harbour-fernschreiber-hu.ts index 5955b06..42c4433 100644 --- a/translations/harbour-fernschreiber-hu.ts +++ b/translations/harbour-fernschreiber-hu.ts @@ -542,6 +542,24 @@ changed the chat title + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,172 @@ + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + SettingsPage @@ -964,5 +1148,41 @@ changed the chat title to %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + + diff --git a/translations/harbour-fernschreiber-it.ts b/translations/harbour-fernschreiber-it.ts index 775556b..839bffb 100644 --- a/translations/harbour-fernschreiber-it.ts +++ b/translations/harbour-fernschreiber-it.ts @@ -542,6 +542,24 @@ changed the chat title Ha modificato il titolo della chat + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,178 @@ Non hai nessuna chat. + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + + SettingsPage @@ -964,5 +1154,41 @@ changed the chat title to %1 ha modificato il titolo della chat in %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + + diff --git a/translations/harbour-fernschreiber-pl.ts b/translations/harbour-fernschreiber-pl.ts index 83b1565..0ad2981 100644 --- a/translations/harbour-fernschreiber-pl.ts +++ b/translations/harbour-fernschreiber-pl.ts @@ -542,6 +542,24 @@ changed the chat title + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,184 @@ Nie masz jeszcze żadnych czatów. + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + + + SettingsPage @@ -964,5 +1160,41 @@ changed the chat title to %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + + diff --git a/translations/harbour-fernschreiber-ru.ts b/translations/harbour-fernschreiber-ru.ts index 263552a..69bf463 100644 --- a/translations/harbour-fernschreiber-ru.ts +++ b/translations/harbour-fernschreiber-ru.ts @@ -542,6 +542,24 @@ changed the chat title + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,184 @@ Тут пока ничего нет + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + + + SettingsPage @@ -964,5 +1160,41 @@ changed the chat title to %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + + diff --git a/translations/harbour-fernschreiber-zh_CN.ts b/translations/harbour-fernschreiber-zh_CN.ts index a18810d..7ef0098 100644 --- a/translations/harbour-fernschreiber-zh_CN.ts +++ b/translations/harbour-fernschreiber-zh_CN.ts @@ -542,6 +542,24 @@ changed the chat title + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,172 @@ 你尚无任何对话。 + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + SettingsPage @@ -964,5 +1148,41 @@ changed the chat title to %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + + diff --git a/translations/harbour-fernschreiber.ts b/translations/harbour-fernschreiber.ts index da0dd42..48bfa9e 100644 --- a/translations/harbour-fernschreiber.ts +++ b/translations/harbour-fernschreiber.ts @@ -542,6 +542,24 @@ changed the chat title + + sent a poll + myself + + + + sent a poll + + + + sent a quiz + myself + + + + sent a quiz + + ImagePage @@ -685,6 +703,172 @@ + + PollCreationPage + + All answers have to contain 1-100 characters. + + + + To send a quiz, you have to specify the right answer. + + + + You have to enter a question. + + + + The question has to be shorter than 256 characters. + + + + A poll requires 2-10 answers. + + + + Create a Poll + Dialog Header + + + + in %1 + After dialog header… Create a Poll in [group name] + + + + Enter your question here + + + + Question (%n1 characters left) + + + + + + Answers + Section header + + + + Enter an answer here + + + + Answer (%n1 characters left) + + + + + + Add an answer + + + + Poll Options + Section header + + + + Anonymous answers + + + + Multiple answers allowed + + + + Quiz Mode + + + + Quizzes have one correct answer. Participants can't revoke their responses. + + + + + PollPreview + + %L1% + % of votes for option + + + + Final Result: + + + + Multiple Answers are allowed. + + + + %L1 vote(s) total + number of total votes + + + + + + Close Poll + + + + Reset Answer + + + + + PollResultsPage + + Quiz Results + + + + Poll Results + + + + %L1 vote(s) total + number of total votes + + + + + + Question + section header + + + + Results + section header + + + + %L1 vote(s) + number of votes for option + + + + + + %L1% + % of votes for option + + + + Chosen by: + This answer has been chosen by the following users + + + + %L1 vote(s) including yours + number of votes for option + + + + + SettingsPage @@ -964,5 +1148,41 @@ changed the chat title to %1 + + sent a poll + myself + + + + sent a poll + + + + sent an anonymous quiz + myself + + + + sent an anonymous quiz + + + + sent a quiz + myself + + + + sent a quiz + + + + sent an anonymous poll + myself + + + + sent an anonymous poll + +