allow cohosts to close the room

peertube
Namekuji 2023-01-28 15:08:15 -05:00
rodzic d4d1ab6792
commit 55ca35c3c8
10 zmienionych plików z 98 dodań i 59 usunięć

Wyświetl plik

@ -48,7 +48,7 @@ export default {
id="mainArea" id="mainArea"
> >
<v-col> <v-col>
<v-responsive class="mx-auto" max-width="600px"> <v-responsive id="mainContainer" class="mx-auto" max-width="600px">
<RouterView /> <RouterView />
</v-responsive> </v-responsive>
</v-col> </v-col>
@ -106,4 +106,8 @@ export default {
background: black; background: black;
color: white; color: white;
} }
#mainContainer {
background-color: rgba(18, 18, 18, 0.8);
}
</style> </style>

Wyświetl plik

@ -29,7 +29,10 @@ body,
#app { #app {
height: 100%; height: 100%;
width: 100%; width: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Hiragino Sans", "Noto Sans CJK JP", "Original Yu Gothic", "Yu Gothic", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Sans Emoji"; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Hiragino Sans", "Noto Sans CJK JP", "Original Yu Gothic", "Yu Gothic",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Sans Emoji";
} }
a.plain { a.plain {

Wyświetl plik

@ -89,7 +89,6 @@ export default {
async joining(indicator) { async joining(indicator) {
try { try {
this.donStore.avatar = this.roomToken.original; this.donStore.avatar = this.roomToken.original;
await this.roomClient.startAudio();
if (indicator && this.uploadEnabled) { if (indicator && this.uploadEnabled) {
this.uploading = true; this.uploading = true;
try { try {

Wyświetl plik

@ -14,8 +14,8 @@ share: "Share"
copy: "Copy" copy: "Copy"
copied: "Copied" copied: "Copied"
enterRoom: "Enter" enterRoom: "Enter"
leaveRoom: "Leave" leaveRoom: "Leave but keep this room open"
closeRoom: "Close" closeRoom: "Close this room"
close: "Close" close: "Close"
connecting: "Connecting" connecting: "Connecting"
server: "Your Mastodon instance" server: "Your Mastodon instance"

Wyświetl plik

@ -14,8 +14,8 @@ share: "シェア"
copy: "コピー" copy: "コピー"
copied: "コピーしました" copied: "コピーしました"
enterRoom: "入室" enterRoom: "入室"
leaveRoom: "退室" leaveRoom: "部屋を閉じずに退室"
closeRoom: "閉室" closeRoom: "部屋を閉じる"
close: "閉じる" close: "閉じる"
connecting: "接続中" connecting: "接続中"
server: "Mastodon サーバー" server: "Mastodon サーバー"

Wyświetl plik

@ -83,7 +83,11 @@ export default {
</v-col> </v-col>
</v-row> </v-row>
<div class="d-flex justify-center mt-6"> <div class="d-flex justify-center mt-6">
<v-alert :icon="mdiLinkVariant" :title="$t('staticLink.title')"> <v-alert
:icon="mdiLinkVariant"
:title="$t('staticLink.title')"
variant="tonal"
>
<div class="my-1"> <div class="my-1">
<h4 style="word-break: break-all"> <h4 style="word-break: break-all">
<a <a

Wyświetl plik

@ -20,6 +20,8 @@ import {
mdiDotsVertical, mdiDotsVertical,
mdiPencil, mdiPencil,
mdiEmoticon, mdiEmoticon,
mdiCloseBoxOutline,
mdiExitRun,
} from "@mdi/js"; } from "@mdi/js";
import { import {
Room, Room,
@ -37,16 +39,6 @@ import boopSound from "../assets/boop.oga";
import messageSound from "../assets/message.oga"; import messageSound from "../assets/message.oga";
import requestSound from "../assets/request.oga"; import requestSound from "../assets/request.oga";
const publishOpts = {
audioBitrate: AudioPresets.music,
};
const captureOpts = {
autoGainControl: true,
noiseSuppression: true,
// echoCancellation: true,
};
export default { export default {
setup() { setup() {
const noSleep = new NoSleep(); const noSleep = new NoSleep();
@ -62,6 +54,7 @@ export default {
webfinger, webfinger,
clone, clone,
noSleep, noSleep,
mdiCloseBoxOutline,
mdiLogout, mdiLogout,
mdiAccountVoice, mdiAccountVoice,
mdiMicrophone, mdiMicrophone,
@ -73,6 +66,7 @@ export default {
mdiDotsVertical, mdiDotsVertical,
mdiPencil, mdiPencil,
mdiEmoticon, mdiEmoticon,
mdiExitRun,
v$: useVuelidate(), v$: useVuelidate(),
donStore: useMastodonStore(), donStore: useMastodonStore(),
decoder: new TextDecoder(), decoder: new TextDecoder(),
@ -119,7 +113,22 @@ export default {
return { return {
roomID: this.$route.params.id, roomID: this.$route.params.id,
loading: false, loading: false,
mainHeight: 700, mainHeight: window.innerHeight - 120,
audioOptions: {
play: {
deviceId: 0,
},
capture: {
autoGainControl: false,
echoCancellation: true,
noiseSuppression: true,
},
publish: {
audioBitrate: AudioPresets.music,
forceStereo: false,
source: Track.Source.Microphone,
},
},
roomInfo: { roomInfo: {
title: this.$t("connecting"), title: this.$t("connecting"),
description: "", description: "",
@ -162,7 +171,6 @@ export default {
}, },
async created() { async created() {
this.onResize(); this.onResize();
// fetch mastodon token
if (!this.donStore.client || !this.donStore.authorized) { if (!this.donStore.client || !this.donStore.authorized) {
try { try {
await this.donStore.fetchToken(); await this.donStore.fetchToken();
@ -225,10 +233,10 @@ export default {
return this.isHost(myAudonID); return this.isHost(myAudonID);
}, },
iamCohost() { iamCohost() {
const myInfo = this.donStore.userinfo; const myInfo = this.donStore.oauth.audon;
if (!myInfo) return false; if (!myInfo) return false;
return this.isCohost({ remote_id: myInfo.id, remote_url: myInfo.url }); return this.isCohost(myInfo);
}, },
iamSpeaker() { iamSpeaker() {
const myAudonID = this.donStore.oauth.audon?.audon_id; const myAudonID = this.donStore.oauth.audon?.audon_id;
@ -250,6 +258,14 @@ export default {
const messages = map(errors, (e) => e.$message); const messages = map(errors, (e) => e.$message);
return messages; return messages;
}, },
isLastHost() {
return !some(
Object.values(this.participants),
(v) =>
v.audon_id !== this.donStore.oauth.audon?.audon_id &&
(this.isHost(v.audon_id) || this.isCohost(v))
);
},
}, },
methods: { methods: {
refreshTimeElapsed() { refreshTimeElapsed() {
@ -266,6 +282,7 @@ export default {
try { try {
this.loading = true; this.loading = true;
await this.connectLivekit(token); await this.connectLivekit(token);
await this.roomClient.startAudio();
} catch (error) { } catch (error) {
alert(this.$t("errors.connectionFailed")); alert(this.$t("errors.connectionFailed"));
} finally { } finally {
@ -378,7 +395,11 @@ export default {
} }
if (self.iamSpeaker && !self.micGranted) { if (self.iamSpeaker && !self.micGranted) {
self.roomClient.localParticipant self.roomClient.localParticipant
.setMicrophoneEnabled(true, captureOpts, publishOpts) .setMicrophoneEnabled(
true,
self.audioOptions.capture,
self.audioOptions.publish
)
.then(() => { .then(() => {
self.micGranted = true; self.micGranted = true;
}) })
@ -408,8 +429,8 @@ export default {
try { try {
await this.roomClient.localParticipant.setMicrophoneEnabled( await this.roomClient.localParticipant.setMicrophoneEnabled(
true, true,
captureOpts, this.audioOptions.capture,
publishOpts this.audioOptions.publish
); );
} catch { } catch {
alert(this.$t("microphoneBlocked")); alert(this.$t("microphoneBlocked"));
@ -429,19 +450,16 @@ export default {
} }
}, },
onResize() { onResize() {
const mainArea = document.getElementById("mainArea"); this.mainHeight = window.innerHeight - 120;
const height = mainArea.clientHeight;
this.mainHeight = height > 720 ? 700 : window.innerHeight - 120;
}, },
isHost(identity) { isHost(identity) {
return identity === this.roomInfo.host?.audon_id; return identity === this.roomInfo.host?.audon_id;
}, },
isCohost(metadata) { isCohost(data) {
return ( return (
metadata && data.webfinger &&
some(this.roomInfo.cohosts, { some(this.roomInfo.cohosts, {
remote_id: metadata.remote_id, webfinger: data.webfinger,
remote_url: metadata.remote_url,
}) })
); );
}, },
@ -589,15 +607,15 @@ export default {
newMicStatus = true; newMicStatus = true;
await this.roomClient.localParticipant.setMicrophoneEnabled( await this.roomClient.localParticipant.setMicrophoneEnabled(
newMicStatus, newMicStatus,
captureOpts, this.audioOptions.capture,
publishOpts this.audioOptions.publish
); );
} else if (myTrack) { } else if (myTrack) {
newMicStatus = myTrack.isMuted; newMicStatus = myTrack.isMuted;
await this.roomClient.localParticipant.setMicrophoneEnabled( await this.roomClient.localParticipant.setMicrophoneEnabled(
newMicStatus, newMicStatus,
captureOpts, this.audioOptions.capture,
publishOpts this.audioOptions.publish
); );
} }
if (newMicStatus) { if (newMicStatus) {
@ -718,7 +736,7 @@ export default {
@connect.once="joinRoom" @connect.once="joinRoom"
></JoinDialog> ></JoinDialog>
<v-dialog v-model="showRequestDialog" max-width="500"> <v-dialog v-model="showRequestDialog" max-width="500">
<v-card max-height="600" class="d-flex flex-column"> <v-card class="d-flex flex-column">
<v-card-title>{{ $t("speakRequest.label") }}</v-card-title> <v-card-title>{{ $t("speakRequest.label") }}</v-card-title>
<v-card-text class="flex-grow-1 overflow-auto py-0"> <v-card-text class="flex-grow-1 overflow-auto py-0">
<v-list v-if="speakRequests.size > 0" lines="two" variant="tonal"> <v-list v-if="speakRequests.size > 0" lines="two" variant="tonal">
@ -906,14 +924,32 @@ export default {
variant="flat" variant="flat"
@click="onToggleMute" @click="onToggleMute"
></v-btn> ></v-btn>
<v-btn <v-menu v-if="iamHost || iamCohost">
v-if="iamHost" <template v-slot:activator="{ props }">
:icon="mdiLogout" <v-btn
color="red" :icon="mdiLogout"
:disabled="loading" color="red"
@click="onRoomClose" :disabled="loading"
variant="flat" variant="flat"
></v-btn> v-bind="props"
></v-btn>
</template>
<v-list>
<v-list-item
:title="$t('closeRoom')"
:prepend-icon="mdiCloseBoxOutline"
@click="onRoomClose"
class="text-red"
></v-list-item>
<v-list-item
:disabled="isLastHost"
:title="$t('leaveRoom')"
:prepend-icon="mdiExitRun"
@click="onLeave"
>
</v-list-item>
</v-list>
</v-menu>
<v-btn <v-btn
v-else v-else
:icon="mdiLogout" :icon="mdiLogout"
@ -943,5 +979,3 @@ export default {
</v-card> </v-card>
</main> </main>
</template> </template>
<style scoped></style>

Wyświetl plik

@ -140,30 +140,25 @@ func (u *AudonUser) createGIF(avatar image.Image, blue bool) ([]byte, error) {
} }
} }
outBuf, _ := os.Create(u.GetWebPAvatarPath()) outBuf, _ := os.Create(u.getWebPAvatarPath())
defer outBuf.Close() defer outBuf.Close()
anim.Encode(outBuf) anim.Encode(outBuf)
imagick.Initialize() imagick.Initialize()
defer imagick.Terminate() defer imagick.Terminate()
if _, err := imagick.ConvertImageCommand([]string{"convert", u.GetWebPAvatarPath(), u.GetGIFAvatarPath()}); err != nil { if _, err := imagick.ConvertImageCommand([]string{"convert", u.getWebPAvatarPath(), u.getGIFAvatarPath()}); err != nil {
return nil, err return nil, err
} }
return os.ReadFile(u.GetGIFAvatarPath()) return os.ReadFile(u.getGIFAvatarPath())
} }
func (u *AudonUser) getOriginalAvatarPath(hash [sha256.Size]byte, mtype *mimetype.MIME) string { func (u *AudonUser) getGIFAvatarPath() string {
filename := fmt.Sprintf("%x%s", hash, mtype.Extension())
return u.getAvatarImagePath(filename)
}
func (u *AudonUser) GetGIFAvatarPath() string {
return u.getAvatarImagePath("indicator.gif") return u.getAvatarImagePath("indicator.gif")
} }
func (u *AudonUser) GetWebPAvatarPath() string { func (u *AudonUser) getWebPAvatarPath() string {
return u.getAvatarImagePath("indicator.webp") return u.getAvatarImagePath("indicator.webp")
} }

Wyświetl plik

@ -450,9 +450,9 @@ func closeRoomHandler(c echo.Context) error {
return ErrAlreadyEnded return ErrAlreadyEnded
} }
// only host can close the room // only host or cohost can close the room
user := c.Get("user").(*AudonUser) user := c.Get("user").(*AudonUser)
if !room.IsHost(user) { if !room.IsHost(user) && !room.IsCoHost(user) {
return ErrOperationNotPermitted return ErrOperationNotPermitted
} }