Merge branch 'style-channels' into 'dev'
Style channels See merge request keanuapp/keanuapp-weblite!319
This commit is contained in:
commit
ecd7bd8090
25 changed files with 821 additions and 88 deletions
200
src/assets/css/channel.scss
Normal file
200
src/assets/css/channel.scss
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
.chat-root.channel {
|
||||
background-color: #f2f2f2;
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
align-self: center;
|
||||
background-color: white;
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
.messageOut,
|
||||
.messageIn,
|
||||
.messageOut.from-admin,
|
||||
.messageIn.from-admin {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
margin: 0 0 8px 0;
|
||||
padding: 16px 0 0 0;
|
||||
text-align: start;
|
||||
.senderAndTime {
|
||||
order: 2;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 2px 12px 0 12px;
|
||||
.sender {
|
||||
font-size: 12 * $chat-text-size;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: #1c1c31;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
.time {
|
||||
font-size: 12 * $chat-text-size;
|
||||
margin: 0;
|
||||
color: #5f5f5f;
|
||||
}
|
||||
.status {
|
||||
font-size: 12 * $chat-text-size;
|
||||
margin: 0 0 0 10px;
|
||||
color: #5f5f5f;
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
order: 1;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
min-width: 40px !important;
|
||||
min-height: 40px !important;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.pin-icon {
|
||||
order: 3;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.op-button {
|
||||
order: 4;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.content {
|
||||
order: 5;
|
||||
flex: 0 0 100%;
|
||||
margin-top: 24px;
|
||||
.message {
|
||||
color: black !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.bubble {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
color: black !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0 15px 0 15px;
|
||||
}
|
||||
.bubble.image-bubble {
|
||||
/* full bleed */
|
||||
padding: 0 0 0 0;
|
||||
border-radius: 0 !important;
|
||||
.v-image {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
.quick-reaction-container {
|
||||
order: 6;
|
||||
flex: 0 0 100%;
|
||||
margin: 24px 7px 0 7px;
|
||||
padding: 0 8px 16px 8px;
|
||||
.emoji {
|
||||
font-size: 12 * $chat-text-size;
|
||||
font-weight: 500;
|
||||
color: #1c1c31;
|
||||
.v-icon {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
}
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Make all images 'cover' */
|
||||
.v-image__image {
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
color: #1c1c31 !important;
|
||||
.currentColor {
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
.v-icon {
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-answer {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.messageOut.pinned,
|
||||
.messageIn.pinned,
|
||||
.messageOut.from-admin.pinned,
|
||||
.messageIn.from-admin.pinned {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
|
||||
.message-operations {
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
background-color: white;
|
||||
height: fit-content;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.15);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Style file items (i.e. PDF files) */
|
||||
.thumbnail-item.file-item {
|
||||
border: 1px solid black;
|
||||
border-radius: 8px;
|
||||
padding: 15px 40px 15px 60px;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
&::after {
|
||||
content: " ";
|
||||
background: url("~@/assets/icons/ic_export.svg") no-repeat;
|
||||
background-position: 0 0;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.swipeable-thumbnails-view {
|
||||
margin-left: -15px;
|
||||
margin-right: -15px;
|
||||
order: 2;
|
||||
.thumbnail-item.file-item {
|
||||
margin: 15px;
|
||||
width: auto;
|
||||
}
|
||||
.indicator-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
.indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
margin: 0 2.5px;
|
||||
background: #D9D9D9;
|
||||
&.current {
|
||||
background: #1C1C31;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
@import "@/assets/css/main.scss";
|
||||
@import "@/assets/css/vendors/v-emoji-picker";
|
||||
@import "@/assets/css/filedrop.scss";
|
||||
@import "@/assets/css/channel.scss";
|
||||
|
||||
$admin-bg: black;
|
||||
$admin-fg: white;
|
||||
|
|
@ -341,8 +342,12 @@ body {
|
|||
|
||||
.scroll-to-end {
|
||||
position: absolute;
|
||||
top: -64px;
|
||||
bottom: 20px;
|
||||
right: 16px;
|
||||
&.reversed {
|
||||
top: 120px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.op-button {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.poll-answer:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.poll-percent-indicator {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
|
|
|
|||
9
src/assets/icons/ic_edit.vue
Normal file
9
src/assets/icons/ic_edit.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 6L18 11" stroke="black" stroke-width="1.2" />
|
||||
<path d="M5 14L10 19" stroke="black" stroke-width="1.2" />
|
||||
<path
|
||||
d="M14.4692 4.98995L5.19822 14.2609C5.18642 14.2727 5.17463 14.2845 5.16288 14.2962C5.00326 14.4554 4.84981 14.6084 4.74141 14.7999C4.63302 14.9913 4.58076 15.2016 4.52639 15.4204C4.52239 15.4365 4.51838 15.4526 4.51433 15.4688L3.62003 19.046C3.61762 19.0557 3.61518 19.0654 3.61272 19.0752C3.57411 19.2293 3.53044 19.4035 3.51593 19.5518C3.49978 19.7169 3.50127 20.0162 3.74255 20.2575C3.98384 20.4987 4.28307 20.5002 4.4482 20.4841C4.59644 20.4696 4.77068 20.4259 4.92474 20.3873C4.93457 20.3848 4.94432 20.3824 4.95397 20.38L4.80845 19.7979L4.95397 20.38L8.53117 19.4857C8.54736 19.4816 8.56352 19.4776 8.57962 19.4736C8.79839 19.4192 9.0087 19.367 9.20015 19.2586L8.90453 18.7365L9.20015 19.2586C9.39159 19.1502 9.5446 18.9967 9.70378 18.8371C9.7155 18.8254 9.72725 18.8136 9.73906 18.8018L19.0101 9.53079L19.0369 9.50395C19.3472 9.19367 19.6215 8.91946 19.8128 8.66869C20.0202 8.39688 20.1858 8.08335 20.1858 7.69231C20.1858 7.30127 20.0202 6.98774 19.8128 6.71592C19.6215 6.46516 19.3472 6.19095 19.0369 5.88066L19.0101 5.85383L18.1462 4.98995L18.1193 4.96311C17.8091 4.65278 17.5348 4.37853 17.2841 4.1872C17.0123 3.97981 16.6987 3.81421 16.3077 3.81421C15.9167 3.81421 15.6031 3.97981 15.3313 4.1872C15.0805 4.37853 14.8063 4.6528 14.496 4.96314L14.4692 4.98995Z"
|
||||
stroke="black" stroke-width="1.2" />
|
||||
</svg>
|
||||
</template>
|
||||
4
src/assets/icons/ic_export.svg
Normal file
4
src/assets/icons/ic_export.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.55 3H7.25C7.71944 3 8.1 3.38056 8.1 3.85C8.1 4.28591 7.77187 4.64518 7.34913 4.69428L7.25 4.7H5.55C5.11409 4.7 4.75482 5.02813 4.70572 5.45087L4.7 5.55V17.45C4.7 17.8859 5.02813 18.2452 5.45087 18.2943L5.55 18.3H17.45C17.8859 18.3 18.2452 17.9719 18.2943 17.5491L18.3 17.45V15.75C18.3 15.2806 18.6806 14.9 19.15 14.9C19.5859 14.9 19.9452 15.2281 19.9943 15.6509L20 15.75V17.45C20 18.808 18.9384 19.9181 17.5998 19.9957L17.45 20H5.55C4.19197 20 3.08189 18.9384 3.00433 17.5998L3 17.45V5.55C3 4.19197 4.06158 3.08189 5.40017 3.00433L5.55 3ZM19.15 3L19.2188 3.00255L19.3206 3.0172L19.4153 3.04228L19.5097 3.07962L19.5926 3.1241L19.6742 3.18087L19.751 3.24896L19.8331 3.34399L19.894 3.43855L19.9204 3.49037L19.9491 3.5596L19.9695 3.62391L19.9941 3.74962L20 3.85V8.95C20 9.41944 19.6194 9.8 19.15 9.8C18.6806 9.8 18.3 9.41944 18.3 8.95V5.9019L12.951 11.251C12.6446 11.5575 12.1625 11.581 11.829 11.3218L11.749 11.251C11.4425 10.9446 11.419 10.4625 11.6782 10.129L11.749 10.049L17.0964 4.7H14.05C13.5806 4.7 13.2 4.31944 13.2 3.85C13.2 3.38056 13.5806 3 14.05 3H19.15Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
7
src/assets/icons/ic_like.vue
Normal file
7
src/assets/icons/ic_like.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0.937496 8.11875C0.937496 12.4078 4.59281 16.6369 10.3003 20.2444C10.6172 20.4347 10.9941 20.625 11.25 20.625C11.5153 20.625 11.8931 20.4347 12.1997 20.2444C17.9062 16.6369 21.5625 12.4069 21.5625 8.11875C21.5625 4.44 18.9797 1.875 15.63 1.875C13.6903 1.875 12.1687 2.75625 11.25 4.08C10.3519 2.7675 8.82 1.875 6.87 1.875C3.53062 1.875 0.937496 4.44 0.937496 8.11875Z"
|
||||
stroke="#1C1C31" stroke-width="1.875" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/ic_like_filled.vue
Normal file
7
src/assets/icons/ic_like_filled.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0.937496 8.11875C0.937496 12.4078 4.59281 16.6369 10.3003 20.2444C10.6172 20.4347 10.9941 20.625 11.25 20.625C11.5153 20.625 11.8931 20.4347 12.1997 20.2444C17.9062 16.6369 21.5625 12.4069 21.5625 8.11875C21.5625 4.44 18.9797 1.875 15.63 1.875C13.6903 1.875 12.1687 2.75625 11.25 4.08C10.3519 2.7675 8.82 1.875 6.87 1.875C3.53062 1.875 0.937496 4.44 0.937496 8.11875Z"
|
||||
stroke="#1C1C31" stroke-width="1.875" fill="#1C1C31" />
|
||||
</svg>
|
||||
</template>
|
||||
8
src/assets/icons/ic_pin.vue
Normal file
8
src/assets/icons/ic_pin.vue
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.7073 13.6872L17.6164 10.9004C17.7545 10.6988 17.9912 10.588 18.2345 10.6111C18.4308 10.6297 18.6254 10.5611 18.7668 10.4236L19.0964 10.1028C19.5543 9.6572 19.7832 9.43441 19.855 9.17105C19.9019 8.9989 19.9019 8.81733 19.855 8.64518C19.7832 8.38182 19.5543 8.15903 19.0964 7.71343L16.4271 5.11574C16.0064 4.7064 15.7961 4.50174 15.5519 4.43288C15.3745 4.38285 15.1866 4.38285 15.0092 4.43288C14.765 4.50174 14.5547 4.7064 14.134 5.11574L13.7465 5.4929C13.5929 5.64236 13.5245 5.85893 13.5645 6.06947C13.613 6.32422 13.5024 6.58294 13.2849 6.72408L10.4028 8.59391L10.4027 8.59393C9.91388 8.91108 9.66945 9.06966 9.41623 9.20907C8.85769 9.51658 8.2644 9.75622 7.64897 9.92288C7.36996 9.99843 7.08396 10.0541 6.51198 10.1654L6.51195 10.1654L5.66826 10.3296C5.08079 10.444 4.80785 11.1271 5.15478 11.6148C7.16198 14.4362 9.66135 16.8728 12.533 18.8075L12.6081 18.858C13.1023 19.191 13.7762 18.9158 13.8962 18.3321L14.0479 17.5941C14.1585 17.0557 14.2138 16.7865 14.2867 16.524C14.4737 15.8504 14.7481 15.2043 15.103 14.6021C15.2414 14.3674 15.3967 14.1406 15.7073 13.6872Z"
|
||||
stroke="#222222" stroke-width="1.2" />
|
||||
<path d="M8.30331 15.668L3.00001 20.9713" stroke="black" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</template>
|
||||
8
src/assets/icons/ic_pin_filled.vue
Normal file
8
src/assets/icons/ic_pin_filled.vue
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.2072 13.6872L18.1163 10.9004C18.2545 10.6988 18.4911 10.588 18.7344 10.6111C18.9307 10.6297 19.1254 10.5611 19.2667 10.4236L19.5963 10.1028C20.0542 9.6572 20.2832 9.43441 20.3549 9.17105C20.4018 8.9989 20.4018 8.81733 20.3549 8.64518C20.2832 8.38182 20.0542 8.15903 19.5963 7.71343L16.927 5.11574L16.927 5.11573C16.5063 4.7064 16.296 4.50174 16.0518 4.43288C15.8744 4.38285 15.6865 4.38285 15.5091 4.43288C15.2649 4.50174 15.0546 4.7064 14.6339 5.11574L14.2464 5.4929C14.0928 5.64236 14.0245 5.85893 14.0645 6.06947C14.1129 6.32422 14.0023 6.58294 13.7848 6.72408L10.9027 8.59391C10.4138 8.91107 10.1694 9.06965 9.91614 9.20907C9.3576 9.51658 8.7643 9.75622 8.14887 9.92288C7.86986 9.99843 7.58386 10.0541 7.01186 10.1654L6.16816 10.3296C5.5807 10.444 5.30775 11.1271 5.65469 11.6148C7.66189 14.4362 10.1613 16.8728 13.0329 18.8075L13.108 18.858C13.6022 19.191 14.2761 18.9158 14.3961 18.3321L14.5478 17.5941L14.5478 17.5941C14.6584 17.0557 14.7137 16.7865 14.7866 16.524C14.9736 15.8504 15.248 15.2043 15.603 14.6021C15.7413 14.3674 15.8966 14.1406 16.2072 13.6872L16.2072 13.6872Z"
|
||||
fill="black" stroke="#222222" stroke-width="1.2" />
|
||||
<path d="M8.80322 15.668L3.49992 20.9713" stroke="black" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -46,7 +46,9 @@
|
|||
"user_kick_and_ban": "Kick out",
|
||||
"user_make_admin": "Make admin",
|
||||
"user_make_moderator": "Make moderator",
|
||||
"user_revoke_moderator": "Revoke moderator"
|
||||
"user_revoke_moderator": "Revoke moderator",
|
||||
"pin": "Pin post",
|
||||
"unpin": "Unpin post"
|
||||
},
|
||||
"message": {
|
||||
"you": "You",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="chat-root fill-height d-flex flex-column" :style="chatContainerStyle">
|
||||
<div :class="{'chat-root': true, 'fill-height': true, 'd-flex': true, 'flex-column': true, 'channel': roomDisplayType == ROOM_TYPE_CHANNEL}" :style="chatContainerStyle">
|
||||
<ChatHeaderPrivate class="chat-header flex-grow-0 flex-shrink-0"
|
||||
v-on:header-click="onHeaderClick"
|
||||
v-on:view-room-details="viewRoomDetails"
|
||||
|
|
@ -16,8 +16,8 @@
|
|||
:readMarker="readMarker"
|
||||
:recordingMembers="typingMembers"
|
||||
v-on:start-recording="setShowRecorder()"
|
||||
v-on:loadnext="handleScrolledToBottom(false)"
|
||||
v-on:loadprevious="handleScrolledToTop()"
|
||||
v-on:loadnext="handleScrolledToLatest(false)"
|
||||
v-on:loadprevious="handleScrolledToOldest()"
|
||||
v-on:mark-read="sendRR"
|
||||
v-on:sendclap="sendClapReactionAtTime"
|
||||
/>
|
||||
|
|
@ -35,17 +35,28 @@
|
|||
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
|
||||
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
|
||||
<div ref="messageOperationsStrut" class="message-operations-strut">
|
||||
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
|
||||
showContextMenu = false;
|
||||
" v-if="showMessageOperations" v-on:addreaction="addReaction" v-on:addquickreaction="addQuickReaction"
|
||||
v-on:addreply="addReply(selectedEvent)" v-on:edit="edit(selectedEvent)" v-on:redact="redact(selectedEvent)"
|
||||
v-on:download="download(selectedEvent)" v-on:more="
|
||||
<component :is="messageOperationsComponent" ref="messageOperations" :style="opStyle"
|
||||
v-if="showMessageOperations"
|
||||
v-on:close="showContextMenu = false;"
|
||||
:emojis="recentEmojis"
|
||||
:originalEvent="selectedEvent"
|
||||
:timelineSet="timelineSet"
|
||||
v-on:pin="pin(selectedEvent)"
|
||||
v-on:unpin="unpin(selectedEvent)"
|
||||
v-on:addreaction="addReaction"
|
||||
v-on:addquickreaction="addQuickReaction"
|
||||
v-on:addreply="addReply(selectedEvent)"
|
||||
v-on:edit="edit(selectedEvent)"
|
||||
v-on:redact="redact(selectedEvent)"
|
||||
v-on:download="download(selectedEvent)"
|
||||
v-on:more="
|
||||
isEmojiQuickReaction= true
|
||||
showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
|
||||
" :originalEvent="selectedEvent" :timelineSet="timelineSet"
|
||||
"
|
||||
:userCanSendReactionAndAnswerPoll="$matrix.userCanSendReactionAndAnswerPollInCurrentRoom"
|
||||
:userCanSendMessage="$matrix.userCanSendMessageInCurrentRoom"
|
||||
/>
|
||||
:userCanPin="canCreatePoll"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Handle resizes, e.g. when soft keyboard is shown/hidden -->
|
||||
|
|
@ -56,23 +67,25 @@
|
|||
<!-- If we have a retention timer, it means we have active message retention. Show header. -->
|
||||
<WelcomeHeaderChannelUser v-if="retentionTimer && !roomWelcomeHeader && newlyJoinedRoom" />
|
||||
|
||||
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
|
||||
<div v-for="(event) in filteredEvents" :key="event.getId()" :eventId="event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER ? event.getId() : undefined">
|
||||
|
||||
<!-- DAY Marker, shown for every new day in the timeline -->
|
||||
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
|
||||
<div v-if="!reverseOrder && showDayMarkerBeforeEvent(event) && !!event.component && event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
|
||||
|
||||
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
|
||||
<div :ref="event.getId()">
|
||||
<MessageErrorHandler>
|
||||
<div class="message-wrapper" v-on:touchstart="
|
||||
(e) => {
|
||||
touchStart(e, event);
|
||||
}
|
||||
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
|
||||
<!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component.
|
||||
|
||||
<!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component.
|
||||
We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event
|
||||
that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}",
|
||||
see below. Otherwise things like context menus won't work as designed.
|
||||
-->
|
||||
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
|
||||
<component :is="event.component" :room="room" :originalEvent="event" :nextEvent="event.nextDisplayedEvent"
|
||||
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
|
||||
:componentFn="componentForEvent"
|
||||
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
|
||||
|
|
@ -89,24 +102,29 @@
|
|||
/>
|
||||
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
||||
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}<br /><br /></div> -->
|
||||
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
|
||||
</div>
|
||||
</MessageErrorHandler>
|
||||
</div>
|
||||
|
||||
<!-- Day marker when reverseOrder is set -->
|
||||
<div v-if="reverseOrder && showDayMarkerBeforeEvent(event) && !!event.component && event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
|
||||
|
||||
</div>
|
||||
|
||||
<NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
|
||||
|
||||
<!-- "Scroll to end"-button -->
|
||||
<v-btn v-if="!useVoiceMode" :class="{'scroll-to-end': true, 'reversed': reverseOrder}" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
|
||||
@click.stop="scrollToEndOfTimeline">
|
||||
<v-icon color="white">arrow_downward</v-icon>
|
||||
</v-btn>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Input area -->
|
||||
<v-container v-if="!useVoiceMode && !useFileModeNonAdmin && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
|
||||
<div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
|
||||
<!-- "Scroll to end"-button -->
|
||||
<v-btn v-if="!useVoiceMode" class="scroll-to-end" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
|
||||
@click.stop="scrollToEndOfTimeline">
|
||||
<v-icon color="white">arrow_downward</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-row class="ma-0 pa-0">
|
||||
<div v-if="replyToEvent" class="row">
|
||||
<div class="col">
|
||||
|
|
@ -274,6 +292,8 @@
|
|||
</v-card-text>
|
||||
</template>
|
||||
<v-divider></v-divider>
|
||||
<v-textarea v-if="showAttachmentCaptionInput" v-model="attachmentCaption" ref="attachmentCaption" color="black" background-color="transparent"
|
||||
solo full-width auto-grow rows="1" no-resize hide-details :placeholder="$t('file_mode.add_a_message')"></v-textarea>
|
||||
<v-card-actions>
|
||||
<v-spacer>
|
||||
<div v-if="currentSendError">{{ currentSendError }}</div>
|
||||
|
|
@ -281,7 +301,7 @@
|
|||
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel" :disabled="sendingStatus != sendStatuses.SENDING && sendingStatus != sendStatuses.INITIAL">
|
||||
{{ $t("menu.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment(undefined)"
|
||||
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment(attachmentCaption)"
|
||||
v-if="currentSendShowSendButton" :disabled="currentSendShowSendButton && sendingStatus != sendStatuses.INITIAL">{{ $t("menu.send") }}</v-btn>
|
||||
</v-card-actions>
|
||||
</template>
|
||||
|
|
@ -360,7 +380,7 @@ import UserProfileDialog from "./UserProfileDialog.vue"
|
|||
import BottomSheet from "./BottomSheet.vue";
|
||||
import ImageResize from "image-resize";
|
||||
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||
import chatMixin from "./chatMixin";
|
||||
import chatMixin, { ROOM_READ_MARKER_EVENT_PLACEHOLDER } from "./chatMixin";
|
||||
import sendAttachmentsMixin from "./sendAttachmentsMixin";
|
||||
import AudioLayout from "./AudioLayout.vue";
|
||||
import FileDropLayout from "./file_mode/FileDropLayout";
|
||||
|
|
@ -368,6 +388,7 @@ import roomTypeMixin from "./roomTypeMixin";
|
|||
import roomMembersMixin from "./roomMembersMixin";
|
||||
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
||||
import MessageErrorHandler from "./MessageErrorHandler";
|
||||
import MessageOperationsChannel from './messages/channel/MessageOperationsChannel.vue';
|
||||
|
||||
const sizeOf = require("image-size");
|
||||
const dataUriToBuffer = require("data-uri-to-buffer");
|
||||
|
|
@ -422,11 +443,15 @@ export default {
|
|||
UserProfileDialog,
|
||||
PurgeRoomDialog,
|
||||
WelcomeHeaderChannelUser,
|
||||
MessageErrorHandler
|
||||
MessageErrorHandler,
|
||||
MessageOperationsChannel
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
|
||||
ROOM_READ_MARKER_EVENT_PLACEHOLDER: ROOM_READ_MARKER_EVENT_PLACEHOLDER,
|
||||
|
||||
waitingForRoomObject: false,
|
||||
events: [],
|
||||
currentInput: "",
|
||||
|
|
@ -442,6 +467,7 @@ export default {
|
|||
currentSendShowSendButton: true,
|
||||
currentSendError: null,
|
||||
currentSendErrorExceededFile: null,
|
||||
attachmentCaption: undefined,
|
||||
showEmojiPicker: false,
|
||||
selectedEvent: null,
|
||||
editedEvent: null,
|
||||
|
|
@ -512,7 +538,8 @@ export default {
|
|||
heartPosition: {
|
||||
top: 0,
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
reverseOrder: false
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -569,6 +596,12 @@ export default {
|
|||
isCurrentFileInputsAnArray() {
|
||||
return Array.isArray(this.currentFileInputs)
|
||||
},
|
||||
showAttachmentCaptionInput() {
|
||||
// IFF we are sending one PDF, add option to set caption.
|
||||
const imageFiles = this.imageFiles || [];
|
||||
const otherFiles = this.nonImageFiles || [];
|
||||
return imageFiles.length == 0 && otherFiles.length == 1 && (otherFiles[0].type === "application/pdf" || (otherFiles[0].name || "").endsWith(".pdf"));
|
||||
},
|
||||
currentFileInputsDialog: {
|
||||
get() {
|
||||
return this.isCurrentFileInputsAnArray
|
||||
|
|
@ -680,6 +713,8 @@ export default {
|
|||
},
|
||||
|
||||
filteredEvents() {
|
||||
let events = this.events;
|
||||
|
||||
if (this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId)) {
|
||||
if (this.room.getHistoryVisibility() == "joined") {
|
||||
// For encrypted rooms where history is set to "joined" we can't read old events.
|
||||
|
|
@ -692,12 +727,46 @@ export default {
|
|||
(!e.getPrevContent() || e.getPrevContent().membership != "join") &&
|
||||
e.getStateKey() == this.$matrix.currentUserId) {
|
||||
// Our own join event.
|
||||
return this.events.slice(idx + 1);
|
||||
events = this.events.slice(idx + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.events;
|
||||
|
||||
// Filter out relations and redactions
|
||||
events = this.events.toReversed().filter((e) => !e.isRelation() && !e.isRedaction());
|
||||
|
||||
// If Channel, remove all redacted events as well.
|
||||
if (this.room && this.room.displayType == ROOM_TYPE_CHANNEL) {
|
||||
events = events.filter((e) => !e.isRedacted());
|
||||
}
|
||||
|
||||
// Add read marker, if it is not newer than the "latest" message we are going to display
|
||||
//
|
||||
let showReadMarker = false;
|
||||
let lastDisplayedEvent = undefined;
|
||||
events = events.flatMap((e) => {
|
||||
let result = [];
|
||||
Vue.set(e, "component", this.componentForEvent(e, false));
|
||||
if (e.getId() == this.readMarker && showReadMarker) {
|
||||
const readMarkerEvent = ROOM_READ_MARKER_EVENT_PLACEHOLDER;
|
||||
Vue.set(readMarkerEvent, "component", this.componentForEvent(readMarkerEvent, false));
|
||||
if (readMarkerEvent.component) {
|
||||
Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
|
||||
}
|
||||
result.push(readMarkerEvent);
|
||||
}
|
||||
if (e.component) {
|
||||
Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
|
||||
lastDisplayedEvent = e;
|
||||
if (e.getSender() !== this.$matrix.currentUserId) {
|
||||
showReadMarker = true;
|
||||
}
|
||||
}
|
||||
result.push(e);
|
||||
return result;
|
||||
})
|
||||
return (this.reverseOrder ? events : events.toReversed()) // Reverse back if needed
|
||||
},
|
||||
|
||||
roomCreatedByUsRecently() {
|
||||
|
|
@ -732,6 +801,12 @@ export default {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
messageOperationsComponent() {
|
||||
if (this.room.displayType == ROOM_TYPE_CHANNEL) {
|
||||
return MessageOperationsChannel;
|
||||
}
|
||||
return MessageOperations;
|
||||
},
|
||||
chatContainerStyle() {
|
||||
if (this.$config.chat_backgrounds && this.room && this.roomId) {
|
||||
const roomType = this.isDirectRoom ? "direct" : this.isPublicRoom ? "public" : "invite";
|
||||
|
|
@ -843,7 +918,7 @@ export default {
|
|||
this.onRoomNotJoined();
|
||||
} else {
|
||||
if (this.room) {
|
||||
this.onRoomJoined(this.readMarker);
|
||||
this.onRoomJoined(this.roomDisplayType == ROOM_TYPE_CHANNEL ? null : this.readMarker);
|
||||
} else {
|
||||
this.waitingForRoomObject = true;
|
||||
return; // no room, wait for it (we know we are joined so need to wait for sync to complete)
|
||||
|
|
@ -860,7 +935,7 @@ export default {
|
|||
// Were we waiting?
|
||||
if (this.room && this.room.roomId == this.roomId && this.waitingForRoomObject) {
|
||||
this.waitingForRoomObject = false;
|
||||
this.onRoomJoined(this.readMarker);
|
||||
this.onRoomJoined(this.roomDisplayType == ROOM_TYPE_CHANNEL ? null : this.readMarker);
|
||||
}
|
||||
},
|
||||
showMessageOperations(show) {
|
||||
|
|
@ -876,6 +951,12 @@ export default {
|
|||
var rectAnchor = this.showContextMenuAnchor.getBoundingClientRect();
|
||||
var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
|
||||
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
|
||||
if (this.room.displayType == ROOM_TYPE_CHANNEL) {
|
||||
top = rectAnchor.top - rectChat.top;
|
||||
let right = rectChat.right - rectAnchor.right;
|
||||
this.opStyle = "top:" + top + "px;right:" + right + "px";
|
||||
return;
|
||||
} else {
|
||||
top = rectAnchor.top - rectChat.top - 50;
|
||||
left = rectAnchor.left - rectChat.left - 75;
|
||||
if (left + rectOps.width + 10 >= rectChat.right) {
|
||||
|
|
@ -884,6 +965,7 @@ export default {
|
|||
left = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.opStyle = "top:" + top + "px;left:" + left + "px";
|
||||
});
|
||||
|
|
@ -906,10 +988,32 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* Set events to display. At the same time, filter out messages that are past rentention period etc.
|
||||
* Set events to display. At the same time, filter out messages that are past rentention period etc. Also, filter pinned events "at the top"
|
||||
*/
|
||||
setEvents(events) {
|
||||
this.events = this.filterOutOldAndInvisible(events);
|
||||
setEvents(events, onlyIfLengthChanges = false) {
|
||||
let updated = this.filterOutOldAndInvisible(events);
|
||||
|
||||
// Handle pinning
|
||||
//
|
||||
if (this.room) {
|
||||
const pinnedEvents = this.$matrix.getPinnedEvents(this.room);
|
||||
updated.forEach((e) => {
|
||||
Vue.set(e, "isPinned", pinnedEvents.includes(e.threadParent ? e.threadParent.getId() : e.getId()));
|
||||
});
|
||||
|
||||
updated = updated.sort((e1, e2) => {
|
||||
if (!e1.isPinned && !e2.isPinned) return 0;
|
||||
else if (e1.isPinned && !e2.isPinned) return this.reverseOrder ? 1 : -1;
|
||||
else if (e2.isPinned && !e1.isPinned) return this.reverseOrder ? -1 : 1;
|
||||
else {
|
||||
// Look at order in "pinned" value in the m.room.pinned_events event!
|
||||
return pinnedEvents.indexOf(e1.getId()) < pinnedEvents.indexOf(e2.getId()) ? (this.reverseOrder ? 1 : -1) : (this.reverseOrder ? -1 : 1)
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!onlyIfLengthChanges || updated.length != this.events.length) {
|
||||
this.events = updated; // Changed
|
||||
}
|
||||
},
|
||||
|
||||
filterOutOldAndInvisible(events) {
|
||||
|
|
@ -952,10 +1056,7 @@ export default {
|
|||
},
|
||||
|
||||
onRetentionTimer() {
|
||||
const events = this.filterOutOldAndInvisible(this.events);
|
||||
if (events.length != this.events.length) {
|
||||
this.events = events; // Changed
|
||||
}
|
||||
this.setEvents(this.events, true);
|
||||
},
|
||||
|
||||
onRoomJoined(initialEventId) {
|
||||
|
|
@ -969,6 +1070,9 @@ export default {
|
|||
this.newlyJoinedRoom = joinEvent.getLocalAge() < 1 * 60000 /* 1 minute */;
|
||||
}
|
||||
|
||||
this.reverseOrder = (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL);
|
||||
Vue.set(this.room, "displayType", this.roomDisplayType);
|
||||
|
||||
// Listen to events
|
||||
this.$matrix.on("Room.timeline", this.onEvent);
|
||||
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
||||
|
|
@ -1080,7 +1184,7 @@ export default {
|
|||
});
|
||||
} else {
|
||||
// Can't paginate, just scroll to bottom of window!
|
||||
this.smoothScrollToEnd();
|
||||
this.smoothScrollToLatest();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -1150,7 +1254,7 @@ export default {
|
|||
this.$nextTick(() => {
|
||||
const container = this.chatContainer;
|
||||
if (container && container.scrollHeight <= container.clientHeight) {
|
||||
this.handleScrolledToTop();
|
||||
this.handleScrolledToOldest();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -1162,15 +1266,23 @@ export default {
|
|||
const bufferHeight = container.clientHeight * WINDOW_BUFFER_SIZE;
|
||||
if (container.scrollTop <= bufferHeight) {
|
||||
// Scrolled to top
|
||||
this.handleScrolledToTop();
|
||||
if (this.reverseOrder) {
|
||||
this.handleScrolledToLatest(false);
|
||||
} else {
|
||||
this.handleScrolledToOldest();
|
||||
}
|
||||
} else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) {
|
||||
this.handleScrolledToBottom(false);
|
||||
if (this.reverseOrder) {
|
||||
this.handleScrolledToOldest();
|
||||
} else {
|
||||
this.handleScrolledToLatest(false);
|
||||
}
|
||||
}
|
||||
|
||||
this.showScrollToEnd =
|
||||
container.scrollHeight === container.clientHeight
|
||||
? false
|
||||
: container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight ||
|
||||
: (this.reverseOrder ? (container.scrollTop.toFixed(0) > 0) : (container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight)) ||
|
||||
(this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
|
||||
|
||||
this.restartRRTimer();
|
||||
|
|
@ -1260,16 +1372,19 @@ export default {
|
|||
this.paginateBackIfNeeded();
|
||||
}
|
||||
|
||||
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread)) {
|
||||
if (loadingDone && event.forwardLooking && (!(event.isRelation() || event.isRedaction()) || event.isMxThread || event.threadRootId || event.parentThread)) {
|
||||
// If we are at bottom, scroll to see new events...
|
||||
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
|
||||
const container = this.chatContainer;
|
||||
if (container) {
|
||||
if (container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
|
||||
if (this.reverseOrder && container.scrollTop.toFixed(0) == 0) {
|
||||
scrollToSeeNew = true;
|
||||
}
|
||||
else if (!this.reverseOrder && container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
|
||||
scrollToSeeNew = true;
|
||||
}
|
||||
}
|
||||
this.handleScrolledToBottom(scrollToSeeNew);
|
||||
this.handleScrolledToLatest(scrollToSeeNew);
|
||||
|
||||
// If kick or ban event, redirect to "goodbye"...
|
||||
if (event.getType() === "m.room.member" &&
|
||||
|
|
@ -1441,6 +1556,7 @@ export default {
|
|||
const promise = this.sendAttachments(text, this.currentFileInputs);
|
||||
promise.then(() => {
|
||||
this.currentFileInputs = null;
|
||||
this.attachmentCaption = undefined;
|
||||
this.sendingStatus = this.sendStatuses.INITIAL;
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
@ -1459,6 +1575,7 @@ export default {
|
|||
this.$refs.attachment.value = null;
|
||||
this.cancelSendAttachments();
|
||||
this.currentFileInputs = null;
|
||||
this.attachmentCaption = undefined;
|
||||
this.currentSendError = null;
|
||||
this.currentSendErrorExceededFile = null;
|
||||
this.sendingStatus = this.sendStatuses.INITIAL;
|
||||
|
|
@ -1494,7 +1611,7 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
handleScrolledToTop() {
|
||||
handleScrolledToOldest() {
|
||||
if (
|
||||
this.timelineWindow &&
|
||||
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS) &&
|
||||
|
|
@ -1505,7 +1622,7 @@ export default {
|
|||
.paginate(EventTimeline.BACKWARDS, 10, true)
|
||||
.then((success) => {
|
||||
if (success && this.scrollPosition) {
|
||||
this.scrollPosition.prepareFor("up");
|
||||
this.scrollPosition.prepareFor(this.reverseOrder ? "down" : "up");
|
||||
this.setEvents(this.timelineWindow.getEvents());
|
||||
this.$nextTick(() => {
|
||||
// restore scroll position!
|
||||
|
|
@ -1520,7 +1637,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
handleScrolledToBottom(scrollToEnd) {
|
||||
handleScrolledToLatest(smoothScrollToLatest) {
|
||||
if (
|
||||
this.timelineWindow &&
|
||||
this.timelineWindow.canPaginate(EventTimeline.FORWARDS) &&
|
||||
|
|
@ -1533,13 +1650,13 @@ export default {
|
|||
if (success) {
|
||||
this.setEvents(this.timelineWindow.getEvents());
|
||||
if (!this.useVoiceMode && this.scrollPosition) {
|
||||
this.scrollPosition.prepareFor("down");
|
||||
this.scrollPosition.prepareFor(this.reverseOrder ? "up" : "down");
|
||||
this.$nextTick(() => {
|
||||
// restore scroll position!
|
||||
console.log("Restore scroll!");
|
||||
this.scrollPosition.restore();
|
||||
if (scrollToEnd) {
|
||||
this.smoothScrollToEnd();
|
||||
if (smoothScrollToLatest) {
|
||||
this.smoothScrollToLatest();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1555,6 +1672,7 @@ export default {
|
|||
* Scroll so that the given event is at the middle of the chat view (if more events) or else at the bottom.
|
||||
*/
|
||||
scrollToEvent(eventId) {
|
||||
console.log("Scroll to event", eventId);
|
||||
const container = this.chatContainer;
|
||||
const ref = this.$refs[eventId];
|
||||
if (container && ref) {
|
||||
|
|
@ -1562,7 +1680,7 @@ export default {
|
|||
const item = ref[0].getBoundingClientRect();
|
||||
let offsetY = (parent.bottom - parent.top) / 2;
|
||||
if (ref[0].clientHeight > offsetY) {
|
||||
offsetY = Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
|
||||
offsetY = this.reverseOrder ? 0 : Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
|
||||
}
|
||||
const targetY = parent.top + offsetY;
|
||||
const currentY = item.top;
|
||||
|
|
@ -1573,10 +1691,21 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
smoothScrollToEnd() {
|
||||
smoothScrollToLatest() {
|
||||
this.$nextTick(function () {
|
||||
const container = this.chatContainer;
|
||||
if (container && container.children.length > 0) {
|
||||
if (this.reverseOrder) {
|
||||
const firstChild = container.children[0];
|
||||
console.log("Scroll into view", firstChild);
|
||||
window.requestAnimationFrame(() => {
|
||||
firstChild.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const lastChild = container.children[container.children.length - 1];
|
||||
console.log("Scroll into view", lastChild);
|
||||
window.requestAnimationFrame(() => {
|
||||
|
|
@ -1587,6 +1716,7 @@ export default {
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -1668,6 +1798,16 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
pin(event) {
|
||||
const eventToPin = event.parentThread ? event.parentThread : event;
|
||||
this.$matrix.setEventPinned(this.room, eventToPin, true);
|
||||
},
|
||||
|
||||
unpin(event) {
|
||||
const eventToUnpin = event.parentThread ? event.parentThread : event;
|
||||
this.$matrix.setEventPinned(this.room, eventToUnpin, false);
|
||||
},
|
||||
|
||||
cancelEditReply() {
|
||||
this.currentInput = "";
|
||||
this.editedEvent = null;
|
||||
|
|
@ -1809,8 +1949,14 @@ export default {
|
|||
const elFirst = util.getFirstVisibleElement(container, (item) => item.hasAttribute("eventId"));
|
||||
const elLast = util.getLastVisibleElement(container, (item) => item.hasAttribute("eventId"));
|
||||
if (elFirst && elLast) {
|
||||
eventIdFirst = elFirst.getAttribute("eventId");
|
||||
eventIdLast = elLast.getAttribute("eventId");
|
||||
if (this.reverseOrder) {
|
||||
// For reverse order, the "first visible" is actually later in time, so swap them
|
||||
eventIdFirst = elLast.getAttribute("eventId");
|
||||
eventIdLast = elFirst.getAttribute("eventId");
|
||||
} else {
|
||||
eventIdFirst = elFirst.getAttribute("eventId");
|
||||
eventIdLast = elLast.getAttribute("eventId");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eventIdFirst && eventIdLast) {
|
||||
|
|
@ -1964,9 +2110,9 @@ export default {
|
|||
if (!this.useVoiceMode) { // Voice mode has own autoplay handling inside "AudioLayout"!
|
||||
// Auto play consecutive audio messages, either incoming or sent.
|
||||
const filteredEvents = this.filteredEvents;
|
||||
const index = filteredEvents.findIndex(e => e.getId() === matrixEventId);
|
||||
if (index >= 0 && index < (filteredEvents.length - 1)) {
|
||||
const nextEvent = filteredEvents[index + 1];
|
||||
const event = filteredEvents.find(e => e.getId() === matrixEventId);
|
||||
if (event && event.nextDisplayedEvent) {
|
||||
const nextEvent = event.nextDisplayedEvent;
|
||||
if (nextEvent.getContent().msgtype === "m.audio") {
|
||||
// Yes, audio event!
|
||||
this.$audioPlayer.play(nextEvent, this.timelineSet);
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ export default {
|
|||
this.cancelled = true;
|
||||
},
|
||||
async getEvents() {
|
||||
// TODO - Handle pinned messages?
|
||||
const eventsPerBatch = 100;
|
||||
let batchToken = null;
|
||||
var nToFetch = null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import util, { STATE_EVENT_ROOM_DELETION_NOTICE } from "../plugins/utils";
|
||||
import util, { ROOM_TYPE_CHANNEL, STATE_EVENT_ROOM_DELETION_NOTICE } from "../plugins/utils";
|
||||
import MessageIncomingText from "./messages/MessageIncomingText";
|
||||
import MessageIncomingFile from "./messages/MessageIncomingFile";
|
||||
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
||||
|
|
@ -53,10 +53,14 @@ import RoomGuestAccessChanged from "./messages/RoomGuestAccessChanged.vue";
|
|||
import RoomEncrypted from "./messages/RoomEncrypted.vue";
|
||||
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
|
||||
import DebugEvent from "./messages/DebugEvent.vue";
|
||||
import ReadMarker from "./messages/ReadMarker.vue";
|
||||
import roomDisplayOptionsMixin from "./roomDisplayOptionsMixin";
|
||||
import roomTypeMixin from "./roomTypeMixin";
|
||||
|
||||
export const ROOM_READ_MARKER_EVENT_PLACEHOLDER = { getId: () => "ROOM_READ_MARKER" };
|
||||
|
||||
export default {
|
||||
mixins: [ roomDisplayOptionsMixin ],
|
||||
mixins: [ roomDisplayOptionsMixin, roomTypeMixin ],
|
||||
components: {
|
||||
ChatHeader,
|
||||
MessageIncomingText,
|
||||
|
|
@ -100,6 +104,7 @@ export default {
|
|||
StickerPickerBottomSheet,
|
||||
BottomSheet,
|
||||
CreatePollDialog,
|
||||
ReadMarker
|
||||
},
|
||||
methods: {
|
||||
showDayMarkerBeforeEvent(event) {
|
||||
|
|
@ -125,8 +130,13 @@ export default {
|
|||
},
|
||||
|
||||
componentForEvent(event, isForExport = false) {
|
||||
const isChannel = this.roomDisplayType === ROOM_TYPE_CHANNEL;
|
||||
if (event === ROOM_READ_MARKER_EVENT_PLACEHOLDER) {
|
||||
return ReadMarker;
|
||||
}
|
||||
switch (event.getType()) {
|
||||
case "m.room.member":
|
||||
if (isChannel) break;
|
||||
if (event.getContent().membership == "join") {
|
||||
if (event.getPrevContent() && event.getPrevContent().membership == "join") {
|
||||
// We we already joined, so this must be a display name and/or avatar update!
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<!-- BASE CLASS FOR INCOMING MESSAGE -->
|
||||
<div :class="messageClasses">
|
||||
<div v-if="showSenderAndTime" class="senderAndTime">
|
||||
<div v-if="showSenderAndTime || room.displayType == ROOM_TYPE_CHANNEL" class="senderAndTime">
|
||||
<div class="sender">{{ eventSenderDisplayName(event) }}</div>
|
||||
<div class="time">
|
||||
{{ utils.formatTime(event.event.origin_server_ts) }}
|
||||
|
|
@ -14,27 +14,34 @@
|
|||
}}</span>
|
||||
</v-avatar>
|
||||
<!-- SLOT FOR CONTENT -->
|
||||
<span ref="messageInOutRef">
|
||||
<span ref="messageInOutRef" class="content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
|
||||
<div class="pin-icon" v-if="isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && room.displayType != ROOM_TYPE_CHANNEL">
|
||||
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<SeenBy :room="room" :event="event"/>
|
||||
<QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<QuickReactions v-else :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<SeenBy v-if="room.displayType != ROOM_TYPE_CHANNEL" :room="room" :event="event"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SeenBy from "./SeenBy.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
|
||||
import QuickReactions from "./QuickReactions.vue";
|
||||
import QuickReactionsChannel from "./channel/QuickReactionsChannel.vue";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: { SeenBy },
|
||||
components: { QuickReactions, QuickReactionsChannel, SeenBy },
|
||||
data() {
|
||||
return { ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL }
|
||||
},
|
||||
mounted() {
|
||||
if(util.isMobileOrTabletBrowser() && this.$refs.messageInOutRef) {
|
||||
this.initMsgHammerJs(this.$refs.messageInOutRef);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1 || event.isRedacted()">
|
||||
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1 || event.isRedacted() || room.displayType == ROOM_TYPE_CHANNEL">
|
||||
<div class="bubble">
|
||||
<div class="original-message" v-if="inReplyToText">
|
||||
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
</div>
|
||||
|
||||
<div class="message">
|
||||
<v-container v-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" v-on="$listeners" />
|
||||
<v-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
||||
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
<v-icon :color="this.senderIsAdminOrModerator(this.event) ? 'white' : ''" size="small">block</v-icon>
|
||||
{{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
|
||||
</i>
|
||||
<span v-html="linkify($sanitize(messageText))" v-else />
|
||||
<span v-html="linkify($sanitize(messageText))" v-else-if="messageText" />
|
||||
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
|
||||
{{ $t('message.edited') }}
|
||||
</span>
|
||||
|
|
@ -38,16 +39,18 @@
|
|||
<script>
|
||||
import MessageIncoming from "./MessageIncoming.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
|
||||
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
||||
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
||||
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
|
||||
|
||||
export default {
|
||||
extends: MessageIncoming,
|
||||
components: { MessageIncoming, GalleryItemsView, ThumbnailView },
|
||||
components: { MessageIncoming, GalleryItemsView, ThumbnailView, SwipeableThumbnailsView },
|
||||
mixins: [messageMixin],
|
||||
data() {
|
||||
return {
|
||||
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
|
||||
items: [],
|
||||
showItem: null,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<!-- BASE CLASS FOR OUTGOING MESSAGE -->
|
||||
<div class="messageOut">
|
||||
<div :class="messageClasses">
|
||||
<div class="senderAndTime">
|
||||
<div class="sender" v-if="room.displayType == ROOM_TYPE_CHANNEL">{{ eventSenderDisplayName(event) }}</div>
|
||||
<div class="time">
|
||||
{{ utils.formatTime(event.event.origin_server_ts) }}
|
||||
</div>
|
||||
|
|
@ -13,8 +14,10 @@
|
|||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="pin-icon" v-if="isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||
|
||||
<!-- SLOT FOR CONTENT -->
|
||||
<span ref="messageInOutRef">
|
||||
<span ref="messageInOutRef" class="content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<v-avatar
|
||||
|
|
@ -26,19 +29,25 @@
|
|||
<img v-if="userAvatar" :src="userAvatar" />
|
||||
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
|
||||
</v-avatar>
|
||||
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<SeenBy :room="room" :event="event"/>
|
||||
<QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<QuickReactions v-else :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<SeenBy v-if="room.displayType != ROOM_TYPE_CHANNEL" :room="room" :event="event"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SeenBy from "./SeenBy.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
|
||||
import QuickReactions from "./QuickReactions.vue";
|
||||
import QuickReactionsChannel from "./channel/QuickReactionsChannel.vue";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: { SeenBy },
|
||||
components: { QuickReactions, QuickReactionsChannel, SeenBy },
|
||||
data() {
|
||||
return { ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL }
|
||||
},
|
||||
mounted() {
|
||||
if(util.isMobileOrTabletBrowser() && this.$refs.messageInOutRef) {
|
||||
this.initMsgHammerJs(this.$refs.messageInOutRef);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1 || event.isRedacted()">
|
||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1 || event.isRedacted() || room.displayType == ROOM_TYPE_CHANNEL">
|
||||
<div class="bubble">
|
||||
<div class="original-message" v-if="inReplyToText">
|
||||
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
|
||||
<div class="message">
|
||||
<v-container v-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" v-on="$listeners" />
|
||||
<v-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
||||
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||
|
|
@ -22,7 +23,7 @@
|
|||
<v-icon size="small">block</v-icon>
|
||||
{{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
|
||||
</i>
|
||||
<span v-html="linkify($sanitize(messageText))" v-else />
|
||||
<span v-html="linkify($sanitize(messageText))" v-else-if="messageText" />
|
||||
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
|
||||
{{ $t('message.edited') }}
|
||||
</span>
|
||||
|
|
@ -39,16 +40,18 @@
|
|||
<script>
|
||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
|
||||
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
||||
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
||||
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
|
||||
|
||||
export default {
|
||||
extends: MessageOutgoing,
|
||||
components: { MessageOutgoing, GalleryItemsView, ThumbnailView },
|
||||
components: { MessageOutgoing, GalleryItemsView, ThumbnailView, SwipeableThumbnailsView },
|
||||
mixins: [messageMixin],
|
||||
data() {
|
||||
return {
|
||||
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
|
||||
items: [],
|
||||
showItem: null,
|
||||
}
|
||||
|
|
|
|||
17
src/components/messages/ReadMarker.vue
Normal file
17
src/components/messages/ReadMarker.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div class="read-marker">
|
||||
<div class="line"></div>
|
||||
<div class="text">{{ $t('message.unread_messages') }}</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
54
src/components/messages/channel/MessageOperationsChannel.vue
Normal file
54
src/components/messages/channel/MessageOperationsChannel.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div :class="{ 'message-operations': true, 'incoming': incoming, 'outgoing': !incoming }">
|
||||
<v-list dense>
|
||||
<v-list-item key="edit" v-if="isEditable" @click.stop="edit">
|
||||
<v-list-item-icon><v-icon>$vuetify.icons.ic_edit</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t("menu.edit") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item key="pin" v-if="userCanPin && !event.isPinned" @click.stop="pin">
|
||||
<v-list-item-icon><v-icon>$vuetify.icons.ic_pin</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t("menu.pin") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item key="unpin" v-if="userCanPin && event.isPinned" @click.stop="unpin">
|
||||
<v-list-item-icon><v-icon>$vuetify.icons.ic_pin</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t("menu.unpin") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item key="redact" v-if="isRedactable" @click.stop="redact">
|
||||
<v-list-item-icon><v-icon color="#222222">delete_outline</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t("menu.delete") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "../messageMixin";
|
||||
import messageOperationsMixin from "../messageOperationsMixin";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin, messageOperationsMixin],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
userCanPin: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
127
src/components/messages/channel/QuickReactionsChannel.vue
Normal file
127
src/components/messages/channel/QuickReactionsChannel.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div class="quick-reaction-container">
|
||||
<div
|
||||
class="emoji"
|
||||
v-for="(value, name) in reactionMap"
|
||||
:key="name"
|
||||
v-show="name == '❤️'"
|
||||
>
|
||||
<v-tooltip top v-if="value.includes($matrix.currentUserId)">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-icon class="ma-1 ml-0 clickable" v-bind="attrs" v-on="on" @click="onClickEmoji(name)">$vuetify.icons.ic_like_filled</v-icon>
|
||||
</template>
|
||||
<span>{{ $t("global.click_to_remove") }}</span>
|
||||
</v-tooltip>
|
||||
<v-icon v-else class="ma-1 ml-0 clickable" @click="onClickEmoji(name)">$vuetify.icons.ic_like</v-icon> {{ value.length }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageOperationsMixin from "../messageOperationsMixin";
|
||||
|
||||
export default {
|
||||
mixins: [messageOperationsMixin],
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
timelineSet: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reactionMap: {"❤️": []},
|
||||
reactions: null,
|
||||
REACTION_LIMIT: 5,
|
||||
showAllReaction: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
|
||||
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||
if (this.reactions) {
|
||||
this.reactions.off('Relations.add', this.onAddRelation);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalReaction() {
|
||||
return Object.keys(this.reactionMap).length
|
||||
},
|
||||
otherReactionText() {
|
||||
return this.showAllReaction ? this.$t("global.show_less") : this.$t("message.reaction_count_more", { reactionCount: this.totalReaction - this.REACTION_LIMIT })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onRelationsCreated() {
|
||||
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
|
||||
},
|
||||
onClickEmoji(emoji) {
|
||||
this.$bubble('send-quick-reaction', {reaction:emoji, event:this.event});
|
||||
},
|
||||
onAddRelation(ignoredevent) {
|
||||
this.processReactions();
|
||||
},
|
||||
onRemoveRelation(ignoredevent) {
|
||||
this.processReactions();
|
||||
},
|
||||
onRedactRelation(ignoredevent) {
|
||||
this.processReactions();
|
||||
},
|
||||
processReactions() {
|
||||
var reactionMap = {"❤️": []};
|
||||
if (this.reactions && this.reactions._eventsCount > 0) {
|
||||
const relations = this.reactions.getRelations();
|
||||
for (const r of relations) {
|
||||
const emoji = r.getRelation().key;
|
||||
const sender = r.getSender();
|
||||
if (reactionMap[emoji]) {
|
||||
const array = reactionMap[emoji];
|
||||
if (r.isRedacted()) {
|
||||
delete array[sender];
|
||||
}
|
||||
if (!array.includes(sender)) {
|
||||
array.push(sender)
|
||||
}
|
||||
} else if (!r.isRedacted()) {
|
||||
reactionMap[emoji] = [sender];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.reactionMap = reactionMap;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
reactions: {
|
||||
handler(newValue, oldValue) {
|
||||
if (oldValue) {
|
||||
oldValue.off('Relations.add', this.onAddRelation);
|
||||
oldValue.off('Relations.remove', this.onRemoveRelation);
|
||||
oldValue.off('Relations.redaction', this.onRedactRelation);
|
||||
}
|
||||
if (newValue) {
|
||||
newValue.on('Relations.add', this.onAddRelation);
|
||||
newValue.on('Relations.remove', this.onRemoveRelation);
|
||||
newValue.on('Relations.redaction', this.onRedactRelation);
|
||||
}
|
||||
this.processReactions();
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
57
src/components/messages/channel/SwipeableThumbnailsView.vue
Normal file
57
src/components/messages/channel/SwipeableThumbnailsView.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="swipeable-thumbnails-view">
|
||||
<v-responsive :aspect-ratio="aspectRatio" class="ma-0 pa-0">
|
||||
<v-carousel height="100%" hide-delimiters :show-arrows="items.length > 1" :show-arrows-on-hover="items.length > 1" v-model="currentIndex">
|
||||
<v-carousel-item v-for="(item,index) in items" :key="item.event.getId()">
|
||||
<ThumbnailView :item="items[index]" :previewOnly="true" v-on:itemclick="itemClicked" />
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
</v-responsive>
|
||||
<div class="indicator-container" v-show="items.length > 1">
|
||||
<div v-for="(item,index) in items" :key="index" :class="{'indicator': true, 'current': index == currentIndex}" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "../../messages/messageMixin";
|
||||
import ThumbnailView from '../../file_mode/ThumbnailView.vue';
|
||||
import util from "../../../plugins/utils";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: { ThumbnailView },
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
aspectRatio() {
|
||||
return this.items.some((item) =>['m.video','m.image'].includes(item.event.getContent().msgtype)) ? 16/9 : undefined;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
itemClicked() {
|
||||
if (this.items.length > 0 && this.currentIndex < this.items.length) {
|
||||
const item = this.items[this.currentIndex];
|
||||
if (util.isFileTypePDF(item.event)) {
|
||||
this.$emit("download", {event: item.event});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
|
@ -109,7 +109,7 @@ export default {
|
|||
* Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages)
|
||||
*/
|
||||
showSenderAndTime() {
|
||||
if (this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) {
|
||||
if (!this.isPinned && this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) {
|
||||
const ts1 = this.nextEvent.event.origin_server_ts;
|
||||
const ts2 = this.event.event.origin_server_ts;
|
||||
return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes
|
||||
|
|
@ -180,12 +180,20 @@ export default {
|
|||
return this.event.getContent().body;
|
||||
},
|
||||
|
||||
isPinned() {
|
||||
return this.event.parentThread ? this.event.parentThread.isPinned : this.event.isPinned;
|
||||
},
|
||||
|
||||
/**
|
||||
* Classes to set for the message. Currently only for "messageIn", TODO: - detect messageIn or messageOut.
|
||||
* Classes to set for the message. Currently only for "messageIn"
|
||||
*/
|
||||
|
||||
messageClasses() {
|
||||
return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event) };
|
||||
if (this.incoming) {
|
||||
return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event), "pinned": this.isPinned };
|
||||
} else {
|
||||
return { messageOut: true, "pinned": this.isPinned };
|
||||
}
|
||||
},
|
||||
|
||||
userAvatar() {
|
||||
|
|
|
|||
|
|
@ -50,5 +50,13 @@ export default {
|
|||
this.$emit("close");
|
||||
this.$emit("more", {event:this.event});
|
||||
},
|
||||
pin() {
|
||||
this.$emit("close");
|
||||
this.$emit("pin", {event:this.event});
|
||||
},
|
||||
unpin() {
|
||||
this.$emit("close");
|
||||
this.$emit("unpin", {event:this.event});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -914,6 +914,7 @@ class Util {
|
|||
}
|
||||
|
||||
download(matrixClient, event) {
|
||||
console.log("DOWNLOAD");
|
||||
this
|
||||
.getAttachment(matrixClient, event)
|
||||
.then((url) => {
|
||||
|
|
@ -924,6 +925,7 @@ class Util {
|
|||
// PDFs are shown inline, not downloaded
|
||||
link.download = event.getContent().body || this.$t("fallbacks.download_name");
|
||||
}
|
||||
console.log("LINK", link);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
setTimeout(function () {
|
||||
|
|
|
|||
|
|
@ -1292,6 +1292,33 @@ export default {
|
|||
});
|
||||
this.notificationCount = count;
|
||||
},
|
||||
|
||||
setEventPinned(room, event, pinned) {
|
||||
if (room && room.currentState && event) {
|
||||
const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {}
|
||||
let pinnedEvents = content["pinned"] || [];
|
||||
if (pinned && !pinnedEvents.includes(event.getId())) {
|
||||
pinnedEvents.push(event.getId());
|
||||
} else if (!pinned && pinnedEvents.includes(event.getId())) {
|
||||
pinnedEvents = pinnedEvents.filter((e) => e != event.getId());
|
||||
} else {
|
||||
return; // no change
|
||||
}
|
||||
content.pinned = pinnedEvents;
|
||||
this.matrixClient.sendStateEvent(room.roomId, "m.room.pinned_events", content);
|
||||
}
|
||||
},
|
||||
|
||||
getPinnedEvents(room) {
|
||||
if (room && room.currentState) {
|
||||
const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {}
|
||||
return content["pinned"] || [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue