Initial implementation of "audio mode"
This commit is contained in:
parent
d5942fdb8e
commit
09173a65f1
14 changed files with 944 additions and 410 deletions
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);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue