Initial implementation of "audio mode"
This commit is contained in:
parent
d5942fdb8e
commit
09173a65f1
14 changed files with 944 additions and 410 deletions
|
|
@ -5,6 +5,17 @@
|
|||
$admin-bg: black;
|
||||
$admin-fg: white;
|
||||
|
||||
body {
|
||||
--v-background-color: white;
|
||||
--v-foreground-color: black;
|
||||
--v-divider-color: #eeeeee;
|
||||
&.dark {
|
||||
--v-background-color: black;
|
||||
--v-foreground-color: white;
|
||||
--v-divider-color: rgba(221, 221, 221, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.home {
|
||||
.v-card {
|
||||
background-color: white;
|
||||
|
|
@ -30,8 +41,8 @@ $admin-fg: white;
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
height: 72px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
background-color: var(--v-background-color);
|
||||
border-bottom: 1px solid var(--v-divider-color);
|
||||
.chat-header-row {
|
||||
margin: 0;
|
||||
padding: 4px 10px;
|
||||
|
|
@ -47,15 +58,15 @@ $admin-fg: white;
|
|||
font-family: "Inter", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12 * $chat-text-size;
|
||||
color: black;
|
||||
color: var(--v-foreground-color);
|
||||
}
|
||||
.v-btn.leave-button {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 11 * $chat-text-size;
|
||||
color: black;
|
||||
background-color: white !important;
|
||||
border: 1px solid black;
|
||||
color: var(--v-foreground-color);
|
||||
background-color: var(--v-background-color) !important;
|
||||
border: 1px solid var(--v-foreground-color);
|
||||
border-radius: $chat-standard-padding / 2;
|
||||
height: $chat-standard-padding;
|
||||
margin-top: $chat-standard-padding-xs;
|
||||
|
|
@ -65,9 +76,9 @@ $admin-fg: white;
|
|||
font-family: "Inter", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 11 * $chat-text-size;
|
||||
color: black;
|
||||
background-color: white !important;
|
||||
border: 1px solid black;
|
||||
color: var(--v-foreground-color);
|
||||
background-color: var(--v-background-color) !important;
|
||||
border: 1px solid var(--v-foreground-color);
|
||||
border-radius: $chat-standard-padding / 2;
|
||||
height: $chat-standard-padding;
|
||||
margin-top: $chat-standard-padding-xs;
|
||||
|
|
@ -699,7 +710,7 @@ $admin-fg: white;
|
|||
font-weight: 700;
|
||||
font-size: 18 * $chat-text-size;
|
||||
text-transform: uppercase;
|
||||
color: black;
|
||||
color: var(--v-foreground-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
@ -1217,3 +1228,104 @@ $admin-fg: white;
|
|||
opacity: 1;
|
||||
transition: opacity 0.3s linear;
|
||||
}
|
||||
|
||||
.auto-audio-player-root {
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
background-color: var(--v-background-color);
|
||||
color: var(--v-foreground-color);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
.load-earlier {
|
||||
flex: 1 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.load-later {
|
||||
flex: 1 0 auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items:center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
.mic-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.senderAndTime {
|
||||
.sender {
|
||||
margin-left: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.time {
|
||||
color: inherit;
|
||||
}
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
}
|
||||
.sound-wave-view {
|
||||
width: 80%;
|
||||
max-width: 40vh;
|
||||
aspect-ratio: 1/1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
.volume-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
div {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
width: 103px !important;
|
||||
height: 103px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
#btn-play, #btn-pause {
|
||||
margin: 26px;
|
||||
}
|
||||
.mic-button {
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-layout.voice-recorder {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
12
src/assets/icons/forward.vue
Normal file
12
src/assets/icons/forward.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<svg width="28" height="30" viewBox="0 0 28 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.1139 12.9866V18.8048H10.8838V14.1542H10.8497L9.5173 14.9894V13.8985L10.9576 12.9866H12.1139Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M16.0217 18.8843C15.6202 18.8843 15.2622 18.8105 14.9478 18.6627C14.6353 18.515 14.3872 18.3114 14.2035 18.0519C14.0198 17.7925 13.9242 17.4951 13.9166 17.1599H15.1098C15.123 17.3853 15.2177 17.568 15.3939 17.7082C15.57 17.8483 15.7793 17.9184 16.0217 17.9184C16.2149 17.9184 16.3853 17.8758 16.5331 17.7906C16.6827 17.7035 16.7992 17.5832 16.8825 17.4298C16.9677 17.2745 17.0103 17.0965 17.0103 16.8957C17.0103 16.6911 16.9668 16.5112 16.8797 16.3559C16.7944 16.2006 16.6761 16.0794 16.5245 15.9923C16.373 15.9052 16.1997 15.8607 16.0047 15.8588C15.8342 15.8588 15.6685 15.8938 15.5075 15.9639C15.3484 16.034 15.2244 16.1296 15.1353 16.2508L14.0416 16.0548L14.3172 12.9866H17.874V13.9923H15.3314L15.1808 15.4497H15.2149C15.3172 15.3057 15.4715 15.1864 15.678 15.0917C15.8844 14.997 16.1155 14.9497 16.3711 14.9497C16.7215 14.9497 17.034 15.0321 17.3086 15.1968C17.5833 15.3616 17.8001 15.5879 17.9592 15.8758C18.1183 16.1618 18.1969 16.4913 18.195 16.8644C18.1969 17.2565 18.106 17.605 17.9223 17.9099C17.7405 18.2129 17.4857 18.4516 17.1581 18.6258C16.8323 18.7982 16.4535 18.8843 16.0217 18.8843Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M23.3999 3.78127C22.5345 3.06825 21.5861 2.44517 20.5684 1.95368C18.6714 1.02603 16.5392 0.499957 14.303 0.499957C10.3568 0.499957 6.77761 2.10604 4.18839 4.69517C1.60608 7.27737 -7.82013e-05 10.8564 -7.82013e-05 14.8024C-7.82013e-05 18.7484 1.60608 22.3275 4.18839 24.9097C6.77767 27.4989 10.3568 29.1049 14.303 29.1049C17.2799 29.1049 20.063 28.1841 22.3545 26.6058C24.7153 24.9927 26.5638 22.6805 27.623 19.9599C27.8099 19.4753 27.5676 18.9215 27.076 18.7346C26.5914 18.5476 26.0376 18.79 25.8507 19.2815C24.9368 21.6353 23.3307 23.6428 21.2884 25.0412C19.3015 26.3981 16.8922 27.2011 14.303 27.2011C10.876 27.2011 7.77451 25.8097 5.53143 23.5667C3.29528 21.3306 1.90374 18.2292 1.90374 14.8024C1.90374 11.3756 3.29522 8.27422 5.53143 6.03816C7.77457 3.79512 10.876 2.40369 14.303 2.40369C16.2553 2.40369 18.1037 2.8606 19.7376 3.65675C20.5614 4.05833 21.3368 4.55668 22.0499 5.12435L18.5122 8.6619C18.3323 8.84184 18.3323 9.14652 18.5122 9.33344C18.6022 9.42346 18.7269 9.47191 18.8515 9.47191H27.2146C27.4777 9.47191 27.6922 9.25727 27.6922 9.00116V0.631513C27.6922 0.368415 27.4776 0.15387 27.2215 0.15387C27.09 0.15387 26.9722 0.209307 26.8822 0.292348L23.3999 3.78143L23.3999 3.78127Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/pause_circle.vue
Normal file
7
src/assets/icons/pause_circle.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M56 28C56 43.464 43.464 56 28 56C12.536 56 0 43.464 0 28C0 12.536 12.536 0 28 0C43.464 0 56 12.536 56 28ZM20 19C20 17.8954 20.8954 17 22 17H23C24.1046 17 25 17.8954 25 19V37C25 38.1046 24.1046 39 23 39H22C20.8954 39 20 38.1046 20 37V19ZM33 17C31.8954 17 31 17.8954 31 19V37C31 38.1046 31.8954 39 33 39H34C35.1046 39 36 38.1046 36 37V19C36 17.8954 35.1046 17 34 17H33Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/play_circle.vue
Normal file
7
src/assets/icons/play_circle.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M28 56C43.464 56 56 43.464 56 28C56 12.536 43.464 0 28 0C12.536 0 0 12.536 0 28C0 43.464 12.536 56 28 56ZM38.6733 29.299C39.6733 28.7217 39.6733 27.2783 38.6733 26.7009L23.7887 18.1073C22.7887 17.53 21.5387 18.2516 21.5387 19.4063V36.5936C21.5387 37.7483 22.7887 38.47 23.7887 37.8927L38.6733 29.299Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
</template>
|
||||
12
src/assets/icons/rewind.vue
Normal file
12
src/assets/icons/rewind.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<svg width="28" height="29" viewBox="0 0 28 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.1138 12.9866V18.8048H10.8837V14.1542H10.8496L9.51721 14.9894V13.8985L10.9576 12.9866H12.1138Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M16.0216 18.8843C15.6201 18.8843 15.2621 18.8105 14.9478 18.6627C14.6353 18.515 14.3871 18.3114 14.2034 18.0519C14.0197 17.7925 13.9241 17.4951 13.9165 17.1599H15.1097C15.1229 17.3853 15.2176 17.568 15.3938 17.7082C15.5699 17.8483 15.7792 17.9184 16.0216 17.9184C16.2148 17.9184 16.3853 17.8758 16.533 17.7906C16.6826 17.7035 16.7991 17.5832 16.8824 17.4298C16.9676 17.2745 17.0103 17.0965 17.0103 16.8957C17.0103 16.6911 16.9667 16.5112 16.8796 16.3559C16.7943 16.2006 16.676 16.0794 16.5245 15.9923C16.3729 15.9052 16.1996 15.8607 16.0046 15.8588C15.8341 15.8588 15.6684 15.8938 15.5074 15.9639C15.3483 16.034 15.2243 16.1296 15.1353 16.2508L14.0415 16.0548L14.3171 12.9866H17.8739V13.9923H15.3313L15.1807 15.4497H15.2148C15.3171 15.3057 15.4714 15.1864 15.6779 15.0917C15.8843 14.997 16.1154 14.9497 16.371 14.9497C16.7214 14.9497 17.0339 15.0321 17.3085 15.1968C17.5832 15.3616 17.8 15.5879 17.9591 15.8758C18.1182 16.1618 18.1968 16.4913 18.1949 16.8644C18.1968 17.2565 18.1059 17.605 17.9222 17.9099C17.7404 18.2129 17.4856 18.4516 17.158 18.6258C16.8322 18.7982 16.4534 18.8843 16.0216 18.8843Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M4.29231 3.75701C5.15769 3.04876 6.10612 2.42985 7.12385 1.94165C9.02083 1.0202 11.1531 0.497642 13.3892 0.497642C17.3354 0.497642 20.9146 2.09299 23.5038 4.66479C26.0862 7.22972 27.6923 10.7849 27.6923 14.7045C27.6923 18.6241 26.0862 22.1792 23.5038 24.7441C20.9146 27.316 17.3354 28.9113 13.3892 28.9113C10.4123 28.9113 7.62923 27.9966 5.33769 26.4288C2.97692 24.8266 1.12846 22.5299 0.0692308 19.8274C-0.117707 19.346 0.12467 18.7959 0.616183 18.6103C1.10083 18.4246 1.65464 18.6653 1.84157 19.1535C2.75541 21.4916 4.36154 23.4857 6.40388 24.8748C8.39077 26.2225 10.8 27.0202 13.3893 27.0202C16.8162 27.0202 19.9177 25.6381 22.1608 23.4101C24.397 21.1889 25.7885 18.1083 25.7885 14.7044C25.7885 11.3006 24.397 8.2199 22.1608 5.9988C19.9177 3.77076 16.8162 2.38865 13.3893 2.38865C11.437 2.38865 9.58849 2.84249 7.95464 3.63332C7.13083 4.03221 6.35538 4.52723 5.64234 5.0911L9.18003 8.60499C9.35998 8.78373 9.35998 9.08637 9.18003 9.27203C9.09 9.36146 8.96537 9.40959 8.84074 9.40959H0.477664C0.214555 9.40959 0 9.19638 0 8.94199V0.628318C0 0.36698 0.214656 0.15387 0.470769 0.15387C0.602268 0.15387 0.720017 0.208936 0.810058 0.291422L4.29237 3.75717L4.29231 3.75701Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -242,7 +242,9 @@
|
|||
"scan_code": "Scan to join the room",
|
||||
"export_room": "Export chat",
|
||||
"user_admin": "Administrator",
|
||||
"user_moderator": "Moderator"
|
||||
"user_moderator": "Moderator",
|
||||
"ui_options": "UI Options",
|
||||
"audio_layout": "Use audio layout"
|
||||
},
|
||||
"room_info_sheet": {
|
||||
"this_room": "This room",
|
||||
|
|
|
|||
406
src/components/AudioLayout.vue
Normal file
406
src/components/AudioLayout.vue
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
<template>
|
||||
<div v-bind="{...$props, ...$attrs}" v-on="$listeners" class="messageIn">
|
||||
<div class="load-earlier clickable" @click="loadPrevious">
|
||||
<v-icon color="white" size="28">expand_less</v-icon>
|
||||
</div>
|
||||
<div class="sound-wave-view">
|
||||
<div class="volume-container">
|
||||
<div ref="volume"></div>
|
||||
</div>
|
||||
<v-avatar v-if="currentAudioEvent" class="avatar" ref="avatar" size="32" color="#ededed"
|
||||
@click.stop="otherAvatarClicked($refs.avatar.$el)">
|
||||
<img v-if="messageEventAvatar(currentAudioEvent)" :src="messageEventAvatar(currentAudioEvent)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
eventSenderDisplayName(currentAudioEvent).substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
</div>
|
||||
<div v-if="currentAudioEvent" class="senderAndTime">
|
||||
<div class="sender">{{ eventSenderDisplayName(currentAudioEvent) }}</div>
|
||||
<div class="time">
|
||||
{{ formatTime(currentAudioEvent.event.origin_server_ts) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="play-time">
|
||||
{{ currentTime }} / {{ totalTime }}
|
||||
</div>
|
||||
<audio ref="player" :src="src" @durationchange="updateDuration">
|
||||
{{ $t('fallbacks.audio_file')}}
|
||||
</audio>
|
||||
<div v-if="currentAudioEvent" class="auto-audio-player">
|
||||
<v-btn id="btn-rewind" @click.stop="rewind" icon>
|
||||
<v-icon size="28">$vuetify.icons.rewind</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="playing" id="btn-pause" @click.stop="pause" icon>
|
||||
<v-icon size="56">$vuetify.icons.pause_circle</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else id="btn-play" @click.stop="play" icon>
|
||||
<v-icon size="56">$vuetify.icons.play_circle</v-icon>
|
||||
</v-btn>
|
||||
<v-btn id="btn-forward" @click.stop="forward" icon>
|
||||
<v-icon size="28">$vuetify.icons.forward</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="load-later">
|
||||
<v-btn v-if="canRecordAudio" class="mic-button" ref="mic_button" fab small elevation="0" v-blur
|
||||
@click.stop="$emit('start-recording')">
|
||||
<v-icon color="white">mic</v-icon>
|
||||
</v-btn>
|
||||
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "./messages/messageMixin";
|
||||
import util from "../plugins/utils";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: {},
|
||||
props: {
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return true
|
||||
}
|
||||
},
|
||||
events: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
readMarker: {
|
||||
type: String,
|
||||
default: function () {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
src: null,
|
||||
currentAudioEvent: null,
|
||||
autoPlayNextEvent: false,
|
||||
currentAudioSource: null,
|
||||
player: null,
|
||||
duration: 0,
|
||||
playPercent: 0,
|
||||
playTime: 0,
|
||||
playing: false,
|
||||
analyzer: null,
|
||||
analyzerDataArray: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.body.classList.add("dark");
|
||||
this.$root.$on('playback-start', this.onPlaybackStart);
|
||||
this.player = this.$refs.player;
|
||||
this.player.addEventListener("timeupdate", this.updateProgressBar);
|
||||
this.player.addEventListener("play", () => {
|
||||
this.playing = true;
|
||||
this.updateVisualization();
|
||||
if (this.currentAudioEvent) {
|
||||
this.$emit("mark-read", this.currentAudioEvent.getId(), this.currentAudioEvent.getId());
|
||||
}
|
||||
});
|
||||
this.player.addEventListener("pause", () => {
|
||||
this.playing = false;
|
||||
this.clearVisualization();
|
||||
});
|
||||
this.player.addEventListener("ended", () => {
|
||||
this.pause();
|
||||
this.playing = false;
|
||||
this.clearVisualization();
|
||||
this.onPlaybackEnd();
|
||||
});
|
||||
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
let audioSource = null;
|
||||
if (audioCtx) {
|
||||
audioSource = audioCtx.createMediaElementSource(this.player);
|
||||
this.analyser = audioCtx.createAnalyser();
|
||||
audioSource.connect(this.analyser);
|
||||
this.analyser.connect(audioCtx.destination);
|
||||
|
||||
this.analyser.fftSize = 128;
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
this.analyzerDataArray = new Uint8Array(bufferLength);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.classList.remove("dark");
|
||||
this.currentAudioEvent = null;
|
||||
this.loadAudioAttachmentSource(); // Release
|
||||
this.$root.$off('playback-start', this.onPlaybackStart);
|
||||
},
|
||||
computed: {
|
||||
canRecordAudio() {
|
||||
if (this.room) {
|
||||
const myUserId = this.$matrix.currentUserId;
|
||||
const me = this.room.getMember(myUserId);
|
||||
return me && me.powerLevelNorm > 0 && util.browserCanRecordAudio();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
currentTime() {
|
||||
return util.formatDuration(this.playTime);
|
||||
},
|
||||
totalTime() {
|
||||
return util.formatDuration(this.duration);
|
||||
},
|
||||
playheadPercent: {
|
||||
get: function () {
|
||||
return this.playPercent;
|
||||
},
|
||||
set: function (percent) {
|
||||
if (this.player.src) {
|
||||
this.playPercent = percent;
|
||||
this.player.currentTime = (percent / 100) * this.player.duration;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
autoplay: {
|
||||
immediate: true,
|
||||
handler(autoplay, ignoredOldValue) {
|
||||
if (!autoplay) {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
},
|
||||
events: {
|
||||
immediate: true,
|
||||
handler(events, ignoredOldValue) {
|
||||
console.log("Events changed", this.currentAudioEvent, this.autoPlayNextEvent);
|
||||
if (!this.currentAudioEvent || this.autoPlayNextEvent) {
|
||||
// Make sure all events are decrypted!
|
||||
const eventsBeingDecrypted = events.filter((e) => e.isBeingDecrypted());
|
||||
if (eventsBeingDecrypted.length > 0) {
|
||||
console.log("All not decrypted, wait");
|
||||
Promise.allSettled(eventsBeingDecrypted.map((e) => e.getDecryptionPromise())).then(() => {
|
||||
console.log("DONE DECRYPTING!")
|
||||
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
||||
});
|
||||
} else {
|
||||
console.log("All decrypted, load next");
|
||||
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
currentAudioEvent: {
|
||||
immediate: true,
|
||||
handler(value, oldValue) {
|
||||
console.log("Current audio derom", value, oldValue);
|
||||
if (value && oldValue && value.getId && oldValue.getId && value.getId() === oldValue.getId()) {
|
||||
console.log("Ignoring change!!!");
|
||||
return;
|
||||
}
|
||||
if (!value || !value.getId) {
|
||||
return;
|
||||
}
|
||||
this.src = null;
|
||||
this.autoPlayNextEvent = false;
|
||||
this.loadAudioAttachmentSource();
|
||||
}
|
||||
},
|
||||
src: {
|
||||
immediate: true,
|
||||
handler(value, ignoredOldValue) {
|
||||
console.log("Source changed to", value, ignoredOldValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play() {
|
||||
if (this.player.src) {
|
||||
this.$root.$emit("playback-start", this);
|
||||
if (this.player.paused) {
|
||||
this.player.play();
|
||||
} else if (this.player.ended) {
|
||||
// restart
|
||||
this.player.currentTime = 0;
|
||||
this.player.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
this.player.autoplay = false;
|
||||
if (this.player.src) {
|
||||
this.player.pause();
|
||||
}
|
||||
},
|
||||
rewind() {
|
||||
if (this.player.src) {
|
||||
this.player.currentTime = Math.max(0, this.player.currentTime - 15);
|
||||
}
|
||||
},
|
||||
forward() {
|
||||
if (this.player.src) {
|
||||
this.player.currentTime = Math.min(this.player.duration, this.player.currentTime + 15);
|
||||
}
|
||||
},
|
||||
updateProgressBar() {
|
||||
if (this.player.duration > 0) {
|
||||
this.playPercent = Math.floor(
|
||||
(100 / this.player.duration) * this.player.currentTime
|
||||
);
|
||||
} else {
|
||||
this.playPercent = 0;
|
||||
}
|
||||
this.playTime = 1000 * this.player.currentTime;
|
||||
},
|
||||
updateDuration() {
|
||||
this.duration = 1000 * this.player.duration;
|
||||
},
|
||||
onPlaybackStart(item) {
|
||||
this.player.autoplay = false;
|
||||
if (item != this && this.playing) {
|
||||
this.pause();
|
||||
}
|
||||
},
|
||||
onPlaybackEnd() {
|
||||
this.loadNext(true && this.autoplay);
|
||||
},
|
||||
loadPrevious() {
|
||||
const audioMessages = this.events.filter((e) => e.getContent().msgtype === "m.audio");
|
||||
for (let i = 0; i < audioMessages.length; i++) {
|
||||
const e = audioMessages[i];
|
||||
if (this.currentAudioEvent && e.getId() === this.currentAudioEvent.getId()) {
|
||||
if (i > 0) {
|
||||
this.pause();
|
||||
this.currentAudioEvent = audioMessages[i - 1];
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.$emit("loadprevious");
|
||||
},
|
||||
loadNext(autoplay = false) {
|
||||
const audioMessages = this.events.filter((e) => e.getContent().msgtype === "m.audio");
|
||||
if (audioMessages.length == 0) {
|
||||
// Try to load earlier
|
||||
this.$emit("loadprevious");
|
||||
return;
|
||||
}
|
||||
if (!this.currentAudioEvent) {
|
||||
// Figure out which audio event to start with, i.e. our "read marker"
|
||||
for (let i = 0; i < audioMessages.length; i++) {
|
||||
const e = audioMessages[i];
|
||||
if (e.getId() === this.readMarker) {
|
||||
if (i < (audioMessages.length - 1)) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = audioMessages[i + 1];
|
||||
} else {
|
||||
this.autoPlayNextEvent = true;
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = e;
|
||||
this.$emit("loadnext");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No read marker found. Just use the first event here...
|
||||
if (audioMessages.length > 0) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = audioMessages[0];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < audioMessages.length; i++) {
|
||||
const e = audioMessages[i];
|
||||
if (e.getId() === this.currentAudioEvent.getId()) {
|
||||
if (i < (audioMessages.length - 1)) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = audioMessages[i + 1];
|
||||
} else {
|
||||
this.autoPlayNextEvent = true;
|
||||
this.player.autoplay = autoplay;
|
||||
this.$emit("loadnext");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateVisualization() {
|
||||
|
||||
const volume = this.$refs.volume;
|
||||
if (volume && this.analyser) {
|
||||
const volumeContainer = volume.parentElement;
|
||||
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
|
||||
this.analyser.getByteFrequencyData(this.analyzerDataArray);
|
||||
var value = 0;
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
value += this.analyzerDataArray[i];
|
||||
}
|
||||
value = value / bufferLength;
|
||||
const avatarWidth = 1.1 * this.$refs.avatar ? this.$refs.avatar.clientWidth : 104;
|
||||
const range = Math.max(0, (volumeContainer.clientWidth - avatarWidth));
|
||||
const w = avatarWidth + (value * range) / 256;
|
||||
volume.style.width = "" + w + "px";
|
||||
volume.style.height = "" + w + "px";
|
||||
const color = 80 + (value * (256 - 80)) / 256;
|
||||
volume.style.backgroundColor = `rgb(${color},${color},${color})`;
|
||||
|
||||
if (this.playing) {
|
||||
requestAnimationFrame(this.updateVisualization);
|
||||
} else {
|
||||
this.clearVisualization();
|
||||
}
|
||||
}
|
||||
},
|
||||
clearVisualization() {
|
||||
const volume = this.$refs.volume;
|
||||
volume.style.width = "0px";
|
||||
volume.style.height = "0px";
|
||||
volume.style.backgroundColor = "transparent";
|
||||
},
|
||||
loadAudioAttachmentSource() {
|
||||
console.log("loadAUto");
|
||||
if (this.src) {
|
||||
const objectUrl = this.src;
|
||||
this.src = null;
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
if (this.currentAudioEvent) {
|
||||
console.log("Will load");
|
||||
if (this.currentAudioSource) {
|
||||
this.currentAudioSource.reject("Aborted");
|
||||
}
|
||||
this.currentAudioSource =
|
||||
util
|
||||
.getAttachment(this.$matrix.matrixClient, this.currentAudioEvent, (progress) => {
|
||||
this.downloadProgress = progress;
|
||||
})
|
||||
.then((url) => {
|
||||
console.log("Loaded", url);
|
||||
this.src = url;
|
||||
this.currentAudioSource = null;
|
||||
this.$nextTick(() => {
|
||||
this.player.load();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to fetch attachment: ", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
|
@ -4,49 +4,39 @@
|
|||
{{ $tc("room.invitations", invitationCount) }}
|
||||
</div>
|
||||
<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"
|
||||
v-on:scroll="onScroll"
|
||||
@click="closeContextMenusIfOpen"
|
||||
>
|
||||
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useAudioLayout" :room="room"
|
||||
:events="events" :autoplay="!showRecorder"
|
||||
:timelineSet="timelineSet"
|
||||
:readMarker="readMarker"
|
||||
v-on:start-recording="showRecorder = true"
|
||||
v-on:loadnext="handleScrolledToBottom(false)"
|
||||
v-on:loadprevious="handleScrolledToTop()"
|
||||
v-on:mark-read="sendRR"
|
||||
/>
|
||||
<VoiceRecorder class="audio-layout" v-if="useAudioLayout" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" />
|
||||
|
||||
|
||||
<div v-if="!useAudioLayout" class="chat-content flex-grow-1 flex-shrink-1" 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;
|
||||
showContextMenuAnchor = null;
|
||||
"
|
||||
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="
|
||||
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
|
||||
showContextMenu = false;
|
||||
showContextMenuAnchor = null;
|
||||
" 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="
|
||||
isEmojiQuickReaction= true
|
||||
showMoreMessageOperations($event)
|
||||
"
|
||||
:event="selectedEvent"
|
||||
/>
|
||||
" :originalEvent="selectedEvent" />
|
||||
</div>
|
||||
|
||||
<div ref="avatarOperationsStrut" class="avatar-operations-strut">
|
||||
<avatar-operations
|
||||
ref="avatarOperations"
|
||||
:style="avatarOpStyle"
|
||||
v-on:close="
|
||||
showAvatarMenu = false;
|
||||
showAvatarMenuAnchor = null;
|
||||
"
|
||||
v-on:start-private-chat="startPrivateChat($event)"
|
||||
v-if="selectedEvent && showAvatarMenu"
|
||||
:room="room"
|
||||
:event="selectedEvent"
|
||||
/>
|
||||
<avatar-operations ref="avatarOperations" :style="avatarOpStyle" v-on:close="
|
||||
showAvatarMenu = false;
|
||||
showAvatarMenuAnchor = null;
|
||||
" v-on:start-private-chat="startPrivateChat($event)" v-if="selectedEvent && showAvatarMenu" :room="room"
|
||||
:originalEvent="selectedEvent" />
|
||||
</div>
|
||||
|
||||
<!-- Handle resizes, e.g. when soft keyboard is shown/hidden -->
|
||||
|
|
@ -59,55 +49,31 @@
|
|||
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dayForEvent(event)" />
|
||||
|
||||
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
|
||||
<div
|
||||
class="message-wrapper"
|
||||
v-on:touchstart="
|
||||
(e) => {
|
||||
touchStart(e, event);
|
||||
}
|
||||
"
|
||||
v-on:touchend="touchEnd"
|
||||
v-on:touchcancel="touchCancel"
|
||||
v-on:touchmove="touchMove"
|
||||
>
|
||||
<component
|
||||
:is="componentForEvent(event)"
|
||||
:room="room"
|
||||
:event="event"
|
||||
:nextEvent="events[index + 1]"
|
||||
:timelineSet="timelineSet"
|
||||
v-on:send-quick-reaction="sendQuickReaction"
|
||||
v-on:context-menu="showContextMenuForEvent($event)"
|
||||
v-on:own-avatar-clicked="viewProfile"
|
||||
v-on:other-avatar-clicked="showAvatarMenuForEvent($event)"
|
||||
v-on:download="download(event)"
|
||||
v-on:poll-closed="pollWasClosed(event)"
|
||||
/>
|
||||
<div class="message-wrapper" v-on:touchstart="
|
||||
(e) => {
|
||||
touchStart(e, event);
|
||||
}
|
||||
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
|
||||
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="events[index + 1]"
|
||||
:timelineSet="timelineSet" v-on:send-quick-reaction="sendQuickReaction"
|
||||
v-on:context-menu="showContextMenuForEvent($event)" v-on:own-avatar-clicked="viewProfile"
|
||||
v-on:other-avatar-clicked="showAvatarMenuForEvent($event)" v-on:download="download(event)"
|
||||
v-on:poll-closed="pollWasClosed(event)" />
|
||||
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
||||
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
||||
<div
|
||||
v-if="event.getId() == readMarker && index < events.length - 1"
|
||||
class="read-marker"
|
||||
:title="$t('message.unread_messages')"
|
||||
/>
|
||||
<div v-if="event.getId() == readMarker && index < events.length - 1" class="read-marker"
|
||||
:title="$t('message.unread_messages')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input area -->
|
||||
<v-container v-if="room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
|
||||
<v-container v-if="!useAudioLayout && 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"
|
||||
v-show="showScrollToEnd"
|
||||
fab
|
||||
x-small
|
||||
elevation="0"
|
||||
color="black"
|
||||
@click.stop="scrollToEndOfTimeline"
|
||||
>
|
||||
<v-btn v-if="!useAudioLayout" 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>
|
||||
|
||||
|
|
@ -121,10 +87,11 @@
|
|||
<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.poll'">{{ $t("message.reply_poll") }}</div>
|
||||
<div v-if="replyToContentType === 'm.poll'">{{ $t("message.reply_poll") }}</div>
|
||||
</div>
|
||||
<div class="col col-auto" v-if="replyToContentType !== 'm.text'">
|
||||
<img v-if="replyToContentType === 'm.image'" width="50px" height="50px" :src="replyToImg" class="rounded" />
|
||||
<img v-if="replyToContentType === 'm.image'" width="50px" height="50px" :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" />
|
||||
<v-icon v-if="replyToContentType === 'm.poll'" light>$vuetify.icons.poll</v-icon>
|
||||
|
|
@ -143,24 +110,13 @@
|
|||
</v-row>
|
||||
<v-row class="input-area-inner align-center">
|
||||
<v-col class="flex-grow-1 flex-shrink-1 ma-0 pa-0">
|
||||
<v-textarea
|
||||
height="undefined"
|
||||
ref="messageInput"
|
||||
full-width
|
||||
auto-grow
|
||||
rows="1"
|
||||
v-model="currentInput"
|
||||
no-resize
|
||||
class="input-area-text"
|
||||
:placeholder="$t('message.your_message')"
|
||||
hide-details
|
||||
background-color="white"
|
||||
v-on:keydown.enter.prevent="
|
||||
<v-textarea height="undefined" ref="messageInput" full-width auto-grow rows="1" v-model="currentInput"
|
||||
no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details
|
||||
background-color="white" v-on:keydown.enter.prevent="
|
||||
() => {
|
||||
sendCurrentTextMessage();
|
||||
}
|
||||
"
|
||||
/>
|
||||
" />
|
||||
</v-col>
|
||||
|
||||
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-if="editedEvent">
|
||||
|
|
@ -169,150 +125,82 @@
|
|||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="(!currentInput || currentInput.length == 0) && !showRecorder && canCreatePoll"
|
||||
class="input-area-button text-center flex-grow-0 flex-shrink-1"
|
||||
>
|
||||
<v-col v-if="(!currentInput || currentInput.length == 0) && !showRecorder && canCreatePoll"
|
||||
class="input-area-button text-center flex-grow-0 flex-shrink-1">
|
||||
<v-btn icon large color="black" @click="showCreatePollDialog = true">
|
||||
<v-icon dark>$vuetify.icons.poll</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="input-area-button text-center flex-grow-0 flex-shrink-1"
|
||||
v-if="!currentInput || currentInput.length == 0 || showRecorder"
|
||||
>
|
||||
<v-btn
|
||||
v-if="canRecordAudio"
|
||||
class="mic-button"
|
||||
ref="mic_button"
|
||||
fab
|
||||
small
|
||||
elevation="0"
|
||||
v-blur
|
||||
v-longTap:250="[showRecordingUI, startRecording]"
|
||||
>
|
||||
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1"
|
||||
v-if="!currentInput || currentInput.length == 0 || showRecorder">
|
||||
<v-btn v-if="canRecordAudio" class="mic-button" ref="mic_button" fab small elevation="0" v-blur
|
||||
v-longTap:250="[showRecordingUI, startRecording]">
|
||||
<v-icon :color="showRecorder ? 'white' : 'black'">mic</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
class="mic-button"
|
||||
ref="mic_button"
|
||||
fab
|
||||
small
|
||||
elevation="0"
|
||||
v-blur
|
||||
@click.stop="showNoRecordingAvailableDialog = true"
|
||||
>
|
||||
<v-btn v-else class="mic-button" ref="mic_button" fab small elevation="0" v-blur
|
||||
@click.stop="showNoRecordingAvailableDialog = true">
|
||||
<v-icon :color="showRecorder ? 'white' : 'black'">mic</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-else>
|
||||
<v-btn
|
||||
fab
|
||||
small
|
||||
elevation="0"
|
||||
color="black"
|
||||
@click.stop="sendCurrentTextMessage"
|
||||
:disabled="sendButtonDisabled"
|
||||
>
|
||||
<v-btn fab small elevation="0" color="black" @click.stop="sendCurrentTextMessage"
|
||||
:disabled="sendButtonDisabled">
|
||||
<v-icon color="white">{{ editedEvent ? "save" : "arrow_upward" }}</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="input-area-button text-center flex-grow-0 flex-shrink-1 input-more-icon"
|
||||
>
|
||||
<v-btn
|
||||
fab
|
||||
small
|
||||
elevation="0"
|
||||
v-blur
|
||||
@click.stop="
|
||||
isEmojiQuickReaction = false
|
||||
showMoreMessageOperations($event)
|
||||
"
|
||||
>
|
||||
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1 input-more-icon">
|
||||
<v-btn fab small elevation="0" v-blur @click.stop="
|
||||
isEmojiQuickReaction = false
|
||||
showMoreMessageOperations($event)
|
||||
">
|
||||
<v-icon>$vuetify.icons.addReaction</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-btn
|
||||
v-if="!showRecorder"
|
||||
id="btn-attach"
|
||||
icon
|
||||
large
|
||||
color="black"
|
||||
@click="showStickerPicker"
|
||||
:disabled="attachButtonDisabled"
|
||||
>
|
||||
<v-btn v-if="!showRecorder" id="btn-attach" icon large color="black" @click="showStickerPicker"
|
||||
:disabled="attachButtonDisabled">
|
||||
<v-icon large>face</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1">
|
||||
<label icon flat ref="attachmentLabel">
|
||||
<v-btn
|
||||
v-if="!showRecorder"
|
||||
icon
|
||||
large
|
||||
color="black"
|
||||
@click="showAttachmentPicker"
|
||||
:disabled="attachButtonDisabled"
|
||||
>
|
||||
<v-btn v-if="!showRecorder" icon large color="black" @click="showAttachmentPicker"
|
||||
:disabled="attachButtonDisabled">
|
||||
<v-icon x-large>add_circle_outline</v-icon>
|
||||
</v-btn>
|
||||
<input
|
||||
ref="attachment"
|
||||
type="file"
|
||||
name="attachment"
|
||||
@change="handlePickedAttachment($event)"
|
||||
accept="image/*|audio/*|video/*|application/pdf"
|
||||
class="d-none"
|
||||
/>
|
||||
</label>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<VoiceRecorder
|
||||
:micButtonRef="$refs.mic_button"
|
||||
:ptt="showRecorderPTT"
|
||||
:show="showRecorder"
|
||||
v-on:close="showRecorder = false"
|
||||
v-on:file="onVoiceRecording"
|
||||
/>
|
||||
<VoiceRecorder :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" />
|
||||
</div>
|
||||
</v-container>
|
||||
|
||||
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
|
||||
accept="image/*|audio/*|video/*|application/pdf" class="d-none" />
|
||||
|
||||
<div v-if="currentImageInputPath">
|
||||
<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
|
||||
v-if="currentImageInput && currentImageInput.image"
|
||||
:aspect-ratio="1"
|
||||
:src="currentImageInput.image"
|
||||
contain
|
||||
class="current-image-input-path"
|
||||
/>
|
||||
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
|
||||
contain class="current-image-input-path" />
|
||||
<div>
|
||||
file: {{ currentImageInputPath.name }}
|
||||
<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
|
||||
>
|
||||
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span>
|
||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||
({{ formatBytes(currentImageInput.scaledSize) }})</span
|
||||
>
|
||||
({{ formatBytes(currentImageInput.scaledSize) }})</span>
|
||||
<span v-else> ({{ formatBytes(currentImageInputPath.size) }})</span>
|
||||
<v-switch
|
||||
v-if="currentImageInput && currentImageInput.scaled"
|
||||
:label="$t('message.scale_image')"
|
||||
v-model="currentImageInput.useScaled"
|
||||
/>
|
||||
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
|
||||
v-model="currentImageInput.useScaled" />
|
||||
</div>
|
||||
<div v-if="currentSendError">{{ currentSendError }}</div>
|
||||
<div v-else>{{ currentSendProgress }}</div>
|
||||
|
|
@ -321,17 +209,10 @@
|
|||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel">{{
|
||||
$t("menu.cancel")
|
||||
$t("menu.cancel")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
id="btn-attachment-send"
|
||||
color="primary"
|
||||
text
|
||||
@click="sendAttachment"
|
||||
v-if="currentSendShowSendButton"
|
||||
:disabled="currentSendOperation != null"
|
||||
>{{ $t("menu.send") }}</v-btn
|
||||
>
|
||||
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment"
|
||||
v-if="currentSendShowSendButton" :disabled="currentSendOperation != null">{{ $t("menu.send") }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
@ -363,7 +244,7 @@
|
|||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn id="btn-ok" color="primary" text @click="showNoRecordingAvailableDialog = false">{{
|
||||
$t("menu.ok")
|
||||
$t("menu.ok")
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
|
@ -389,6 +270,7 @@ import BottomSheet from "./BottomSheet.vue";
|
|||
import ImageResize from "image-resize";
|
||||
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||
import chatMixin from "./chatMixin";
|
||||
import AudioLayout from "./AudioLayout.vue";
|
||||
|
||||
const sizeOf = require("image-size");
|
||||
const dataUriToBuffer = require("data-uri-to-buffer");
|
||||
|
|
@ -405,7 +287,7 @@ 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;
|
||||
} else {
|
||||
|
|
@ -413,7 +295,7 @@ ScrollPosition.prototype.restore = function() {
|
|||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
@ -436,6 +318,7 @@ export default {
|
|||
BottomSheet,
|
||||
AvatarOperations,
|
||||
CreatePollDialog,
|
||||
AudioLayout
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -516,9 +399,13 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
const container = this.$refs.chatContainer;
|
||||
this.scrollPosition = new ScrollPosition(container);
|
||||
this.chatContainerSize = this.$refs.chatContainerResizer.$el.clientHeight;
|
||||
const container = this.chatContainer;
|
||||
if (container) {
|
||||
this.scrollPosition = new ScrollPosition(container);
|
||||
if (this.$refs.chatContainerResizer) {
|
||||
this.chatContainerSize = this.$refs.chatContainerResizer.$el.clientHeight;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
|
@ -531,6 +418,14 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
chatContainer() {
|
||||
const container = this.$refs.chatContainer;
|
||||
console.log("GOT CONTAINER", container);
|
||||
if (this.useAudioLayout) {
|
||||
return container.$el;
|
||||
}
|
||||
return container;
|
||||
},
|
||||
senderDisplayName() {
|
||||
return this.room.getMember(this.replyToEvent.sender.userId).name;
|
||||
},
|
||||
|
|
@ -627,9 +522,28 @@ export default {
|
|||
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
||||
return isAdmin;
|
||||
},
|
||||
useAudioLayout: {
|
||||
get: function () {
|
||||
if (this.room) {
|
||||
const tags = this.room.tags;
|
||||
if (tags && tags["ui_options"]) {
|
||||
return tags["ui_options"]["audio_layout"] === 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
initialLoadDone: {
|
||||
immediate: true,
|
||||
handler(value, oldValue) {
|
||||
if (value && !oldValue) {
|
||||
console.log("Loading finished!");
|
||||
}
|
||||
}
|
||||
},
|
||||
roomId: {
|
||||
immediate: true,
|
||||
handler(value, oldValue) {
|
||||
|
|
@ -729,6 +643,7 @@ export default {
|
|||
const getMoreIfNeeded = function _getMoreIfNeeded() {
|
||||
const container = self.$refs.chatContainer;
|
||||
if (
|
||||
container &&
|
||||
container.scrollHeight <= (1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
|
||||
self.timelineWindow &&
|
||||
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
|
||||
|
|
@ -867,22 +782,22 @@ export default {
|
|||
handleChatContainerResize({ ignoredWidth, height }) {
|
||||
const delta = height - this.chatContainerSize;
|
||||
this.chatContainerSize = height;
|
||||
const container = this.$refs.chatContainer;
|
||||
if (delta < 0) {
|
||||
const container = this.chatContainer;
|
||||
if (container && delta < 0) {
|
||||
container.scrollTop -= delta;
|
||||
}
|
||||
},
|
||||
|
||||
paginateBackIfNeeded() {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.chatContainer;
|
||||
if (container.scrollHeight <= container.clientHeight) {
|
||||
const container = this.chatContainer;
|
||||
if (container && container.scrollHeight <= container.clientHeight) {
|
||||
this.handleScrolledToTop();
|
||||
}
|
||||
});
|
||||
},
|
||||
onScroll(ignoredevent) {
|
||||
const container = this.$refs.chatContainer;
|
||||
const container = this.chatContainer;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -898,7 +813,7 @@ export default {
|
|||
container.scrollHeight === container.clientHeight
|
||||
? false
|
||||
: container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight ||
|
||||
(this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
|
||||
(this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
|
||||
|
||||
this.restartRRTimer();
|
||||
},
|
||||
|
|
@ -908,19 +823,20 @@ export default {
|
|||
return; // Not for this room
|
||||
}
|
||||
|
||||
const loadingDone = this.initialLoadDone;
|
||||
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
|
||||
|
||||
if (this.initialLoadDone) {
|
||||
if (this.initialLoadDone && !this.useAudioLayout) {
|
||||
this.paginateBackIfNeeded();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
scrollToSeeNew = true;
|
||||
}
|
||||
if (this.initialLoadDone && event.forwardLooking && !event.isRelation()) {
|
||||
if (loadingDone && event.forwardLooking && !event.isRelation()) {
|
||||
// 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 && container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
|
||||
scrollToSeeNew = true;
|
||||
}
|
||||
this.handleScrolledToBottom(scrollToSeeNew);
|
||||
}
|
||||
},
|
||||
|
|
@ -1140,16 +1056,18 @@ export default {
|
|||
.paginate(EventTimeline.FORWARDS, 10, true)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
this.scrollPosition.prepareFor("down");
|
||||
this.events = this.timelineWindow.getEvents();
|
||||
this.$nextTick(() => {
|
||||
// restore scroll position!
|
||||
console.log("Restore scroll!");
|
||||
this.scrollPosition.restore();
|
||||
if (scrollToEnd) {
|
||||
this.smoothScrollToEnd();
|
||||
}
|
||||
});
|
||||
if (!this.useAudioLayout) {
|
||||
this.scrollPosition.prepareFor("down");
|
||||
this.$nextTick(() => {
|
||||
// restore scroll position!
|
||||
console.log("Restore scroll!");
|
||||
this.scrollPosition.restore();
|
||||
if (scrollToEnd) {
|
||||
this.smoothScrollToEnd();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -1162,7 +1080,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) {
|
||||
const container = this.$refs.chatContainer;
|
||||
const container = this.chatContainer;
|
||||
const ref = this.$refs[eventId];
|
||||
if (container && ref) {
|
||||
const targetY = container.clientHeight / 2;
|
||||
|
|
@ -1172,9 +1090,9 @@ export default {
|
|||
},
|
||||
|
||||
smoothScrollToEnd() {
|
||||
this.$nextTick(function() {
|
||||
const container = this.$refs.chatContainer;
|
||||
if (container.children.length > 0) {
|
||||
this.$nextTick(function () {
|
||||
const container = this.chatContainer;
|
||||
if (container && container.children.length > 0) {
|
||||
const lastChild = container.children[container.children.length - 1];
|
||||
console.log("Scroll into view", lastChild);
|
||||
window.requestAnimationFrame(() => {
|
||||
|
|
@ -1251,7 +1169,7 @@ export default {
|
|||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 200);
|
||||
|
|
@ -1268,8 +1186,8 @@ export default {
|
|||
},
|
||||
|
||||
emojiSelected(e) {
|
||||
if(this.isEmojiQuickReaction) {
|
||||
// When quick emoji picker is clicked
|
||||
if (this.isEmojiQuickReaction) {
|
||||
// When quick emoji picker is clicked
|
||||
if (this.selectedEvent) {
|
||||
const event = this.selectedEvent;
|
||||
this.selectedEvent = null;
|
||||
|
|
@ -1369,65 +1287,78 @@ export default {
|
|||
* Start/restart the timer to Read Receipts.
|
||||
*/
|
||||
restartRRTimer() {
|
||||
console.log("Restart RR timer");
|
||||
this.stopRRTimer();
|
||||
this.rrTimer = setTimeout(this.rrTimerElapsed, READ_RECEIPT_TIMEOUT);
|
||||
|
||||
let eventIdFirst = null;
|
||||
let eventIdLast = null;
|
||||
if (!this.useAudioLayout) {
|
||||
const container = this.chatContainer;
|
||||
const elFirst = util.getFirstVisibleElement(container);
|
||||
const elLast = util.getLastVisibleElement(container);
|
||||
if (elFirst && elLast) {
|
||||
eventIdFirst = elFirst.getAttribute("eventId");
|
||||
eventIdLast = elLast.getAttribute("eventId");
|
||||
}
|
||||
}
|
||||
if (eventIdFirst && eventIdLast) {
|
||||
this.rrTimer = setTimeout(() => { this.rrTimerElapsed(eventIdFirst, eventIdLast) }, READ_RECEIPT_TIMEOUT);
|
||||
}
|
||||
},
|
||||
|
||||
rrTimerElapsed() {
|
||||
rrTimerElapsed(eventIdFirst, eventIdLast) {
|
||||
console.log("RR timer elapsed", eventIdFirst, eventIdLast);
|
||||
this.rrTimer = null;
|
||||
this.sendRR(eventIdFirst, eventIdLast);
|
||||
this.restartRRTimer();
|
||||
},
|
||||
|
||||
const container = this.$refs.chatContainer;
|
||||
const elFirst = util.getFirstVisibleElement(container);
|
||||
const elLast = util.getLastVisibleElement(container);
|
||||
if (elFirst && elLast) {
|
||||
const eventIdFirst = elFirst.getAttribute("eventId");
|
||||
const eventIdLast = elLast.getAttribute("eventId");
|
||||
if (eventIdLast && this.room) {
|
||||
var event = this.room.findEventById(eventIdLast);
|
||||
const index = this.events.indexOf(event);
|
||||
sendRR(eventIdFirst, eventIdLast) {
|
||||
console.log("SEND RR", eventIdFirst, eventIdLast);
|
||||
if (eventIdLast && this.room) {
|
||||
var event = this.room.findEventById(eventIdLast);
|
||||
const index = this.events.indexOf(event);
|
||||
|
||||
// Walk backwards through visible events to the first one that is incoming
|
||||
//
|
||||
var lastTimestamp = 0;
|
||||
if (this.lastRR) {
|
||||
lastTimestamp = this.lastRR.getTs();
|
||||
// Walk backwards through visible events to the first one that is incoming
|
||||
//
|
||||
var lastTimestamp = 0;
|
||||
if (this.lastRR) {
|
||||
lastTimestamp = this.lastRR.getTs();
|
||||
}
|
||||
|
||||
for (var i = index; i >= 0; i--) {
|
||||
event = this.events[i];
|
||||
if (event == this.lastRR || event.getTs() <= lastTimestamp) {
|
||||
// Already sent this or too old...
|
||||
break;
|
||||
}
|
||||
// Make sure it's not a local echo event...
|
||||
if (!event.getId().startsWith("~")) {
|
||||
// Send read receipt
|
||||
this.$matrix.matrixClient
|
||||
.sendReadReceipt(event)
|
||||
.then(() => {
|
||||
this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, event.getId());
|
||||
})
|
||||
.then(() => {
|
||||
console.log("RR sent for event: " + event.getId());
|
||||
this.lastRR = event;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to update read marker: ", err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.restartRRTimer();
|
||||
});
|
||||
return; // Bail out here
|
||||
}
|
||||
|
||||
for (var i = index; i >= 0; i--) {
|
||||
event = this.events[i];
|
||||
if (event == this.lastRR || event.getTs() <= lastTimestamp) {
|
||||
// Already sent this or too old...
|
||||
break;
|
||||
}
|
||||
// Make sure it's not a local echo event...
|
||||
if (!event.getId().startsWith("~")) {
|
||||
// Send read receipt
|
||||
this.$matrix.matrixClient
|
||||
.sendReadReceipt(event)
|
||||
.then(() => {
|
||||
this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, event.getId());
|
||||
})
|
||||
.then(() => {
|
||||
console.log("RR sent for event: " + event.getId());
|
||||
this.lastRR = event;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to update read marker: ", err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.restartRRTimer();
|
||||
});
|
||||
return; // Bail out here
|
||||
}
|
||||
|
||||
// Stop iterating at first visible
|
||||
if (event.getId() == eventIdFirst) {
|
||||
break;
|
||||
}
|
||||
// Stop iterating at first visible
|
||||
if (event.getId() == eventIdFirst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.restartRRTimer();
|
||||
},
|
||||
|
||||
showRecordingUI() {
|
||||
|
|
@ -1499,9 +1430,9 @@ export default {
|
|||
let div = document.createElement("div");
|
||||
div.classList.add("toast");
|
||||
div.innerText = this.$t("poll_create.results_shared");
|
||||
this.$refs.chatContainer.parentElement.appendChild(div);
|
||||
this.chatContainer.parentElement.appendChild(div);
|
||||
setTimeout(() => {
|
||||
this.$refs.chatContainer.parentElement.removeChild(div);
|
||||
this.chatContainer.parentElement.removeChild(div);
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
<component
|
||||
:is="componentForEvent(event, true)"
|
||||
:room="room"
|
||||
:event="event"
|
||||
:originalEvent="event"
|
||||
:nextEvent="events[index + 1]"
|
||||
:timelineSet="timelineSet"
|
||||
ref="exportedEvent"
|
||||
|
|
|
|||
|
|
@ -165,6 +165,16 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="account ma-3" flat>
|
||||
<v-card-title class="h2">{{ $t("room_info.ui_options") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-switch
|
||||
v-model="useAudioLayout"
|
||||
:label="$t('room_info.audio_layout')"
|
||||
></v-switch>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="members ma-3" flat>
|
||||
<v-card-title class="h2"
|
||||
>{{ $t("room_info.members") }}<v-spacer></v-spacer>
|
||||
|
|
@ -319,7 +329,7 @@ export default {
|
|||
],
|
||||
SHOW_MEMBER_LIMIT: 5,
|
||||
exporting: false,
|
||||
};
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$matrix.on("Room.timeline", this.onEvent);
|
||||
|
|
@ -362,6 +372,26 @@ export default {
|
|||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
useAudioLayout: {
|
||||
get: function () {
|
||||
if (this.room) {
|
||||
const tags = this.room.tags;
|
||||
if (tags && tags["ui_options"]) {
|
||||
return tags["ui_options"]["audio_layout"] === 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
set: function (audioLayout) {
|
||||
if (this.room && this.room.tags) {
|
||||
let options = this.room.tags["ui_options"] || {}
|
||||
options["audio_layout"] = (audioLayout ? 1 : 0);
|
||||
this.room.tags["ui_options"] = options;
|
||||
this.$matrix.matrixClient.setRoomTag(this.room.roomId, "ui_options", options);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
|
|||
|
|
@ -54,9 +54,10 @@ export default {
|
|||
this.player.addEventListener("pause", () => {
|
||||
this.playing = false;
|
||||
});
|
||||
this.player.addEventListener("ended", function () {
|
||||
this.player.addEventListener("ended", () => {
|
||||
this.pause();
|
||||
this.playing = false;
|
||||
this.$emit("playback-ended");
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
|
|
|||
|
|
@ -7,25 +7,41 @@ export default {
|
|||
downloadProgress: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log("Mounted with event:", JSON.stringify(this.event.getContent()))
|
||||
util
|
||||
.getAttachment(this.$matrix.matrixClient, this.event, (progress) => {
|
||||
this.downloadProgress = progress;
|
||||
console.log("Progress: " + progress);
|
||||
})
|
||||
.then((url) => {
|
||||
this.src = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to fetch attachment: ", err);
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.src) {
|
||||
const objectUrl = this.src;
|
||||
this.src = null;
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
watch: {
|
||||
event: {
|
||||
immediate: false,
|
||||
handler(value, ignoredOldValue) {
|
||||
this.loadAttachmentSource(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadAttachmentSource(this.event);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.loadAttachmentSource(null); // Release
|
||||
},
|
||||
methods: {
|
||||
loadAttachmentSource(event) {
|
||||
if (this.src) {
|
||||
const objectUrl = this.src;
|
||||
this.src = null;
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
if (event) {
|
||||
util
|
||||
.getAttachment(this.$matrix.matrixClient, event, (progress) => {
|
||||
this.downloadProgress = progress;
|
||||
console.log("Progress: " + progress);
|
||||
})
|
||||
.then((url) => {
|
||||
this.src = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to fetch attachment: ", err);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +1,51 @@
|
|||
import QuickReactions from './QuickReactions.vue';
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyHtml = require('linkifyjs/html');
|
||||
import QuickReactions from "./QuickReactions.vue";
|
||||
var linkify = require("linkifyjs");
|
||||
var linkifyHtml = require("linkifyjs/html");
|
||||
linkify.options.defaults.className = "link";
|
||||
linkify.options.defaults.target = { url: '_blank' };
|
||||
linkify.options.defaults.target = { url: "_blank" };
|
||||
|
||||
export default {
|
||||
components: {
|
||||
QuickReactions
|
||||
QuickReactions,
|
||||
},
|
||||
props: {
|
||||
room: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
event: {
|
||||
originalEvent: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
nextEvent: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
timelineSet: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
event: {},
|
||||
inReplyToEvent: null,
|
||||
inReplyToSender: null
|
||||
}
|
||||
inReplyToSender: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const relatesTo = this.event.getWireContent()['m.relates_to'];
|
||||
if (relatesTo && relatesTo['m.in_reply_to'])
|
||||
{
|
||||
const relatesTo = this.validEvent && this.event.getWireContent()["m.relates_to"];
|
||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||
// Can we find the original message?
|
||||
const originalEventId = relatesTo['m.in_reply_to'].event_id;
|
||||
const originalEventId = relatesTo["m.in_reply_to"].event_id;
|
||||
if (originalEventId && this.timelineSet) {
|
||||
const originalEvent = this.timelineSet.findEventById(originalEventId);
|
||||
if (originalEvent) {
|
||||
|
|
@ -55,7 +55,29 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
originalEvent: {
|
||||
immediate: true,
|
||||
handler(originalEvent, ignoredOldValue) {
|
||||
this.event = originalEvent;
|
||||
// Check not null and not {}
|
||||
if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) {
|
||||
this.originalEvent.getDecryptionPromise().then(() => {
|
||||
this.event = originalEvent;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
*
|
||||
* @returns true if event is non-null and contains data
|
||||
*/
|
||||
validEvent() {
|
||||
return this.event && Object.keys(this.event).length !== 0;
|
||||
},
|
||||
|
||||
incoming() {
|
||||
return this.event && this.event.getSender() != this.$matrix.currentUserId;
|
||||
},
|
||||
|
|
@ -67,21 +89,20 @@ export default {
|
|||
if (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
|
||||
return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
inReplyToText() {
|
||||
const relatesTo = this.event.getWireContent()['m.relates_to'];
|
||||
if (relatesTo && relatesTo['m.in_reply_to'])
|
||||
{
|
||||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||
const content = this.event.getContent();
|
||||
const lines = content.body.split('\n').reverse();
|
||||
while (lines.length && !lines[0].startsWith('> ')) lines.shift();
|
||||
const lines = content.body.split("\n").reverse();
|
||||
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||
if (lines[0] === '') lines.shift();
|
||||
const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, '');
|
||||
if (lines[0] === "") lines.shift();
|
||||
const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, "");
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
|
|
@ -92,23 +113,22 @@ export default {
|
|||
}
|
||||
|
||||
// We don't have the original text (at the moment at least)
|
||||
return this.$t('fallbacks.original_text');
|
||||
return this.$t("fallbacks.original_text");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
messageText() {
|
||||
const relatesTo = this.event.getWireContent()['m.relates_to'];
|
||||
if (relatesTo && relatesTo['m.in_reply_to'])
|
||||
{
|
||||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||
const content = this.event.getContent();
|
||||
|
||||
// Remove the new text and strip "> " from the old original text
|
||||
const lines = content.body.split('\n');
|
||||
while (lines.length && lines[0].startsWith('> ')) lines.shift();
|
||||
const lines = content.body.split("\n");
|
||||
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||
if (lines[0] === '') lines.shift();
|
||||
return lines.join('\n');
|
||||
if (lines[0] === "") lines.shift();
|
||||
return lines.join("\n");
|
||||
}
|
||||
return this.event.getContent().body;
|
||||
},
|
||||
|
|
@ -118,40 +138,36 @@ export default {
|
|||
*/
|
||||
|
||||
messageClasses() {
|
||||
return {'messageIn':true,'from-admin':this.senderIsAdminOrModerator(this.event)}
|
||||
return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event) };
|
||||
},
|
||||
|
||||
userAvatar() {
|
||||
if (!this.$matrix.userAvatar) {
|
||||
return null;
|
||||
}
|
||||
return this.$matrix.matrixClient.mxcUrlToHttp(
|
||||
this.$matrix.userAvatar,
|
||||
80,
|
||||
80,
|
||||
"scale",
|
||||
true
|
||||
);
|
||||
return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, "scale", true);
|
||||
},
|
||||
|
||||
userAvatarLetter() {
|
||||
if (!this.$matrix.currentUser) {
|
||||
return null;
|
||||
}
|
||||
return (this.$matrix.currentUserDisplayName || this.$matrix.currentUserId.substring(1)).substring(0, 1).toUpperCase();
|
||||
}
|
||||
return (this.$matrix.currentUserDisplayName || this.$matrix.currentUserId.substring(1))
|
||||
.substring(0, 1)
|
||||
.toUpperCase();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
ownAvatarClicked() {
|
||||
this.$emit("own-avatar-clicked", {event: this.event});
|
||||
this.$emit("own-avatar-clicked", { event: this.event });
|
||||
},
|
||||
|
||||
otherAvatarClicked(avatarRef) {
|
||||
this.$emit("other-avatar-clicked", {event: this.event, anchor: avatarRef});
|
||||
this.$emit("other-avatar-clicked", { event: this.event, anchor: avatarRef });
|
||||
},
|
||||
|
||||
showContextMenu(buttonRef) {
|
||||
this.$emit("context-menu", {event: this.event,anchor: buttonRef});
|
||||
this.$emit("context-menu", { event: this.event, anchor: buttonRef });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -159,7 +175,7 @@ export default {
|
|||
*/
|
||||
eventSenderDisplayName(event) {
|
||||
if (event.getSender() == this.$matrix.currentUserId) {
|
||||
return this.$t('message.you');
|
||||
return this.$t("message.you");
|
||||
}
|
||||
if (this.room) {
|
||||
const member = this.room.getMember(event.getSender());
|
||||
|
|
@ -173,12 +189,12 @@ export default {
|
|||
/**
|
||||
* In the case where the state_key points out a userId for an operation (e.g. membership events)
|
||||
* return the display name of the affected user.
|
||||
* @param event
|
||||
* @returns
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
eventStateKeyDisplayName(event) {
|
||||
if (event.getStateKey() == this.$matrix.currentUserId) {
|
||||
return this.$t('message.you');
|
||||
return this.$t("message.you");
|
||||
}
|
||||
if (this.room) {
|
||||
const member = this.room.getMember(event.getStateKey());
|
||||
|
|
@ -193,13 +209,7 @@ export default {
|
|||
if (this.room) {
|
||||
const member = this.room.getMember(event.getSender());
|
||||
if (member) {
|
||||
return member.getAvatarUrl(
|
||||
this.$matrix.matrixClient.getHomeserverUrl(),
|
||||
40,
|
||||
40,
|
||||
"scale",
|
||||
true
|
||||
);
|
||||
return member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
@ -236,6 +246,6 @@ export default {
|
|||
|
||||
linkify(text) {
|
||||
return linkifyHtml(text);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,37 +1,25 @@
|
|||
import Vue from 'vue';
|
||||
import Vuetify from 'vuetify/lib';
|
||||
import icUser from '@/assets/icons/user.vue';
|
||||
import icPassword from '@/assets/icons/password.vue';
|
||||
import icEdit from '@/assets/icons/edit.vue';
|
||||
import icGlobe from '@/assets/icons/globe.vue';
|
||||
import icAddReaction from '@/assets/icons/addReaction.vue';
|
||||
import icPoll from '@/assets/icons/poll.vue';
|
||||
|
||||
// Import all .vue icons and process them, so they can be used
|
||||
// as $vuetify.icons.<iconname>
|
||||
var icons = {}
|
||||
function importAll(r) {
|
||||
return r.keys().map(res => {
|
||||
// Remove"./"
|
||||
const parts = res.split("/");
|
||||
const iconName = parts[1].split(".")[0];
|
||||
icons[iconName] = { component: r(res).default };
|
||||
});
|
||||
}
|
||||
importAll(require.context('@/assets/icons/', true, /\.vue$/));
|
||||
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: 'md',
|
||||
values: {
|
||||
user: {
|
||||
component: icUser
|
||||
},
|
||||
password: {
|
||||
component: icPassword
|
||||
},
|
||||
edit: {
|
||||
component: icEdit
|
||||
},
|
||||
globe: {
|
||||
component: icGlobe
|
||||
},
|
||||
addReaction: {
|
||||
component: icAddReaction
|
||||
},
|
||||
poll: {
|
||||
component: icPoll
|
||||
},
|
||||
},
|
||||
user: icUser
|
||||
values: icons,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue