kopia lustrzana https://codeberg.org/nmkj/audon
allow cohosts to close the room
rodzic
d4d1ab6792
commit
55ca35c3c8
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -14,8 +14,8 @@ share: "シェア"
|
||||||
copy: "コピー"
|
copy: "コピー"
|
||||||
copied: "コピーしました"
|
copied: "コピーしました"
|
||||||
enterRoom: "入室"
|
enterRoom: "入室"
|
||||||
leaveRoom: "退室"
|
leaveRoom: "部屋を閉じずに退室"
|
||||||
closeRoom: "閉室"
|
closeRoom: "部屋を閉じる"
|
||||||
close: "閉じる"
|
close: "閉じる"
|
||||||
connecting: "接続中"
|
connecting: "接続中"
|
||||||
server: "Mastodon サーバー"
|
server: "Mastodon サーバー"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
15
avatar.go
15
avatar.go
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
room.go
4
room.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue