support emoji reactions #12

peertube
Namekuji 2023-01-13 10:01:02 -05:00
rodzic 7545855b00
commit 1f084e12bc
5 zmienionych plików z 165 dodań i 16 usunięć

Wyświetl plik

@ -29,7 +29,7 @@
{% end %} {% end %}
</head> </head>
<body> <body>
<div id="app" data-version='0.1.0-alpha'></div> <div id="app" data-version='0.1.0-alpha2'></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

53
audon-fe/package-lock.json wygenerowano
Wyświetl plik

@ -1,14 +1,15 @@
{ {
"name": "audon-fe", "name": "audon-fe",
"version": "0.1.0-dev", "version": "0.1.0-alpha",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audon-fe", "name": "audon-fe",
"version": "0.1.0-dev", "version": "0.1.0-alpha",
"dependencies": { "dependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.1",
"@picmo/popup-picker": "^5.7.2",
"@uriopass/nosleep.js": "^0.12.2", "@uriopass/nosleep.js": "^0.12.2",
"@vuelidate/core": "^2.0.0", "@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0", "@vuelidate/validators": "^2.0.0",
@ -18,6 +19,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"luxon": "^3.1.1", "luxon": "^3.1.1",
"masto": "^4.9.0", "masto": "^4.9.0",
"picmo": "^5.7.2",
"pinia": "^2.0.26", "pinia": "^2.0.26",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
@ -100,6 +102,19 @@
"url": "https://opencollective.com/eslint" "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": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.8", "version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@ -331,6 +346,20 @@
"node": ">= 8" "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": { "node_modules/@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@ -1103,6 +1132,15 @@
"tslib": "^2.0.3" "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": { "node_modules/esbuild": {
"version": "0.15.18", "version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", "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", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.0.0.tgz",
"integrity": "sha512-nPdMG0Pd09HuSsr7QOKUXO2Jr9eqaDiZvDwdyIhNG5SHYujkQHYKDfGQkulBxvbDHz8oHLsTgKN86LSwYzSHAg==" "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": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",

Wyświetl plik

@ -1,6 +1,6 @@
{ {
"name": "audon-fe", "name": "audon-fe",
"version": "0.1.0-alpha", "version": "0.1.0-alpha2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cp -v index.dev.html index.html && vite", "dev": "cp -v index.dev.html index.html && vite",
@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.1",
"@picmo/popup-picker": "^5.7.2",
"@uriopass/nosleep.js": "^0.12.2", "@uriopass/nosleep.js": "^0.12.2",
"@vuelidate/core": "^2.0.0", "@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0", "@vuelidate/validators": "^2.0.0",
@ -19,6 +20,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"luxon": "^3.1.1", "luxon": "^3.1.1",
"masto": "^4.9.0", "masto": "^4.9.0",
"picmo": "^5.7.2",
"pinia": "^2.0.26", "pinia": "^2.0.26",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",

Wyświetl plik

@ -2,19 +2,24 @@
import { mdiMicrophone, mdiMicrophoneOff } from "@mdi/js"; import { mdiMicrophone, mdiMicrophoneOff } from "@mdi/js";
import { webfinger } from "../assets/utils"; import { webfinger } from "../assets/utils";
export default { export default {
setup() {
return {
mdiMicrophone,
mdiMicrophoneOff,
webfinger,
};
},
props: { props: {
talking: Boolean, talking: Boolean,
type: String, type: String,
data: Object, data: Object,
muted: Boolean, muted: Boolean,
}, emoji: String,
data() {
return {
mdiMicrophone,
mdiMicrophoneOff,
};
}, },
computed: { computed: {
showEmoji() {
return this.emoji !== undefined;
},
canSpeak() { canSpeak() {
return ( return (
this.type === "host" || this.type === "host" ||
@ -47,9 +52,6 @@ export default {
} }
}, },
}, },
methods: {
webfinger,
},
}; };
</script> </script>
@ -62,6 +64,17 @@ export default {
:color="badgeProps.colour" :color="badgeProps.colour"
> >
<v-avatar :class="{ rounded: true, talk: talking }" size="70"> <v-avatar :class="{ rounded: true, talk: talking }" size="70">
<v-overlay
v-model="showEmoji"
contained
persistent
scroll-strategy="none"
no-click-animation
scrim="#000000"
class="align-center justify-center reaction"
>
<span>{{ emoji }}</span>
</v-overlay>
<v-img :src="data?.avatar"></v-img> <v-img :src="data?.avatar"></v-img>
</v-avatar> </v-avatar>
</v-badge> </v-badge>
@ -70,6 +83,17 @@ export default {
:class="{ rounded: true, talk: talking, 'mt-2': true }" :class="{ rounded: true, talk: talking, 'mt-2': true }"
size="70" size="70"
> >
<v-overlay
v-model="showEmoji"
contained
persistent
scroll-strategy="none"
no-click-animation
scrim="#000000"
class="align-center justify-center reaction"
>
<span>{{ emoji }}</span>
</v-overlay>
<v-img :src="data?.avatar"></v-img> <v-img :src="data?.avatar"></v-img>
</v-avatar> </v-avatar>
<h4 :class="canSpeak ? 'mt-1' : 'mt-2'"> <h4 :class="canSpeak ? 'mt-1' : 'mt-2'">
@ -88,4 +112,10 @@ export default {
.talk { .talk {
outline: 3px solid cornflowerblue; outline: 3px solid cornflowerblue;
} }
.reaction span {
font-size: 2rem;
color: white;
text-align: center;
}
</style> </style>

Wyświetl plik

@ -3,12 +3,13 @@ import axios from "axios";
import { pushNotFound, webfinger } from "../assets/utils"; import { pushNotFound, webfinger } from "../assets/utils";
import { useMastodonStore } from "../stores/mastodon"; import { useMastodonStore } from "../stores/mastodon";
import { map, some, omit, filter, trim, clone } from "lodash-es"; 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 Participant from "../components/Participant.vue";
import { import {
mdiMicrophone, mdiMicrophone,
mdiMicrophoneOff, mdiMicrophoneOff,
mdiMicrophoneQuestion, mdiMicrophoneQuestion,
mdiDoorClosed,
mdiVolumeOff, mdiVolumeOff,
mdiClose, mdiClose,
mdiCheck, mdiCheck,
@ -16,6 +17,7 @@ import {
mdiLogout, mdiLogout,
mdiDotsVertical, mdiDotsVertical,
mdiPencil, mdiPencil,
mdiEmoticon,
} from "@mdi/js"; } from "@mdi/js";
import { import {
Room, Room,
@ -68,10 +70,13 @@ export default {
mdiCheck, mdiCheck,
mdiDotsVertical, mdiDotsVertical,
mdiPencil, mdiPencil,
mdiEmoticon,
v$: useVuelidate(), v$: useVuelidate(),
donStore: useMastodonStore(), donStore: useMastodonStore(),
decoder: new TextDecoder(), decoder: new TextDecoder(),
encoder: new TextEncoder(), encoder: new TextEncoder(),
roomClient: new Room(),
emojiPicker: null,
}; };
}, },
components: { components: {
@ -98,7 +103,6 @@ export default {
roomID: this.$route.params.id, roomID: this.$route.params.id,
loading: true, loading: true,
mainHeight: 700, mainHeight: 700,
roomClient: new Room(),
roomInfo: { roomInfo: {
title: this.$t("connecting"), title: this.$t("connecting"),
description: "", description: "",
@ -122,6 +126,7 @@ export default {
{ title: this.$t("form.relationships.private"), value: "private" }, { title: this.$t("form.relationships.private"), value: "private" },
], ],
participants: {}, participants: {},
emojiReactions: {},
cachedMastoData: {}, cachedMastoData: {},
activeSpeakerIDs: new Set(), activeSpeakerIDs: new Set(),
mutedSpeakerIDs: new Set(), mutedSpeakerIDs: new Set(),
@ -315,11 +320,15 @@ export default {
{ "kind": "speak_request" } { "kind": "speak_request" }
{ "kind": "chat", "data": "..." } { "kind": "chat", "data": "..." }
{ "kind": "request_declined", "audon_id": "..."} { "kind": "request_declined", "audon_id": "..."}
{ "kind": "emoji", "emoji": "..." }
*/ */
const strData = self.decoder.decode(payload); const strData = self.decoder.decode(payload);
const jsonData = JSON.parse(strData); const jsonData = JSON.parse(strData);
const metadata = JSON.parse(participant.metadata); const metadata = JSON.parse(participant.metadata);
switch (jsonData?.kind) { switch (jsonData?.kind) {
case "emoji":
self.addEmojiReaction(participant.identity, jsonData.emoji);
break;
case "speak_request": // someone is wanting to be a speaker case "speak_request": // someone is wanting to be a speaker
self.onSpeakRequestReceived(participant); self.onSpeakRequestReceived(participant);
break; break;
@ -496,6 +505,53 @@ export default {
this.showRequestedNotification = true; 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) { async publishDataToHostAndCohosts(data) {
const payload = this.encoder.encode(JSON.stringify(data)); const payload = this.encoder.encode(JSON.stringify(data));
// participants - speakers // participants - speakers
@ -807,6 +863,7 @@ export default {
type="host" type="host"
:data="cachedMastoData[key]" :data="cachedMastoData[key]"
:muted="mutedSpeakerIDs.has(key)" :muted="mutedSpeakerIDs.has(key)"
:emoji="emojiReactions[key]?.emoji"
></Participant> ></Participant>
<Participant <Participant
v-if="isCohost(value)" v-if="isCohost(value)"
@ -814,6 +871,7 @@ export default {
type="cohost" type="cohost"
:data="cachedMastoData[key]" :data="cachedMastoData[key]"
:muted="mutedSpeakerIDs.has(key)" :muted="mutedSpeakerIDs.has(key)"
:emoji="emojiReactions[key]?.emoji"
></Participant> ></Participant>
<Participant <Participant
v-if="isSpeaker(key)" v-if="isSpeaker(key)"
@ -821,6 +879,7 @@ export default {
type="speaker" type="speaker"
:data="cachedMastoData[key]" :data="cachedMastoData[key]"
:muted="mutedSpeakerIDs.has(key)" :muted="mutedSpeakerIDs.has(key)"
:emoji="emojiReactions[key]?.emoji"
> >
</Participant> </Participant>
</template> </template>
@ -831,6 +890,7 @@ export default {
v-if="!isHost(key) && !isCohost(value) && !isSpeaker(key)" v-if="!isHost(key) && !isCohost(value) && !isSpeaker(key)"
:data="cachedMastoData[key]" :data="cachedMastoData[key]"
type="listener" type="listener"
:emoji="emojiReactions[key]?.emoji"
></Participant> ></Participant>
</template> </template>
</v-row> </v-row>
@ -845,7 +905,15 @@ export default {
>{{ $t("enterRoom") }}</v-btn >{{ $t("enterRoom") }}</v-btn
> >
</v-card-actions> </v-card-actions>
<v-card-actions v-else class="justify-center" style="gap: 50px"> <v-card-actions v-else class="justify-center" style="gap: 20px">
<v-btn
:icon="mdiEmoticon"
color="white"
variant="flat"
@click="onPickerPopup"
id="pickerButton"
>
</v-btn>
<v-btn <v-btn
:icon="micStatusIcon" :icon="micStatusIcon"
color="white" color="white"