Support for polls (can be created by room admins)
This commit is contained in:
parent
2a064f4a06
commit
0d1ac1d441
11 changed files with 676 additions and 6 deletions
60
src/components/messages/MessageIncomingPoll.vue
Normal file
60
src/components/messages/MessageIncomingPoll.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||
<div class="bubble poll-bubble">
|
||||
{{ pollQuestion }}
|
||||
<v-container fluid>
|
||||
<v-row
|
||||
v-for="answer in pollAnswers"
|
||||
:key="answer.id"
|
||||
@click="pollAnswer(answer.id)"
|
||||
:class="{ 'poll-answer': true, selected: answer.hasMyVote }"
|
||||
>
|
||||
<v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }}</v-col>
|
||||
<v-col
|
||||
v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
|
||||
cols="auto"
|
||||
class="ma-0 pa-0 poll-answer-num-votes"
|
||||
>{{ answer.numVotes }}</v-col
|
||||
>
|
||||
<div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator">
|
||||
<div class="bar" :style="{ width: `${answer.percentage}%` }"></div>
|
||||
</div>
|
||||
</v-row>
|
||||
<v-row class="poll-status">
|
||||
<v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{
|
||||
pollIsClosed
|
||||
? $t("poll_create.poll_status_closed")
|
||||
: pollIsDisclosed
|
||||
? userHasVoted
|
||||
? $t("poll_create.poll_status_open")
|
||||
: $t("poll_create.poll_status_open_not_voted")
|
||||
: $t("poll_create.poll_status_disclosed")
|
||||
}}</v-col>
|
||||
<v-col
|
||||
cols="auto"
|
||||
class="ma-0 pa-0 poll-status-close clickable"
|
||||
v-if="!pollIsClosed && userCanClosePoll"
|
||||
@click.stop="pollClose"
|
||||
>{{ $t("poll_create.close_poll") }}</v-col
|
||||
>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</message-incoming>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import pollMixin from "./pollMixin";
|
||||
import MessageIncoming from "./MessageIncoming.vue";
|
||||
|
||||
export default {
|
||||
extends: MessageIncoming,
|
||||
mixins: [pollMixin],
|
||||
components: { MessageIncoming },
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
@import "@/assets/css/components/poll.scss";
|
||||
</style>
|
||||
60
src/components/messages/MessageOutgoingPoll.vue
Normal file
60
src/components/messages/MessageOutgoingPoll.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||
<div class="bubble poll-bubble">
|
||||
{{ pollQuestion }}
|
||||
<v-container fluid>
|
||||
<v-row
|
||||
v-for="answer in pollAnswers"
|
||||
:key="answer.id"
|
||||
@click="pollAnswer(answer.id)"
|
||||
:class="{ 'poll-answer': true, selected: answer.hasMyVote }"
|
||||
>
|
||||
<v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }}</v-col>
|
||||
<v-col
|
||||
v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
|
||||
cols="auto"
|
||||
class="ma-0 pa-0 poll-answer-num-votes"
|
||||
>{{ answer.numVotes }}</v-col
|
||||
>
|
||||
<div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator">
|
||||
<div class="bar" :style="{ width: `${answer.percentage}%` }"></div>
|
||||
</div>
|
||||
</v-row>
|
||||
<v-row class="poll-status">
|
||||
<v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{
|
||||
pollIsClosed
|
||||
? $t("poll_create.poll_status_closed")
|
||||
: pollIsDisclosed
|
||||
? userHasVoted
|
||||
? $t("poll_create.poll_status_open")
|
||||
: $t("poll_create.poll_status_open_not_voted")
|
||||
: $t("poll_create.poll_status_disclosed")
|
||||
}}</v-col>
|
||||
<v-col
|
||||
cols="auto"
|
||||
class="ma-0 pa-0 poll-status-close clickable"
|
||||
v-if="!pollIsClosed && userCanClosePoll"
|
||||
@click.stop="pollClose"
|
||||
>{{ $t("poll_create.close_poll") }}</v-col
|
||||
>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</message-outgoing>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import pollMixin from "./pollMixin";
|
||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
|
||||
export default {
|
||||
extends: MessageOutgoing,
|
||||
mixins: [pollMixin],
|
||||
components: { MessageOutgoing },
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
@import "@/assets/css/components/poll.scss";
|
||||
</style>
|
||||
185
src/components/messages/pollMixin.js
Normal file
185
src/components/messages/pollMixin.js
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import util from "../../plugins/utils";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
pollQuestion: "",
|
||||
pollAnswers: [],
|
||||
pollResponseRelations: null,
|
||||
pollEndRelations: null,
|
||||
pollEndTs: null,
|
||||
pollIsDisclosed: true,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$matrix.on("Room.timeline", this.pollMixinOnEvent);
|
||||
this.pollQuestion = (this.event && this.event.getContent()["org.matrix.msc3381.poll.start"]["question"]["body"]) || "";
|
||||
this.updateAnswers();
|
||||
},
|
||||
destroyed() {
|
||||
this.$matrix.off("Room.timeline", this.pollMixinOnEvent);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.pollResponseRelations) {
|
||||
this.pollResponseRelations.off('Relations.add', this.onAddRelation);
|
||||
this.pollResponseRelations = null;
|
||||
}
|
||||
if (this.pollEndRelations) {
|
||||
this.pollEndRelations.off('Relations.add', this.onAddRelation);
|
||||
this.pollEndRelations = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateAnswers() {
|
||||
let answers = (this.event && this.event.getContent()["org.matrix.msc3381.poll.start"]["answers"]) || [];
|
||||
var answerMap = {};
|
||||
var answerArray = [];
|
||||
answers.forEach(a => {
|
||||
let text = a["org.matrix.msc1767.text"];
|
||||
let answer = {id: a.id, text: text, numVotes: 0, percentage: 0}
|
||||
answerMap[a.id] = answer;
|
||||
answerArray.push(answer);
|
||||
});
|
||||
|
||||
// Kind of poll
|
||||
this.pollIsDisclosed = (this.event && this.event.getContent()["org.matrix.msc3381.poll.start"]["kind"] != "org.matrix.msc3381.poll.undisclosed") || false;
|
||||
|
||||
// Look for poll end
|
||||
this.pollEndRelations = this.timelineSet.getRelationsForEvent(
|
||||
this.event.getId(),
|
||||
'm.reference',
|
||||
'org.matrix.msc3381.poll.end'
|
||||
);
|
||||
if (this.pollEndRelations) {
|
||||
const endMessages = this.pollEndRelations.getRelations() || [];
|
||||
if (endMessages.length > 0) {
|
||||
this.pollEndTs = endMessages[endMessages.length - 1].getTs();
|
||||
}
|
||||
}
|
||||
|
||||
// Process votes
|
||||
this.pollResponseRelations = this.timelineSet.getRelationsForEvent(
|
||||
this.event.getId(),
|
||||
'm.reference',
|
||||
'org.matrix.msc3381.poll.response'
|
||||
);
|
||||
var userVotes = {};
|
||||
if (this.pollResponseRelations) {
|
||||
const votes = this.pollResponseRelations.getRelations() || [];
|
||||
for (const vote of votes) {
|
||||
//const emoji = r.getRelation().key;
|
||||
if (this.pollEndTs && vote.getTs() > this.pollEndTs) {
|
||||
continue; // Invalid vote, after poll was closed.
|
||||
}
|
||||
const sender = vote.getSender();
|
||||
const answersFromThisUser = vote.getContent()["org.matrix.msc3381.poll.response"]["answers"] || [];
|
||||
if (answersFromThisUser.length == 0) {
|
||||
delete userVotes[sender];
|
||||
} else {
|
||||
userVotes[sender] = answersFromThisUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
var totalVotes = 0;
|
||||
for (const [user, answersFromThisUser] of Object.entries(userVotes)) {
|
||||
for (const a of answersFromThisUser) {
|
||||
if (answerMap[a]) {
|
||||
answerMap[a].numVotes += 1;
|
||||
totalVotes += 1;
|
||||
if (user == this.$matrix.currentUserId) {
|
||||
answerMap[a].hasMyVote = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update percentage
|
||||
answerArray.forEach(a => {
|
||||
a.percentage = parseInt(((100 * a.numVotes) / totalVotes).toFixed(0));
|
||||
});
|
||||
this.pollAnswers = answerArray;
|
||||
},
|
||||
pollAnswer(id) {
|
||||
if (this.pollIsClosed) {
|
||||
return;
|
||||
}
|
||||
util
|
||||
.sendPollAnswer(
|
||||
this.$matrix.matrixClient,
|
||||
this.room.roomId,
|
||||
[id],
|
||||
this.event
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log("Failed to send:", err);
|
||||
});
|
||||
},
|
||||
pollClose() {
|
||||
util
|
||||
.closePoll(
|
||||
this.$matrix.matrixClient,
|
||||
this.room.roomId,
|
||||
this.event
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log("Failed to send:", err);
|
||||
});
|
||||
},
|
||||
onAddRelation(ignoredevent) {
|
||||
this.updateAnswers();
|
||||
},
|
||||
pollMixinOnEvent(event) {
|
||||
if (event.getRoomId() !== this.room.roomId) {
|
||||
return; // Not for this room
|
||||
}
|
||||
if (
|
||||
event.getType().startsWith("org.matrix.msc3381.poll.")) {
|
||||
this.updateAnswers();
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pollIsClosed() {
|
||||
return this.pollEndTs != null && this.pollEndTs !== undefined;
|
||||
},
|
||||
userCanClosePoll() {
|
||||
return this.room && this.room.currentState && this.room.currentState.maySendRedactionForEvent(this.event, this.$matrix.currentUserId);
|
||||
},
|
||||
userHasVoted() {
|
||||
return this.pollAnswers.some(a => a.hasMyVote);
|
||||
},
|
||||
pollIsAdmin() {
|
||||
// Admins can view results of not-yet-closed undisclosed polls.
|
||||
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
|
||||
let isAdmin = me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
||||
return isAdmin;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pollResponseRelations: {
|
||||
handler(newValue, oldValue) {
|
||||
if (oldValue) {
|
||||
oldValue.off('Relations.add', this.onAddRelation);
|
||||
}
|
||||
if (newValue) {
|
||||
newValue.on('Relations.add', this.onAddRelation);
|
||||
}
|
||||
this.updateAnswers();
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
pollEndRelations: {
|
||||
handler(newValue, oldValue) {
|
||||
if (oldValue) {
|
||||
oldValue.off('Relations.add', this.onAddRelation);
|
||||
}
|
||||
if (newValue) {
|
||||
newValue.on('Relations.add', this.onAddRelation);
|
||||
}
|
||||
this.updateAnswers();
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue