diff --git a/audon-fe/index.prod.html b/audon-fe/index.prod.html index da0d0c2..302316c 100644 --- a/audon-fe/index.prod.html +++ b/audon-fe/index.prod.html @@ -29,7 +29,7 @@ {% end %} -
+
diff --git a/audon-fe/package-lock.json b/audon-fe/package-lock.json index 2715add..8ce9ce5 100644 --- a/audon-fe/package-lock.json +++ b/audon-fe/package-lock.json @@ -1,14 +1,15 @@ { "name": "audon-fe", - "version": "0.1.0-dev", + "version": "0.1.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audon-fe", - "version": "0.1.0-dev", + "version": "0.1.0-alpha", "dependencies": { "@intlify/unplugin-vue-i18n": "^0.8.1", + "@picmo/popup-picker": "^5.7.2", "@uriopass/nosleep.js": "^0.12.2", "@vuelidate/core": "^2.0.0", "@vuelidate/validators": "^2.0.0", @@ -18,6 +19,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.1.1", "masto": "^4.9.0", + "picmo": "^5.7.2", "pinia": "^2.0.26", "vue": "^3.2.45", "vue-i18n": "^9.2.2", @@ -100,6 +102,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@floating-ui/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.0.tgz", + "integrity": "sha512-zbsLwtnHo84w1Kc8rScAo5GMk1GdecSlrflIbfnEBJwvTSj1SL6kkOYV+nHraMCPEy+RNZZUaZyL8JosDGCtGQ==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.0.tgz", + "integrity": "sha512-TSogMPVxbRe77QCj1dt8NmRiJasPvuc+eT5jnJ6YpLqgOD2zXc5UA3S1qwybN+GVCDNdKfpKy1oj8RpzLJvh6A==", + "dependencies": { + "@floating-ui/core": "^1.0.5" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -331,6 +346,20 @@ "node": ">= 8" } }, + "node_modules/@picmo/popup-picker": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@picmo/popup-picker/-/popup-picker-5.7.2.tgz", + "integrity": "sha512-AORn2fsYph2NSHZaXViIhDhr0KE2QBjDQnX02TU78Jt76GWxWVV17Y7Kv9aKTO4K7lMSuCCMnq6cZnz+zjgxOQ==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/joeattardi" + }, + "peerDependencies": { + "picmo": "^5.7.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1103,6 +1132,15 @@ "tslib": "^2.0.3" } }, + "node_modules/emojibase": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-6.1.0.tgz", + "integrity": "sha512-1GkKJPXP6tVkYJHOBSJHoGOr/6uaDxZ9xJ6H7m6PfdGXTmQgbALHLWaVRY4Gi/qf5x/gT/NUXLPuSHYLqtLtrQ==", + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + } + }, "node_modules/esbuild": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", @@ -2622,6 +2660,17 @@ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.0.0.tgz", "integrity": "sha512-nPdMG0Pd09HuSsr7QOKUXO2Jr9eqaDiZvDwdyIhNG5SHYujkQHYKDfGQkulBxvbDHz8oHLsTgKN86LSwYzSHAg==" }, + "node_modules/picmo": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/picmo/-/picmo-5.7.2.tgz", + "integrity": "sha512-A7c5O8x1Xwq11KBYFY93+GIbHnw9PVz35HaWWHn/dgT08GA67M6cXKjjwzLnEAyXSdxXKrEk8/gPyTs+ibzWfQ==", + "dependencies": { + "emojibase": "^6.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/joeattardi" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", diff --git a/audon-fe/package.json b/audon-fe/package.json index 06adbed..9a2a7c7 100644 --- a/audon-fe/package.json +++ b/audon-fe/package.json @@ -1,6 +1,6 @@ { "name": "audon-fe", - "version": "0.1.0-alpha", + "version": "0.1.0-alpha2", "private": true, "scripts": { "dev": "cp -v index.dev.html index.html && vite", @@ -10,6 +10,7 @@ }, "dependencies": { "@intlify/unplugin-vue-i18n": "^0.8.1", + "@picmo/popup-picker": "^5.7.2", "@uriopass/nosleep.js": "^0.12.2", "@vuelidate/core": "^2.0.0", "@vuelidate/validators": "^2.0.0", @@ -19,6 +20,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.1.1", "masto": "^4.9.0", + "picmo": "^5.7.2", "pinia": "^2.0.26", "vue": "^3.2.45", "vue-i18n": "^9.2.2", diff --git a/audon-fe/src/components/Participant.vue b/audon-fe/src/components/Participant.vue index 1c54b79..e6ddac4 100644 --- a/audon-fe/src/components/Participant.vue +++ b/audon-fe/src/components/Participant.vue @@ -2,19 +2,24 @@ import { mdiMicrophone, mdiMicrophoneOff } from "@mdi/js"; import { webfinger } from "../assets/utils"; export default { + setup() { + return { + mdiMicrophone, + mdiMicrophoneOff, + webfinger, + }; + }, props: { talking: Boolean, type: String, data: Object, muted: Boolean, - }, - data() { - return { - mdiMicrophone, - mdiMicrophoneOff, - }; + emoji: String, }, computed: { + showEmoji() { + return this.emoji !== undefined; + }, canSpeak() { return ( this.type === "host" || @@ -47,9 +52,6 @@ export default { } }, }, - methods: { - webfinger, - }, }; @@ -62,6 +64,17 @@ export default { :color="badgeProps.colour" > + + {{ emoji }} + @@ -70,6 +83,17 @@ export default { :class="{ rounded: true, talk: talking, 'mt-2': true }" size="70" > + + {{ emoji }} +

@@ -88,4 +112,10 @@ export default { .talk { outline: 3px solid cornflowerblue; } + +.reaction span { + font-size: 2rem; + color: white; + text-align: center; +} diff --git a/audon-fe/src/views/RoomView.vue b/audon-fe/src/views/RoomView.vue index 84d0962..6a6187e 100644 --- a/audon-fe/src/views/RoomView.vue +++ b/audon-fe/src/views/RoomView.vue @@ -3,12 +3,13 @@ import axios from "axios"; import { pushNotFound, webfinger } from "../assets/utils"; import { useMastodonStore } from "../stores/mastodon"; import { map, some, omit, filter, trim, clone } from "lodash-es"; +import { darkTheme } from "picmo"; +import { createPopup } from "@picmo/popup-picker"; import Participant from "../components/Participant.vue"; import { mdiMicrophone, mdiMicrophoneOff, mdiMicrophoneQuestion, - mdiDoorClosed, mdiVolumeOff, mdiClose, mdiCheck, @@ -16,6 +17,7 @@ import { mdiLogout, mdiDotsVertical, mdiPencil, + mdiEmoticon, } from "@mdi/js"; import { Room, @@ -68,10 +70,13 @@ export default { mdiCheck, mdiDotsVertical, mdiPencil, + mdiEmoticon, v$: useVuelidate(), donStore: useMastodonStore(), decoder: new TextDecoder(), encoder: new TextEncoder(), + roomClient: new Room(), + emojiPicker: null, }; }, components: { @@ -98,7 +103,6 @@ export default { roomID: this.$route.params.id, loading: true, mainHeight: 700, - roomClient: new Room(), roomInfo: { title: this.$t("connecting"), description: "", @@ -122,6 +126,7 @@ export default { { title: this.$t("form.relationships.private"), value: "private" }, ], participants: {}, + emojiReactions: {}, cachedMastoData: {}, activeSpeakerIDs: new Set(), mutedSpeakerIDs: new Set(), @@ -315,11 +320,15 @@ export default { { "kind": "speak_request" } { "kind": "chat", "data": "..." } { "kind": "request_declined", "audon_id": "..."} + { "kind": "emoji", "emoji": "..." } */ const strData = self.decoder.decode(payload); const jsonData = JSON.parse(strData); const metadata = JSON.parse(participant.metadata); switch (jsonData?.kind) { + case "emoji": + self.addEmojiReaction(participant.identity, jsonData.emoji); + break; case "speak_request": // someone is wanting to be a speaker self.onSpeakRequestReceived(participant); break; @@ -496,6 +505,53 @@ export default { this.showRequestedNotification = true; } }, + onPickerPopup() { + const btn = document.getElementById("pickerButton"); + if (!this.emojiPicker) { + const picker = createPopup( + { + theme: darkTheme, + emojiSize: "1.8rem", + autoFocus: "none", + }, + { + referenceElement: btn, + triggerElement: btn, + position: "top", + hideOnEmojiSelect: true, + } + ); + const self = this; + picker.addEventListener("emoji:select", ({ emoji }) => { + self.onEmojiSelected(emoji); + }); + this.emojiPicker = picker; + } + this.emojiPicker.open(); + }, + async onEmojiSelected(emoji) { + this.showEmojiMenu = false; + const data = { kind: "emoji", emoji }; + const payload = this.encoder.encode(JSON.stringify(data)); + await this.roomClient.localParticipant.publishData( + payload, + DataPacket_Kind.RELIABLE + ); + this.addEmojiReaction(this.roomClient.localParticipant.identity, emoji); + }, + addEmojiReaction(identity, emoji) { + const self = this; + if (self.emojiReactions[identity]) { + clearTimeout(self.emojiReactions[identity].timeoutID); + } + const timeoutID = setTimeout(() => { + self.emojiReactions = omit(self.emojiReactions, identity); + }, 5000); + self.emojiReactions[identity] = { + timeoutID, + emoji, + }; + }, async publishDataToHostAndCohosts(data) { const payload = this.encoder.encode(JSON.stringify(data)); // participants - speakers @@ -807,6 +863,7 @@ export default { type="host" :data="cachedMastoData[key]" :muted="mutedSpeakerIDs.has(key)" + :emoji="emojiReactions[key]?.emoji" > @@ -831,6 +890,7 @@ export default { v-if="!isHost(key) && !isCohost(value) && !isSpeaker(key)" :data="cachedMastoData[key]" type="listener" + :emoji="emojiReactions[key]?.emoji" > @@ -845,7 +905,15 @@ export default { >{{ $t("enterRoom") }} - + + +