diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index 2f4d128d051..1124dd1e64f 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -410,6 +410,7 @@ public function parseMessage(Message $chatMessage): void { } } elseif ($message === 'object_shared') { $parsedParameters['object'] = $parameters['metaData']; + $parsedParameters['object']['id'] = (string) $parsedParameters['object']['id']; $parsedMessage = '{object}'; if (isset($parsedParameters['object']['type']) @@ -494,12 +495,14 @@ public function parseMessage(Message $chatMessage): void { } } elseif ($message === 'poll_closed') { $parsedParameters['poll'] = $parameters['poll']; + $parsedParameters['poll']['id'] = (string) $parsedParameters['poll']['id']; $parsedMessage = $this->l->t('{actor} closed the poll {poll}'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You closed the poll {poll}'); } } elseif ($message === 'poll_voted') { $parsedParameters['poll'] = $parameters['poll']; + $parsedParameters['poll']['id'] = (string) $parsedParameters['poll']['id']; $parsedMessage = $this->l->t('Someone voted on the poll {poll}'); unset($parsedParameters['actor']); } else { diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index 5e55dac4332..1ea27ae873e 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -55,6 +55,14 @@ the main body of the message as well as a quote. +
+ + + +
@@ -220,6 +228,7 @@ export default { NcEmojiPicker, EmoticonOutline, NcPopover, + Poll, }, mixins: [ @@ -457,6 +466,10 @@ export default { && !this.isInCall }, + showResultsButton() { + return this.systemMessage === 'poll_closed' + }, + isSingleEmoji() { const regex = emojiRegex() let match @@ -503,7 +516,7 @@ export default { component: Location, props: this.messageParameters[p], } - } else if (type === 'talk-poll') { + } else if (type === 'talk-poll' && this.systemMessage !== 'poll_closed') { const props = Object.assign({}, this.messageParameters[p]) // Add the token to the component props props.token = this.token diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Poll.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Poll.vue index 38d7a541629..3b94890d011 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Poll.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Poll.vue @@ -21,11 +21,13 @@ @@ -162,7 +186,7 @@ export default { }, id: { - type: Number, + type: String, required: true, }, @@ -170,6 +194,11 @@ export default { type: String, required: true, }, + + showAsButton: { + type: Boolean, + default: false, + }, }, data() { @@ -207,6 +236,19 @@ export default { }, selfHasVoted() { + if (this.pollLoaded) { + if (typeof this.votedSelf === 'object') { + return this.votedSelf.length > 0 + } else { + return !!this.votedSelf + } + } else { + return undefined + } + }, + + // The actual vote of the user as returned from the server + votedSelf() { return this.pollLoaded ? this.poll.votedSelf : undefined }, @@ -214,6 +256,10 @@ export default { return this.pollLoaded ? this.poll.resultMode : undefined }, + pollIsPublic() { + return this.resultMode === 0 + }, + status() { return this.pollLoaded ? this.poll.status : undefined }, @@ -222,12 +268,24 @@ export default { return this.status === 0 }, + pollIsClosed() { + return this.status === 1 + }, + checkboxRadioSwitchType() { - return this.poll.maxVotes === 0 ? 'checkbox' : 'radio' + if (this.pollLoaded) { + return this.poll.maxVotes === 0 ? 'checkbox' : 'radio' + } else { + return undefined + } }, canSubmitVote() { - return this.vote !== undefined && this.vote !== '' && this.vote !== [] + if (typeof this.vote === 'object') { + return this.vote.length > 0 + } else { + return this.vote !== undefined && this.vote !== '' + } }, getVotePercentage() { @@ -235,7 +293,7 @@ export default { if (this.pollVotes[`option-${index}`] === undefined) { return 0 } - return this.pollVotes[`option-${index}`] / this.votersNumber * 100 + return parseInt(this.pollVotes[`option-${index}`] / this.votersNumber * 100) } }, @@ -269,19 +327,38 @@ export default { return [PARTICIPANT.TYPE.OWNER, PARTICIPANT.TYPE.MODERATOR, PARTICIPANT.TYPE.GUEST_MODERATOR].indexOf(this.participantType) !== -1 }, - canClosePoll() { - return this.currentUserIsPollCreator || this.currentUserIsModerator + canEndPoll() { + return (this.currentUserIsPollCreator || this.currentUserIsModerator) && this.pollIsOpen + }, + + pollFooterText() { + if (this.pollIsOpen) { + return this.selfHasVoted ? t('spreed', 'Poll ・ You voted') : t('spreed', 'Poll ・ Click to vote') + } else if (this.pollIsClosed) { + return t('spreed', 'Poll ・ Closed') + } + return '' }, }, watch: { pollLoaded() { - this.setComponentData() + this.setVoteData() + }, + + modalPage(value) { + if (value === 'voting') { + this.setVoteData() + } }, }, + mounted() { + this.setVoteData() + }, + methods: { getPollData() { if (!this.pollLoaded) { @@ -292,13 +369,27 @@ export default { } }, - setComponentData() { + setVoteData() { if (this.checkboxRadioSwitchType === 'radio') { this.vote = '' + if (this.selfHasVoted) { + this.vote = this.votedSelf[0].toString() + } } else { this.vote = [] + if (this.selfHasVoted) { + this.vote = this.votedSelf.map(element => element.toString()) + } } - this.pollIsOpen ? this.modalPage = 'voting' : this.modalPage = 'results' + }, + + openPoll() { + if (this.selfHasVoted || this.pollIsClosed) { + this.modalPage = 'results' + } else { + this.modalPage = 'voting' + } + this.showModal = true }, dismissModal() { @@ -321,11 +412,20 @@ export default { this.modalPage = 'results' }, - closePoll() { - this.$store.dispatch('closePoll', { + endPoll() { + this.$store.dispatch('endPoll', { token: this.token, pollId: this.id, }) + this.modalPage = 'results' + }, + + selfHasVotedOption(index) { + if (this.votedSelf.includes(index)) { + return true + } else { + return false + } }, }, } @@ -354,10 +454,8 @@ export default { gap: 8px; white-space: normal; align-items: flex-start; - position: sticky; top: 0; padding: 0 0 8px 0; - background-color: var(--color-main-background); word-wrap: anywhere; padding-top: 20px; @@ -374,7 +472,7 @@ export default { &__modal { position: relative; - padding: 0 20px; + padding: 20px 20px 0 20px; } &__modal-title { @@ -393,7 +491,7 @@ export default { bottom: 0; display: flex; justify-content: center; - gap: 4px; + gap: 8px; padding: 12px 0 0 0; background-color: var(--color-main-background); padding-bottom: 20px; @@ -415,7 +513,13 @@ export default { .results__option { display: flex; flex-direction: column; - gap: 8px; + &-subtitle { + color: var(--color-text-maxcontrast); + } + + &-progress { + margin-top: 4px; + } } .results__option-title { @@ -428,6 +532,12 @@ export default { } } +.poll-closed { + display: flex; + justify-content: center; + margin-top: 4px; +} + // Upstream ::v-deep .checkbox-radio-switch { &__label { diff --git a/src/components/NewMessageForm/SimplePollsEditor/SimplePollsEditor.vue b/src/components/NewMessageForm/SimplePollsEditor/SimplePollsEditor.vue index 07fa4a580cc..d63aa48f9d1 100644 --- a/src/components/NewMessageForm/SimplePollsEditor/SimplePollsEditor.vue +++ b/src/components/NewMessageForm/SimplePollsEditor/SimplePollsEditor.vue @@ -37,6 +37,7 @@

{ + const indexOfNewPollOption = this.pollOptions.length - 1 + const refOfNewPollOption = `pollOption${indexOfNewPollOption}` + this.$refs[refOfNewPollOption][0].$el.querySelector('.input-field__input').focus() + }) }, async createPoll() { diff --git a/src/services/pollService.js b/src/services/pollService.js index ca973afc37f..f218daee0f9 100644 --- a/src/services/pollService.js +++ b/src/services/pollService.js @@ -68,13 +68,13 @@ const pollService = { }, /** - * Closes the poll + * Ends the poll * * @param {string} token The conversation token * @param {number} pollId ID of the poll * @return {object} The poll object */ - async closePoll(token, pollId) { + async endPoll(token, pollId) { return axios.delete(generateOcsUrl('apps/spreed/api/v1/poll/{token}/{pollId}', { token, pollId })) }, } diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js index 810278f7834..3ddbfa1acd1 100644 --- a/src/store/conversationsStore.js +++ b/src/store/conversationsStore.js @@ -422,7 +422,6 @@ const actions = { || lastMessage.actorId === 'changelog') && lastMessage.systemMessage !== 'reaction' && lastMessage.systemMessage !== 'poll_voted' - && lastMessage.systemMessage !== 'poll_closed' && lastMessage.systemMessage !== 'reaction_deleted' && lastMessage.systemMessage !== 'reaction_revoked' && lastMessage.systemMessage !== 'message_deleted' diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js index ccc7d1b6f4a..b19b0054501 100644 --- a/src/store/messagesStore.js +++ b/src/store/messagesStore.js @@ -146,7 +146,6 @@ const getters = { || message.systemMessage === 'reaction_deleted' || message.systemMessage === 'reaction_revoked' || message.systemMessage === 'poll_voted' - || message.systemMessage === 'poll_closed' ) { return false } else { @@ -423,6 +422,13 @@ const actions = { }) } + if (message.systemMessage === 'poll_closed') { + context.dispatch('getPollData', { + token: message.token, + pollId: message.messageParameters.poll.id, + }) + } + context.commit('addMessage', message) if ((message.messageType === 'comment' && message.message === '{file}' && message.messageParameters?.file) diff --git a/src/store/pollStore.js b/src/store/pollStore.js index 1d0d0c861fd..a0a7fcee24e 100644 --- a/src/store/pollStore.js +++ b/src/store/pollStore.js @@ -114,16 +114,16 @@ const actions = { } }, - async closePoll(context, { token, pollId }) { - console.debug('Closing poll') + async endPoll(context, { token, pollId }) { + console.debug('Ending poll') try { - const response = await pollService.closePoll(token, pollId) + const response = await pollService.endPoll(token, pollId) const poll = response.data.ocs.data context.dispatch('addPoll', { token, poll }) console.debug('polldata', response) } catch (error) { console.error(error) - showError(t('spreed', 'An error occurred while closing the poll')) + showError(t('spreed', 'An error occurred while ending the poll')) } }, } diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 738a691eebd..ba930a1956d 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1934,7 +1934,7 @@ protected function compareDataResponse(TableNode $formData = null) { $result = preg_match('/POLL_ID\(([^)]+)\)/', $expected[$i]['messageParameters'], $matches); if ($result) { - $expected[$i]['messageParameters'] = str_replace($matches[0], self::$questionToPollId[$matches[1]], $expected[$i]['messageParameters']); + $expected[$i]['messageParameters'] = str_replace($matches[0], '"' . self::$questionToPollId[$matches[1]] . '"', $expected[$i]['messageParameters']); } }