Merge branch 'dev' into '419-push-notifications'
# Conflicts: # src/assets/translations/en.json
This commit is contained in:
commit
fa51d4a9ec
28 changed files with 1000 additions and 507 deletions
|
|
@ -9,6 +9,7 @@
|
|||
"productLink": "letsconvene.im",
|
||||
"defaultServer": "https://neo.keanu.im",
|
||||
"identityServer_unset": "",
|
||||
"registrationToken_unset": "",
|
||||
"rtl": false,
|
||||
"accentColor_unset": "",
|
||||
"logo_unset": "",
|
||||
|
|
|
|||
|
|
@ -101,22 +101,17 @@ body {
|
|||
}
|
||||
|
||||
.notification-alert {
|
||||
display: inline-block;
|
||||
background-color: $alert-bg-color;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
.icon-circle {
|
||||
color: $alert-bg-color;
|
||||
}
|
||||
&.popup-open::after {
|
||||
top: 20px;
|
||||
top: 35px;
|
||||
color: #246bfd;
|
||||
}
|
||||
.missed-items-popup {
|
||||
position: absolute;
|
||||
bottom: -17px;
|
||||
left: -20px;
|
||||
left: -175px;
|
||||
transform: translateY(100%);
|
||||
background: #246bfd;
|
||||
border-radius: 8px;
|
||||
|
|
@ -125,8 +120,10 @@ body {
|
|||
padding: 22px 18px 23px 18px;
|
||||
z-index: 300;
|
||||
user-select: none;
|
||||
width: 300px;
|
||||
justify-content: space-between;
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
white-space: break-spaces;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
|
|
@ -161,7 +158,12 @@ body {
|
|||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-root {
|
||||
|
|
@ -791,6 +793,7 @@ body {
|
|||
|
||||
.room-name-inline {
|
||||
text-align: start;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.room-name.no-upper {
|
||||
|
|
@ -1286,7 +1289,7 @@ body {
|
|||
.option-warning {
|
||||
background: linear-gradient(0deg, #FFF3F3, #FFF3F3), #FFFBED;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
padding: 18px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
|
|
@ -1294,7 +1297,7 @@ body {
|
|||
line-height: 17px;
|
||||
.v-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1433,6 +1436,14 @@ body {
|
|||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
margin-left: -8px !important;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
.reaction-emoji {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
|
|
@ -1452,6 +1463,9 @@ body {
|
|||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
.clap-button {
|
||||
font-size: 24px;
|
||||
}
|
||||
.mic-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
|
|
|||
BIN
src/assets/sounds/clapping.mp3
Normal file
BIN
src/assets/sounds/clapping.mp3
Normal file
Binary file not shown.
|
|
@ -11,7 +11,13 @@
|
|||
"show_less": "Show less",
|
||||
"show_more": "Show more",
|
||||
"add_reaction": "Add reaction",
|
||||
"click_to_remove": "Click to remove"
|
||||
"click_to_remove": "Click to remove",
|
||||
"time": {
|
||||
"recently": "just now",
|
||||
"minutes": "1 minute ago | {n} minutes ago",
|
||||
"hours": "1 hour ago | {n} hours ago",
|
||||
"days": "1 day ago | {n} days ago"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"start_private_chat": "Private chat with this user",
|
||||
|
|
@ -90,7 +96,11 @@
|
|||
"incoming_message_deleted_text": "This message was deleted.",
|
||||
"not_allowed_to_send": "Only admins and moderators are allowed to send to the room",
|
||||
"reaction_count_more": "{reactionCount} more",
|
||||
"seen_by": "Seen by no members | Seen by 1 member | Seen by {count} members"
|
||||
"seen_by": "Seen by no members | Seen by 1 member | Seen by {count} members",
|
||||
"file": "File",
|
||||
"files": "Files",
|
||||
"images": "Images",
|
||||
"send_attachements_dialog_title": "Do you want to send following attachments ?"
|
||||
},
|
||||
"room": {
|
||||
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",
|
||||
|
|
@ -164,7 +174,10 @@
|
|||
"send_verification": "Send verification email",
|
||||
"sent_verification": "An email has been sent to {email}. Please use your regular email client to verify the address.",
|
||||
"resend_verification": "Resend verification email",
|
||||
"email_not_valid": "Email address not valid"
|
||||
"email_not_valid": "Email address not valid",
|
||||
"registration_token": "Please enter registration token",
|
||||
"send_token": "Send token",
|
||||
"token_not_valid": "Invalid token"
|
||||
},
|
||||
"profile": {
|
||||
"title": "My Profile",
|
||||
|
|
@ -206,7 +219,8 @@
|
|||
"status_logging_in": "Logging in...",
|
||||
"status_joining": "Joining room...",
|
||||
"join_failed": "Failed to join room.",
|
||||
"choose_name": "Choose a name to use"
|
||||
"choose_name": "Choose a name to use",
|
||||
"you_have_been_banned": "You have been banned from this room."
|
||||
},
|
||||
"invite": {
|
||||
"title": "Add Friends",
|
||||
|
|
@ -332,5 +346,19 @@
|
|||
},
|
||||
"notification": {
|
||||
"title": "New message received"
|
||||
},
|
||||
"emoji": {
|
||||
"search": "Search...",
|
||||
"categories": {
|
||||
"activity": "Activity",
|
||||
"flags": "Flags",
|
||||
"foods": "Foods",
|
||||
"frequently": "Frequently used",
|
||||
"objects": "Objects",
|
||||
"nature": "Nature",
|
||||
"peoples": "Peoples",
|
||||
"symbols": "Symbols",
|
||||
"places": "Places"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,36 +30,20 @@
|
|||
"edited": "تەھرىرلەندى",
|
||||
"file_prefix": "ھۆججەت ",
|
||||
"user_said": "{user} سۆزلىدى:",
|
||||
"user_left": "قوللانغۇچى مۇنازىرىدىن چېكىندى",
|
||||
"user_joined": "قوللانغۇچى مۇنازىرىغا قاتناشتى",
|
||||
"user_was_invited": "قوللانغۇچى مۇنازىرە ئۆيىگە تەكلىپ قىلىندى...",
|
||||
"user_encrypted_room": "قوللانغۇچى مۇنازىرە-خانىنى سىفىرلاشتۇردى",
|
||||
"user_changed_room_avatar": "قوللانغۇچى مۇنازىرە-خانىدىكى كۆرىنىشىنى ئۆزگەرتتى",
|
||||
"user_changed_avatar": "قوللانغۇچى كۆرىنىشىنى ئۆزگەرتتى",
|
||||
"user_changed_display_name": "قوللانغۇچىنىڭ ئىسمى«يېڭى ئىسىمغا» ئۆزگەرتىلدى",
|
||||
"user_aliased_room": "مۇنازىرە ئۆيىنىڭ ئىسمى ئۆزگەرتىلدى",
|
||||
"user_created_room": "يېڭى مۇنازىرە ئۆيى قۇرۇلدى",
|
||||
"you": "سىز",
|
||||
"room_powerlevel_change": "{user} دەرىجىسىنى ئۆزگەرتىش {changes}",
|
||||
"user_changed_guest_access_open": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلىشىغا رۇخسەت قىلدى",
|
||||
"user_changed_guest_access_closed": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلۇشتىن رەت قىلىندى",
|
||||
"user_powerlevel_change_from_to": "قوللانغۇچى بۇرۇنقى دەرىجىسىدىن يېڭى دەرىجىسىگە كۆتۈرىلدى",
|
||||
"scale_image": "كىچىكلەنمە رەسىم",
|
||||
"users_are_typing": "ئەزالىرى يېزىۋاتىدۇ {count}",
|
||||
"user_is_typing": "قوللانغۇچى يېزىۋاتىدۇ",
|
||||
"your_message": "ئۇچۇرىڭىز ...",
|
||||
"replying_to": "{user}",
|
||||
"unread_messages": "ئوقۇلمىغان ئۇچۇرلار",
|
||||
"user_changed_room_topic": "قوللانغۇچى مۇنازىرەخانىنىڭ تېمىسىنى ئۆزگەرتتى",
|
||||
"user_changed_room_name": "قوللانغۇچى مۇنازىرەخانىنىڭ ئىسمىنى ئۆزگەرتتى",
|
||||
"room_joinrule_public": "كۆپچىلىك",
|
||||
"room_joinrule_invite": "تەكلىپ قىلىڭ",
|
||||
"user_changed_join_rules": "قوللانغۇچى مۇنازىرەخانىنى مەلۇم تىپقا ئۆزگەرتتى",
|
||||
"room_history_joined": "ئەزالار قاتناشقاندىن باشلاپ ئوقۇغىلى بولىدۇ",
|
||||
"room_history_invited": "ئەزالار تەكلىپ قىلىنغان ۋاقىتتىن باشلاپ ئوقۇغىلى بولىدۇ",
|
||||
"room_history_shared": "مۇنازىرەخانىدىكى ھەركىم ئوقۇيالايدۇ",
|
||||
"room_history_world_readable": "ھەركىم ئوقۇيالايدۇ",
|
||||
"user_changed_room_history": "قوللانغۇچى» مۇنازىرەخانىنىڭ تارىخىنى قۇردى»"
|
||||
"room_history_world_readable": "ھەركىم ئوقۇيالايدۇ"
|
||||
},
|
||||
"language_display_name": "ئۇيغۇرچە",
|
||||
"new_room": {
|
||||
|
|
|
|||
|
|
@ -28,38 +28,53 @@
|
|||
}}</span>
|
||||
</v-avatar>
|
||||
</div>
|
||||
|
||||
<!-- Current emoji reactions -->
|
||||
<div class="typing-users">
|
||||
<transition-group name="list" tag="div">
|
||||
<v-avatar v-for="reaction in reactions" :key="reaction.member.userId" class="typing-user" size="32" color="grey">
|
||||
<img v-if="memberAvatar(reaction.member)" :src="memberAvatar(reaction.member)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
reaction.member.name.substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
<div class="reaction-emoji">{{ reaction.emoji }}</div>
|
||||
</v-avatar>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<div v-if="currentAudioEvent" class="senderAndTime">
|
||||
<div class="sender">{{ eventSenderDisplayName(currentAudioEvent) }}</div>
|
||||
<div class="time">
|
||||
{{ formatTime(currentAudioEvent.event.origin_server_ts) }}
|
||||
{{ formatTimeAgo(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-btn id="btn-rewind" :disabled="!info || info.loading" @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-progress-circular v-if="info && info.loading" :value="info.loadPercent" @click.stop="pause" size="36" width="2" style="margin:26px"></v-progress-circular>
|
||||
<v-btn v-else-if="info && info.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-btn id="btn-forward" :disabled="!info || info.loading" @click.stop="forward" icon>
|
||||
<v-icon size="28">$vuetify.icons.forward</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="load-later">
|
||||
<v-btn :class="{'mic-button': true, 'dimmed': !canRecordAudio}" ref="mic_button" fab small elevation="0" v-blur
|
||||
@click.stop="micButtonClicked()">
|
||||
<v-icon color="white">mic</v-icon>
|
||||
</v-btn>
|
||||
<div style="align-self: flex-end;">
|
||||
<v-btn class="clap-button clickable" text elevation="0" v-blur @click.stop="clapButtonClicked()">👏</v-btn>
|
||||
<v-btn :class="{'mic-button': true, 'dimmed': !canRecordAudio}" ref="mic_button" fab small elevation="0" v-blur
|
||||
@click.stop="micButtonClicked()">
|
||||
<v-icon color="white">mic</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
|
||||
</div>
|
||||
|
||||
|
|
@ -102,87 +117,46 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
src: null,
|
||||
REACTION_ANIMATION_TIME: 2500,
|
||||
info: null,
|
||||
currentAudioEvent: null,
|
||||
autoPlayNextEvent: false,
|
||||
currentAudioSource: null,
|
||||
player: null,
|
||||
duration: 0,
|
||||
playPercent: 0,
|
||||
playTime: 0,
|
||||
playing: false,
|
||||
analyzer: null,
|
||||
analyzerDataArray: null,
|
||||
showReadOnlyToast: false,
|
||||
reactions: [],
|
||||
updateReactionsTimer: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('audio-playback-started', this.audioPlaybackStarted);
|
||||
this.$root.$on('audio-playback-paused', this.audioPlaybackPaused);
|
||||
this.$root.$on('audio-playback-ended', this.audioPlaybackEnded);
|
||||
this.$root.$on('audio-playback-reaction', this.audioPlaybackReaction);
|
||||
document.body.classList.add("dark");
|
||||
this.$root.$on('playback-start', this.onPlaybackStart);
|
||||
this.player = this.$refs.player;
|
||||
this.player.autoplay = false;
|
||||
this.player.addEventListener("timeupdate", this.updateProgressBar);
|
||||
this.player.addEventListener("play", () => {
|
||||
if (!this.analyser) {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
});
|
||||
this.$audioPlayer.setAutoplay(false);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off('audio-playback-started', this.audioPlaybackStarted);
|
||||
this.$root.$off('audio-playback-paused', this.audioPlaybackPaused);
|
||||
this.$root.$off('audio-playback-ended', this.audioPlaybackEnded);
|
||||
this.$root.$off('audio-playback-reaction', this.audioPlaybackReaction);
|
||||
document.body.classList.remove("dark");
|
||||
this.$audioPlayer.removeListener(this._uid);
|
||||
this.currentAudioEvent = null;
|
||||
this.loadAudioAttachmentSource(); // Release
|
||||
this.$root.$off('playback-start', this.onPlaybackStart);
|
||||
},
|
||||
computed: {
|
||||
canRecordAudio() {
|
||||
return !this.$matrix.currentRoomIsReadOnlyForUser && util.browserCanRecordAudio();
|
||||
},
|
||||
currentTime() {
|
||||
return util.formatDuration(this.playTime);
|
||||
return util.formatDuration(this.info ? this.info.currentTime : 0);
|
||||
},
|
||||
currentTimeMs() {
|
||||
return this.info ? this.info.currentTime : 0;
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
return util.formatDuration(this.info ? this.info.duration : 0);
|
||||
},
|
||||
recordingMembersExceptMe() {
|
||||
return this.recordingMembers.filter((member) => {
|
||||
|
|
@ -202,18 +176,14 @@ export default {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -222,87 +192,96 @@ export default {
|
|||
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.clearReactions();
|
||||
|
||||
this.info = this.$audioPlayer.addListener(this._uid, value);
|
||||
|
||||
const autoPlayWasSet = this.autoPlayNextEvent;
|
||||
this.autoPlayNextEvent = false;
|
||||
|
||||
if (value.getSender() == this.$matrix.currentUserId) {
|
||||
// Sent by us. Don't autoplay if we just sent this (i.e. it is ahead of our read marker)
|
||||
if (this.room && !this.room.getReceiptsForEvent(value).includes(value.getSender())) {
|
||||
this.player.autoplay = false;
|
||||
this.$audioPlayer.setAutoplay(false);
|
||||
this.autoPlayNextEvent = autoPlayWasSet;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadAudioAttachmentSource();
|
||||
this.$audioPlayer.load(value, this.timelineSet);
|
||||
}
|
||||
},
|
||||
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();
|
||||
}
|
||||
if (this.currentAudioEvent) {
|
||||
this.$audioPlayer.setAutoplay(false);
|
||||
this.$audioPlayer.play(this.currentAudioEvent, this.timelineSet);
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
this.player.autoplay = false;
|
||||
if (this.player.src) {
|
||||
this.player.pause();
|
||||
this.$audioPlayer.setAutoplay(false);
|
||||
if (this.currentAudioEvent) {
|
||||
this.$audioPlayer.pause(this.currentAudioEvent);
|
||||
}
|
||||
},
|
||||
rewind() {
|
||||
if (this.player.src) {
|
||||
this.player.currentTime = Math.max(0, this.player.currentTime - 15);
|
||||
if (this.currentAudioEvent) {
|
||||
this.$audioPlayer.seekRelative(this.currentAudioEvent, -15000);
|
||||
}
|
||||
},
|
||||
forward() {
|
||||
if (this.player.src) {
|
||||
this.player.currentTime = Math.min(this.player.duration, this.player.currentTime + 15);
|
||||
if (this.currentAudioEvent) {
|
||||
this.$audioPlayer.seekRelative(this.currentAudioEvent, 15000);
|
||||
}
|
||||
},
|
||||
updateProgressBar() {
|
||||
if (this.player.duration > 0) {
|
||||
this.playPercent = Math.floor(
|
||||
(100 / this.player.duration) * this.player.currentTime
|
||||
);
|
||||
} else {
|
||||
this.playPercent = 0;
|
||||
audioPlaybackStarted() {
|
||||
if (!this.analyser) {
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
let audioSource = null;
|
||||
if (audioCtx) {
|
||||
audioSource = audioCtx.createMediaElementSource(this.$audioPlayer.getPlayerElement());
|
||||
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);
|
||||
}
|
||||
}
|
||||
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();
|
||||
this.updateVisualization();
|
||||
if (this.currentAudioEvent) {
|
||||
this.$emit("mark-read", this.currentAudioEvent.getId(), this.currentAudioEvent.getId());
|
||||
}
|
||||
},
|
||||
onPlaybackEnd() {
|
||||
audioPlaybackPaused() {
|
||||
this.clearVisualization();
|
||||
},
|
||||
audioPlaybackEnded() {
|
||||
this.clearVisualization();
|
||||
this.loadNext(true && this.autoplay);
|
||||
},
|
||||
audioPlaybackReaction(reaction) {
|
||||
// Play sound!
|
||||
const audio = new Audio(require("@/assets/sounds/clapping.mp3"));
|
||||
audio.volume = 0.6;
|
||||
audio.play();
|
||||
|
||||
const member = this.room.getMember(reaction.sender);
|
||||
if (member) {
|
||||
this.reactions.push(Object.assign({ addedAt: Date.now(), member: member}, reaction));
|
||||
if (!this.updateReactionsTimer) {
|
||||
this.updateReactionsTimer = setInterval(this.updateReactions, 300);
|
||||
}
|
||||
}
|
||||
},
|
||||
loadPrevious() {
|
||||
const audioMessages = this.events.filter((e) => e.getContent().msgtype === "m.audio");
|
||||
for (let i = 0; i < audioMessages.length; i++) {
|
||||
|
|
@ -332,11 +311,11 @@ export default {
|
|||
if (e.getId() === this.readMarker) {
|
||||
if (i < (audioMessages.length - 1)) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.$audioPlayer.setAutoplay(autoplay);
|
||||
this.currentAudioEvent = audioMessages[i + 1];
|
||||
} else {
|
||||
this.autoPlayNextEvent = true;
|
||||
this.player.autoplay = autoplay;
|
||||
this.$audioPlayer.setAutoplay(autoplay);
|
||||
this.currentAudioEvent = e;
|
||||
this.$emit("loadnext");
|
||||
}
|
||||
|
|
@ -347,7 +326,7 @@ export default {
|
|||
// No read marker found. Just use the first event here...
|
||||
if (audioMessages.length > 0) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.$audioPlayer.setAutoplay(autoplay);
|
||||
this.currentAudioEvent = audioMessages[0];
|
||||
}
|
||||
return;
|
||||
|
|
@ -358,11 +337,11 @@ export default {
|
|||
if (e.getId() === this.currentAudioEvent.getId()) {
|
||||
if (i < (audioMessages.length - 1)) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.$audioPlayer.setAutoplay(autoplay);
|
||||
this.currentAudioEvent = audioMessages[i + 1];
|
||||
} else {
|
||||
this.autoPlayNextEvent = true;
|
||||
this.player.autoplay = autoplay;
|
||||
this.$audioPlayer.setAutoplay(autoplay);
|
||||
this.$emit("loadnext");
|
||||
}
|
||||
break;
|
||||
|
|
@ -390,8 +369,7 @@ export default {
|
|||
volume.style.height = "" + w + "px";
|
||||
const color = 80 + (value * (256 - 80)) / 256;
|
||||
volume.style.backgroundColor = `rgb(${color},${color},${color})`;
|
||||
|
||||
if (this.playing) {
|
||||
if (this.info && this.info.playing) {
|
||||
requestAnimationFrame(this.updateVisualization);
|
||||
} else {
|
||||
this.clearVisualization();
|
||||
|
|
@ -404,36 +382,24 @@ export default {
|
|||
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);
|
||||
});
|
||||
updateReactions() {
|
||||
const now = Date.now();
|
||||
this.reactions = this.reactions.filter(r => {
|
||||
return (r.addedAt + this.REACTION_ANIMATION_TIME > now);
|
||||
});
|
||||
if (this.reactions.length == 0) {
|
||||
this.clearReactions();
|
||||
}
|
||||
},
|
||||
|
||||
clearReactions() {
|
||||
if (this.updateReactionsTimer) {
|
||||
clearInterval(this.updateReactionsTimer);
|
||||
this.updateReactionsTimer = null;
|
||||
}
|
||||
this.reactions = [];
|
||||
},
|
||||
|
||||
memberAvatar(member) {
|
||||
if (member) {
|
||||
return member.getAvatarUrl(
|
||||
|
|
@ -456,6 +422,18 @@ export default {
|
|||
} else {
|
||||
this.$emit('start-recording');
|
||||
}
|
||||
},
|
||||
|
||||
clapButtonClicked() {
|
||||
if (this.currentAudioEvent) {
|
||||
this.$emit("sendclap", { event: this.currentAudioEvent, timeOffset: this.currentTimeMs })
|
||||
|
||||
// Also, play locally
|
||||
this.audioPlaybackReaction({
|
||||
sender: this.$matrix.currentUserId,
|
||||
emoji: "👏"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
v-on:loadnext="handleScrolledToBottom(false)"
|
||||
v-on:loadprevious="handleScrolledToTop()"
|
||||
v-on:mark-read="sendRR"
|
||||
v-on:sendclap="sendClapReactionAtTime"
|
||||
/>
|
||||
<VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
||||
|
|
@ -188,35 +189,52 @@
|
|||
</v-container>
|
||||
|
||||
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
|
||||
accept="image/*, audio/*, video/*, .pdf" class="d-none" />
|
||||
accept="image/*, audio/*, video/*, .pdf" class="d-none" multiple/>
|
||||
|
||||
<div v-if="currentImageInputPath">
|
||||
<v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'">
|
||||
<div v-if="currentFileInputsDialog">
|
||||
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
|
||||
<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" />
|
||||
<div>
|
||||
file: {{ currentImageInputPath.name }}
|
||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
|
||||
<span v-else-if="currentImageInput && currentImageInput.dimensions">
|
||||
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span>
|
||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||
({{ 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" />
|
||||
</div>
|
||||
<div v-if="currentSendError">{{ currentSendError }}</div>
|
||||
<div v-else>{{ currentSendProgress }}</div>
|
||||
</v-card-text>
|
||||
<v-card-title>{{ $t('message.send_attachements_dialog_title') }}</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<template v-if="Array.isArray(currentImageInputs) && currentImageInputs.length">
|
||||
<v-card-title v-if="currentImageInputs.length > 1"> {{ $t('message.images') }} </v-card-title>
|
||||
<v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': currentImageInputs.length > 1}">
|
||||
<div :class="{'col-4': currentImageInputs.length > 1}" v-for="(currentImageInput, id) in currentImageInputs" :key="id">
|
||||
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
|
||||
contain class="current-image-input-path" />
|
||||
<div>
|
||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
|
||||
<span v-else-if="currentImageInput && currentImageInput.dimensions">
|
||||
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span>
|
||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||
({{ formatBytes(currentImageInput.scaledSize) }})</span>
|
||||
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
|
||||
v-model="currentImageInput.useScaled" />
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</template>
|
||||
<template v-if="Array.isArray(currentFileInputs) && currentFileInputs.length">
|
||||
<v-card-title v-if="nonImageFiles.length > 1">{{ $t('message.files') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-for="(currentImageInputPath, id) in currentFileInputs" :key="id">
|
||||
<div v-if="!currentImageInputPath.type.includes('image/')">
|
||||
<span> {{ $t('message.file') }}: {{ currentImageInputPath.name }}</span>
|
||||
<span> ({{ formatBytes(currentImageInputPath.size) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</template>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel">{{
|
||||
$t("menu.cancel")
|
||||
}}</v-btn>
|
||||
<v-spacer>
|
||||
<div v-if="currentSendError">{{ currentSendError }}</div>
|
||||
<div v-else>{{ currentSendProgress }}</div>
|
||||
</v-spacer>
|
||||
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-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-card-actions>
|
||||
|
|
@ -225,7 +243,7 @@
|
|||
</div>
|
||||
|
||||
<MessageOperationsBottomSheet ref="messageOperationsSheet">
|
||||
<VEmojiPicker ref="emojiPicker" @select="emojiSelected" />
|
||||
<VEmojiPicker ref="emojiPicker" @select="emojiSelected" :i18n="i18nEmoji"/>
|
||||
</MessageOperationsBottomSheet>
|
||||
|
||||
<StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" />
|
||||
|
|
@ -342,8 +360,8 @@ export default {
|
|||
timelineWindowPaginating: false,
|
||||
|
||||
scrollPosition: null,
|
||||
currentImageInput: null,
|
||||
currentImageInputPath: null,
|
||||
currentImageInputs: null,
|
||||
currentFileInputs: null,
|
||||
currentSendOperation: null,
|
||||
currentSendProgress: null,
|
||||
currentSendShowSendButton: true,
|
||||
|
|
@ -393,7 +411,21 @@ export default {
|
|||
/** Calculated style for message operations. We position the "popup" at the selected message. */
|
||||
opStyle: "",
|
||||
|
||||
isEmojiQuickReaction: true
|
||||
isEmojiQuickReaction: true,
|
||||
i18nEmoji: {
|
||||
search: this.$t("emoji.search"),
|
||||
categories: {
|
||||
Activity: this.$t("emoji.categories.activity"),
|
||||
Flags: this.$t("emoji.categories.flags"),
|
||||
Foods: this.$t("emoji.categories.foods"),
|
||||
Frequently: this.$t("emoji.categories.frequently"),
|
||||
Objects: this.$t("emoji.categories.objects"),
|
||||
Nature: this.$t("emoji.categories.nature"),
|
||||
Peoples: this.$t("emoji.categories.peoples"),
|
||||
Symbols: this.$t("emoji.categories.symbols"),
|
||||
Places: this.$t("emoji.categories.places")
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -408,6 +440,7 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.$root.$on('audio-playback-ended', this.audioPlaybackEnded);
|
||||
const container = this.chatContainer;
|
||||
if (container) {
|
||||
this.scrollPosition = new ScrollPosition(container);
|
||||
|
|
@ -418,6 +451,8 @@ export default {
|
|||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.$root.$off('audio-playback-ended', this.audioPlaybackEnded);
|
||||
this.$audioPlayer.pause();
|
||||
this.stopRRTimer();
|
||||
},
|
||||
|
||||
|
|
@ -427,6 +462,20 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
nonImageFiles() {
|
||||
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/"))
|
||||
},
|
||||
isCurrentFileInputsAnArray() {
|
||||
return Array.isArray(this.currentFileInputs)
|
||||
},
|
||||
currentFileInputsDialog: {
|
||||
get() {
|
||||
return this.isCurrentFileInputsAnArray
|
||||
},
|
||||
set() {
|
||||
this.currentFileInputs = null
|
||||
}
|
||||
},
|
||||
chatContainer() {
|
||||
const container = this.$refs.chatContainer;
|
||||
if (this.useVoiceMode) {
|
||||
|
|
@ -500,19 +549,24 @@ export default {
|
|||
//
|
||||
const ref = this.selectedEvent && this.$refs[this.selectedEvent.getId()];
|
||||
var top = 0;
|
||||
var left = 0;
|
||||
var left = "unset";
|
||||
var right = "unset";
|
||||
if (ref && ref[0]) {
|
||||
if (this.showAvatarMenuAnchor) {
|
||||
var rectAnchor = this.showAvatarMenuAnchor.getBoundingClientRect();
|
||||
var rectChat = this.$refs.avatarOperationsStrut.getBoundingClientRect();
|
||||
top = rectAnchor.top - rectChat.top;
|
||||
left = rectAnchor.left - rectChat.left;
|
||||
if (this.$vuetify.rtl) {
|
||||
right = (rectAnchor.right - rectChat.right)+ "px";
|
||||
} else {
|
||||
left = (rectAnchor.left - rectChat.left) + "px";
|
||||
}
|
||||
// if (left + 250 > rectChat.right) {
|
||||
// left = rectChat.right - 250; // Pretty ugly, but we want to make sure it does not escape the screen, and we don't have the exakt width of it (yet)!
|
||||
// }
|
||||
}
|
||||
}
|
||||
return "top:" + top + "px;left:" + left + "px";
|
||||
return "top:" + top + "px;left:" + left + ";right:" + right;
|
||||
},
|
||||
canRecordAudio() {
|
||||
return util.browserCanRecordAudio();
|
||||
|
|
@ -878,7 +932,20 @@ export default {
|
|||
scrollToSeeNew = true;
|
||||
}
|
||||
this.handleScrolledToBottom(scrollToSeeNew);
|
||||
}
|
||||
|
||||
// If kick or ban event, redirect to "goodbye"...
|
||||
if (event.getType() === "m.room.member" &&
|
||||
event.getStateKey() == this.$matrix.currentUserId &&
|
||||
(event.getPrevContent() || {}).membership == "join" &&
|
||||
(
|
||||
(event.getContent().membership == "leave" && event.getSender() != this.currentUserId) ||
|
||||
(event.getContent().membership == "ban" ))
|
||||
) {
|
||||
this.$store.commit("setCurrentRoomId", null);
|
||||
const wasPurged = event.getContent().reason == "Room Deleted";
|
||||
this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onUserTyping(event, member) {
|
||||
|
|
@ -928,64 +995,64 @@ export default {
|
|||
this.$refs.attachment.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle picked attachment
|
||||
*/
|
||||
handlePickedAttachment(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
optimizeImage(e,event,file) {
|
||||
let currentImageInput = {
|
||||
image: e.target.result,
|
||||
dimensions: null,
|
||||
};
|
||||
try {
|
||||
currentImageInput.dimensions = sizeOf(dataUriToBuffer(e.target.result));
|
||||
|
||||
// Need to resize?
|
||||
const w = currentImageInput.dimensions.width;
|
||||
const h = currentImageInput.dimensions.height;
|
||||
if (w > 640 || h > 640) {
|
||||
var aspect = w / h;
|
||||
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
|
||||
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
|
||||
var imageResize = new ImageResize({
|
||||
format: "png",
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
outputType: "blob",
|
||||
});
|
||||
imageResize
|
||||
.play(event.target)
|
||||
.then((img) => {
|
||||
Vue.set(
|
||||
currentImageInput,
|
||||
"scaled",
|
||||
new File([img], file.name, {
|
||||
type: img.type,
|
||||
lastModified: Date.now(),
|
||||
})
|
||||
);
|
||||
Vue.set(currentImageInput, "useScaled", true);
|
||||
Vue.set(currentImageInput, "scaledSize", img.size);
|
||||
Vue.set(currentImageInput, "scaledDimensions", {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Resize failed:", err);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get image dimensions: " + error);
|
||||
}
|
||||
return currentImageInput
|
||||
},
|
||||
handleFileReader(event, file) {
|
||||
if (file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const file = event.target.files[0];
|
||||
if (file.type.startsWith("image/")) {
|
||||
this.currentImageInput = {
|
||||
image: e.target.result,
|
||||
dimensions: null,
|
||||
};
|
||||
try {
|
||||
this.currentImageInput.dimensions = sizeOf(dataUriToBuffer(e.target.result));
|
||||
|
||||
// Need to resize?
|
||||
const w = this.currentImageInput.dimensions.width;
|
||||
const h = this.currentImageInput.dimensions.height;
|
||||
if (w > 640 || h > 640) {
|
||||
var aspect = w / h;
|
||||
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
|
||||
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
|
||||
var imageResize = new ImageResize({
|
||||
format: "png",
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
outputType: "blob",
|
||||
});
|
||||
imageResize
|
||||
.play(event.target)
|
||||
.then((img) => {
|
||||
Vue.set(
|
||||
this.currentImageInput,
|
||||
"scaled",
|
||||
new File([img], file.name, {
|
||||
type: img.type,
|
||||
lastModified: Date.now(),
|
||||
})
|
||||
);
|
||||
Vue.set(this.currentImageInput, "useScaled", true);
|
||||
Vue.set(this.currentImageInput, "scaledSize", img.size);
|
||||
Vue.set(this.currentImageInput, "scaledDimensions", {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Resize failed:", err);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get image dimensions: " + error);
|
||||
}
|
||||
const currentImageInput = this.optimizeImage(e, event, file)
|
||||
this.currentImageInputs = Array.isArray(this.currentImageInputs) ? [...this.currentImageInputs, currentImageInput] : [currentImageInput]
|
||||
}
|
||||
console.log(this.currentImageInput);
|
||||
this.$matrix.matrixClient.getMediaConfig().then((config) => {
|
||||
this.currentImageInputPath = file;
|
||||
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, file] : [file];
|
||||
if (config["m.upload.size"] && file.size > config["m.upload.size"]) {
|
||||
this.currentSendError = this.$t("message.upload_file_too_large");
|
||||
this.currentSendShowSendButton = false;
|
||||
|
|
@ -994,9 +1061,15 @@ export default {
|
|||
}
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handle picked attachment
|
||||
*/
|
||||
handlePickedAttachment(event) {
|
||||
Object.values(event.target.files).forEach(file => this.handleFileReader(event, file));
|
||||
},
|
||||
|
||||
showStickerPicker() {
|
||||
this.$refs.stickerPickerSheet.open();
|
||||
|
|
@ -1014,41 +1087,35 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
|
||||
sendAttachment(withText) {
|
||||
this.$refs.attachment.value = null;
|
||||
if (this.currentImageInputPath) {
|
||||
var inputFile = this.currentImageInputPath;
|
||||
if (this.currentImageInput && this.currentImageInput.scaled && this.currentImageInput.useScaled) {
|
||||
if (this.isCurrentFileInputsAnArray) {
|
||||
let inputFiles = this.currentFileInputs;
|
||||
if (Array.isArray(this.currentImageInputs) && this.currentImageInputs.scaled && this.currentImageInputs.useScaled) {
|
||||
// Send scaled version of image instead!
|
||||
inputFile = this.currentImageInput.scaled;
|
||||
inputFiles = this.currentImageInputs.map(({scaled}) => scaled)
|
||||
}
|
||||
|
||||
const promises = inputFiles.map(inputFile => util.sendImage(this.$matrix.matrixClient, this.roomId, inputFile, this.onUploadProgress));
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
this.currentSendOperation = null;
|
||||
this.currentImageInputs = null;
|
||||
this.currentFileInputs = null;
|
||||
this.currentSendProgress = null;
|
||||
this.currentSendOperation = util.sendImage(
|
||||
this.$matrix.matrixClient,
|
||||
this.roomId,
|
||||
inputFile,
|
||||
this.onUploadProgress
|
||||
);
|
||||
this.currentSendOperation
|
||||
.then(() => {
|
||||
this.currentSendOperation = null;
|
||||
this.currentImageInput = null;
|
||||
this.currentImageInputPath = null;
|
||||
this.currentSendProgress = null;
|
||||
if (withText) {
|
||||
this.sendMessage(withText);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === "AbortError" || err === "Abort") {
|
||||
this.currentSendError = null;
|
||||
} else {
|
||||
this.currentSendError = err.LocaleString();
|
||||
}
|
||||
this.currentSendOperation = null;
|
||||
this.currentSendProgress = null;
|
||||
});
|
||||
if (withText) {
|
||||
this.sendMessage(withText);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === "AbortError" || err === "Abort") {
|
||||
this.currentSendError = null;
|
||||
} else {
|
||||
this.currentSendError = err.LocaleString();
|
||||
}
|
||||
this.currentSendOperation = null;
|
||||
this.currentSendProgress = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -1058,8 +1125,8 @@ export default {
|
|||
this.currentSendOperation.abort();
|
||||
}
|
||||
this.currentSendOperation = null;
|
||||
this.currentImageInput = null;
|
||||
this.currentImageInputPath = null;
|
||||
this.currentImageInputs = null;
|
||||
this.currentFileInputs = null;
|
||||
this.currentSendProgress = null;
|
||||
this.currentSendError = null;
|
||||
},
|
||||
|
|
@ -1248,6 +1315,17 @@ export default {
|
|||
this.$refs.messageOperationsSheet.close();
|
||||
},
|
||||
|
||||
sendClapReactionAtTime(e) {
|
||||
util
|
||||
.sendQuickReaction(this.$matrix.matrixClient, this.roomId, "👏", e.event, { timeOffset: e.timeOffset.toFixed(0)})
|
||||
.then(() => {
|
||||
console.log("Send clap reaction at time", e.timeOffset);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to send clap reaction:", err);
|
||||
});
|
||||
},
|
||||
|
||||
sendQuickReaction(e) {
|
||||
let previousReaction = null;
|
||||
|
||||
|
|
@ -1439,7 +1517,7 @@ export default {
|
|||
|
||||
onVoiceRecording(event) {
|
||||
this.currentSendShowSendButton = false;
|
||||
this.currentImageInputPath = event.file;
|
||||
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, event.file] : [event.file];
|
||||
var text = undefined;
|
||||
if (this.currentInput && this.currentInput.length > 0) {
|
||||
text = this.currentInput;
|
||||
|
|
@ -1508,8 +1586,27 @@ export default {
|
|||
} else {
|
||||
this.showNoRecordingAvailableDialog = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when an audio message has played to the end. We listen to this so we can optionally auto-play
|
||||
* the next audio event.
|
||||
* @param matrixEvent The event that stopped playing
|
||||
*/
|
||||
audioPlaybackEnded(matrixEventId) {
|
||||
if (!this.useVoiceMode) { // Voice mode has own autoplay handling inside "AudioLayout"!
|
||||
// Auto play consecutive audio messages, either incoming or sent.
|
||||
const filteredEvents = this.filteredEvents;
|
||||
const index = filteredEvents.findIndex(e => e.getId() === matrixEventId);
|
||||
if (index >= 0 && index < (filteredEvents.length - 1)) {
|
||||
const nextEvent = filteredEvents[index + 1];
|
||||
if (nextEvent.getContent().msgtype === "m.audio") {
|
||||
// Yes, audio event!
|
||||
this.$audioPlayer.play(nextEvent, this.timelineSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
</div>
|
||||
<v-icon class="icon-dropdown" size="11">$vuetify.icons.ic_dropdown</v-icon>
|
||||
<div :class="{ 'notification-alert': true, 'popup-open': showMissedItemsInfo }" v-if="notifications">
|
||||
<v-icon class="icon-circle" size="11">circle</v-icon>
|
||||
<!-- MISSED ITEMS POPUP -->
|
||||
<!-- <div class="missed-items-popup-background" v-if="showMissedItemsInfo" @click.stop="setHasShownMissedItemsHint()"></div> -->
|
||||
<div class="missed-items-popup" v-if="showMissedItemsInfo" @click.stop="setHasShownMissedItemsHint()">
|
||||
|
|
@ -144,7 +145,7 @@ export default {
|
|||
this.$matrix.invites.length > 0;
|
||||
},
|
||||
notificationsText() {
|
||||
const invitationCount = this.$matrix.invites.length
|
||||
const invitationCount = this.$matrix.invites.length
|
||||
if (invitationCount > 0) {
|
||||
return this.$tc('room.invitations', invitationCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,13 +34,17 @@
|
|||
</v-row>
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("new_room.name_room") }}</div>
|
||||
<div class="text-start font-weight-light">{{ $t("new_room.name_room") }}</div>
|
||||
<v-text-field v-model="roomName" color="black" :rules="roomNamerules" counter="50" maxlength="50"
|
||||
background-color="white" v-on:keyup.enter="$refs.topic.focus()" :disabled="step > steps.INITIAL" autofocus
|
||||
solo @update:error="updateErrorState"></v-text-field>
|
||||
<div class="text-left font-weight-light" v-show="roomName.length > 0">{{ $t("new_room.room_topic") }}</div>
|
||||
<v-text-field v-model="roomTopic" v-show="roomName.length > 0" ref="topic" color="black" background-color="white"
|
||||
v-on:keyup.enter="$refs.create.$el.focus()" :disabled="step > steps.INITIAL" solo></v-text-field>
|
||||
<div class="text-start font-weight-light" v-show="roomName.length > 0">{{ $t("new_room.room_topic") }}</div>
|
||||
<v-textarea v-model="roomTopic" v-show="roomName.length > 0" ref="topic" color="black" background-color="white"
|
||||
v-on:keydown.enter.prevent="
|
||||
() => {
|
||||
$refs.create.$el.focus()
|
||||
}
|
||||
" :disabled="step > steps.INITIAL" solo full-width auto-grow rows="1" no-resize hide-details></v-textarea>
|
||||
|
||||
<!-- Our only option right now is voice mode, so if not enabled, hide the 'options' drop down as well -->
|
||||
<template v-if="$config.experimental_voice_mode || $config.experimental_read_only_room || $config.experimental_public_room">
|
||||
|
|
@ -102,7 +106,7 @@
|
|||
<v-container v-if="canEditProfile" class="pa-10">
|
||||
<v-row class="align-center">
|
||||
<v-col class="py-0">
|
||||
<div class="text-left font-weight-bold">{{ $t("join.choose_name") }}</div>
|
||||
<div class="text-start font-weight-bold">{{ $t("join.choose_name") }}</div>
|
||||
<v-select ref="avatar" :items="availableAvatars" cache-items outlined dense @change="selectAvatar"
|
||||
:value="availableAvatars[0]" single-line autofocus>
|
||||
<template v-slot:selection>
|
||||
|
|
@ -295,7 +299,7 @@ export default {
|
|||
}
|
||||
if (room) {
|
||||
this.publicRoomLink = this.$router.getRoomLink(
|
||||
room.getCanonicalAlias() || roomId
|
||||
room.getCanonicalAlias(), roomId, room.name
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -424,10 +428,7 @@ export default {
|
|||
console.log(
|
||||
"CreateRoom: Set display name to: " + this.selectedProfile.name
|
||||
);
|
||||
return this.$matrix.matrixClient.setDisplayName(
|
||||
this.selectedProfile.name,
|
||||
undefined
|
||||
);
|
||||
return this.$matrix.setUserDisplayName(this.selectedProfile.name);
|
||||
}
|
||||
}.bind(this)
|
||||
)
|
||||
|
|
@ -492,7 +493,7 @@ export default {
|
|||
this.$matrix.matrixClient,
|
||||
room_id,
|
||||
this.roomAvatarFile,
|
||||
function (p) {
|
||||
(p) => {
|
||||
if (p.total) {
|
||||
self.status = this.$t("new_room.status_avatar_total", {
|
||||
count: p.loaded || 0,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<v-container fluid class="mt-40" v-if="step == steps.ENTER_EMAIL">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("login.email") }}</div>
|
||||
<div class="text-start font-weight-light">{{ $t("login.email") }}</div>
|
||||
<v-text-field v-model="email" color="black" :rules="emailRules" type="email" maxlength="200"
|
||||
background-color="white" v-on:keyup.enter="onEmailEntered(email)" autofocus solo></v-text-field>
|
||||
<v-btn :disabled="!emailIsValid" color="black" depressed class="filled-button"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
<v-container fluid class="mt-40" v-if="step == steps.TERMS">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("login.terms") }}</div>
|
||||
<div class="text-start font-weight-light">{{ $t("login.terms") }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div>
|
||||
<div class="text-left font-weight-light">{{ $t("login.sent_verification", { email: this.email }) }}</div>
|
||||
<div class="text-start font-weight-light">{{ $t("login.sent_verification", { email: this.email }) }}</div>
|
||||
<v-progress-circular style="display: inline-flex" indeterminate color="primary"
|
||||
size="20"></v-progress-circular>
|
||||
</div>
|
||||
|
|
@ -61,6 +61,21 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-container fluid class="mt-40" v-if="step == steps.ENTER_TOKEN">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-start font-weight-light">{{ $t("login.registration_token") }}</div>
|
||||
<v-text-field v-model="token" color="black" :rules="tokenRules" type="text" maxlength="64"
|
||||
background-color="white" v-on:keyup.enter="onTokenEntered(token)" autofocus solo></v-text-field>
|
||||
<v-btn :disabled="!tokenIsValid" color="black" depressed class="filled-button"
|
||||
@click.stop="onTokenEntered(token)">
|
||||
{{ $t("login.send_token") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
</v-fade-transition>
|
||||
</template>
|
||||
|
||||
|
|
@ -74,6 +89,7 @@ const steps = Object.freeze({
|
|||
EXTERNAL_AUTH: 3,
|
||||
ENTER_EMAIL: 4,
|
||||
AWAITING_EMAIL_VERIFICATION: 5,
|
||||
ENTER_TOKEN: 6,
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
@ -85,6 +101,9 @@ export default {
|
|||
emailRules: [
|
||||
v => this.validEmail(v) || this.$t("login.email_not_valid")
|
||||
],
|
||||
tokenRules: [
|
||||
v => this.validToken(v) || this.$t("login.token_not_valid")
|
||||
],
|
||||
policies: null,
|
||||
onPoliciesAccepted: () => { },
|
||||
onEmailResend: () => { },
|
||||
|
|
@ -95,6 +114,7 @@ export default {
|
|||
emailVerificationAttempt: 1,
|
||||
emailVerificationSid: null,
|
||||
emailVerificationPollTimer: null,
|
||||
token: "",
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -105,6 +125,9 @@ export default {
|
|||
emailIsValid() {
|
||||
return this.validEmail(this.email);
|
||||
},
|
||||
tokenIsValid() {
|
||||
return this.validToken(this.token);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -116,6 +139,15 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
validToken(token) {
|
||||
// https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3231-token-authenticated-registration.md
|
||||
if (/^[A-Za-z0-9._~-]{1,64}$/.test(token)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
registrationFlowHandler(client, authData) {
|
||||
const flows = authData.flows;
|
||||
if (!flows || flows.length == 0) {
|
||||
|
|
@ -257,6 +289,34 @@ export default {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
case "m.login.registration_token": {
|
||||
this.step = steps.CREATING;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.$config.registrationToken) {
|
||||
// We have a token in config, use that!
|
||||
//
|
||||
const data = {
|
||||
session: authData.session,
|
||||
type: nextStage,
|
||||
token: this.$config.registrationToken
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
} else {
|
||||
this.step = steps.ENTER_TOKEN;
|
||||
this.onTokenEntered = (token) => {
|
||||
this.step = steps.CREATING;
|
||||
const data = {
|
||||
session: authData.session,
|
||||
type: nextStage,
|
||||
token: token
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
this.step = steps.EXTERNAL_AUTH;
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
{{ roomId && roomId.startsWith("@") ? $t("join.title_user") : $t("join.title") }}
|
||||
</div>
|
||||
<div class="join-title">
|
||||
{{ roomName }}
|
||||
{{ roomDisplayName || roomName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -82,14 +82,15 @@
|
|||
|
||||
<interactive-auth ref="interactiveAuth" />
|
||||
|
||||
<v-btn id="btn-join" class="btn-dark" large @click.stop="handleJoin" :loading="loading" v-if="!currentUser">{{
|
||||
<v-btn id="btn-join" class="btn-dark" :disabled="room && room.selfMembership == 'ban'" large @click.stop="handleJoin" :loading="loading" v-if="!currentUser">{{
|
||||
roomId && roomId.startsWith("@") ? $t("join.enter_room_user") : $t("join.enter_room")
|
||||
}}</v-btn>
|
||||
<v-btn id="btn-join" class="btn-dark" large block @click.stop="handleJoin" :loading="loading" v-else>{{
|
||||
<v-btn id="btn-join" class="btn-dark" :disabled="room && room.selfMembership == 'ban'" large block @click.stop="handleJoin" :loading="loading" v-else>{{
|
||||
roomId && roomId.startsWith("@") ? $t("join.join_user") : $t("join.join")
|
||||
}}</v-btn>
|
||||
|
||||
<div v-if="loadingMessage" class="text-center">{{ loadingMessage }}</div>
|
||||
<div v-if="room && room.selfMembership == 'ban'" class="text-center">{{ $t("join.you_have_been_banned") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -215,6 +216,14 @@ export default {
|
|||
let activeLanguages = [...this.getLanguages()];
|
||||
return activeLanguages.filter((lang) => lang.value === this.$i18n.locale);
|
||||
},
|
||||
roomDisplayName() {
|
||||
// If there is a display name in to invite link, use that!
|
||||
try {
|
||||
return new URL(location.href).searchParams.get('roomName');
|
||||
} catch(ignoredError) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
roomId: {
|
||||
|
|
@ -363,7 +372,7 @@ export default {
|
|||
return Promise.resolve(user);
|
||||
} else {
|
||||
console.log("Join: Set display name to: " + this.selectedProfile.name);
|
||||
return this.$matrix.matrixClient.setDisplayName(this.selectedProfile.name, undefined);
|
||||
return this.$matrix.setUserDisplayName(this.selectedProfile.name);
|
||||
}
|
||||
}.bind(this)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<v-col>
|
||||
<div class="room-name no-upper">{{ $t("login.title") }}</div>
|
||||
</v-col>
|
||||
<v-col class="text-right">
|
||||
<v-col class="text-end">
|
||||
<v-btn id="btn-close" text v-if="showCloseButton" @click.stop="$navigation.pop">
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
<ActionRow
|
||||
@click="showEditPasswordDialog = true"
|
||||
:icon="'$vuetify.icons.password'"
|
||||
:text="$t('profile.set_password')"
|
||||
:text="$matrix.currentUser.is_guest ? $t('profile.set_password') : $t('profile.change_password')"
|
||||
/>
|
||||
<ActionRow
|
||||
@click="
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
:width="$vuetify.breakpoint.smAndUp ? '940px' : '80%'"
|
||||
>
|
||||
<v-card :disabled="settingPassword">
|
||||
<v-card-title>{{ $t("profile.change_password") }}</v-card-title>
|
||||
<v-card-title>{{ $matrix.currentUser.is_guest ? $t("profile.set_password") : $t("profile.change_password") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-if="!$matrix.currentUser.is_guest"
|
||||
|
|
@ -232,42 +232,6 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
user() {
|
||||
if (!this.$matrix.matrixClient) {
|
||||
return null;
|
||||
}
|
||||
return this.$matrix.matrixClient.getUser(this.$matrix.currentUserId);
|
||||
},
|
||||
|
||||
displayName() {
|
||||
if (!this.user) {
|
||||
return null;
|
||||
}
|
||||
return this.user.displayName || this.user.userId;
|
||||
},
|
||||
|
||||
userAvatar() {
|
||||
if (!this.user || !this.user.avatarUrl) {
|
||||
return null;
|
||||
}
|
||||
return this.$matrix.matrixClient.mxcUrlToHttp(
|
||||
this.user.avatarUrl,
|
||||
80,
|
||||
80,
|
||||
"scale",
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
userAvatarLetter() {
|
||||
if (!this.user) {
|
||||
return null;
|
||||
}
|
||||
return (this.user.displayName || this.user.userId.substring(1))
|
||||
.substring(0, 1)
|
||||
.toUpperCase();
|
||||
},
|
||||
|
||||
passwordsMatch() {
|
||||
return (
|
||||
!this.newPasswordHasError &&
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ export default {
|
|||
this.$emit("file", { file: this.recordedFile });
|
||||
},
|
||||
getFile(send) {
|
||||
//const duration = Date.now() - this.recordStartedAt;
|
||||
const duration = Date.now() - this.recordStartedAt;
|
||||
this.recorder
|
||||
.stop()
|
||||
.getMp3()
|
||||
|
|
@ -468,6 +468,7 @@ export default {
|
|||
lastModified: Date.now(),
|
||||
}
|
||||
);
|
||||
this.recordedFile.duration = duration;
|
||||
if (send) {
|
||||
this.send();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,13 @@
|
|||
<template>
|
||||
<div class="audio-player d-flex flex-row align-center">
|
||||
<audio ref="player" :src="src" @durationchange="updateDuration">
|
||||
<slot></slot>
|
||||
</audio>
|
||||
<v-btn v-if="playing" id="btn-pause" @click.stop="pause" icon
|
||||
><v-icon size="20">pause</v-icon></v-btn
|
||||
>
|
||||
<v-btn v-else id="btn-play" @click.stop="play" icon
|
||||
><v-icon size="20">play_arrow</v-icon></v-btn
|
||||
>
|
||||
<v-progress-circular v-if="info.loading" @click.stop="pause" :value="info.loadPercent" size="24" width="2" style="margin:6px"></v-progress-circular>
|
||||
<v-btn v-else-if="info.playing" id="btn-pause" @click.stop="pause" icon><v-icon size="20">pause</v-icon></v-btn>
|
||||
<v-btn v-else id="btn-play" @click.stop="play" icon><v-icon size="20">play_arrow</v-icon></v-btn>
|
||||
<div class="play-time">
|
||||
{{ currentTime }} / {{ totalTime }}
|
||||
</div>
|
||||
<v-slider
|
||||
color="currentColor"
|
||||
track-color="#cccccc"
|
||||
class="play-progress"
|
||||
v-model="playheadPercent"
|
||||
min="0"
|
||||
max="100"
|
||||
/>
|
||||
<v-slider @change="seeked" :disabled="!info.url" color="currentColor" track-color="#cccccc" class="play-progress" :value="info.playPercent" min="0"
|
||||
max="100" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -28,8 +16,14 @@ import util from "../../plugins/utils";
|
|||
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
event: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
timelineSet: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null;
|
||||
},
|
||||
|
|
@ -37,86 +31,37 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
player: null,
|
||||
duration: 0,
|
||||
playPercent: 0,
|
||||
playTime: 0,
|
||||
playing: false,
|
||||
info: this.install(),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
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.player.addEventListener("pause", () => {
|
||||
this.playing = false;
|
||||
});
|
||||
this.player.addEventListener("ended", () => {
|
||||
this.pause();
|
||||
this.playing = false;
|
||||
this.$emit("playback-ended");
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off('playback-start', this.onPlaybackStart);
|
||||
this.$audioPlayer.removeListener(this._uid);
|
||||
},
|
||||
computed: {
|
||||
currentTime() {
|
||||
return util.formatDuration(this.playTime);
|
||||
return util.formatDuration(this.info.currentTime);
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
return util.formatDuration(this.info.duration);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
install() {
|
||||
return this.$audioPlayer.addListener(this._uid, this.event);
|
||||
},
|
||||
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();
|
||||
}
|
||||
}
|
||||
this.$audioPlayer.play(this.event, this.timelineSet);
|
||||
},
|
||||
pause() {
|
||||
if (this.player.src) {
|
||||
this.player.pause();
|
||||
}
|
||||
},
|
||||
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;
|
||||
this.$audioPlayer.pause(this.event);
|
||||
},
|
||||
onPlaybackStart(item) {
|
||||
if (item != this && this.playing) {
|
||||
if (item != this.src && this.info.playing) {
|
||||
this.pause();
|
||||
}
|
||||
},
|
||||
seeked(percent) {
|
||||
this.$audioPlayer.seek(this.event, percent);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
<template>
|
||||
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
||||
<div class="bubble audio-bubble">
|
||||
<audio-player :src="src">{{ $t('fallbacks.audio_file')}}</audio-player>
|
||||
<audio-player :event="event" :timelineSet="timelineSet">{{ $t('fallbacks.audio_file')}}</audio-player>
|
||||
</div>
|
||||
</message-incoming>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import attachmentMixin from "./attachmentMixin";
|
||||
import MessageIncoming from './MessageIncoming.vue';
|
||||
import AudioPlayer from './AudioPlayer.vue';
|
||||
|
||||
export default {
|
||||
extends: MessageIncoming,
|
||||
mixins: [attachmentMixin],
|
||||
components: { MessageIncoming, AudioPlayer }
|
||||
components: { MessageIncoming, AudioPlayer },
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
<template>
|
||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||
<div class="audio-bubble">
|
||||
<audio-player :src="src">{{ $t('fallbacks.audio_file')}}</audio-player>
|
||||
<audio-player :event="event" :timelineSet="timelineSet">{{ $t('fallbacks.audio_file')}}</audio-player>
|
||||
</div>
|
||||
</message-outgoing>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import attachmentMixin from "./attachmentMixin";
|
||||
import AudioPlayer from './AudioPlayer.vue';
|
||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
|
||||
export default {
|
||||
extends: MessageOutgoing,
|
||||
components: { MessageOutgoing, AudioPlayer },
|
||||
mixins: [attachmentMixin],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -247,6 +247,46 @@ export default {
|
|||
return date.toLocaleString();
|
||||
},
|
||||
|
||||
formatTimeAgo(time) {
|
||||
const date = new Date();
|
||||
date.setTime(time);
|
||||
var ti = Math.abs(new Date().getTime() - date.getTime());
|
||||
ti = ti / 1000; // Convert to seconds
|
||||
let s = "";
|
||||
if (ti < 60) {
|
||||
s = this.$t("global.time.recently");
|
||||
} else if (ti < 3600 && Math.round(ti / 60) < 60) {
|
||||
s = this.$tc("global.time.minutes", Math.round(ti / 60));
|
||||
} else if (ti < 86400 && Math.round(ti / 60 / 60) < 24) {
|
||||
s = this.$tc("global.time.hours", Math.round(ti / 60 / 60));
|
||||
} else {
|
||||
s = this.$tc("global.time.days", Math.round(ti / 60 / 60 / 24));
|
||||
}
|
||||
return this.toLocalNumbers(s);
|
||||
},
|
||||
|
||||
/**
|
||||
* Possibly convert numerals to local representation (currently only for "bo" locale)
|
||||
* @param str String in which to convert numerals [0-9]
|
||||
* @returns converted string
|
||||
*/
|
||||
toLocalNumbers(str) {
|
||||
if (this.$i18n.locale == "bo") {
|
||||
// Translate to tibetan numerals
|
||||
let result = "";
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let c = str.charCodeAt(i);
|
||||
if (c >= 48 && c <= 57) {
|
||||
result += String.fromCharCode(c + 0x0f20 - 48);
|
||||
} else {
|
||||
result += String.fromCharCode(c);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return str;
|
||||
},
|
||||
|
||||
linkify(text) {
|
||||
return linkifyHtml(text);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default {
|
|||
if (!this.user) {
|
||||
return null;
|
||||
}
|
||||
return this.user.displayName;
|
||||
return this.$matrix.userDisplayName || this.user.displayName;
|
||||
},
|
||||
set(newValue) {
|
||||
this.user.displayName = newValue
|
||||
|
|
@ -20,17 +20,17 @@ export default {
|
|||
},
|
||||
|
||||
userAvatar() {
|
||||
if (!this.user || !this.user.avatarUrl) {
|
||||
if (!this.$matrix.userAvatar) {
|
||||
return null;
|
||||
}
|
||||
return this.$matrix.matrixClient.mxcUrlToHttp(this.user.avatarUrl, 80, 80, 'scale', true);
|
||||
return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, 'scale', true);
|
||||
},
|
||||
|
||||
userAvatarLetter() {
|
||||
if (!this.user) {
|
||||
return null;
|
||||
}
|
||||
return (this.user.displayName || this.user.userId.substring(1)).substring(0, 1).toUpperCase();
|
||||
return (this.$matrix.userDisplayName || this.user.displayName || this.user.userId.substring(1)).substring(0, 1).toUpperCase();
|
||||
},
|
||||
|
||||
passwordsMatch() {
|
||||
|
|
@ -46,7 +46,7 @@ export default {
|
|||
})
|
||||
},
|
||||
updateDisplayName(name) {
|
||||
this.$matrix.matrixClient.setDisplayName(name || this.user.userId);
|
||||
this.$matrix.setUserDisplayName(name || this.user.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ export default {
|
|||
publicRoomLink() {
|
||||
if (this.room && this.roomJoinRule == "public") {
|
||||
return this.$router.getRoomLink(
|
||||
this.room.getCanonicalAlias() || this.room.roomId
|
||||
this.room.getCanonicalAlias(), this.room.roomId, this.room.name
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
16
src/main.js
16
src/main.js
|
|
@ -7,6 +7,7 @@ import matrix from './services/matrix.service'
|
|||
import navigation from './services/navigation.service'
|
||||
import config from './services/config.service'
|
||||
import analytics from './services/analytics.service'
|
||||
import audioPlayer from './services/audio.service';
|
||||
import 'roboto-fontface/css/roboto/roboto-fontface.css'
|
||||
import 'material-design-icons-iconfont/dist/material-design-icons.css'
|
||||
import VEmojiPicker from 'v-emoji-picker';
|
||||
|
|
@ -31,10 +32,11 @@ const configLoadedPromise = new Promise((resolve, ignoredreject) => {
|
|||
// eslint-disable-next-line
|
||||
Vue.use(config, globalThis.window.location.origin, (config) => {
|
||||
resolve(config);
|
||||
}); // Use this before cleaninsights below, it depends on config!
|
||||
}); // Use this before cleaninsights below, it depends on config!
|
||||
});
|
||||
Vue.use(analytics);
|
||||
Vue.use(VueClipboard);
|
||||
Vue.use(audioPlayer);
|
||||
|
||||
const vuetify = createVuetify(config);
|
||||
|
||||
|
|
@ -176,11 +178,17 @@ const vueInstance = new Vue({
|
|||
matrix,
|
||||
config,
|
||||
analytics,
|
||||
render: h => h(App)
|
||||
audioPlayer,
|
||||
render: h => h(App),
|
||||
});
|
||||
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
|
||||
if (vueInstance.$config.accentColor) {
|
||||
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
|
||||
}
|
||||
vueInstance.$audioPlayer.$root = vueInstance; // Make sure a $root is available here
|
||||
configLoadedPromise.then((config) => {
|
||||
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor;
|
||||
if (config.accentColor) {
|
||||
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor;
|
||||
}
|
||||
vueInstance.$mount('#app');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,22 @@ class UploadPromise extends Promise {
|
|||
}
|
||||
|
||||
class Util {
|
||||
getAttachment(matrixClient, event, progressCallback, asBlob = false) {
|
||||
getAttachmentUrlAndDuration(event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = event.getContent();
|
||||
if (content.url != null) {
|
||||
resolve([content.url, content.info.duration]);
|
||||
return;
|
||||
}
|
||||
if (content.file && content.file.url) {
|
||||
resolve([content.file.url, content.info.duration]);
|
||||
} else {
|
||||
reject("No url found!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAttachment(matrixClient, event, progressCallback, asBlob = false, abortController = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = event.getContent();
|
||||
if (content.url != null) {
|
||||
|
|
@ -73,6 +88,7 @@ class Util {
|
|||
}
|
||||
|
||||
axios.get(url, {
|
||||
signal: abortController ? abortController.signal : undefined,
|
||||
responseType: 'arraybuffer', onDownloadProgress: progressEvent => {
|
||||
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
|
||||
if (progressCallback) {
|
||||
|
|
@ -206,13 +222,13 @@ class Util {
|
|||
return this.sendMessage(matrixClient, roomId, "m.room.message", content);
|
||||
}
|
||||
|
||||
sendQuickReaction(matrixClient, roomId, emoji, event) {
|
||||
sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) {
|
||||
const content = {
|
||||
'm.relates_to': {
|
||||
'm.relates_to': Object.assign(extraData, {
|
||||
key: emoji,
|
||||
rel_type: 'm.annotation',
|
||||
event_id: event.getId()
|
||||
}
|
||||
})
|
||||
};
|
||||
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
|
||||
}
|
||||
|
|
@ -337,6 +353,11 @@ class Util {
|
|||
mimetype: file.type,
|
||||
size: file.size
|
||||
};
|
||||
|
||||
// If audio, send duration in ms as well
|
||||
if (file.duration) {
|
||||
info.duration = file.duration;
|
||||
}
|
||||
|
||||
var description = file.name;
|
||||
var msgtype = 'm.image';
|
||||
|
|
@ -778,6 +799,13 @@ class Util {
|
|||
return _browserCanRecordAudio;
|
||||
}
|
||||
|
||||
getRoomNameFromAlias(alias) {
|
||||
if (alias && alias.startsWith('#') && alias.indexOf(':') > 0) {
|
||||
return alias.slice(1).split(':')[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getUniqueAliasForRoomName(matrixClient, roomName, homeServer, iterationCount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var preferredAlias = roomName.replace(/\s/g, "").toLowerCase();
|
||||
|
|
|
|||
|
|
@ -140,8 +140,13 @@ router.beforeEach((to, from, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.getRoomLink = function (roomId) {
|
||||
return window.location.origin + window.location.pathname + "#/room/" + encodeURIComponent(util.sanitizeRoomId(roomId));
|
||||
router.getRoomLink = function (alias, roomId, roomName) {
|
||||
if ((!alias || roomName.replace(/\s/g, "").toLowerCase() !== util.getRoomNameFromAlias(alias)) && roomName) {
|
||||
// There is no longer a correlation between alias and room name, probably because room name has
|
||||
// changed. Include the "?roomName" part
|
||||
return window.location.origin + window.location.pathname + "?roomName=" + encodeURIComponent(roomName) + "#/room/" + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
|
||||
}
|
||||
return window.location.origin + window.location.pathname + "#/room/" + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
|
||||
}
|
||||
|
||||
export default router
|
||||
|
|
|
|||
268
src/services/audio.service.js
Normal file
268
src/services/audio.service.js
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import utils from "../plugins/utils";
|
||||
|
||||
/**
|
||||
* This plugin (available in all vue components as $audioPlayer) handles
|
||||
* access to the shared audio player, and events related to loading and
|
||||
* playback of audio attachments.
|
||||
*
|
||||
* Components use this by calling "addListener" (and corresponding removeListener) with
|
||||
* an audio matrix event and a unique component id (for example the ._uid property).
|
||||
*/
|
||||
export default {
|
||||
install(Vue) {
|
||||
class SharedAudioPlayer {
|
||||
constructor() {
|
||||
this.player = new Audio();
|
||||
this.currentEvent = null;
|
||||
this.currentClapReactions = [];
|
||||
this.infoMap = new Map();
|
||||
this.player.addEventListener("durationchange", this.onDurationChange.bind(this));
|
||||
this.player.addEventListener("timeupdate", this.onTimeUpdate.bind(this));
|
||||
this.player.addEventListener("play", this.onPlay.bind(this));
|
||||
this.player.addEventListener("pause", this.onPause.bind(this));
|
||||
this.player.addEventListener("ended", this.onEnded.bind(this));
|
||||
}
|
||||
|
||||
getPlayerElement() {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
addListener(uid, event) {
|
||||
const eventId = event.getId();
|
||||
var entry = this.infoMap.get(eventId);
|
||||
if (!entry) {
|
||||
// Listeners is just a Set of component "uid" entries for now.
|
||||
entry = { url: null, listeners: new Set() };
|
||||
// Make these reactive, so AudioPlayer (and others) can listen to them
|
||||
Vue.set(entry, "loading", false);
|
||||
Vue.set(entry, "loadPercent", 0);
|
||||
Vue.set(entry, "duration", 0);
|
||||
Vue.set(entry, "currentTime", 0);
|
||||
Vue.set(entry, "playPercent", 0);
|
||||
Vue.set(entry, "playing", false);
|
||||
this.infoMap.set(eventId, entry);
|
||||
|
||||
// Get duration information
|
||||
utils
|
||||
.getAttachmentUrlAndDuration(event)
|
||||
.then(([ignoredurl, duration]) => {
|
||||
entry.duration = duration;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch attachment duration: ", err);
|
||||
});
|
||||
}
|
||||
entry.listeners.add(uid);
|
||||
return entry;
|
||||
}
|
||||
removeListener(uid) {
|
||||
[...this.infoMap].forEach(([ignoredeventid, info]) => {
|
||||
info.listeners.delete(uid);
|
||||
if (info.listeners.size == 0 && info.url) {
|
||||
// No more listeners, release audio blob
|
||||
URL.revokeObjectURL(info.url);
|
||||
info.url = null;
|
||||
}
|
||||
});
|
||||
this.infoMap = new Map(
|
||||
[...this.infoMap].filter(([ignoredeventid, info]) => {
|
||||
return info.listeners.size > 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
play(event, timelineSet) {
|
||||
this.play_(event, timelineSet, false);
|
||||
}
|
||||
|
||||
load(event, timelineSet) {
|
||||
this.play_(event, timelineSet, true);
|
||||
}
|
||||
|
||||
play_(event, timelineSet, onlyLoad) {
|
||||
const eventId = event.getId();
|
||||
if (this.currentEvent != eventId) {
|
||||
// Media change, pause the one currently playing.
|
||||
this.player.pause();
|
||||
var entry = this.infoMap.get(this.currentEvent);
|
||||
if (entry) {
|
||||
entry.playing = false;
|
||||
}
|
||||
}
|
||||
this.currentEvent = eventId;
|
||||
const info = this.infoMap.get(eventId);
|
||||
if (info) {
|
||||
|
||||
// Get all clap reactions
|
||||
this.initializeClapEvents(event, timelineSet);
|
||||
|
||||
if (info.url) {
|
||||
// Restart from beginning?
|
||||
if (info.currentTime == info.duration) {
|
||||
info.currentTime = 0;
|
||||
info.playPercent = 0;
|
||||
}
|
||||
if (this.player.src != info.url) {
|
||||
this.player.src = info.url;
|
||||
this.player.currentTime = (info.currentTime || 0) / 1000;
|
||||
}
|
||||
if (onlyLoad) {
|
||||
this.player.load();
|
||||
} else {
|
||||
this.player.play();
|
||||
}
|
||||
} else {
|
||||
// Download it!
|
||||
info.loadPercent = 0;
|
||||
info.loading = true;
|
||||
info.abortController = new AbortController();
|
||||
utils
|
||||
.getAttachment(this.$root.$matrix.matrixClient, event, (progress) => {
|
||||
info.loadPercent = progress;
|
||||
}, false, info.abortController)
|
||||
.then((url) => {
|
||||
info.url = url;
|
||||
|
||||
// Still on this item? Call ourselves recursively.
|
||||
if (this.currentEvent == eventId) {
|
||||
if (onlyLoad) {
|
||||
this.load(event, timelineSet);
|
||||
} else {
|
||||
this.play(event, timelineSet);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch attachment: ", err);
|
||||
})
|
||||
.finally(() => {
|
||||
info.loading = false;
|
||||
info.abortController = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "autoplay" property on the underlying player object.
|
||||
* @param {} autoplay
|
||||
*/
|
||||
setAutoplay(autoplay) {
|
||||
this.player.autoplay = autoplay;
|
||||
}
|
||||
|
||||
pause(event) {
|
||||
if (!event || this.currentEvent == event.getId()) {
|
||||
this.player.pause();
|
||||
}
|
||||
|
||||
if (event) {
|
||||
// If downloading, abort that!
|
||||
var entry = this.infoMap.get(event.getId());
|
||||
if (entry && entry.abortController) {
|
||||
entry.abortController.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
seek(event, percent) {
|
||||
var entry = this.infoMap.get(event.getId());
|
||||
if (entry) {
|
||||
entry.currentTime = ((percent / 100) * (entry.duration || 0));
|
||||
this.updatePlayPercent(entry);
|
||||
if (this.currentEvent == event.getId()) {
|
||||
this.player.currentTime = entry.currentTime / 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
seekRelative(event, milliseconds) {
|
||||
var entry = this.infoMap.get(event.getId());
|
||||
if (entry) {
|
||||
entry.currentTime = Math.max(0, Math.min(entry.currentTime + milliseconds, entry.duration));
|
||||
this.updatePlayPercent(entry);
|
||||
if (this.currentEvent == event.getId()) {
|
||||
this.player.currentTime = entry.currentTime / 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
onPlay() {
|
||||
var entry = this.infoMap.get(this.currentEvent);
|
||||
if (entry) {
|
||||
entry.playing = true;
|
||||
}
|
||||
this.$root.$emit("audio-playback-started", this.currentEvent);
|
||||
}
|
||||
onPause() {
|
||||
var entry = this.infoMap.get(this.currentEvent);
|
||||
if (entry) {
|
||||
entry.playing = false;
|
||||
}
|
||||
this.$root.$emit("audio-playback-paused", this.currentEvent);
|
||||
}
|
||||
onEnded() {
|
||||
var entry = this.infoMap.get(this.currentEvent);
|
||||
if (entry) {
|
||||
entry.playing = false;
|
||||
entry.currentTime = entry.duration; // Next time restart
|
||||
}
|
||||
this.$root.$emit("audio-playback-ended", this.currentEvent);
|
||||
}
|
||||
onTimeUpdate() {
|
||||
var entry = this.infoMap.get(this.currentEvent);
|
||||
if (entry) {
|
||||
const oldTime = entry.currentTime;
|
||||
entry.currentTime = 1000 * this.player.currentTime;
|
||||
this.updatePlayPercent(entry);
|
||||
this.maybePlayClapEvent(oldTime, entry.currentTime);
|
||||
}
|
||||
}
|
||||
onDurationChange() {
|
||||
const duration =
|
||||
this.player.duration && isFinite(this.player.duration) && !isNaN(this.player.duration)
|
||||
? 1000 * this.player.duration
|
||||
: 0;
|
||||
var entry = this.infoMap.get(this.currentEvent);
|
||||
if (entry) {
|
||||
entry.duration = duration;
|
||||
this.updatePlayPercent(entry);
|
||||
}
|
||||
}
|
||||
updatePlayPercent(entry) {
|
||||
if (entry.duration > 0) {
|
||||
entry.playPercent = Math.floor((100 / entry.duration) * entry.currentTime);
|
||||
} else {
|
||||
entry.playPercent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
initializeClapEvents(event, timelineSet) {
|
||||
if (event) {
|
||||
const reactions = timelineSet.relations.getChildEventsForEvent(event.getId(), 'm.annotation', 'm.reaction');
|
||||
if (reactions) {
|
||||
this.currentClapReactions = reactions.getRelations()
|
||||
.filter(r => r.getRelation().key == "👏" && r.getRelation().timeOffset && parseInt(r.getRelation().timeOffset) > 0)
|
||||
.map(r => {
|
||||
return {
|
||||
sender: r.getSender(),
|
||||
emoji: r.getRelation().key,
|
||||
timeOffset: parseInt(r.getRelation().timeOffset)
|
||||
}
|
||||
})
|
||||
.sort((a,b) => a.timeOffset - b.timeOffset);
|
||||
}
|
||||
} else {
|
||||
this.currentClapReactions = [];
|
||||
}
|
||||
}
|
||||
|
||||
maybePlayClapEvent(previousTimeMs, timeNowMs) {
|
||||
(this.currentClapReactions || []).forEach(reaction => {
|
||||
if (previousTimeMs < reaction.timeOffset && timeNowMs >= reaction.timeOffset) {
|
||||
this.$root.$emit("audio-playback-reaction", reaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Vue.prototype.$audioPlayer = new SharedAudioPlayer();
|
||||
},
|
||||
};
|
||||
|
|
@ -369,29 +369,7 @@ export default {
|
|||
}
|
||||
break;
|
||||
|
||||
case "m.room.member":
|
||||
{
|
||||
if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) {
|
||||
// Don't use this.currentRoomId, may be an alias. We need the real id!
|
||||
if (
|
||||
(event.getContent().membership == "leave" &&
|
||||
(event.getPrevContent() || {}).membership == "join" &&
|
||||
event.getStateKey() == this.currentUserId &&
|
||||
event.getSender() != this.currentUserId) ||
|
||||
(event.getContent().membership == "ban" && event.getStateKey() == this.currentUserId)
|
||||
) {
|
||||
// We were kicked or banned
|
||||
// If this is a live event (not just backpaging) then redirect to goodbye!
|
||||
if (this.matrixClientReady) {
|
||||
const wasPurged = event.getContent().reason == "Room Deleted";
|
||||
this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.power_levels":
|
||||
case "m.room.power_levels":
|
||||
{
|
||||
if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) {
|
||||
this.currentRoomIsReadOnlyForUser = this.isReadOnlyRoomForUser(event.getRoomId(), this.currentUserId);
|
||||
|
|
@ -946,6 +924,14 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
setUserDisplayName(name) {
|
||||
if (this.matrixClient) {
|
||||
return this.matrixClient.setDisplayName(name || this.user.userId).then(() => this.userDisplayName = name).catch(err => console.err("Failed to set display name", err));
|
||||
} else {
|
||||
return Promise.reject("No matrix client");
|
||||
}
|
||||
},
|
||||
|
||||
setPassword(oldPassword, newPassword) {
|
||||
if (this.matrixClient && this.currentUser) {
|
||||
const authDict = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue