diff --git a/src/assets/css/chat.scss b/src/assets/css/chat.scss
index e58b88b..582b07f 100644
--- a/src/assets/css/chat.scss
+++ b/src/assets/css/chat.scss
@@ -1431,6 +1431,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 {
@@ -1450,6 +1458,9 @@ body {
justify-content: flex-end;
width: 100%;
}
+ .clap-button {
+ font-size: 24px;
+ }
.mic-button {
align-self: flex-end;
}
diff --git a/src/assets/sounds/clapping.mp3 b/src/assets/sounds/clapping.mp3
new file mode 100644
index 0000000..bbc9886
Binary files /dev/null and b/src/assets/sounds/clapping.mp3 differ
diff --git a/src/components/AudioLayout.vue b/src/components/AudioLayout.vue
index 081891c..3a6d74e 100644
--- a/src/components/AudioLayout.vue
+++ b/src/components/AudioLayout.vue
@@ -28,6 +28,20 @@
}}
+
+
+
{{ eventSenderDisplayName(currentAudioEvent) }}
@@ -54,10 +68,13 @@
-
- mic
-
+
+ 👏
+
+ mic
+
+
expand_more
@@ -100,18 +117,22 @@ export default {
},
data() {
return {
+ REACTION_ANIMATION_TIME: 2500,
info: null,
currentAudioEvent: null,
autoPlayNextEvent: 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.$audioPlayer.setAutoplay(false);
},
@@ -119,6 +140,7 @@ export default {
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;
@@ -130,6 +152,9 @@ export default {
currentTime() {
return util.formatDuration(this.info ? this.info.currentTime : 0);
},
+ currentTimeMs() {
+ return this.info ? this.info.currentTime : 0;
+ },
totalTime() {
return util.formatDuration(this.info ? this.info.duration : 0);
},
@@ -174,6 +199,8 @@ export default {
return;
}
+ this.clearReactions();
+
this.info = this.$audioPlayer.addListener(this._uid, value);
const autoPlayWasSet = this.autoPlayNextEvent;
@@ -187,7 +214,7 @@ export default {
}
}
- this.$audioPlayer.load(value);
+ this.$audioPlayer.load(value, this.timelineSet);
}
},
},
@@ -195,7 +222,7 @@ export default {
play() {
if (this.currentAudioEvent) {
this.$audioPlayer.setAutoplay(false);
- this.$audioPlayer.play(this.currentAudioEvent);
+ this.$audioPlayer.play(this.currentAudioEvent, this.timelineSet);
}
},
pause() {
@@ -241,6 +268,20 @@ export default {
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++) {
@@ -328,7 +369,6 @@ export default {
volume.style.height = "" + w + "px";
const color = 80 + (value * (256 - 80)) / 256;
volume.style.backgroundColor = `rgb(${color},${color},${color})`;
-
if (this.info && this.info.playing) {
requestAnimationFrame(this.updateVisualization);
} else {
@@ -342,6 +382,24 @@ export default {
volume.style.height = "0px";
volume.style.backgroundColor = "transparent";
},
+ 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(
@@ -364,6 +422,12 @@ export default {
} else {
this.$emit('start-recording');
}
+ },
+
+ clapButtonClicked() {
+ if (this.currentAudioEvent) {
+ this.$emit("sendclap", { event: this.currentAudioEvent, timeOffset: this.currentTimeMs })
+ }
}
}
};
diff --git a/src/components/Chat.vue b/src/components/Chat.vue
index c2b3ffb..2353475 100644
--- a/src/components/Chat.vue
+++ b/src/components/Chat.vue
@@ -10,6 +10,7 @@
v-on:loadnext="handleScrolledToBottom(false)"
v-on:loadprevious="handleScrolledToTop()"
v-on:mark-read="sendRR"
+ v-on:sendclap="sendClapReactionAtTime"
/>
@@ -1301,6 +1302,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;
@@ -1577,7 +1589,7 @@ export default {
const nextEvent = filteredEvents[index + 1];
if (nextEvent.getContent().msgtype === "m.audio") {
// Yes, audio event!
- this.$audioPlayer.play(nextEvent);
+ this.$audioPlayer.play(nextEvent, this.timelineSet);
}
}
}
diff --git a/src/components/messages/AudioPlayer.vue b/src/components/messages/AudioPlayer.vue
index 1f21d2b..26b74d7 100644
--- a/src/components/messages/AudioPlayer.vue
+++ b/src/components/messages/AudioPlayer.vue
@@ -22,6 +22,12 @@ export default {
return null;
},
},
+ timelineSet: {
+ type: Object,
+ default: function () {
+ return null;
+ },
+ },
},
data() {
return {
@@ -44,7 +50,7 @@ export default {
return this.$audioPlayer.addListener(this._uid, this.event);
},
play() {
- this.$audioPlayer.play(this.event);
+ this.$audioPlayer.play(this.event, this.timelineSet);
},
pause() {
this.$audioPlayer.pause(this.event);
diff --git a/src/components/messages/MessageIncomingAudio.vue b/src/components/messages/MessageIncomingAudio.vue
index 763781e..f1ed2c6 100644
--- a/src/components/messages/MessageIncomingAudio.vue
+++ b/src/components/messages/MessageIncomingAudio.vue
@@ -1,7 +1,7 @@
-
{{ $t('fallbacks.audio_file')}}
+
{{ $t('fallbacks.audio_file')}}
diff --git a/src/components/messages/MessageOutgoingAudio.vue b/src/components/messages/MessageOutgoingAudio.vue
index c28e6ee..b589b6d 100644
--- a/src/components/messages/MessageOutgoingAudio.vue
+++ b/src/components/messages/MessageOutgoingAudio.vue
@@ -1,7 +1,7 @@
-
{{ $t('fallbacks.audio_file')}}
+
{{ $t('fallbacks.audio_file')}}
diff --git a/src/plugins/utils.js b/src/plugins/utils.js
index 38b862e..05dbbff 100644
--- a/src/plugins/utils.js
+++ b/src/plugins/utils.js
@@ -222,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);
}
diff --git a/src/services/audio.service.js b/src/services/audio.service.js
index 2470e5c..6037be4 100644
--- a/src/services/audio.service.js
+++ b/src/services/audio.service.js
@@ -14,6 +14,7 @@ export default {
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));
@@ -70,15 +71,15 @@ export default {
);
}
- play(event) {
- this.play_(event, false);
+ play(event, timelineSet) {
+ this.play_(event, timelineSet, false);
}
- load(event) {
- this.play_(event, true);
+ load(event, timelineSet) {
+ this.play_(event, timelineSet, true);
}
- play_(event, onlyLoad) {
+ play_(event, timelineSet, onlyLoad) {
const eventId = event.getId();
if (this.currentEvent != eventId) {
// Media change, pause the one currently playing.
@@ -91,6 +92,10 @@ export default {
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) {
@@ -121,9 +126,9 @@ export default {
// Still on this item? Call ourselves recursively.
if (this.currentEvent == eventId) {
if (onlyLoad) {
- this.load(event);
+ this.load(event, timelineSet);
} else {
- this.play(event);
+ this.play(event, timelineSet);
}
}
})
@@ -204,8 +209,10 @@ export default {
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() {
@@ -226,6 +233,34 @@ export default {
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();