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
|
|
@ -14,4 +14,6 @@ $chat-button-height: 50px;
|
|||
|
||||
$voice-recorder-color: #6f6f6f;
|
||||
$voice-recording-color: red;
|
||||
$voice-recorded-color: #3ae17d;
|
||||
$voice-recorded-color: #3ae17d;
|
||||
|
||||
$poll-hilite-color: $very-very-purple;
|
||||
61
src/assets/css/components/_poll.scss
Normal file
61
src/assets/css/components/_poll.scss
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
.poll-bubble {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.poll-answer {
|
||||
border: 1px solid #666;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
&.selected {
|
||||
border: 1px solid $poll-hilite-color;
|
||||
}
|
||||
.poll-answer-title {
|
||||
color: #444;
|
||||
}
|
||||
.poll-answer-num-votes {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poll-percent-indicator {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
height: 4px;
|
||||
.bar {
|
||||
background-color: $poll-hilite-color;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-status {
|
||||
margin: 10px;
|
||||
justify-content: space-between;
|
||||
.poll-status-title {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.poll-status-close {
|
||||
font-size: 0.7rem;
|
||||
color: $poll-hilite-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Creation dialog
|
||||
//
|
||||
.poll-create-dialog-content {
|
||||
max-height: 50vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.poll-create-status {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
|
@ -233,5 +233,25 @@
|
|||
"video_file": "Video file",
|
||||
"original_text": "<original text>",
|
||||
"download_name": "Download"
|
||||
},
|
||||
"poll_create": {
|
||||
"title": "Create poll",
|
||||
"intro": "Please fill in details below.",
|
||||
"create": "Create",
|
||||
"creating": "Creating poll",
|
||||
"poll_disclosed": "Open - current results are shown at all times.",
|
||||
"poll_undisclosed": "Closed - users will see the results when poll is closed.",
|
||||
"add_answer": "Add answer",
|
||||
"failed": "Failed to create poll, please try again later.",
|
||||
"question_label": "Type your question here",
|
||||
"question_required": "You need to enter a question!",
|
||||
"answer_label": "Answer no {index}",
|
||||
"answer_required": "Answer can't be empty. Please enter some text or remove this option.",
|
||||
"create_poll_menu_option": "Create poll",
|
||||
"poll_status_closed": "Poll is closed",
|
||||
"poll_status_disclosed": "Results will be shown when poll is closed.",
|
||||
"poll_status_open": "Poll is open",
|
||||
"poll_status_open_not_voted": "Poll is open - vote to see the results",
|
||||
"close_poll": "Close poll"
|
||||
}
|
||||
}
|
||||
|
|
@ -73,9 +73,7 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
!event.isRelation() && !event.isRedacted() && !event.isRedaction()
|
||||
"
|
||||
v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()"
|
||||
:ref="event.getId()"
|
||||
>
|
||||
<div
|
||||
|
|
@ -303,6 +301,23 @@
|
|||
/>
|
||||
</label>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="!showRecorder && canCreatePoll"
|
||||
ref="sendOptions"
|
||||
class="input-area-button text-center flex-grow-0 flex-shrink-1"
|
||||
>
|
||||
<v-menu close-on-click>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="showCreatePollDialog = true"><v-list-item-title>{{ $t("poll_create.create_poll_menu_option") }}</v-list-item-title></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<VoiceRecorder
|
||||
:micButtonRef="$refs.mic_button"
|
||||
|
|
@ -458,6 +473,11 @@
|
|||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<CreatePollDialog
|
||||
:show="showCreatePollDialog"
|
||||
@close="showCreatePollDialog = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -470,12 +490,14 @@ import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
|||
import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
|
||||
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
|
||||
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
|
||||
import MessageIncomingPoll from "./messages/MessageIncomingPoll.vue";
|
||||
import MessageOutgoingText from "./messages/MessageOutgoingText";
|
||||
import MessageOutgoingFile from "./messages/MessageOutgoingFile";
|
||||
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
|
||||
import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
|
||||
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
|
||||
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
|
||||
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
|
||||
import ContactJoin from "./messages/ContactJoin.vue";
|
||||
import ContactLeave from "./messages/ContactLeave.vue";
|
||||
import ContactInvited from "./messages/ContactInvited.vue";
|
||||
|
|
@ -505,6 +527,8 @@ import stickers from "../plugins/stickers";
|
|||
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
|
||||
import BottomSheet from "./BottomSheet.vue";
|
||||
import ImageResize from "image-resize";
|
||||
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||
|
||||
const sizeOf = require("image-size");
|
||||
const dataUriToBuffer = require("data-uri-to-buffer");
|
||||
const prettyBytes = require("pretty-bytes");
|
||||
|
|
@ -555,6 +579,7 @@ export default {
|
|||
MessageOutgoingAudio,
|
||||
MessageOutgoingVideo,
|
||||
MessageOutgoingSticker,
|
||||
MessageOutgoingPoll,
|
||||
ContactJoin,
|
||||
ContactLeave,
|
||||
ContactInvited,
|
||||
|
|
@ -580,6 +605,7 @@ export default {
|
|||
StickerPickerBottomSheet,
|
||||
BottomSheet,
|
||||
AvatarOperations,
|
||||
CreatePollDialog
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -606,6 +632,7 @@ export default {
|
|||
replyToEvent: null,
|
||||
replyToImg: null,
|
||||
replyToContentType: null,
|
||||
showCreatePollDialog: false,
|
||||
showNoRecordingAvailableDialog: false,
|
||||
showContextMenu: false,
|
||||
showContextMenuAnchor: null,
|
||||
|
|
@ -765,6 +792,12 @@ export default {
|
|||
},
|
||||
invitationCount() {
|
||||
return this.$matrix.invites.length;
|
||||
},
|
||||
canCreatePoll() {
|
||||
// We say that if you can redact events, you are allowed to create 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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -1133,6 +1166,14 @@ export default {
|
|||
case "m.room.encryption":
|
||||
return RoomEncrypted;
|
||||
|
||||
case "m.poll.start":
|
||||
case "org.matrix.msc3381.poll.start":
|
||||
if (event.getSender() != this.$matrix.currentUserId) {
|
||||
return MessageIncomingPoll;
|
||||
} else {
|
||||
return MessageOutgoingPoll;
|
||||
}
|
||||
|
||||
case "im.keanu.room_deletion_notice": {
|
||||
// Custom event for notice 30 seconds before a room is deleted/purged.
|
||||
const deletionNotices = this.room.currentState.getStateEvents(
|
||||
|
|
@ -1815,7 +1856,7 @@ export default {
|
|||
},
|
||||
onInvitationsClick() {
|
||||
this.$navigation.push({ name: "Home" }, -1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
195
src/components/CreatePollDialog.vue
Normal file
195
src/components/CreatePollDialog.vue
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<v-dialog v-model="showDialog" v-show="room" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '688px' : '95%'">
|
||||
<div class="dialog-content text-center">
|
||||
<template>
|
||||
<v-icon color="black" size="30">poll</v-icon>
|
||||
<h2 class="dialog-title">
|
||||
{{ $t("poll_create.title") }}
|
||||
</h2>
|
||||
<div class="dialog-text">{{ $t("poll_create.intro") }}</div>
|
||||
</template>
|
||||
<v-form v-model="isValid" ref="pollcreateform">
|
||||
<v-container fluid class="poll-create-dialog-content">
|
||||
<v-row cols="12">
|
||||
<v-col cols="12">
|
||||
<v-switch
|
||||
v-model="pollIsDisclosed"
|
||||
inset
|
||||
:label="pollIsDisclosed ? $t('poll_create.poll_disclosed') : $t('poll_create.poll_undisclosed')"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row cols="12">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
outlined
|
||||
:disabled="isCreating"
|
||||
v-model="pollQuestion"
|
||||
:label="$t('poll_create.question_label')"
|
||||
:rules="[(v) => !!v || $t('poll_create.question_required')]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-for="(answer, index) in pollAnswers" cols="12" :key="answer.id">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
outlined
|
||||
:disabled="isCreating"
|
||||
v-model="answer.text"
|
||||
:label="$t('poll_create.answer_label', { index: index + 1 })"
|
||||
:append-icon="pollAnswers.length > 1 ? 'delete' : null"
|
||||
@click:append="removeAnswer(index)"
|
||||
:rules="[(v) => !!v || $t('poll_create.answer_required')]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row
|
||||
><v-col
|
||||
><v-btn @click.stop="addAnswer">{{ $t("poll_create.add_answer") }}</v-btn></v-col
|
||||
></v-row
|
||||
>
|
||||
</v-container>
|
||||
</v-form>
|
||||
<div class="poll-create-status">
|
||||
{{ status }}
|
||||
<v-progress-circular v-if="isCreating" indeterminate color="primary" size="14"></v-progress-circular>
|
||||
</div>
|
||||
<v-container fluid>
|
||||
<v-row cols="12">
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
id="btn-create-poll-cancel"
|
||||
depressed
|
||||
text
|
||||
block
|
||||
class="text-button"
|
||||
@click="showDialog = false"
|
||||
:disabled="isCreating"
|
||||
>{{ $t("menu.cancel") }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="6" align="center">
|
||||
<v-btn
|
||||
id="btn-create-poll"
|
||||
color="red"
|
||||
depressed
|
||||
block
|
||||
class="filled-button"
|
||||
@click.stop="onCreatePoll()"
|
||||
:disabled="isCreating"
|
||||
>{{ $t("poll_create.create") }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import roomInfoMixin from "./roomInfoMixin";
|
||||
import util from "../plugins/utils";
|
||||
|
||||
export default {
|
||||
name: "CreatePollDialog",
|
||||
mixins: [roomInfoMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: function() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return this.defaultData();
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal, ignoredOldVal) {
|
||||
this.showDialog = newVal;
|
||||
},
|
||||
},
|
||||
showDialog() {
|
||||
if (!this.showDialog) {
|
||||
this.$emit("close");
|
||||
} else {
|
||||
// Reset values
|
||||
let defaults = this.defaultData();
|
||||
this.isValid = defaults.isValid;
|
||||
this.isCreating = defaults.isCreating;
|
||||
this.status = defaults.status;
|
||||
this.pollIsDisclosed = defaults.pollIsDisclosed;
|
||||
this.pollQuestion = defaults.pollQuestion;
|
||||
this.pollAnswers = defaults.pollAnswers;
|
||||
this.autoIncrementId = defaults.autoIncrementId;
|
||||
if (this.$refs.pollcreateform) {
|
||||
this.$refs.pollcreateform.resetValidation();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
defaultData() {
|
||||
return {
|
||||
showDialog: false,
|
||||
isValid: true,
|
||||
isCreating: false,
|
||||
status: String.fromCharCode(160),
|
||||
pollIsDisclosed: true,
|
||||
pollQuestion: "",
|
||||
pollAnswers: [
|
||||
{ id: "1", text: "" },
|
||||
{ id: "2", text: "" },
|
||||
],
|
||||
autoIncrementId: 2,
|
||||
};
|
||||
},
|
||||
addAnswer() {
|
||||
if (this.pollAnswers.length < 20) {
|
||||
// MAX length according to spec
|
||||
this.autoIncrementId += 1;
|
||||
this.pollAnswers.push({ id: "" + this.autoIncrementId, text: "" });
|
||||
}
|
||||
},
|
||||
removeAnswer(index) {
|
||||
this.pollAnswers.splice(index, 1);
|
||||
},
|
||||
onCreatePoll() {
|
||||
if (this.$refs.pollcreateform.validate()) {
|
||||
this.isCreating = true;
|
||||
this.status = this.$t("poll_create.creating");
|
||||
util
|
||||
.createPoll(
|
||||
this.$matrix.matrixClient,
|
||||
this.room.roomId,
|
||||
this.pollQuestion,
|
||||
this.pollAnswers,
|
||||
this.pollIsDisclosed
|
||||
)
|
||||
.then(() => {
|
||||
console.log("Sent message");
|
||||
this.isCreating = false;
|
||||
this.showDialog = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to send:", err);
|
||||
this.isCreating = false;
|
||||
this.status = this.$t("poll_create.failed");
|
||||
});
|
||||
|
||||
setTimeout(() => {}, 3000);
|
||||
} else {
|
||||
this.isValid = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
@import "@/assets/css/components/poll.scss";
|
||||
</style>
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
})
|
||||
}}
|
||||
</span>
|
||||
<div v-if="member.userId != $matrix.currentUserId && !$matrix.isDirectRoomWith(room, member.userId) && expandedMembers.includes(member)" class="start-private-chat clickable" @click="startPrivateChat(member.userId)">Start private chat</div>
|
||||
<div v-if="member.userId != $matrix.currentUserId && !$matrix.isDirectRoomWith(room, member.userId) && expandedMembers.includes(member)" class="start-private-chat clickable" @click="startPrivateChat(member.userId)">{{ $t("menu.start_private_chat") }}</div>
|
||||
<DeviceList
|
||||
v-if="expandedMembers.includes(member)"
|
||||
:member="member"
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -180,6 +180,51 @@ class Util {
|
|||
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
|
||||
}
|
||||
|
||||
createPoll(matrixClient, roomId, question, answers, isDisclosed) {
|
||||
var idx = 0;
|
||||
let answerData = answers.map(a => {
|
||||
idx++;
|
||||
return {id: "" + idx, 'org.matrix.msc1767.text': a.text }
|
||||
});
|
||||
const content = {
|
||||
'org.matrix.msc3381.poll.start': {
|
||||
question: {
|
||||
'org.matrix.msc1767.text': question,
|
||||
body: question
|
||||
},
|
||||
kind: isDisclosed ? "org.matrix.msc3381.poll.disclosed" : "org.matrix.msc3381.poll.undisclosed",
|
||||
max_selections: 1,
|
||||
answers: answerData
|
||||
},
|
||||
};
|
||||
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.start", content);
|
||||
}
|
||||
|
||||
closePoll(matrixClient, roomId, event) {
|
||||
const content = {
|
||||
'm.relates_to': {
|
||||
rel_type: 'm.reference',
|
||||
event_id: event.getId()
|
||||
},
|
||||
'org.matrix.msc3381.poll.end': {
|
||||
},
|
||||
};
|
||||
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.end", content);
|
||||
}
|
||||
|
||||
sendPollAnswer(matrixClient, roomId, answers, event) {
|
||||
const content = {
|
||||
'm.relates_to': {
|
||||
rel_type: 'm.reference',
|
||||
event_id: event.getId()
|
||||
},
|
||||
'org.matrix.msc3381.poll.response': {
|
||||
answers: answers
|
||||
},
|
||||
};
|
||||
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content);
|
||||
}
|
||||
|
||||
sendMessage(matrixClient, roomId, eventType, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
matrixClient.sendEvent(roomId, eventType, content, undefined, undefined)
|
||||
|
|
|
|||
|
|
@ -306,6 +306,7 @@ export default {
|
|||
|
||||
addMatrixClientListeners(client) {
|
||||
if (client) {
|
||||
client.setMaxListeners(100); // Increate max number of listeners.
|
||||
client.on("event", this.onEvent);
|
||||
client.on("Room", this.onRoom);
|
||||
client.on("Session.logged_out", this.onSessionLoggedOut);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue