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-recording-color: red;
$voice-recorded-color: #3ae17d;
$poll-hilite-color: $very-very-purple;
$poll-hilite-color: #6360f0;
$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);
}
.send-options {
z-index: 11; // Above mic button
}
.message-operations-picker {
background-color: white;
text-align: center;

View file

@ -2,19 +2,57 @@
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 {
border: 1px solid #666;
border-radius: 5px;
padding: 10px;
margin: 10px;
border: 1px solid currentColor;
border-radius: 4px;
padding: 15px 14px;
margin: 0px;
&.winner {
font-weight: 700;
}
&.selected {
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 {
color: #444;
}
.poll-answer-num-votes {
font-size: 0.7rem;
font-size: 0.75rem;
}
justify-content: space-between;
position: relative;
@ -23,32 +61,49 @@
.poll-percent-indicator {
position: absolute;
bottom: 2px;
left: 2px;
right: 2px;
height: 4px;
left: 0px;
right: 0px;
height: 8px;
margin-top: 4px;
.bar {
background-color: $poll-hilite-color;
background-color: #7e7cf8;
position: absolute;
bottom: 0px;
left: 0px;
top: 0px;
border-radius: 3px;
border-radius: 4px;
}
}
.poll-status {
margin: 10px;
justify-content: space-between;
font-size: 13px;
line-height: 117%;
margin: 0px;
.poll-status-title {
font-size: 0.7rem;
}
.poll-status-close {
font-size: 0.7rem;
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
//
.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_open": "Poll is open",
"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>
<div class="chat-root fill-height d-flex flex-column">
<div class="chat-room-invitations clickable" v-if="invitationCount > 0" @click.stop="onInvitationsClick">
{{ $tc("room.invitations", invitationCount)}}
{{ $tc("room.invitations", invitationCount) }}
</div>
<ChatHeader
class="chat-header flex-grow-0 flex-shrink-0"
v-on:header-click="onHeaderClick"
/>
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" />
<div
class="chat-content flex-grow-1 flex-shrink-1"
ref="chatContainer"
@ -50,32 +47,15 @@
</div>
<!-- Handle resizes, e.g. when soft keyboard is shown/hidden -->
<resize-observer
ref="chatContainerResizer"
@notify="handleChatContainerResize"
/>
<resize-observer ref="chatContainerResizer" @notify="handleChatContainerResize" />
<CreatedRoomWelcomeHeader
v-if="showCreatedRoomWelcomeHeader"
v-on:close="closeCreateRoomWelcomeHeader"
/>
<CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />
<div
v-for="(event, index) in events"
:key="event.getId()"
:eventId="event.getId()"
>
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline -->
<div
v-if="showDayMarkerBeforeEvent(event)"
class="day-marker"
:title="dayForEvent(event)"
/>
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dayForEvent(event)" />
<div
v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()"
:ref="event.getId()"
>
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
<div
class="message-wrapper"
v-on:touchstart="
@ -92,13 +72,7 @@
:room="room"
:event="event"
:nextEvent="events[index + 1]"
:reactions="
timelineSet.getRelationsForEvent(
event.getId(),
'm.annotation',
'm.reaction'
)
"
:reactions="timelineSet.getRelationsForEvent(event.getId(), 'm.annotation', 'm.reaction')"
:timelineSet="timelineSet"
v-on:send-quick-reaction="sendQuickReaction"
v-on:context-menu="showContextMenuForEvent($event)"
@ -119,8 +93,8 @@
</div>
<!-- Input area -->
<v-container v-if="room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to':'']">
<div :class="[replyToEvent ? 'iput-area-inner-box':'']">
<v-container v-if="room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
<div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
<!-- "Scroll to end"-button -->
<v-btn
class="scroll-to-end"
@ -138,35 +112,20 @@
<div v-if="replyToEvent" class="row">
<div class="col">
<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.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 v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
{{ replyToEvent.getContent().body | latestReply }}
</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 class="col col-auto" v-if="replyToContentType !== 'm.text'">
<img
v-if="replyToContentType === 'm.image'"
width="150px"
: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"
/>
<img v-if="replyToContentType === 'm.image'" width="150px" :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 class="col col-auto">
<v-btn
fab
x-small
elevation="0"
color="black"
@click.stop="cancelEditReply"
>
<v-btn fab x-small elevation="0" color="black" @click.stop="cancelEditReply">
<v-icon color="white">cancel</v-icon>
</v-btn>
</div>
@ -199,17 +158,8 @@
/>
</v-col>
<v-col
class="input-area-button text-center flex-grow-0 flex-shrink-1"
v-if="editedEvent"
>
<v-btn
fab
small
elevation="0"
color="black"
@click.stop="cancelEditReply"
>
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-if="editedEvent">
<v-btn fab small elevation="0" color="black" @click.stop="cancelEditReply">
<v-icon color="white">cancel</v-icon>
</v-btn>
</v-col>
@ -244,10 +194,7 @@
</v-btn>
</v-col>
<v-col
class="input-area-button text-center flex-grow-0 flex-shrink-1"
v-else
>
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-else>
<v-btn
fab
small
@ -256,16 +203,11 @@
@click.stop="sendCurrentTextMessage"
:disabled="sendButtonDisabled"
>
<v-icon color="white">{{
editedEvent ? "save" : "arrow_upward"
}}</v-icon>
<v-icon color="white">{{ editedEvent ? "save" : "arrow_upward" }}</v-icon>
</v-btn>
</v-col>
<v-col
v-if="$config.shortCodeStickers"
class="input-area-button text-center flex-grow-0 flex-shrink-1"
>
<v-col v-if="$config.shortCodeStickers" class="input-area-button text-center flex-grow-0 flex-shrink-1">
<v-btn
v-if="!showRecorder"
id="btn-attach"
@ -305,7 +247,7 @@
<v-col
v-if="!showRecorder && canCreatePoll"
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>
<template v-slot:activator="{ on, attrs }">
@ -314,7 +256,29 @@
</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-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-menu>
</v-col>
@ -330,11 +294,7 @@
</v-container>
<div v-if="currentImageInputPath">
<v-dialog
v-model="currentImageInputPath"
class="ma-0 pa-0"
:width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'"
>
<v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'">
<v-card class="ma-0 pa-0">
<v-card-text class="ma-0 pa-2">
<v-img
@ -346,34 +306,16 @@
/>
<div>
file: {{ currentImageInputPath.name }}
<span
v-if="
currentImageInput &&
currentImageInput.scaled &&
currentImageInput.useScaled
"
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span
>
{{ currentImageInput.scaledDimensions.width }} x
{{ currentImageInput.scaledDimensions.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-else-if="currentImageInput && currentImageInput.dimensions">
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span
>
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
({{ formatBytes(currentImageInput.scaledSize) }})</span
>
<span v-else>
({{ formatBytes(currentImageInputPath.size) }})</span
>
<span v-else> ({{ formatBytes(currentImageInputPath.size) }})</span>
<v-switch
v-if="currentImageInput && currentImageInput.scaled"
:label="$t('message.scale_image')"
@ -403,33 +345,17 @@
</v-dialog>
</div>
<MessageOperationsBottomSheet
ref="messageOperationsSheet"
>
<VEmojiPicker
ref="emojiPicker"
@select="emojiSelected"
/>
<MessageOperationsBottomSheet ref="messageOperationsSheet">
<VEmojiPicker ref="emojiPicker" @select="emojiSelected" />
</MessageOperationsBottomSheet>
<StickerPickerBottomSheet
ref="stickerPickerSheet"
v-on:selectSticker="sendSticker"
/>
<StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" />
<!-- Loading indicator -->
<v-container
fluid
class="loading-indicator"
fill-height
v-if="!initialLoadDone || loading"
>
<v-container fluid class="loading-indicator" fill-height v-if="!initialLoadDone || loading">
<v-row align="center" justify="center">
<v-col class="text-center">
<v-progress-circular
indeterminate
color="primary"
></v-progress-circular>
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</v-col>
</v-row>
</v-container>
@ -437,36 +363,21 @@
<RoomInfoBottomSheet ref="roomInfoSheet" />
<!-- Dialog for audio recording not supported! -->
<v-dialog
v-model="showNoRecordingAvailableDialog"
class="ma-0 pa-0"
width="80%"
>
<v-dialog v-model="showNoRecordingAvailableDialog" class="ma-0 pa-0" width="80%">
<v-card>
<v-card-title>{{
$t("voice_recorder.not_supported_title")
}}</v-card-title>
<v-card-text
>{{ $t("voice_recorder.not_supported_text") }}
</v-card-text>
<v-card-title>{{ $t("voice_recorder.not_supported_title") }}</v-card-title>
<v-card-text>{{ $t("voice_recorder.not_supported_text") }} </v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
id="btn-ok"
color="primary"
text
@click="showNoRecordingAvailableDialog = false"
>{{ $t("menu.ok") }}</v-btn
>
<v-btn id="btn-ok" color="primary" text @click="showNoRecordingAvailableDialog = false">{{
$t("menu.ok")
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<CreatePollDialog
:show="showCreatePollDialog"
@close="showCreatePollDialog = false"
/>
<CreatePollDialog :show="showCreatePollDialog" @close="showCreatePollDialog = false" />
</div>
</template>
@ -532,20 +443,18 @@ function ScrollPosition(node) {
this.readyFor = "up";
}
ScrollPosition.prototype.restore = function () {
ScrollPosition.prototype.restore = function() {
if (this.readyFor === "up") {
this.node.scrollTop =
this.node.scrollHeight - this.previousScrollHeightMinusTop;
this.node.scrollTop = this.node.scrollHeight - this.previousScrollHeightMinusTop;
} else {
this.node.scrollTop = this.previousScrollTop;
}
};
ScrollPosition.prototype.prepareFor = function (direction) {
ScrollPosition.prototype.prepareFor = function(direction) {
this.readyFor = direction || "up";
if (this.readyFor === "up") {
this.previousScrollHeightMinusTop =
this.node.scrollHeight - this.node.scrollTop;
this.previousScrollHeightMinusTop = this.node.scrollHeight - this.node.scrollTop;
} else {
this.previousScrollTop = this.node.scrollTop;
}
@ -592,7 +501,7 @@ export default {
StickerPickerBottomSheet,
BottomSheet,
AvatarOperations,
CreatePollDialog
CreatePollDialog,
},
data() {
@ -662,12 +571,12 @@ export default {
filters: {
latestReply(contents) {
const contentArr = contents.split('\n').reverse();
if (contentArr[0] === '') {
const contentArr = contents.split("\n").reverse();
if (contentArr[0] === "") {
contentArr.shift();
}
return contentArr[0].replace(/^> (<.*> )?/g, '');
}
return contentArr[0].replace(/^> (<.*> )?/g, "");
},
},
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)
return this.lastRR.getId();
}
return (
this.fullyReadMarker ||
this.room.getEventReadUpTo(this.$matrix.currentUserId, false)
);
return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false);
},
fullyReadMarker() {
const readEvent = this.room.getAccountData("m.fully_read");
@ -727,11 +633,7 @@ export default {
return null;
},
attachButtonDisabled() {
return (
this.editedEvent != null ||
this.replyToEvent != null ||
this.currentInput.length > 0
);
return this.editedEvent != null || this.replyToEvent != null || this.currentInput.length > 0;
},
sendButtonDisabled() {
return this.currentInput.length == 0;
@ -760,8 +662,7 @@ export default {
if (ref && ref[0]) {
if (this.showAvatarMenuAnchor) {
var rectAnchor = this.showAvatarMenuAnchor.getBoundingClientRect();
var rectChat =
this.$refs.avatarOperationsStrut.getBoundingClientRect();
var rectChat = this.$refs.avatarOperationsStrut.getBoundingClientRect();
top = rectAnchor.top - rectChat.top;
left = rectAnchor.left - rectChat.left;
// if (left + 250 > rectChat.right) {
@ -783,9 +684,10 @@ export default {
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);
let isAdmin =
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
return isAdmin;
}
},
},
watch: {
@ -795,9 +697,7 @@ export default {
if (value && value == oldValue) {
return; // No change.
}
console.log(
"Chat: Current room changed to " + (value ? value : "null")
);
console.log("Chat: Current room changed to " + (value ? value : "null"));
// Clear old events
this.$matrix.off("Room.timeline", this.onEvent);
@ -839,18 +739,14 @@ export default {
this.$nextTick(() => {
// Calculate where to show the context menu.
//
const ref =
this.selectedEvent && this.$refs[this.selectedEvent.getId()];
const ref = this.selectedEvent && this.$refs[this.selectedEvent.getId()];
var top = 0;
var left = 0;
if (ref && ref[0]) {
if (this.showContextMenuAnchor) {
var rectAnchor =
this.showContextMenuAnchor.getBoundingClientRect();
var rectChat =
this.$refs.messageOperationsStrut.getBoundingClientRect();
var rectOps =
this.$refs.messageOperations.$el.getBoundingClientRect();
var rectAnchor = this.showContextMenuAnchor.getBoundingClientRect();
var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
top = rectAnchor.top - rectChat.top - 50;
left = rectAnchor.left - rectChat.left - 50;
if (left + rectOps.width >= rectChat.right) {
@ -868,16 +764,10 @@ export default {
onRoomJoined(initialEventId) {
// Was this room just created (by you)? Show a small info header in
// that case!
const createEvent = this.room.currentState.getStateEvents(
"m.room.create",
""
);
const createEvent = this.room.currentState.getStateEvents("m.room.create", "");
if (createEvent) {
const creatorId = createEvent.getContent().creator;
if (
creatorId == this.$matrix.currentUserId &&
createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */
) {
if (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */) {
this.showCreatedRoomWelcomeHeader = true;
}
}
@ -890,11 +780,7 @@ export default {
//initialEventId = null;
this.timelineSet = this.room.getUnfilteredTimelineSet();
this.timelineWindow = new TimelineWindow(
this.$matrix.matrixClient,
this.timelineSet,
{}
);
this.timelineWindow = new TimelineWindow(this.$matrix.matrixClient, this.timelineSet, {});
const self = this;
this.timelineWindow
.load(initialEventId, 20)
@ -904,21 +790,18 @@ export default {
const getMoreIfNeeded = function _getMoreIfNeeded() {
const container = self.$refs.chatContainer;
if (
container.scrollHeight <=
(1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
container.scrollHeight <= (1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
self.timelineWindow &&
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
) {
return self.timelineWindow
.paginate(EventTimeline.BACKWARDS, 10, true, 5)
.then((success) => {
self.events = self.timelineWindow.getEvents();
if (success) {
return _getMoreIfNeeded.call(self);
} else {
return Promise.reject("Failed to paginate");
}
});
return self.timelineWindow.paginate(EventTimeline.BACKWARDS, 10, true, 5).then((success) => {
self.events = self.timelineWindow.getEvents();
if (success) {
return _getMoreIfNeeded.call(self);
} else {
return Promise.reject("Failed to paginate");
}
});
} else {
return Promise.resolve("Done");
}
@ -967,18 +850,11 @@ export default {
},
scrollToEndOfTimeline() {
if (
this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS)
) {
if (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
this.loading = true;
// Instead of paging though ALL history, just reload a timeline at the live marker...
var timelineSet = this.room.getUnfilteredTimelineSet();
var timelineWindow = new TimelineWindow(
this.$matrix.matrixClient,
timelineSet,
{}
);
var timelineWindow = new TimelineWindow(this.$matrix.matrixClient, timelineSet, {});
const self = this;
timelineWindow
.load(null, 20)
@ -1062,10 +938,7 @@ export default {
switch (event.getType()) {
case "m.room.member":
if (event.getContent().membership == "join") {
if (
event.getPrevContent() &&
event.getPrevContent().membership == "join"
) {
if (event.getPrevContent() && event.getPrevContent().membership == "join") {
// We we already joined, so this must be a display name and/or avatar update!
return ContactChanged;
} else {
@ -1163,14 +1036,8 @@ export default {
case "im.keanu.room_deletion_notice": {
// Custom event for notice 30 seconds before a room is deleted/purged.
const deletionNotices = this.room.currentState.getStateEvents(
"im.keanu.room_deletion_notice"
);
if (
deletionNotices &&
deletionNotices.length > 0 &&
deletionNotices[deletionNotices.length - 1] == event
) {
const deletionNotices = this.room.currentState.getStateEvents("im.keanu.room_deletion_notice");
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".
if (event.getContent().status != "cancel") {
return RoomDeletionNotice;
@ -1198,19 +1065,12 @@ export default {
if (container.scrollTop <= bufferHeight) {
// Scrolled to top
this.handleScrolledToTop();
} else if (
container.scrollHeight -
container.scrollTop.toFixed(0) -
container.clientHeight <=
bufferHeight
) {
} else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) {
this.handleScrolledToBottom(false);
}
this.showScrollToEnd =
container.scrollHeight - container.scrollTop.toFixed(0) >
container.clientHeight ||
(this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight ||
(this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
this.restartRRTimer();
},
@ -1229,10 +1089,7 @@ export default {
// If we are at bottom, scroll to see new events...
const container = this.$refs.chatContainer;
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
if (
container.scrollHeight - container.scrollTop.toFixed(0) ==
container.clientHeight
) {
if (container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
scrollToSeeNew = true;
}
if (this.initialLoadDone && event.forwardLooking && !event.isRelation()) {
@ -1270,13 +1127,7 @@ export default {
sendMessage(text) {
if (text && text.length > 0) {
util
.sendTextMessage(
this.$matrix.matrixClient,
this.roomId,
text,
this.editedEvent,
this.replyToEvent
)
.sendTextMessage(this.$matrix.matrixClient, this.roomId, text, this.editedEvent, this.replyToEvent)
.then(() => {
console.log("Sent message");
})
@ -1308,9 +1159,7 @@ export default {
dimensions: null,
};
try {
this.currentImageInput.dimensions = sizeOf(
dataUriToBuffer(e.target.result)
);
this.currentImageInput.dimensions = sizeOf(dataUriToBuffer(e.target.result));
// Need to resize?
const w = this.currentImageInput.dimensions.width;
@ -1318,9 +1167,7 @@ export default {
if (w > 640 || h > 640) {
var aspect = w / h;
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
var newHeight = parseInt(
(w > h ? 640 / aspect : 640).toFixed()
);
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
var imageResize = new ImageResize({
format: "png",
width: newWidth,
@ -1366,10 +1213,10 @@ export default {
onUploadProgress(p) {
if (p.total) {
this.currentSendProgress = this.$t(
"message.upload_progress_with_total",
{ count: p.loaded || 0, total: p.total }
);
this.currentSendProgress = this.$t("message.upload_progress_with_total", {
count: p.loaded || 0,
total: p.total,
});
} else {
this.currentSendProgress = this.$t("message.upload_progress", {
count: p.loaded || 0,
@ -1381,11 +1228,7 @@ export default {
this.$refs.attachment.value = null;
if (this.currentImageInputPath) {
var inputFile = this.currentImageInputPath;
if (
this.currentImageInput &&
this.currentImageInput.scaled &&
this.currentImageInput.useScaled
) {
if (this.currentImageInput && this.currentImageInput.scaled && this.currentImageInput.useScaled) {
// Send scaled version of image instead!
inputFile = this.currentImageInput.scaled;
}
@ -1497,7 +1340,7 @@ export default {
},
smoothScrollToEnd() {
this.$nextTick(function () {
this.$nextTick(function() {
const container = this.$refs.chatContainer;
if (container.children.length > 0) {
const lastChild = container.children[container.children.length - 1];
@ -1532,19 +1375,19 @@ export default {
setReplyToImage(event) {
util
.getThumbnail(this.$matrix.matrixClient, event)
.then((url) => {
this.replyToImg = url;
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);
});
.getThumbnail(this.$matrix.matrixClient, event)
.then((url) => {
this.replyToImg = url;
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);
});
},
addReply(event) {
this.replyToEvent = event;
this.$refs.messageInput.focus();
this.replyToContentType= event.getContent().msgtype;
this.replyToContentType = event.getContent().msgtype;
this.setReplyToImage(event);
},
@ -1576,10 +1419,10 @@ export default {
document.body.appendChild(link);
link.click();
setTimeout(function(){
setTimeout(function() {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 200)
}, 200);
})
.catch((err) => {
console.log("Failed to fetch attachment: ", err);
@ -1604,12 +1447,7 @@ export default {
sendQuickReaction(e) {
util
.sendQuickReaction(
this.$matrix.matrixClient,
this.roomId,
e.reaction,
e.event
)
.sendQuickReaction(this.$matrix.matrixClient, this.roomId, e.reaction, e.event)
.then(() => {
console.log("Quick reaction message");
})
@ -1651,9 +1489,7 @@ export default {
{
name: "Chat",
params: {
roomId: util.sanitizeRoomId(
room.getCanonicalAlias() || room.roomId
),
roomId: util.sanitizeRoomId(room.getCanonicalAlias() || room.roomId),
},
},
-1
@ -1729,10 +1565,7 @@ export default {
this.$matrix.matrixClient
.sendReadReceipt(event)
.then(() => {
this.$matrix.matrixClient.setRoomReadMarkers(
this.room.roomId,
event.getId()
);
this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, event.getId());
})
.then(() => {
console.log("RR sent for event: " + event.getId());
@ -1830,11 +1663,7 @@ export default {
onHeaderClick() {
const joinedRooms = this.$matrix.joinedRooms;
if (
joinedRooms &&
joinedRooms.length == 1 &&
joinedRooms[0].roomId == this.room.roomId
) {
if (joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == this.room.roomId) {
// Only joined to this room, go directly to room details!
this.$navigation.push({ name: "RoomInfo" });
return;
@ -1842,7 +1671,7 @@ export default {
this.$refs.roomInfoSheet.open();
},
onInvitationsClick() {
this.$navigation.push({ name: "Home" }, -1);
this.$navigation.push({ name: "Home" }, -1);
},
},
};
@ -1850,4 +1679,4 @@ export default {
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>
</style>

View file

@ -7,7 +7,7 @@
@click.stop="onHeaderClicked"
>
<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-col>

View file

@ -18,7 +18,10 @@
:value="room.roomId"
>
<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-content>
<v-list-item-title>{{ room.name }}</v-list-item-title>
@ -50,7 +53,10 @@
:value="room.roomId"
>
<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>
<div class="room-list-notification-count" v-if="notificationCount(room) > 0">
{{ notificationCount(room) }}

View file

@ -1,27 +1,60 @@
<template>
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble poll-bubble">
{{ pollQuestion }}
<v-container fluid>
<div class="poll-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>
</div>
<div class="poll-question">{{ pollQuestion }}</div>
<v-container fluid ma-0 pa-0>
<v-row
v-for="answer in pollAnswers"
:key="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-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
cols="auto"
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 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">{{
<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
? $t("poll_create.poll_status_closed")
: pollIsDisclosed
@ -29,7 +62,7 @@
? $t("poll_create.poll_status_open")
: $t("poll_create.poll_status_open_not_voted")
: $t("poll_create.poll_status_disclosed")
}}</v-col>
}}</v-col> -->
<v-col
cols="auto"
class="ma-0 pa-0 poll-status-close clickable"
@ -38,6 +71,13 @@
>{{ $t("poll_create.close_poll") }}</v-col
>
</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>
</div>
</message-incoming>

View file

@ -1,27 +1,61 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble poll-bubble">
{{ pollQuestion }}
<v-container fluid>
<div class="poll-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>
</div>
<div class="poll-question">{{ pollQuestion }}</div>
<v-container fluid ma-0 pa-0>
<v-row
v-for="answer in pollAnswers"
:key="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-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
cols="auto"
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 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">{{
<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
? $t("poll_create.poll_status_closed")
: pollIsDisclosed
@ -29,7 +63,7 @@
? $t("poll_create.poll_status_open")
: $t("poll_create.poll_status_open_not_voted")
: $t("poll_create.poll_status_disclosed")
}}</v-col>
}}</v-col> -->
<v-col
cols="auto"
class="ma-0 pa-0 poll-status-close clickable"
@ -38,6 +72,13 @@
>{{ $t("poll_create.close_poll") }}</v-col
>
</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>
</div>
</message-outgoing>

View file

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