More poll styling

This commit is contained in:
N Pex 2022-05-17 15:16:53 +00:00
parent 98ed17723d
commit 32ebd86c5d
12 changed files with 361 additions and 344 deletions

View file

@ -15,5 +15,5 @@ $chat-button-height: 50px;
$voice-recorder-color: #6f6f6f; $voice-recorder-color: #6f6f6f;
$voice-recording-color: red; $voice-recording-color: red;
$voice-recorded-color: #3ae17d; $voice-recorded-color: #3ae17d;
$poll-hilite-color: #6360f0;
$poll-hilite-color: $very-very-purple; $poll-hilite-color-bg: #d6d5fc;

View file

@ -611,6 +611,10 @@ $admin-fg: white;
box-shadow: 4px 4px 8px rgba(0,0,0,0.15); box-shadow: 4px 4px 8px rgba(0,0,0,0.15);
} }
.send-options {
z-index: 11; // Above mic button
}
.message-operations-picker { .message-operations-picker {
background-color: white; background-color: white;
text-align: center; text-align: center;

View file

@ -2,19 +2,57 @@
width: 70%; width: 70%;
} }
.poll-bubble {
color: black;
padding: $chat-standard-padding-s !important;
font-family: "Inter", sans-serif;
font-size: 16 * $chat-text-size;
line-height: 16 * $chat-text-size;
}
.from-admin .poll-bubble {
color: rgba(white, 0.9);
}
.poll-icon {
path {
fill: currentColor;
}
}
.poll-check-icon {
width: 14.18px;
height: 12px;
}
.poll-question {
font-weight: 700;
margin-top: $chat-standard-padding-xs;
margin-bottom: $chat-standard-padding-s;
}
.poll-answer { .poll-answer {
border: 1px solid #666; border: 1px solid currentColor;
border-radius: 5px; border-radius: 4px;
padding: 10px; padding: 15px 14px;
margin: 10px; margin: 0px;
&.winner {
font-weight: 700;
}
&.selected { &.selected {
border: 1px solid $poll-hilite-color; border: 1px solid $poll-hilite-color;
background-color: $poll-hilite-color-bg;
color: #1d1d1d;
font-weight: 700;
}
&.result {
border: none;
padding: 15px 0px;
} }
.poll-answer-title { .poll-answer-title {
color: #444;
} }
.poll-answer-num-votes { .poll-answer-num-votes {
font-size: 0.7rem; font-size: 0.75rem;
} }
justify-content: space-between; justify-content: space-between;
position: relative; position: relative;
@ -23,32 +61,49 @@
.poll-percent-indicator { .poll-percent-indicator {
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;
left: 2px; left: 0px;
right: 2px; right: 0px;
height: 4px; height: 8px;
margin-top: 4px;
.bar { .bar {
background-color: $poll-hilite-color; background-color: #7e7cf8;
position: absolute; position: absolute;
bottom: 0px; bottom: 0px;
left: 0px; left: 0px;
top: 0px; top: 0px;
border-radius: 3px; border-radius: 4px;
} }
} }
.poll-status { .poll-status {
margin: 10px;
justify-content: space-between; justify-content: space-between;
font-size: 13px;
line-height: 117%;
margin: 0px;
.poll-status-title { .poll-status-title {
font-size: 0.7rem;
} }
.poll-status-close { .poll-status-close {
font-size: 0.7rem;
color: $poll-hilite-color; color: $poll-hilite-color;
} }
} }
.poll-submit {
.v-btn {
font-family: "Inter", sans-serif;
font-weight: 700;
font-size: 11 * $chat-text-size;
color: white;
text-transform: uppercase;
background-color: $poll-hilite-color !important;
border: 1px solid black;
border-radius: 21px !important;
height: 42px !important;
margin-top: $chat-standard-padding-xs;
margin-bottom: $chat-standard-padding-xs;
}
}
// Creation dialog // Creation dialog
// //
.poll-create-dialog-content { .poll-create-dialog-content {

View file

@ -0,0 +1,3 @@
<svg width="15" height="12" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.9265 0.278549C13.6041 -0.0706278 13.0413 -0.0953257 12.6944 0.225985L4.74201 7.59645C4.66894 7.6675 4.55947 7.67068 4.48341 7.6057L2.44807 5.93997C2.20767 5.74531 1.92476 5.64018 1.62665 5.64018C1.22503 5.64018 0.844759 5.83181 0.586146 6.15932L0.321439 6.49622C-0.153089 7.10185 -0.095373 7.992 0.449243 8.52339L3.64365 11.6229C3.89315 11.8671 4.2187 12 4.55335 12C4.93661 12 5.30168 11.827 5.5603 11.521L13.9508 1.5886C14.2672 1.21487 14.2581 0.64014 13.9265 0.278521L13.9265 0.278549Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View file

@ -0,0 +1,6 @@
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.31462 16.4718C3.31462 16.9496 3.70026 17.3368 4.17609 17.3368L16.1385 17.3368C16.6144 17.3368 17 16.9496 17 16.4718L16.9998 13.6229C16.9998 13.1452 16.6142 12.7579 16.1383 12.7579L4.1764 12.7579C3.70056 12.7579 3.31492 13.1452 3.31492 13.6229L3.31512 16.4718L3.31462 16.4718Z" fill="white"/>
<path d="M3.31462 10.4557C3.31462 10.9335 3.70026 11.3208 4.17609 11.3208L11.3428 11.3208C11.8186 11.3208 12.2043 10.9335 12.2043 10.4557L12.2043 7.60711C12.2043 7.12931 11.8186 6.74208 11.3428 6.74208L4.17609 6.74208C3.70026 6.74208 3.31462 7.12932 3.31462 7.60711L3.31462 10.4557Z" fill="white"/>
<path d="M3.31451 1.59127L3.31451 4.44011C3.31451 4.91791 3.70016 5.30514 4.17598 5.30514L6.99509 5.30514C7.47093 5.30514 7.85657 4.91791 7.85657 4.44011L7.85637 1.59127C7.85637 1.11347 7.47073 0.726242 6.9949 0.726242L4.17599 0.726242C3.70035 0.726242 3.31452 1.11348 3.31452 1.59127L3.31451 1.59127Z" fill="white"/>
<path d="M-2.00529e-05 0.587841L-2.0791e-05 17.4747C-2.08052e-05 17.7995 0.262306 18.0625 0.585404 18.0625L1.38198 18.0625C1.70528 18.0625 1.96741 17.7995 1.96741 17.4747L1.96741 0.587841C1.96741 0.263208 1.70508 -1.14667e-08 1.38198 -2.55897e-08L0.585405 -6.04092e-08C0.261911 -7.45496e-08 -2.00387e-05 0.263213 -2.00529e-05 0.587841Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -254,6 +254,8 @@
"poll_status_disclosed": "Results will be shown when poll is closed.", "poll_status_disclosed": "Results will be shown when poll is closed.",
"poll_status_open": "Poll is open", "poll_status_open": "Poll is open",
"poll_status_open_not_voted": "Poll is open - vote to see the results", "poll_status_open_not_voted": "Poll is open - vote to see the results",
"close_poll": "Close poll" "close_poll": "Close poll",
"poll_submit": "Submit",
"num_answered": "{count} have answered"
} }
} }

View file

@ -1,12 +1,9 @@
<template> <template>
<div class="chat-root fill-height d-flex flex-column"> <div class="chat-root fill-height d-flex flex-column">
<div class="chat-room-invitations clickable" v-if="invitationCount > 0" @click.stop="onInvitationsClick"> <div class="chat-room-invitations clickable" v-if="invitationCount > 0" @click.stop="onInvitationsClick">
{{ $tc("room.invitations", invitationCount)}} {{ $tc("room.invitations", invitationCount) }}
</div> </div>
<ChatHeader <ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" />
class="chat-header flex-grow-0 flex-shrink-0"
v-on:header-click="onHeaderClick"
/>
<div <div
class="chat-content flex-grow-1 flex-shrink-1" class="chat-content flex-grow-1 flex-shrink-1"
ref="chatContainer" ref="chatContainer"
@ -50,32 +47,15 @@
</div> </div>
<!-- Handle resizes, e.g. when soft keyboard is shown/hidden --> <!-- Handle resizes, e.g. when soft keyboard is shown/hidden -->
<resize-observer <resize-observer ref="chatContainerResizer" @notify="handleChatContainerResize" />
ref="chatContainerResizer"
@notify="handleChatContainerResize"
/>
<CreatedRoomWelcomeHeader <CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />
v-if="showCreatedRoomWelcomeHeader"
v-on:close="closeCreateRoomWelcomeHeader"
/>
<div <div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
v-for="(event, index) in events"
:key="event.getId()"
:eventId="event.getId()"
>
<!-- DAY Marker, shown for every new day in the timeline --> <!-- DAY Marker, shown for every new day in the timeline -->
<div <div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dayForEvent(event)" />
v-if="showDayMarkerBeforeEvent(event)"
class="day-marker"
:title="dayForEvent(event)"
/>
<div <div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()"
:ref="event.getId()"
>
<div <div
class="message-wrapper" class="message-wrapper"
v-on:touchstart=" v-on:touchstart="
@ -92,13 +72,7 @@
:room="room" :room="room"
:event="event" :event="event"
:nextEvent="events[index + 1]" :nextEvent="events[index + 1]"
:reactions=" :reactions="timelineSet.getRelationsForEvent(event.getId(), 'm.annotation', 'm.reaction')"
timelineSet.getRelationsForEvent(
event.getId(),
'm.annotation',
'm.reaction'
)
"
:timelineSet="timelineSet" :timelineSet="timelineSet"
v-on:send-quick-reaction="sendQuickReaction" v-on:send-quick-reaction="sendQuickReaction"
v-on:context-menu="showContextMenuForEvent($event)" v-on:context-menu="showContextMenuForEvent($event)"
@ -119,8 +93,8 @@
</div> </div>
<!-- Input area --> <!-- Input area -->
<v-container v-if="room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to':'']"> <v-container v-if="room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
<div :class="[replyToEvent ? 'iput-area-inner-box':'']"> <div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
<!-- "Scroll to end"-button --> <!-- "Scroll to end"-button -->
<v-btn <v-btn
class="scroll-to-end" class="scroll-to-end"
@ -138,35 +112,20 @@
<div v-if="replyToEvent" class="row"> <div v-if="replyToEvent" class="row">
<div class="col"> <div class="col">
<div class="font-weight-medium">{{ $t("message.replying_to", { user: replyToEvent.sender.name }) }}</div> <div class="font-weight-medium">{{ $t("message.replying_to", { user: replyToEvent.sender.name }) }}</div>
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body"> {{ replyToEvent.getContent().body | latestReply }} </div> <div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
<div v-if="replyToContentType === 'm.image'"> {{ $t("message.reply_image") }} </div> {{ replyToEvent.getContent().body | latestReply }}
<div v-if="replyToContentType === 'm.audio'"> {{ $t("message.reply_audio_message") }} </div> </div>
<div v-if="replyToContentType === 'm.video'"> {{ $t("message.reply_video") }} </div> <div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
<div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
</div> </div>
<div class="col col-auto" v-if="replyToContentType !== 'm.text'"> <div class="col col-auto" v-if="replyToContentType !== 'm.text'">
<img <img v-if="replyToContentType === 'm.image'" width="150px" :src="replyToImg" class="rounded" />
v-if="replyToContentType === 'm.image'" <v-img v-if="replyToContentType === 'm.audio'" src="@/assets/icons/audio_message.svg" />
width="150px" <v-img v-if="replyToContentType === 'm.video'" src="@/assets/icons/video_message.svg" />
:src="replyToImg"
class="rounded"
/>
<v-img
v-if="replyToContentType === 'm.audio'"
src="@/assets/icons/audio_message.svg"
/>
<v-img
v-if="replyToContentType === 'm.video'"
src="@/assets/icons/video_message.svg"
/>
</div> </div>
<div class="col col-auto"> <div class="col col-auto">
<v-btn <v-btn fab x-small elevation="0" color="black" @click.stop="cancelEditReply">
fab
x-small
elevation="0"
color="black"
@click.stop="cancelEditReply"
>
<v-icon color="white">cancel</v-icon> <v-icon color="white">cancel</v-icon>
</v-btn> </v-btn>
</div> </div>
@ -199,17 +158,8 @@
/> />
</v-col> </v-col>
<v-col <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-if="editedEvent">
class="input-area-button text-center flex-grow-0 flex-shrink-1" <v-btn fab small elevation="0" color="black" @click.stop="cancelEditReply">
v-if="editedEvent"
>
<v-btn
fab
small
elevation="0"
color="black"
@click.stop="cancelEditReply"
>
<v-icon color="white">cancel</v-icon> <v-icon color="white">cancel</v-icon>
</v-btn> </v-btn>
</v-col> </v-col>
@ -244,10 +194,7 @@
</v-btn> </v-btn>
</v-col> </v-col>
<v-col <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-else>
class="input-area-button text-center flex-grow-0 flex-shrink-1"
v-else
>
<v-btn <v-btn
fab fab
small small
@ -256,16 +203,11 @@
@click.stop="sendCurrentTextMessage" @click.stop="sendCurrentTextMessage"
:disabled="sendButtonDisabled" :disabled="sendButtonDisabled"
> >
<v-icon color="white">{{ <v-icon color="white">{{ editedEvent ? "save" : "arrow_upward" }}</v-icon>
editedEvent ? "save" : "arrow_upward"
}}</v-icon>
</v-btn> </v-btn>
</v-col> </v-col>
<v-col <v-col v-if="$config.shortCodeStickers" class="input-area-button text-center flex-grow-0 flex-shrink-1">
v-if="$config.shortCodeStickers"
class="input-area-button text-center flex-grow-0 flex-shrink-1"
>
<v-btn <v-btn
v-if="!showRecorder" v-if="!showRecorder"
id="btn-attach" id="btn-attach"
@ -305,7 +247,7 @@
<v-col <v-col
v-if="!showRecorder && canCreatePoll" v-if="!showRecorder && canCreatePoll"
ref="sendOptions" ref="sendOptions"
class="input-area-button text-center flex-grow-0 flex-shrink-1" class="input-area-button text-center flex-grow-0 flex-shrink-1 send-options"
> >
<v-menu close-on-click> <v-menu close-on-click>
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator="{ on, attrs }">
@ -314,7 +256,29 @@
</v-btn> </v-btn>
</template> </template>
<v-list> <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-item @click="showCreatePollDialog = true">
<v-list-item-icon>
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.31462 16.4718C3.31462 16.9496 3.70026 17.3368 4.17609 17.3368L16.1385 17.3368C16.6144 17.3368 17 16.9496 17 16.4718L16.9998 13.6229C16.9998 13.1452 16.6142 12.7579 16.1383 12.7579L4.1764 12.7579C3.70056 12.7579 3.31492 13.1452 3.31492 13.6229L3.31512 16.4718L3.31462 16.4718Z"
fill="currentColor"
/>
<path
d="M3.31462 10.4557C3.31462 10.9335 3.70026 11.3208 4.17609 11.3208L11.3428 11.3208C11.8186 11.3208 12.2043 10.9335 12.2043 10.4557L12.2043 7.60711C12.2043 7.12931 11.8186 6.74208 11.3428 6.74208L4.17609 6.74208C3.70026 6.74208 3.31462 7.12932 3.31462 7.60711L3.31462 10.4557Z"
fill="currentColor"
/>
<path
d="M3.31451 1.59127L3.31451 4.44011C3.31451 4.91791 3.70016 5.30514 4.17598 5.30514L6.99509 5.30514C7.47093 5.30514 7.85657 4.91791 7.85657 4.44011L7.85637 1.59127C7.85637 1.11347 7.47073 0.726242 6.9949 0.726242L4.17599 0.726242C3.70035 0.726242 3.31452 1.11348 3.31452 1.59127L3.31451 1.59127Z"
fill="currentColor"
/>
<path
d="M-2.00529e-05 0.587841L-2.0791e-05 17.4747C-2.08052e-05 17.7995 0.262306 18.0625 0.585404 18.0625L1.38198 18.0625C1.70528 18.0625 1.96741 17.7995 1.96741 17.4747L1.96741 0.587841C1.96741 0.263208 1.70508 -1.14667e-08 1.38198 -2.55897e-08L0.585405 -6.04092e-08C0.261911 -7.45496e-08 -2.00387e-05 0.263213 -2.00529e-05 0.587841Z"
fill="currentColor"
/>
</svg>
</v-list-item-icon>
<v-list-item-title>{{ $t("poll_create.create_poll_menu_option") }}</v-list-item-title></v-list-item
>
</v-list> </v-list>
</v-menu> </v-menu>
</v-col> </v-col>
@ -330,11 +294,7 @@
</v-container> </v-container>
<div v-if="currentImageInputPath"> <div v-if="currentImageInputPath">
<v-dialog <v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'">
v-model="currentImageInputPath"
class="ma-0 pa-0"
:width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'"
>
<v-card class="ma-0 pa-0"> <v-card class="ma-0 pa-0">
<v-card-text class="ma-0 pa-2"> <v-card-text class="ma-0 pa-2">
<v-img <v-img
@ -346,34 +306,16 @@
/> />
<div> <div>
file: {{ currentImageInputPath.name }} file: {{ currentImageInputPath.name }}
<span <span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
v-if=" {{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span
currentImageInput &&
currentImageInput.scaled &&
currentImageInput.useScaled
"
> >
{{ currentImageInput.scaledDimensions.width }} x <span v-else-if="currentImageInput && currentImageInput.dimensions">
{{ currentImageInput.scaledDimensions.height }}</span {{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span
>
<span
v-else-if="currentImageInput && currentImageInput.dimensions"
>
{{ currentImageInput.dimensions.width }} x
{{ currentImageInput.dimensions.height }}</span
>
<span
v-if="
currentImageInput &&
currentImageInput.scaled &&
currentImageInput.useScaled
"
> >
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
({{ formatBytes(currentImageInput.scaledSize) }})</span ({{ formatBytes(currentImageInput.scaledSize) }})</span
> >
<span v-else> <span v-else> ({{ formatBytes(currentImageInputPath.size) }})</span>
({{ formatBytes(currentImageInputPath.size) }})</span
>
<v-switch <v-switch
v-if="currentImageInput && currentImageInput.scaled" v-if="currentImageInput && currentImageInput.scaled"
:label="$t('message.scale_image')" :label="$t('message.scale_image')"
@ -403,33 +345,17 @@
</v-dialog> </v-dialog>
</div> </div>
<MessageOperationsBottomSheet <MessageOperationsBottomSheet ref="messageOperationsSheet">
ref="messageOperationsSheet" <VEmojiPicker ref="emojiPicker" @select="emojiSelected" />
>
<VEmojiPicker
ref="emojiPicker"
@select="emojiSelected"
/>
</MessageOperationsBottomSheet> </MessageOperationsBottomSheet>
<StickerPickerBottomSheet <StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" />
ref="stickerPickerSheet"
v-on:selectSticker="sendSticker"
/>
<!-- Loading indicator --> <!-- Loading indicator -->
<v-container <v-container fluid class="loading-indicator" fill-height v-if="!initialLoadDone || loading">
fluid
class="loading-indicator"
fill-height
v-if="!initialLoadDone || loading"
>
<v-row align="center" justify="center"> <v-row align="center" justify="center">
<v-col class="text-center"> <v-col class="text-center">
<v-progress-circular <v-progress-circular indeterminate color="primary"></v-progress-circular>
indeterminate
color="primary"
></v-progress-circular>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
@ -437,36 +363,21 @@
<RoomInfoBottomSheet ref="roomInfoSheet" /> <RoomInfoBottomSheet ref="roomInfoSheet" />
<!-- Dialog for audio recording not supported! --> <!-- Dialog for audio recording not supported! -->
<v-dialog <v-dialog v-model="showNoRecordingAvailableDialog" class="ma-0 pa-0" width="80%">
v-model="showNoRecordingAvailableDialog"
class="ma-0 pa-0"
width="80%"
>
<v-card> <v-card>
<v-card-title>{{ <v-card-title>{{ $t("voice_recorder.not_supported_title") }}</v-card-title>
$t("voice_recorder.not_supported_title") <v-card-text>{{ $t("voice_recorder.not_supported_text") }} </v-card-text>
}}</v-card-title>
<v-card-text
>{{ $t("voice_recorder.not_supported_text") }}
</v-card-text>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn id="btn-ok" color="primary" text @click="showNoRecordingAvailableDialog = false">{{
id="btn-ok" $t("menu.ok")
color="primary" }}</v-btn>
text
@click="showNoRecordingAvailableDialog = false"
>{{ $t("menu.ok") }}</v-btn
>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<CreatePollDialog <CreatePollDialog :show="showCreatePollDialog" @close="showCreatePollDialog = false" />
:show="showCreatePollDialog"
@close="showCreatePollDialog = false"
/>
</div> </div>
</template> </template>
@ -532,20 +443,18 @@ function ScrollPosition(node) {
this.readyFor = "up"; this.readyFor = "up";
} }
ScrollPosition.prototype.restore = function () { ScrollPosition.prototype.restore = function() {
if (this.readyFor === "up") { if (this.readyFor === "up") {
this.node.scrollTop = this.node.scrollTop = this.node.scrollHeight - this.previousScrollHeightMinusTop;
this.node.scrollHeight - this.previousScrollHeightMinusTop;
} else { } else {
this.node.scrollTop = this.previousScrollTop; this.node.scrollTop = this.previousScrollTop;
} }
}; };
ScrollPosition.prototype.prepareFor = function (direction) { ScrollPosition.prototype.prepareFor = function(direction) {
this.readyFor = direction || "up"; this.readyFor = direction || "up";
if (this.readyFor === "up") { if (this.readyFor === "up") {
this.previousScrollHeightMinusTop = this.previousScrollHeightMinusTop = this.node.scrollHeight - this.node.scrollTop;
this.node.scrollHeight - this.node.scrollTop;
} else { } else {
this.previousScrollTop = this.node.scrollTop; this.previousScrollTop = this.node.scrollTop;
} }
@ -592,7 +501,7 @@ export default {
StickerPickerBottomSheet, StickerPickerBottomSheet,
BottomSheet, BottomSheet,
AvatarOperations, AvatarOperations,
CreatePollDialog CreatePollDialog,
}, },
data() { data() {
@ -662,12 +571,12 @@ export default {
filters: { filters: {
latestReply(contents) { latestReply(contents) {
const contentArr = contents.split('\n').reverse(); const contentArr = contents.split("\n").reverse();
if (contentArr[0] === '') { if (contentArr[0] === "") {
contentArr.shift(); contentArr.shift();
} }
return contentArr[0].replace(/^> (<.*> )?/g, ''); return contentArr[0].replace(/^> (<.*> )?/g, "");
} },
}, },
mounted() { mounted() {
@ -714,10 +623,7 @@ export default {
// If we have sent a RR, use that as read marker (so we don't have to wait for server round trip) // If we have sent a RR, use that as read marker (so we don't have to wait for server round trip)
return this.lastRR.getId(); return this.lastRR.getId();
} }
return ( return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false);
this.fullyReadMarker ||
this.room.getEventReadUpTo(this.$matrix.currentUserId, false)
);
}, },
fullyReadMarker() { fullyReadMarker() {
const readEvent = this.room.getAccountData("m.fully_read"); const readEvent = this.room.getAccountData("m.fully_read");
@ -727,11 +633,7 @@ export default {
return null; return null;
}, },
attachButtonDisabled() { attachButtonDisabled() {
return ( return this.editedEvent != null || this.replyToEvent != null || this.currentInput.length > 0;
this.editedEvent != null ||
this.replyToEvent != null ||
this.currentInput.length > 0
);
}, },
sendButtonDisabled() { sendButtonDisabled() {
return this.currentInput.length == 0; return this.currentInput.length == 0;
@ -760,8 +662,7 @@ export default {
if (ref && ref[0]) { if (ref && ref[0]) {
if (this.showAvatarMenuAnchor) { if (this.showAvatarMenuAnchor) {
var rectAnchor = this.showAvatarMenuAnchor.getBoundingClientRect(); var rectAnchor = this.showAvatarMenuAnchor.getBoundingClientRect();
var rectChat = var rectChat = this.$refs.avatarOperationsStrut.getBoundingClientRect();
this.$refs.avatarOperationsStrut.getBoundingClientRect();
top = rectAnchor.top - rectChat.top; top = rectAnchor.top - rectChat.top;
left = rectAnchor.left - rectChat.left; left = rectAnchor.left - rectChat.left;
// if (left + 250 > rectChat.right) { // if (left + 250 > rectChat.right) {
@ -783,9 +684,10 @@ export default {
canCreatePoll() { canCreatePoll() {
// We say that if you can redact events, you are allowed to create polls. // We say that if you can redact events, you are allowed to create polls.
const me = this.room && this.room.getMember(this.$matrix.currentUserId); const me = this.room && this.room.getMember(this.$matrix.currentUserId);
let isAdmin = me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel); let isAdmin =
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
return isAdmin; return isAdmin;
} },
}, },
watch: { watch: {
@ -795,9 +697,7 @@ export default {
if (value && value == oldValue) { if (value && value == oldValue) {
return; // No change. return; // No change.
} }
console.log( console.log("Chat: Current room changed to " + (value ? value : "null"));
"Chat: Current room changed to " + (value ? value : "null")
);
// Clear old events // Clear old events
this.$matrix.off("Room.timeline", this.onEvent); this.$matrix.off("Room.timeline", this.onEvent);
@ -839,18 +739,14 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
// Calculate where to show the context menu. // Calculate where to show the context menu.
// //
const ref = const ref = this.selectedEvent && this.$refs[this.selectedEvent.getId()];
this.selectedEvent && this.$refs[this.selectedEvent.getId()];
var top = 0; var top = 0;
var left = 0; var left = 0;
if (ref && ref[0]) { if (ref && ref[0]) {
if (this.showContextMenuAnchor) { if (this.showContextMenuAnchor) {
var rectAnchor = var rectAnchor = this.showContextMenuAnchor.getBoundingClientRect();
this.showContextMenuAnchor.getBoundingClientRect(); var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
var rectChat = var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
this.$refs.messageOperationsStrut.getBoundingClientRect();
var rectOps =
this.$refs.messageOperations.$el.getBoundingClientRect();
top = rectAnchor.top - rectChat.top - 50; top = rectAnchor.top - rectChat.top - 50;
left = rectAnchor.left - rectChat.left - 50; left = rectAnchor.left - rectChat.left - 50;
if (left + rectOps.width >= rectChat.right) { if (left + rectOps.width >= rectChat.right) {
@ -868,16 +764,10 @@ export default {
onRoomJoined(initialEventId) { onRoomJoined(initialEventId) {
// Was this room just created (by you)? Show a small info header in // Was this room just created (by you)? Show a small info header in
// that case! // that case!
const createEvent = this.room.currentState.getStateEvents( const createEvent = this.room.currentState.getStateEvents("m.room.create", "");
"m.room.create",
""
);
if (createEvent) { if (createEvent) {
const creatorId = createEvent.getContent().creator; const creatorId = createEvent.getContent().creator;
if ( if (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */) {
creatorId == this.$matrix.currentUserId &&
createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */
) {
this.showCreatedRoomWelcomeHeader = true; this.showCreatedRoomWelcomeHeader = true;
} }
} }
@ -890,11 +780,7 @@ export default {
//initialEventId = null; //initialEventId = null;
this.timelineSet = this.room.getUnfilteredTimelineSet(); this.timelineSet = this.room.getUnfilteredTimelineSet();
this.timelineWindow = new TimelineWindow( this.timelineWindow = new TimelineWindow(this.$matrix.matrixClient, this.timelineSet, {});
this.$matrix.matrixClient,
this.timelineSet,
{}
);
const self = this; const self = this;
this.timelineWindow this.timelineWindow
.load(initialEventId, 20) .load(initialEventId, 20)
@ -904,21 +790,18 @@ export default {
const getMoreIfNeeded = function _getMoreIfNeeded() { const getMoreIfNeeded = function _getMoreIfNeeded() {
const container = self.$refs.chatContainer; const container = self.$refs.chatContainer;
if ( if (
container.scrollHeight <= container.scrollHeight <= (1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
(1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
self.timelineWindow && self.timelineWindow &&
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS) self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
) { ) {
return self.timelineWindow return self.timelineWindow.paginate(EventTimeline.BACKWARDS, 10, true, 5).then((success) => {
.paginate(EventTimeline.BACKWARDS, 10, true, 5) self.events = self.timelineWindow.getEvents();
.then((success) => { if (success) {
self.events = self.timelineWindow.getEvents(); return _getMoreIfNeeded.call(self);
if (success) { } else {
return _getMoreIfNeeded.call(self); return Promise.reject("Failed to paginate");
} else { }
return Promise.reject("Failed to paginate"); });
}
});
} else { } else {
return Promise.resolve("Done"); return Promise.resolve("Done");
} }
@ -967,18 +850,11 @@ export default {
}, },
scrollToEndOfTimeline() { scrollToEndOfTimeline() {
if ( if (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS)
) {
this.loading = true; this.loading = true;
// Instead of paging though ALL history, just reload a timeline at the live marker... // Instead of paging though ALL history, just reload a timeline at the live marker...
var timelineSet = this.room.getUnfilteredTimelineSet(); var timelineSet = this.room.getUnfilteredTimelineSet();
var timelineWindow = new TimelineWindow( var timelineWindow = new TimelineWindow(this.$matrix.matrixClient, timelineSet, {});
this.$matrix.matrixClient,
timelineSet,
{}
);
const self = this; const self = this;
timelineWindow timelineWindow
.load(null, 20) .load(null, 20)
@ -1062,10 +938,7 @@ export default {
switch (event.getType()) { switch (event.getType()) {
case "m.room.member": case "m.room.member":
if (event.getContent().membership == "join") { if (event.getContent().membership == "join") {
if ( if (event.getPrevContent() && event.getPrevContent().membership == "join") {
event.getPrevContent() &&
event.getPrevContent().membership == "join"
) {
// We we already joined, so this must be a display name and/or avatar update! // We we already joined, so this must be a display name and/or avatar update!
return ContactChanged; return ContactChanged;
} else { } else {
@ -1163,14 +1036,8 @@ export default {
case "im.keanu.room_deletion_notice": { case "im.keanu.room_deletion_notice": {
// Custom event for notice 30 seconds before a room is deleted/purged. // Custom event for notice 30 seconds before a room is deleted/purged.
const deletionNotices = this.room.currentState.getStateEvents( const deletionNotices = this.room.currentState.getStateEvents("im.keanu.room_deletion_notice");
"im.keanu.room_deletion_notice" if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
);
if (
deletionNotices &&
deletionNotices.length > 0 &&
deletionNotices[deletionNotices.length - 1] == event
) {
// This is the latest/last one. Look at the status flag. Show nothing if it is "cancel". // This is the latest/last one. Look at the status flag. Show nothing if it is "cancel".
if (event.getContent().status != "cancel") { if (event.getContent().status != "cancel") {
return RoomDeletionNotice; return RoomDeletionNotice;
@ -1198,19 +1065,12 @@ export default {
if (container.scrollTop <= bufferHeight) { if (container.scrollTop <= bufferHeight) {
// Scrolled to top // Scrolled to top
this.handleScrolledToTop(); this.handleScrolledToTop();
} else if ( } else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) {
container.scrollHeight -
container.scrollTop.toFixed(0) -
container.clientHeight <=
bufferHeight
) {
this.handleScrolledToBottom(false); this.handleScrolledToBottom(false);
} }
this.showScrollToEnd = this.showScrollToEnd =
container.scrollHeight - container.scrollTop.toFixed(0) > container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight ||
container.clientHeight || (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
(this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
this.restartRRTimer(); this.restartRRTimer();
}, },
@ -1229,10 +1089,7 @@ export default {
// If we are at bottom, scroll to see new events... // If we are at bottom, scroll to see new events...
const container = this.$refs.chatContainer; const container = this.$refs.chatContainer;
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
if ( if (container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
container.scrollHeight - container.scrollTop.toFixed(0) ==
container.clientHeight
) {
scrollToSeeNew = true; scrollToSeeNew = true;
} }
if (this.initialLoadDone && event.forwardLooking && !event.isRelation()) { if (this.initialLoadDone && event.forwardLooking && !event.isRelation()) {
@ -1270,13 +1127,7 @@ export default {
sendMessage(text) { sendMessage(text) {
if (text && text.length > 0) { if (text && text.length > 0) {
util util
.sendTextMessage( .sendTextMessage(this.$matrix.matrixClient, this.roomId, text, this.editedEvent, this.replyToEvent)
this.$matrix.matrixClient,
this.roomId,
text,
this.editedEvent,
this.replyToEvent
)
.then(() => { .then(() => {
console.log("Sent message"); console.log("Sent message");
}) })
@ -1308,9 +1159,7 @@ export default {
dimensions: null, dimensions: null,
}; };
try { try {
this.currentImageInput.dimensions = sizeOf( this.currentImageInput.dimensions = sizeOf(dataUriToBuffer(e.target.result));
dataUriToBuffer(e.target.result)
);
// Need to resize? // Need to resize?
const w = this.currentImageInput.dimensions.width; const w = this.currentImageInput.dimensions.width;
@ -1318,9 +1167,7 @@ export default {
if (w > 640 || h > 640) { if (w > 640 || h > 640) {
var aspect = w / h; var aspect = w / h;
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed()); var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
var newHeight = parseInt( var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
(w > h ? 640 / aspect : 640).toFixed()
);
var imageResize = new ImageResize({ var imageResize = new ImageResize({
format: "png", format: "png",
width: newWidth, width: newWidth,
@ -1366,10 +1213,10 @@ export default {
onUploadProgress(p) { onUploadProgress(p) {
if (p.total) { if (p.total) {
this.currentSendProgress = this.$t( this.currentSendProgress = this.$t("message.upload_progress_with_total", {
"message.upload_progress_with_total", count: p.loaded || 0,
{ count: p.loaded || 0, total: p.total } total: p.total,
); });
} else { } else {
this.currentSendProgress = this.$t("message.upload_progress", { this.currentSendProgress = this.$t("message.upload_progress", {
count: p.loaded || 0, count: p.loaded || 0,
@ -1381,11 +1228,7 @@ export default {
this.$refs.attachment.value = null; this.$refs.attachment.value = null;
if (this.currentImageInputPath) { if (this.currentImageInputPath) {
var inputFile = this.currentImageInputPath; var inputFile = this.currentImageInputPath;
if ( if (this.currentImageInput && this.currentImageInput.scaled && this.currentImageInput.useScaled) {
this.currentImageInput &&
this.currentImageInput.scaled &&
this.currentImageInput.useScaled
) {
// Send scaled version of image instead! // Send scaled version of image instead!
inputFile = this.currentImageInput.scaled; inputFile = this.currentImageInput.scaled;
} }
@ -1497,7 +1340,7 @@ export default {
}, },
smoothScrollToEnd() { smoothScrollToEnd() {
this.$nextTick(function () { this.$nextTick(function() {
const container = this.$refs.chatContainer; const container = this.$refs.chatContainer;
if (container.children.length > 0) { if (container.children.length > 0) {
const lastChild = container.children[container.children.length - 1]; const lastChild = container.children[container.children.length - 1];
@ -1532,19 +1375,19 @@ export default {
setReplyToImage(event) { setReplyToImage(event) {
util util
.getThumbnail(this.$matrix.matrixClient, event) .getThumbnail(this.$matrix.matrixClient, event)
.then((url) => { .then((url) => {
this.replyToImg = url; this.replyToImg = url;
}) })
.catch((err) => { .catch((err) => {
console.log("Failed to fetch thumbnail: ", err); console.log("Failed to fetch thumbnail: ", err);
}); });
}, },
addReply(event) { addReply(event) {
this.replyToEvent = event; this.replyToEvent = event;
this.$refs.messageInput.focus(); this.$refs.messageInput.focus();
this.replyToContentType= event.getContent().msgtype; this.replyToContentType = event.getContent().msgtype;
this.setReplyToImage(event); this.setReplyToImage(event);
}, },
@ -1576,10 +1419,10 @@ export default {
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
setTimeout(function(){ setTimeout(function() {
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, 200) }, 200);
}) })
.catch((err) => { .catch((err) => {
console.log("Failed to fetch attachment: ", err); console.log("Failed to fetch attachment: ", err);
@ -1604,12 +1447,7 @@ export default {
sendQuickReaction(e) { sendQuickReaction(e) {
util util
.sendQuickReaction( .sendQuickReaction(this.$matrix.matrixClient, this.roomId, e.reaction, e.event)
this.$matrix.matrixClient,
this.roomId,
e.reaction,
e.event
)
.then(() => { .then(() => {
console.log("Quick reaction message"); console.log("Quick reaction message");
}) })
@ -1651,9 +1489,7 @@ export default {
{ {
name: "Chat", name: "Chat",
params: { params: {
roomId: util.sanitizeRoomId( roomId: util.sanitizeRoomId(room.getCanonicalAlias() || room.roomId),
room.getCanonicalAlias() || room.roomId
),
}, },
}, },
-1 -1
@ -1729,10 +1565,7 @@ export default {
this.$matrix.matrixClient this.$matrix.matrixClient
.sendReadReceipt(event) .sendReadReceipt(event)
.then(() => { .then(() => {
this.$matrix.matrixClient.setRoomReadMarkers( this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, event.getId());
this.room.roomId,
event.getId()
);
}) })
.then(() => { .then(() => {
console.log("RR sent for event: " + event.getId()); console.log("RR sent for event: " + event.getId());
@ -1830,11 +1663,7 @@ export default {
onHeaderClick() { onHeaderClick() {
const joinedRooms = this.$matrix.joinedRooms; const joinedRooms = this.$matrix.joinedRooms;
if ( if (joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == this.room.roomId) {
joinedRooms &&
joinedRooms.length == 1 &&
joinedRooms[0].roomId == this.room.roomId
) {
// Only joined to this room, go directly to room details! // Only joined to this room, go directly to room details!
this.$navigation.push({ name: "RoomInfo" }); this.$navigation.push({ name: "RoomInfo" });
return; return;
@ -1842,7 +1671,7 @@ export default {
this.$refs.roomInfoSheet.open(); this.$refs.roomInfoSheet.open();
}, },
onInvitationsClick() { onInvitationsClick() {
this.$navigation.push({ name: "Home" }, -1); this.$navigation.push({ name: "Home" }, -1);
}, },
}, },
}; };
@ -1850,4 +1679,4 @@ export default {
<style lang="scss"> <style lang="scss">
@import "@/assets/css/chat.scss"; @import "@/assets/css/chat.scss";
</style> </style>

View file

@ -7,7 +7,7 @@
@click.stop="onHeaderClicked" @click.stop="onHeaderClicked"
> >
<v-avatar size="40" class="me-2"> <v-avatar size="40" class="me-2">
<v-img :src="room.avatar || memberAvatar" /> <v-img v-if="room.avatar || memberAvatar" :src="room.avatar || memberAvatar" />
</v-avatar> </v-avatar>
</v-col> </v-col>

View file

@ -18,7 +18,10 @@
:value="room.roomId" :value="room.roomId"
> >
<v-list-item-avatar size="40" color="#e0e0e0"> <v-list-item-avatar size="40" color="#e0e0e0">
<v-img :src="room.avatar" /> <v-img v-if="room.avatar" :src="room.avatar" />
<span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase()
}}</span>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>{{ room.name }}</v-list-item-title> <v-list-item-title>{{ room.name }}</v-list-item-title>
@ -50,7 +53,10 @@
:value="room.roomId" :value="room.roomId"
> >
<v-list-item-avatar size="40" color="#e0e0e0"> <v-list-item-avatar size="40" color="#e0e0e0">
<v-img :src="room.avatar" /> <v-img v-if="room.avatar" :src="room.avatar" />
<span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase()
}}</span>
</v-list-item-avatar> </v-list-item-avatar>
<div class="room-list-notification-count" v-if="notificationCount(room) > 0"> <div class="room-list-notification-count" v-if="notificationCount(room) > 0">
{{ notificationCount(room) }} {{ notificationCount(room) }}

View file

@ -1,27 +1,60 @@
<template> <template>
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners"> <message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble poll-bubble"> <div class="bubble poll-bubble">
{{ pollQuestion }} <div class="poll-icon">
<v-container fluid> <svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.31462 16.4718C3.31462 16.9496 3.70026 17.3368 4.17609 17.3368L16.1385 17.3368C16.6144 17.3368 17 16.9496 17 16.4718L16.9998 13.6229C16.9998 13.1452 16.6142 12.7579 16.1383 12.7579L4.1764 12.7579C3.70056 12.7579 3.31492 13.1452 3.31492 13.6229L3.31512 16.4718L3.31462 16.4718Z"
fill="currentColor"
/>
<path
d="M3.31462 10.4557C3.31462 10.9335 3.70026 11.3208 4.17609 11.3208L11.3428 11.3208C11.8186 11.3208 12.2043 10.9335 12.2043 10.4557L12.2043 7.60711C12.2043 7.12931 11.8186 6.74208 11.3428 6.74208L4.17609 6.74208C3.70026 6.74208 3.31462 7.12932 3.31462 7.60711L3.31462 10.4557Z"
fill="currentColor"
/>
<path
d="M3.31451 1.59127L3.31451 4.44011C3.31451 4.91791 3.70016 5.30514 4.17598 5.30514L6.99509 5.30514C7.47093 5.30514 7.85657 4.91791 7.85657 4.44011L7.85637 1.59127C7.85637 1.11347 7.47073 0.726242 6.9949 0.726242L4.17599 0.726242C3.70035 0.726242 3.31452 1.11348 3.31452 1.59127L3.31451 1.59127Z"
fill="currentColor"
/>
<path
d="M-2.00529e-05 0.587841L-2.0791e-05 17.4747C-2.08052e-05 17.7995 0.262306 18.0625 0.585404 18.0625L1.38198 18.0625C1.70528 18.0625 1.96741 17.7995 1.96741 17.4747L1.96741 0.587841C1.96741 0.263208 1.70508 -1.14667e-08 1.38198 -2.55897e-08L0.585405 -6.04092e-08C0.261911 -7.45496e-08 -2.00387e-05 0.263213 -2.00529e-05 0.587841Z"
fill="currentColor"
/>
</svg>
</div>
<div class="poll-question">{{ pollQuestion }}</div>
<v-container fluid ma-0 pa-0>
<v-row <v-row
v-for="answer in pollAnswers" v-for="answer in pollAnswers"
:key="answer.id" :key="answer.id"
@click="pollAnswer(answer.id)" @click="pollAnswer(answer.id)"
:class="{ 'poll-answer': true, selected: answer.hasMyVote }" :class="{
'poll-answer': true,
selected: !userHasVoted && answer.id == pollTentativeAnswer,
result: userHasVoted || pollIsClosed,
winner: answer.winner,
}"
ma-0
pa-0
> >
<v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }}</v-col> <v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }} {{ answer.max }}</v-col>
<v-col v-if="answer.id == pollTentativeAnswer" cols="auto" class="ma-0 pa-0 poll-answer-title"
><v-img class="poll-check-icon" src="@/assets/icons/ic_check.svg"
/></v-col>
<v-col <v-col
v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
cols="auto" cols="auto"
class="ma-0 pa-0 poll-answer-num-votes" class="ma-0 pa-0 poll-answer-num-votes"
>{{ answer.numVotes }}</v-col >{{ answer.percentage }}%</v-col
> >
<div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator"> <div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator">
<div class="bar" :style="{ width: `${answer.percentage}%` }"></div> <div class="bar" :style="{ width: `${answer.percentage}%` }"></div>
</div> </div>
</v-row> </v-row>
<v-row class="poll-status"> <v-row class="poll-status">
<v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{ <v-col cols="auto" class="ma-0 pa-0 poll-status-title">
{{ $t("poll_create.num_answered", { count: pollNumAnswers }) }}
</v-col>
<!-- <v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{
pollIsClosed pollIsClosed
? $t("poll_create.poll_status_closed") ? $t("poll_create.poll_status_closed")
: pollIsDisclosed : pollIsDisclosed
@ -29,7 +62,7 @@
? $t("poll_create.poll_status_open") ? $t("poll_create.poll_status_open")
: $t("poll_create.poll_status_open_not_voted") : $t("poll_create.poll_status_open_not_voted")
: $t("poll_create.poll_status_disclosed") : $t("poll_create.poll_status_disclosed")
}}</v-col> }}</v-col> -->
<v-col <v-col
cols="auto" cols="auto"
class="ma-0 pa-0 poll-status-close clickable" class="ma-0 pa-0 poll-status-close clickable"
@ -38,6 +71,13 @@
>{{ $t("poll_create.close_poll") }}</v-col >{{ $t("poll_create.close_poll") }}</v-col
> >
</v-row> </v-row>
<v-row v-if="pollTentativeAnswer" justify="center">
<v-col cols="auto" class="ma-0 pa-0 poll-submit">
<v-btn @click.stop="pollAnswerSubmit">
{{ $t("poll_create.poll_submit") }}
</v-btn>
</v-col>
</v-row>
</v-container> </v-container>
</div> </div>
</message-incoming> </message-incoming>

View file

@ -1,27 +1,61 @@
<template> <template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners"> <message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble poll-bubble"> <div class="bubble poll-bubble">
{{ pollQuestion }} <div class="poll-icon">
<v-container fluid> <svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.31462 16.4718C3.31462 16.9496 3.70026 17.3368 4.17609 17.3368L16.1385 17.3368C16.6144 17.3368 17 16.9496 17 16.4718L16.9998 13.6229C16.9998 13.1452 16.6142 12.7579 16.1383 12.7579L4.1764 12.7579C3.70056 12.7579 3.31492 13.1452 3.31492 13.6229L3.31512 16.4718L3.31462 16.4718Z"
fill="currentColor"
/>
<path
d="M3.31462 10.4557C3.31462 10.9335 3.70026 11.3208 4.17609 11.3208L11.3428 11.3208C11.8186 11.3208 12.2043 10.9335 12.2043 10.4557L12.2043 7.60711C12.2043 7.12931 11.8186 6.74208 11.3428 6.74208L4.17609 6.74208C3.70026 6.74208 3.31462 7.12932 3.31462 7.60711L3.31462 10.4557Z"
fill="currentColor"
/>
<path
d="M3.31451 1.59127L3.31451 4.44011C3.31451 4.91791 3.70016 5.30514 4.17598 5.30514L6.99509 5.30514C7.47093 5.30514 7.85657 4.91791 7.85657 4.44011L7.85637 1.59127C7.85637 1.11347 7.47073 0.726242 6.9949 0.726242L4.17599 0.726242C3.70035 0.726242 3.31452 1.11348 3.31452 1.59127L3.31451 1.59127Z"
fill="currentColor"
/>
<path
d="M-2.00529e-05 0.587841L-2.0791e-05 17.4747C-2.08052e-05 17.7995 0.262306 18.0625 0.585404 18.0625L1.38198 18.0625C1.70528 18.0625 1.96741 17.7995 1.96741 17.4747L1.96741 0.587841C1.96741 0.263208 1.70508 -1.14667e-08 1.38198 -2.55897e-08L0.585405 -6.04092e-08C0.261911 -7.45496e-08 -2.00387e-05 0.263213 -2.00529e-05 0.587841Z"
fill="currentColor"
/>
</svg>
</div>
<div class="poll-question">{{ pollQuestion }}</div>
<v-container fluid ma-0 pa-0>
<v-row <v-row
v-for="answer in pollAnswers" v-for="answer in pollAnswers"
:key="answer.id" :key="answer.id"
@click="pollAnswer(answer.id)" @click="pollAnswer(answer.id)"
:class="{ 'poll-answer': true, selected: answer.hasMyVote }" :class="{
'poll-answer': true,
selected: !userHasVoted && answer.id == pollTentativeAnswer,
result: userHasVoted || pollIsClosed,
winner: answer.winner,
}"
ma-0
pa-0
> >
<v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }}</v-col> <v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }} {{ answer.max }}</v-col>
<v-col v-if="answer.id == pollTentativeAnswer" cols="auto" class="ma-0 pa-0 poll-answer-title"
><v-img class="poll-check-icon" src="@/assets/icons/ic_check.svg"
/></v-col>
<v-col <v-col
v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
cols="auto" cols="auto"
class="ma-0 pa-0 poll-answer-num-votes" class="ma-0 pa-0 poll-answer-num-votes"
>{{ answer.numVotes }}</v-col >{{ answer.percentage }}%</v-col
> >
<div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator"> <div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator">
<div class="bar" :style="{ width: `${answer.percentage}%` }"></div> <div class="bar" :style="{ width: `${answer.percentage}%` }"></div>
</div> </div>
</v-row> </v-row>
<v-row class="poll-status"> <v-row class="poll-status">
<v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{ <v-col cols="auto" class="ma-0 pa-0 poll-status-title">
{{ $t("poll_create.num_answered", { count: pollNumAnswers }) }}
</v-col>
<!-- <v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{
pollIsClosed pollIsClosed
? $t("poll_create.poll_status_closed") ? $t("poll_create.poll_status_closed")
: pollIsDisclosed : pollIsDisclosed
@ -29,7 +63,7 @@
? $t("poll_create.poll_status_open") ? $t("poll_create.poll_status_open")
: $t("poll_create.poll_status_open_not_voted") : $t("poll_create.poll_status_open_not_voted")
: $t("poll_create.poll_status_disclosed") : $t("poll_create.poll_status_disclosed")
}}</v-col> }}</v-col> -->
<v-col <v-col
cols="auto" cols="auto"
class="ma-0 pa-0 poll-status-close clickable" class="ma-0 pa-0 poll-status-close clickable"
@ -38,6 +72,13 @@
>{{ $t("poll_create.close_poll") }}</v-col >{{ $t("poll_create.close_poll") }}</v-col
> >
</v-row> </v-row>
<v-row v-if="pollTentativeAnswer" justify="center">
<v-col cols="auto" class="ma-0 pa-0 poll-submit">
<v-btn @click.stop="pollAnswerSubmit">
{{ $t("poll_create.poll_submit") }}
</v-btn>
</v-col>
</v-row>
</v-container> </v-container>
</div> </div>
</message-outgoing> </message-outgoing>

View file

@ -5,10 +5,12 @@ export default {
return { return {
pollQuestion: "", pollQuestion: "",
pollAnswers: [], pollAnswers: [],
pollTotalVotes: 0,
pollResponseRelations: null, pollResponseRelations: null,
pollEndRelations: null, pollEndRelations: null,
pollEndTs: null, pollEndTs: null,
pollIsDisclosed: true, pollIsDisclosed: true,
pollTentativeAnswer: null
} }
}, },
mounted() { mounted() {
@ -93,25 +95,51 @@ export default {
} }
} }
// Update percentage // Update percentages. For algorithm, see here: https://revs.runtime-revolution.com/getting-100-with-rounded-percentages-273ffa70252b
answerArray.forEach(a => { // (need to add up to 100%)
a.percentage = parseInt(((100 * a.numVotes) / totalVotes).toFixed(0)); let a = answerArray.map(a => {
let votes = (100 * a.numVotes) / totalVotes;
return {
answer: a,
unrounded: votes,
floor: Math.floor(votes)
}
});
a.sort((item1,item2) => {
Math.abs(item2.floor) - Math.abs(item1.floor);
});
var diff = 100 - a.reduce((previousValue, currentValue) => {
return previousValue + currentValue.floor;
}, 0);
const maxVotes = Math.max(...a.map(item => item.unrounded));
a.map((answer, index) => {
answer.answer.percentage = (index < diff) ? (answer.floor + 1) : answer.floor;
answer.answer.winner = (answer.unrounded == maxVotes);
}); });
this.pollAnswers = answerArray; this.pollAnswers = answerArray;
this.pollTotalVotes = totalVotes;
}, },
pollAnswer(id) { pollAnswer(id) {
if (this.pollIsClosed) { if (!this.userHasVoted) {
this.pollTentativeAnswer = id;
}
},
pollAnswerSubmit() {
if (!this.pollTentativeAnswer || this.pollIsClosed) {
return; return;
} }
util util
.sendPollAnswer( .sendPollAnswer(
this.$matrix.matrixClient, this.$matrix.matrixClient,
this.room.roomId, this.room.roomId,
[id], [this.pollTentativeAnswer],
this.event this.event
) )
.catch((err) => { .catch((err) => {
console.log("Failed to send:", err); console.log("Failed to send:", err);
})
.finally(() => {
this.pollTentativeAnswer = null;
}); });
}, },
pollClose() { pollClose() {
@ -148,6 +176,9 @@ export default {
userHasVoted() { userHasVoted() {
return this.pollAnswers.some(a => a.hasMyVote); return this.pollAnswers.some(a => a.hasMyVote);
}, },
pollNumAnswers() {
return this.pollTotalVotes;
},
pollIsAdmin() { pollIsAdmin() {
// Admins can view results of not-yet-closed undisclosed polls. // Admins can view results of not-yet-closed undisclosed polls.
const me = this.room && this.room.getMember(this.$matrix.currentUserId); const me = this.room && this.room.getMember(this.$matrix.currentUserId);