Merge branch 'more-audio-layout' into 'dev'
Fix playback bug on first load See merge request keanuapp/keanuapp-weblite!140
This commit is contained in:
commit
b7d3a1dc95
11 changed files with 424 additions and 252 deletions
|
|
@ -39,5 +39,6 @@
|
|||
"siteId": "25"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"experimental_voice_mode": true
|
||||
}
|
||||
|
|
@ -896,6 +896,26 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.with-right-label {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
text-align: start;
|
||||
& > * {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
& > *:last-child {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.option-title {
|
||||
color: #000;
|
||||
font-size: 16 * $chat-text-size;
|
||||
}
|
||||
.option-text {
|
||||
font-size: 13 * $chat-text-size;
|
||||
}
|
||||
}
|
||||
|
||||
.header-button-left {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
|
|
@ -1111,6 +1131,21 @@ body {
|
|||
border: 1px solid #808080 !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
color: black;
|
||||
font-size: 14 * $chat-text-size;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
[dir="rtl"] & {
|
||||
margin-left: initial;
|
||||
margin-right: 10px;
|
||||
}
|
||||
text-transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.room-link .v-input__slot::before {
|
||||
|
|
@ -1236,6 +1271,27 @@ body {
|
|||
flex: 1 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.typing-users {
|
||||
flex: 1 0 52px;
|
||||
min-height: 52px;
|
||||
padding: 20px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
.typing-user {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
margin-left: -8px !important;
|
||||
}
|
||||
.list-enter-active, .list-leave-active {
|
||||
transition: all 1s;
|
||||
}
|
||||
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
}
|
||||
}
|
||||
.load-later {
|
||||
flex: 1 0 auto;
|
||||
padding: 20px;
|
||||
|
|
|
|||
9
src/assets/icons/audio_import.vue
Normal file
9
src/assets/icons/audio_import.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<svg width="35" height="24" viewBox="0 0 35 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M34.5045 2.00009C34.4955 1.99996 34.4865 1.99996 34.4773 2.00022C34.472 2.00022 34.4667 2.00009 34.4614 2.00022V2.00009H34.4468L34.4321 2.00191C34.3889 2.00685 34.3463 2.01738 34.3055 2.03338L34.3047 2.03364L34.3042 2.0339L22.3976 6.75159L22.3971 6.75172L22.3966 6.75198C22.2004 6.83079 22.0788 7.01118 22.08 7.22109H22.0798V7.22369C22.0796 10.8211 22.0798 14.4168 22.0798 18.0165C21.4362 17.3614 20.5389 16.9551 19.5466 16.9551C17.595 16.9551 16 18.5363 16 20.4775C16 22.4169 17.5946 24 19.5466 24C21.4924 24 23.0798 22.4297 23.0932 20.5018C23.0932 20.4994 23.0933 20.497 23.0933 20.4946H23.0932C23.0933 20.489 23.0933 20.4833 23.0932 20.4777H23.0933V10.8472L33.9865 6.7827V13.6522C33.3419 12.9981 32.4433 12.5915 31.4533 12.5915C29.4988 12.5915 27.9067 14.1736 27.9067 16.1139C27.9067 18.0522 29.4987 19.6364 31.4533 19.6364C33.3813 19.6364 34.9586 18.0837 34.9999 16.1792L35 16.1764V2.50378V2.50365C35.0001 2.22209 34.7842 2.00541 34.5048 2.00035L34.5045 2.00009Z"
|
||||
fill="white" />
|
||||
<rect x="15" y="6" width="3" height="15" rx="1.3" transform="rotate(90 15 6)" fill="white" />
|
||||
<rect x="6" width="3" height="15" rx="1.3" fill="white" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/audio_import_play.vue
Normal file
7
src/assets/icons/audio_import_play.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="16" height="21" viewBox="0 0 16 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M15.2035 11.7865L1.92305 20.668C0.860979 21.3783 0 20.9103 0 19.6316V1.36924C0 0.0865306 0.861044 -0.377469 1.92305 0.33277L15.2035 9.21432C16.2655 9.9246 16.2655 11.0762 15.2035 11.7865Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -127,7 +127,8 @@
|
|||
"status_avatar_total": "Uploading avatar: {count} of {total}",
|
||||
"status_avatar": "Uploading avatar: {count}",
|
||||
"room_name_limit_error_msg": "Maximum 50 characters allowed",
|
||||
"colon_not_allowed": "Colon is not allowed"
|
||||
"colon_not_allowed": "Colon is not allowed",
|
||||
"options": "Options"
|
||||
},
|
||||
"device_list": {
|
||||
"title": "DEVICES",
|
||||
|
|
@ -243,8 +244,9 @@
|
|||
"export_room": "Export chat",
|
||||
"user_admin": "Administrator",
|
||||
"user_moderator": "Moderator",
|
||||
"ui_options": "UI Options",
|
||||
"audio_layout": "Use audio layout"
|
||||
"experimental_features": "Experimental Features",
|
||||
"voice_mode": "Voice mode",
|
||||
"voice_mode_info": "Switches the chat interface to a 'listen and record' mode"
|
||||
},
|
||||
"room_info_sheet": {
|
||||
"this_room": "This room",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
<template>
|
||||
<div v-bind="{...$props, ...$attrs}" v-on="$listeners" class="messageIn">
|
||||
<div v-bind="{ ...$props, ...$attrs }" v-on="$listeners" class="messageIn">
|
||||
<div class="load-earlier clickable" @click="loadPrevious">
|
||||
<v-icon color="white" size="28">expand_less</v-icon>
|
||||
</div>
|
||||
|
||||
<!-- Currently recording users -->
|
||||
<div class="typing-users">
|
||||
<transition-group name="list" tag="div">
|
||||
<v-avatar v-for="(member) in recordingMembersExceptMe" :key="member.userId" class="typing-user" size="32" color="grey">
|
||||
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
member.name.substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<div class="sound-wave-view">
|
||||
<div class="volume-container">
|
||||
<div ref="volume"></div>
|
||||
|
|
@ -11,7 +24,7 @@
|
|||
@click.stop="otherAvatarClicked($refs.avatar.$el)">
|
||||
<img v-if="messageEventAvatar(currentAudioEvent)" :src="messageEventAvatar(currentAudioEvent)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
eventSenderDisplayName(currentAudioEvent).substring(0, 1).toUpperCase()
|
||||
eventSenderDisplayName(currentAudioEvent).substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
</div>
|
||||
|
|
@ -25,7 +38,7 @@
|
|||
{{ currentTime }} / {{ totalTime }}
|
||||
</div>
|
||||
<audio ref="player" :src="src" @durationchange="updateDuration">
|
||||
{{ $t('fallbacks.audio_file')}}
|
||||
{{ $t('fallbacks.audio_file') }}
|
||||
</audio>
|
||||
<div v-if="currentAudioEvent" class="auto-audio-player">
|
||||
<v-btn id="btn-rewind" @click.stop="rewind" icon>
|
||||
|
|
@ -77,7 +90,13 @@ export default {
|
|||
default: function () {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
recordingMembers: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -98,8 +117,26 @@ export default {
|
|||
document.body.classList.add("dark");
|
||||
this.$root.$on('playback-start', this.onPlaybackStart);
|
||||
this.player = this.$refs.player;
|
||||
this.player.autoplay = false;
|
||||
this.player.addEventListener("timeupdate", this.updateProgressBar);
|
||||
this.player.addEventListener("play", () => {
|
||||
if (!this.analyser) {
|
||||
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
let audioSource = null;
|
||||
if (audioCtx) {
|
||||
audioSource = audioCtx.createMediaElementSource(this.player);
|
||||
this.analyser = audioCtx.createAnalyser();
|
||||
audioSource.connect(this.analyser);
|
||||
this.analyser.connect(audioCtx.destination);
|
||||
|
||||
this.analyser.fftSize = 128;
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
this.analyzerDataArray = new Uint8Array(bufferLength);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.playing = true;
|
||||
this.updateVisualization();
|
||||
if (this.currentAudioEvent) {
|
||||
|
|
@ -116,19 +153,6 @@ export default {
|
|||
this.clearVisualization();
|
||||
this.onPlaybackEnd();
|
||||
});
|
||||
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
let audioSource = null;
|
||||
if (audioCtx) {
|
||||
audioSource = audioCtx.createMediaElementSource(this.player);
|
||||
this.analyser = audioCtx.createAnalyser();
|
||||
audioSource.connect(this.analyser);
|
||||
this.analyser.connect(audioCtx.destination);
|
||||
|
||||
this.analyser.fftSize = 128;
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
this.analyzerDataArray = new Uint8Array(bufferLength);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.classList.remove("dark");
|
||||
|
|
@ -162,6 +186,11 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
recordingMembersExceptMe() {
|
||||
return this.recordingMembers.filter((member) => {
|
||||
return member.userId !== this.$matrix.currentUserId;
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
autoplay: {
|
||||
|
|
@ -204,7 +233,17 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.src = null;
|
||||
const autoPlayWasSet = this.autoPlayNextEvent;
|
||||
this.autoPlayNextEvent = false;
|
||||
|
||||
if (value.getSender() == this.$matrix.currentUserId) {
|
||||
// Sent by us. Don't autoplay if we just sent this (i.e. it is ahead of our read marker)
|
||||
if (this.room && !this.room.getReceiptsForEvent(value).includes(value.getSender())) {
|
||||
this.player.autoplay = false;
|
||||
this.autoPlayNextEvent = autoPlayWasSet;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadAudioAttachmentSource();
|
||||
}
|
||||
},
|
||||
|
|
@ -397,6 +436,18 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
memberAvatar(member) {
|
||||
if (member) {
|
||||
return member.getAvatarUrl(
|
||||
this.$matrix.matrixClient.getHomeserverUrl(),
|
||||
40,
|
||||
40,
|
||||
"scale",
|
||||
true
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,20 +4,21 @@
|
|||
{{ $tc("room.invitations", invitationCount) }}
|
||||
</div>
|
||||
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" />
|
||||
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useAudioLayout" :room="room"
|
||||
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
|
||||
:events="events" :autoplay="!showRecorder"
|
||||
:timelineSet="timelineSet"
|
||||
:readMarker="readMarker"
|
||||
:recordingMembers="typingMembers"
|
||||
v-on:start-recording="showRecorder = true"
|
||||
v-on:loadnext="handleScrolledToBottom(false)"
|
||||
v-on:loadprevious="handleScrolledToTop()"
|
||||
v-on:mark-read="sendRR"
|
||||
/>
|
||||
<VoiceRecorder class="audio-layout" v-if="useAudioLayout" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" />
|
||||
<VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
||||
|
||||
|
||||
<div v-if="!useAudioLayout" class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer"
|
||||
<div v-if="!useVoiceMode" class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer"
|
||||
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
|
||||
<div ref="messageOperationsStrut" class="message-operations-strut">
|
||||
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
|
||||
|
|
@ -69,10 +70,10 @@
|
|||
</div>
|
||||
|
||||
<!-- Input area -->
|
||||
<v-container v-if="!useAudioLayout && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
|
||||
<v-container v-if="!useVoiceMode && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
|
||||
<div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
|
||||
<!-- "Scroll to end"-button -->
|
||||
<v-btn v-if="!useAudioLayout" class="scroll-to-end" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
|
||||
<v-btn v-if="!useVoiceMode" class="scroll-to-end" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
|
||||
@click.stop="scrollToEndOfTimeline">
|
||||
<v-icon color="white">arrow_downward</v-icon>
|
||||
</v-btn>
|
||||
|
|
@ -421,7 +422,7 @@ export default {
|
|||
chatContainer() {
|
||||
const container = this.$refs.chatContainer;
|
||||
console.log("GOT CONTAINER", container);
|
||||
if (this.useAudioLayout) {
|
||||
if (this.useVoiceMode) {
|
||||
return container.$el;
|
||||
}
|
||||
return container;
|
||||
|
|
@ -522,15 +523,10 @@ export default {
|
|||
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
||||
return isAdmin;
|
||||
},
|
||||
useAudioLayout: {
|
||||
useVoiceMode: {
|
||||
get: function () {
|
||||
if (this.room) {
|
||||
const tags = this.room.tags;
|
||||
if (tags && tags["ui_options"]) {
|
||||
return tags["ui_options"]["audio_layout"] === 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (!this.$config.experimental_voice_mode) return false;
|
||||
return util.useVoiceMode(this.room);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
@ -611,6 +607,12 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
showRecorder(show) {
|
||||
if (this.useVoiceMode) {
|
||||
// Send typing indicators when recorder UI is opened/closed
|
||||
this.$matrix.matrixClient.sendTyping(this.roomId, show, 10 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -826,7 +828,7 @@ export default {
|
|||
const loadingDone = this.initialLoadDone;
|
||||
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
|
||||
|
||||
if (this.initialLoadDone && !this.useAudioLayout) {
|
||||
if (this.initialLoadDone && !this.useVoiceMode) {
|
||||
this.paginateBackIfNeeded();
|
||||
}
|
||||
|
||||
|
|
@ -1055,7 +1057,7 @@ export default {
|
|||
.then((success) => {
|
||||
if (success) {
|
||||
this.events = this.timelineWindow.getEvents();
|
||||
if (!this.useAudioLayout) {
|
||||
if (!this.useVoiceMode) {
|
||||
this.scrollPosition.prepareFor("down");
|
||||
this.$nextTick(() => {
|
||||
// restore scroll position!
|
||||
|
|
@ -1312,7 +1314,7 @@ export default {
|
|||
|
||||
let eventIdFirst = null;
|
||||
let eventIdLast = null;
|
||||
if (!this.useAudioLayout) {
|
||||
if (!this.useVoiceMode) {
|
||||
const container = this.chatContainer;
|
||||
const elFirst = util.getFirstVisibleElement(container);
|
||||
const elLast = util.getLastVisibleElement(container);
|
||||
|
|
|
|||
|
|
@ -3,29 +3,23 @@
|
|||
<div>
|
||||
<v-container fluid>
|
||||
<div class="room-name no-upper">{{ $t("new_room.new_room") }}</div>
|
||||
<v-btn
|
||||
id="btn-back"
|
||||
text
|
||||
class="header-button-left"
|
||||
v-show="$navigation && $navigation.canPop()"
|
||||
@click.stop="goBack"
|
||||
:disabled="step > steps.NAME_SET"
|
||||
>
|
||||
<v-btn id="btn-back" text class="header-button-left" v-show="$navigation && $navigation.canPop()"
|
||||
@click.stop="goBack" :disabled="step > steps.NAME_SET">
|
||||
<v-icon>arrow_back</v-icon>
|
||||
<span class="d-none d-sm-block">{{ $t("menu.back") }}</span>
|
||||
</v-btn>
|
||||
<!-- <v-btn
|
||||
text
|
||||
:disabled="
|
||||
!roomName || (step != steps.INITIAL && step != steps.CREATED)
|
||||
"
|
||||
class="header-button-right"
|
||||
@click.stop="onCreate"
|
||||
>
|
||||
<span>{{
|
||||
step == steps.CREATED ? $t("new_room.done") : $t("new_room.next")
|
||||
}}</span>
|
||||
</v-btn> -->
|
||||
text
|
||||
:disabled="
|
||||
!roomName || (step != steps.INITIAL && step != steps.CREATED)
|
||||
"
|
||||
class="header-button-right"
|
||||
@click.stop="onCreate"
|
||||
>
|
||||
<span>{{
|
||||
step == steps.CREATED ? $t("new_room.done") : $t("new_room.next")
|
||||
}}</span>
|
||||
</v-btn> -->
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
|
|
@ -39,51 +33,43 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("new_room.name_room") }}</div>
|
||||
<v-text-field
|
||||
v-model="roomName"
|
||||
color="black"
|
||||
:rules="roomNamerules"
|
||||
counter="50"
|
||||
maxlength="50"
|
||||
background-color="white"
|
||||
v-on:keyup.enter="$refs.topic.focus()"
|
||||
:disabled="step > steps.INITIAL"
|
||||
autofocus
|
||||
solo
|
||||
@update:error="updateErrorState"
|
||||
></v-text-field>
|
||||
<div class="text-left font-weight-light" v-show="roomName.length> 0">{{ $t("new_room.room_topic") }}</div>
|
||||
<v-text-field
|
||||
v-model="roomTopic"
|
||||
v-show="roomName.length > 0"
|
||||
color="black"
|
||||
background-color="white"
|
||||
v-on:keyup.enter="$refs.create.focus()"
|
||||
:disabled="step > steps.INITIAL"
|
||||
solo
|
||||
></v-text-field>
|
||||
<div class="error--text" v-if="roomCreationErrorMsg"> {{roomCreationErrorMsg}}</div>
|
||||
<v-btn
|
||||
id="btn-room-create"
|
||||
color="black"
|
||||
depressed
|
||||
class="filled-button"
|
||||
@click.stop="onCreate"
|
||||
:disabled="isDisabled"
|
||||
>
|
||||
<div v-if="status && !enterRoomDialog" class="text-center">
|
||||
{{ status }}
|
||||
<v-progress-circular
|
||||
v-if="step == steps.CREATING"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="20"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
<span v-else>{{ $t("new_room.create") }}</span>
|
||||
</v-btn>
|
||||
<v-text-field v-model="roomName" color="black" :rules="roomNamerules" counter="50" maxlength="50"
|
||||
background-color="white" v-on:keyup.enter="$refs.topic.focus()" :disabled="step > steps.INITIAL" autofocus
|
||||
solo @update:error="updateErrorState"></v-text-field>
|
||||
<div class="text-left font-weight-light" v-show="roomName.length > 0">{{ $t("new_room.room_topic") }}</div>
|
||||
<v-text-field v-model="roomTopic" v-show="roomName.length > 0" color="black" background-color="white"
|
||||
v-on:keyup.enter="$refs.create.focus()" :disabled="step > steps.INITIAL" solo></v-text-field>
|
||||
|
||||
<!-- Our only option right now is voice mode, so if not enabled, hide the 'options' drop down as well -->
|
||||
<template v-if="$config.experimental_voice_mode">
|
||||
<div @click.stop="showOptions = !showOptions" v-show="roomName.length > 0" class="options clickable">
|
||||
<div>{{ $t("new_room.options") }}</div>
|
||||
<v-icon v-if="!showOptions">expand_more</v-icon>
|
||||
<v-icon v-else>expand_less</v-icon>
|
||||
</div>
|
||||
<v-card v-show="showOptions" class="account ma-3" flat>
|
||||
<v-card-text class="with-right-label">
|
||||
<div>
|
||||
<div class="option-title">{{ $t('room_info.voice_mode') }}</div>
|
||||
<div class="option-text">{{ $t('room_info.voice_mode_info') }}</div>
|
||||
</div>
|
||||
<v-switch v-model="useVoiceMode"></v-switch>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<div class="error--text" v-if="roomCreationErrorMsg"> {{ roomCreationErrorMsg }}</div>
|
||||
<v-btn id="btn-room-create" color="black" depressed class="filled-button" @click.stop="onCreate"
|
||||
:disabled="isDisabled">
|
||||
<div v-if="status && !enterRoomDialog" class="text-center">
|
||||
{{ status }}
|
||||
<v-progress-circular v-if="step == steps.CREATING" indeterminate color="primary"
|
||||
size="20"></v-progress-circular>
|
||||
</div>
|
||||
<span v-else>{{ $t("new_room.create") }}</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
|
@ -91,126 +77,101 @@
|
|||
<v-fade-transition>
|
||||
<!-- <div class="section ma-3" flat v-if="step > steps.INITIAL"> -->
|
||||
<!-- <div class="h4 text-left">{{ $t("new_room.join_permissions") }}</div>
|
||||
<div class="h2 text-left">
|
||||
{{ $t("new_room.set_join_permissions") }}
|
||||
</div>
|
||||
<div>{{ $t("new_room.join_permissions_info") }}</div>
|
||||
<v-select
|
||||
:disabled="step >= steps.CREATING"
|
||||
:items="joinRules"
|
||||
class="mt-4"
|
||||
v-model="joinRule"
|
||||
item-value="id"
|
||||
>
|
||||
<template v-slot:selection="{ item }">
|
||||
{{ item.text }}
|
||||
</template>
|
||||
<template v-slot:item="{ item, attrs, on }">
|
||||
<v-list-item v-on="on" v-bind="attrs" #default="{ active }">
|
||||
<v-list-item-avatar>
|
||||
<v-icon class="grey lighten-1" dark>{{ item.icon }}</v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.text"></v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-text="item.descr"
|
||||
></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn icon v-if="active">
|
||||
<v-icon color="grey lighten-1">check</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<div class="h2 text-left">
|
||||
{{ $t("new_room.set_join_permissions") }}
|
||||
</div>
|
||||
<div>{{ $t("new_room.join_permissions_info") }}</div>
|
||||
<v-select
|
||||
:disabled="step >= steps.CREATING"
|
||||
:items="joinRules"
|
||||
class="mt-4"
|
||||
v-model="joinRule"
|
||||
item-value="id"
|
||||
>
|
||||
<template v-slot:selection="{ item }">
|
||||
{{ item.text }}
|
||||
</template>
|
||||
<template v-slot:item="{ item, attrs, on }">
|
||||
<v-list-item v-on="on" v-bind="attrs" #default="{ active }">
|
||||
<v-list-item-avatar>
|
||||
<v-icon class="grey lighten-1" dark>{{ item.icon }}</v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.text"></v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-text="item.descr"
|
||||
></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn icon v-if="active">
|
||||
<v-icon color="grey lighten-1">check</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-divider style="margin-bottom: 20px" />
|
||||
<v-divider style="margin-bottom: 20px" />
|
||||
|
||||
<v-text-field
|
||||
v-if="publicRoomLink"
|
||||
:value="publicRoomLink"
|
||||
class="room-link"
|
||||
readonly
|
||||
filled
|
||||
background-color="transparent"
|
||||
append-icon="content_copy"
|
||||
type="text"
|
||||
@click:append.stop="copyRoomLink"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
v-else-if="joinRule == 'public'"
|
||||
:loading="step == steps.CREATING"
|
||||
block
|
||||
depressed
|
||||
class="outlined-button"
|
||||
@click.stop="getPublicLink"
|
||||
><v-icon class="me-2">link</v-icon
|
||||
>{{ $t("new_room.get_link") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-else-if="joinRule == 'invite'"
|
||||
block
|
||||
depressed
|
||||
class="outlined-button"
|
||||
@click.stop="addPeople"
|
||||
><v-icon class="me-2">person_add</v-icon
|
||||
>{{ $t("new_room.add_people") }}</v-btn
|
||||
>
|
||||
<v-text-field
|
||||
v-if="publicRoomLink"
|
||||
:value="publicRoomLink"
|
||||
class="room-link"
|
||||
readonly
|
||||
filled
|
||||
background-color="transparent"
|
||||
append-icon="content_copy"
|
||||
type="text"
|
||||
@click:append.stop="copyRoomLink"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
v-else-if="joinRule == 'public'"
|
||||
:loading="step == steps.CREATING"
|
||||
block
|
||||
depressed
|
||||
class="outlined-button"
|
||||
@click.stop="getPublicLink"
|
||||
><v-icon class="me-2">link</v-icon
|
||||
>{{ $t("new_room.get_link") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-else-if="joinRule == 'invite'"
|
||||
block
|
||||
depressed
|
||||
class="outlined-button"
|
||||
@click.stop="addPeople"
|
||||
><v-icon class="me-2">person_add</v-icon
|
||||
>{{ $t("new_room.add_people") }}</v-btn
|
||||
>
|
||||
|
||||
<div v-if="publicRoomLinkCopied" class="link-copied">
|
||||
{{ $t("new_room.link_copied") }}
|
||||
</div>
|
||||
-->
|
||||
<div v-if="publicRoomLinkCopied" class="link-copied">
|
||||
{{ $t("new_room.link_copied") }}
|
||||
</div>
|
||||
-->
|
||||
<!-- <div v-if="status && !enterRoomDialog" class="text-center">
|
||||
<v-progress-circular
|
||||
v-if="step == steps.CREATING"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="20"
|
||||
></v-progress-circular>
|
||||
{{ status }}
|
||||
</div> -->
|
||||
<v-progress-circular
|
||||
v-if="step == steps.CREATING"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="20"
|
||||
></v-progress-circular>
|
||||
{{ status }}
|
||||
</div> -->
|
||||
<!-- </div> -->
|
||||
</v-fade-transition>
|
||||
<input
|
||||
id="room-avatar-picker"
|
||||
ref="avatar"
|
||||
type="file"
|
||||
name="avatar"
|
||||
@change="handlePickedAvatar($event)"
|
||||
accept="image/*"
|
||||
class="d-none"
|
||||
/>
|
||||
<v-dialog
|
||||
v-model="enterRoomDialog"
|
||||
:width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'"
|
||||
>
|
||||
<input id="room-avatar-picker" ref="avatar" type="file" name="avatar" @change="handlePickedAvatar($event)"
|
||||
accept="image/*" class="d-none" />
|
||||
<v-dialog v-model="enterRoomDialog" :width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'">
|
||||
<v-card>
|
||||
<v-container v-if="canEditProfile" class="pa-10">
|
||||
<v-row class="align-center">
|
||||
<v-col class="py-0">
|
||||
<div class="text-left font-weight-bold">{{ $t("join.choose_name") }}</div>
|
||||
<v-select
|
||||
ref="avatar"
|
||||
:items="availableAvatars"
|
||||
cache-items
|
||||
outlined
|
||||
dense
|
||||
@change="selectAvatar"
|
||||
:value="availableAvatars[0]"
|
||||
single-line
|
||||
autofocus
|
||||
>
|
||||
<v-select ref="avatar" :items="availableAvatars" cache-items outlined dense @change="selectAvatar"
|
||||
:value="availableAvatars[0]" single-line autofocus>
|
||||
<template v-slot:selection>
|
||||
<v-text-field
|
||||
background-color="transparent"
|
||||
solo
|
||||
flat
|
||||
hide-details
|
||||
@click.native.stop="{}"
|
||||
v-model="selectedProfile.name"
|
||||
></v-text-field>
|
||||
<v-text-field background-color="transparent" solo flat hide-details @click.native.stop="{}"
|
||||
v-model="selectedProfile.name"></v-text-field>
|
||||
</template>
|
||||
<template v-slot:item="data">
|
||||
<v-avatar size="32">
|
||||
|
|
@ -219,33 +180,23 @@
|
|||
<div class="ms-2">{{ data.item.name }}</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="2" class="py-0">
|
||||
<v-avatar @click="showAvatarPickerList">
|
||||
</v-col>
|
||||
<v-col cols="2" class="py-0">
|
||||
<v-avatar @click="showAvatarPickerList">
|
||||
<v-img v-if="selectedProfile" :src="selectedProfile.image" />
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-0">
|
||||
<v-col class="py-0">
|
||||
<v-checkbox
|
||||
id="chk-remember-me"
|
||||
class="mt-0"
|
||||
v-model="rememberMe"
|
||||
@change="onRememberMe"
|
||||
:label="$t('join.remember_me')"
|
||||
/>
|
||||
<v-checkbox id="chk-remember-me" class="mt-0" v-model="rememberMe" @change="onRememberMe"
|
||||
:label="$t('join.remember_me')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn
|
||||
color="black"
|
||||
depressed
|
||||
class="filled-button"
|
||||
@click.stop="onEnterRoom"
|
||||
:disabled="!selectedProfile.name"
|
||||
>
|
||||
{{ $t("join.enter_room") }}
|
||||
</v-btn>
|
||||
<v-btn color="black" depressed class="filled-button" @click.stop="onEnterRoom"
|
||||
:disabled="!selectedProfile.name">
|
||||
{{ $t("join.enter_room") }}
|
||||
</v-btn>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
@ -253,7 +204,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import util from "../plugins/utils";
|
||||
import util, { ROOM_TYPE_VOICE_MODE } from "../plugins/utils";
|
||||
import rememberMeMixin from "./rememberMeMixin";
|
||||
|
||||
const steps = Object.freeze({
|
||||
|
|
@ -265,7 +216,7 @@ const steps = Object.freeze({
|
|||
|
||||
export default {
|
||||
name: "CreateRoom",
|
||||
mixins:[rememberMeMixin],
|
||||
mixins: [rememberMeMixin],
|
||||
data() {
|
||||
return {
|
||||
steps,
|
||||
|
|
@ -305,7 +256,9 @@ export default {
|
|||
v => !v.includes(':') || this.$t("new_room.colon_not_allowed")
|
||||
],
|
||||
roomNameHasError: false,
|
||||
roomCreationErrorMsg: ""
|
||||
roomCreationErrorMsg: "",
|
||||
showOptions: false,
|
||||
useVoiceMode: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -314,7 +267,7 @@ export default {
|
|||
this.availableAvatars = util.getDefaultAvatars();
|
||||
this.selectAvatar(
|
||||
this.availableAvatars[
|
||||
Math.floor(Math.random() * this.availableAvatars.length)
|
||||
Math.floor(Math.random() * this.availableAvatars.length)
|
||||
]
|
||||
);
|
||||
},
|
||||
|
|
@ -359,7 +312,7 @@ export default {
|
|||
},
|
||||
|
||||
onCreate() {
|
||||
if(this.currentUser) {
|
||||
if (this.currentUser) {
|
||||
this.onEnterRoom();
|
||||
} else {
|
||||
this.enterRoomDialog = true;
|
||||
|
|
@ -486,6 +439,11 @@ export default {
|
|||
// Add topic
|
||||
createRoomOptions.topic = this.roomTopic;
|
||||
}
|
||||
if (this.useVoiceMode) {
|
||||
createRoomOptions.creation_content = {
|
||||
type: ROOM_TYPE_VOICE_MODE
|
||||
}
|
||||
}
|
||||
|
||||
return this.$matrix
|
||||
.getLoginPromise()
|
||||
|
|
@ -592,7 +550,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
this.status = ""
|
||||
this.roomCreationErrorMsg = (error.data && error.data.error) || error.message || error.toString();
|
||||
this.roomCreationErrorMsg = (error.data && error.data.error) || error.message || error.toString();
|
||||
this.step = steps.INITIAL; // revert
|
||||
return null;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -165,12 +165,15 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="account ma-3" flat>
|
||||
<v-card-title class="h2">{{ $t("room_info.ui_options") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card class="account ma-3" flat v-if="$config.experimental_voice_mode">
|
||||
<v-card-title class="h2 with-right-label"><div>{{ $t("room_info.experimental_features") }}</div><div></div></v-card-title>
|
||||
<v-card-text class="with-right-label">
|
||||
<div>
|
||||
<div class="option-title">{{ $t('room_info.voice_mode') }}</div>
|
||||
<div class="option-text">{{ $t('room_info.voice_mode_info') }}</div>
|
||||
</div>
|
||||
<v-switch
|
||||
v-model="useAudioLayout"
|
||||
:label="$t('room_info.audio_layout')"
|
||||
v-model="useVoiceMode"
|
||||
></v-switch>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
|
@ -373,20 +376,14 @@ export default {
|
|||
return "";
|
||||
},
|
||||
|
||||
useAudioLayout: {
|
||||
useVoiceMode: {
|
||||
get: function () {
|
||||
if (this.room) {
|
||||
const tags = this.room.tags;
|
||||
if (tags && tags["ui_options"]) {
|
||||
return tags["ui_options"]["audio_layout"] === 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return util.useVoiceMode(this.room);
|
||||
},
|
||||
set: function (audioLayout) {
|
||||
if (this.room && this.room.tags) {
|
||||
let options = this.room.tags["ui_options"] || {}
|
||||
options["audio_layout"] = (audioLayout ? 1 : 0);
|
||||
options["voice_mode"] = (audioLayout ? 1 : 0);
|
||||
this.room.tags["ui_options"] = options;
|
||||
this.$matrix.matrixClient.setRoomTag(this.room.roomId, "ui_options", options);
|
||||
}
|
||||
|
|
@ -668,4 +665,5 @@ export default {
|
|||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
|
||||
</style>
|
||||
|
|
@ -11,6 +11,20 @@
|
|||
<v-btn v-show="state == states.RECORDED" icon @click.stop="redo">
|
||||
<v-icon color="white">undo</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-show="state == states.INITIAL" icon @click.stop="importAudio">
|
||||
<v-icon color="white">$vuetify.icons.audio_import</v-icon>
|
||||
<input
|
||||
ref="audio_import"
|
||||
type="file"
|
||||
name="audio_import"
|
||||
@change="handleAudioImport($event)"
|
||||
accept="audio/*"
|
||||
class="d-none"
|
||||
/>
|
||||
</v-btn>
|
||||
<v-btn v-show="state == states.IMPORTED" icon @click.stop="previewAudio">
|
||||
<v-icon color="white">$vuetify.icons.audio_import_play</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4" align="center">
|
||||
<v-btn
|
||||
|
|
@ -23,7 +37,7 @@
|
|||
<v-icon color="white">stop</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="state == states.RECORDED"
|
||||
v-else-if="state == states.RECORDED || state == states.IMPORTED"
|
||||
id="btn-send"
|
||||
class="voice-recorder-btn recorded"
|
||||
icon
|
||||
|
|
@ -145,6 +159,7 @@ const State = {
|
|||
RECORDING: "recording",
|
||||
RECORDED: "recorded",
|
||||
ERROR: "error",
|
||||
IMPORTED: "imported"
|
||||
};
|
||||
import util from "../plugins/utils";
|
||||
import VoiceRecorderLock from "./VoiceRecorderLock";
|
||||
|
|
@ -192,7 +207,8 @@ export default {
|
|||
recordingLocked: false,
|
||||
recordedFile: null,
|
||||
errorMessage: null,
|
||||
recorder: null
|
||||
recorder: null,
|
||||
previewPlayer: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -242,8 +258,10 @@ export default {
|
|||
this.startRecording();
|
||||
} else {
|
||||
console.log("Not PTT");
|
||||
if (this.micButtonRef) {
|
||||
//eslint-disable-next-line
|
||||
this.micButtonRef.$el.style.display = "none";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove listeners
|
||||
|
|
@ -257,10 +275,17 @@ export default {
|
|||
this.startCoordinateX = null;
|
||||
this.startCoordinateY = null;
|
||||
this.recordingLocked = false;
|
||||
if (this.micButtonRef) {
|
||||
//eslint-disable-next-line
|
||||
this.micButtonRef.$el.style.display = "block";
|
||||
}
|
||||
}
|
||||
},
|
||||
state() {
|
||||
if (this.state != State.IMPORTED && this.previewPlayer) {
|
||||
this.previewPlayer.pause();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lockButtonStyle() {
|
||||
|
|
@ -351,10 +376,10 @@ export default {
|
|||
},
|
||||
cancelRecording() {
|
||||
if(this.recorder) {
|
||||
this.state = State.INITIAL;
|
||||
this.recorder.stop();
|
||||
this.recorder = null;
|
||||
}
|
||||
this.state = State.INITIAL;
|
||||
this.close();
|
||||
},
|
||||
pauseRecording() {
|
||||
|
|
@ -433,6 +458,37 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show import picker to select file
|
||||
*/
|
||||
importAudio() {
|
||||
this.$refs.audio_import.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle picked audio file
|
||||
*/
|
||||
handleAudioImport(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
this.recordedFile = event.target.files[0];
|
||||
this.state = State.IMPORTED;
|
||||
}
|
||||
},
|
||||
|
||||
previewAudio() {
|
||||
if (this.recordedFile) {
|
||||
if (!this.previewPlayer) {
|
||||
this.previewPlayer = new Audio();
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.previewPlayer.src = e.target.result;
|
||||
this.previewPlayer.play();
|
||||
};
|
||||
reader.readAsDataURL(this.recordedFile);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
|
|||
import dataUriToBuffer from "data-uri-to-buffer";
|
||||
import ImageResize from "image-resize";
|
||||
|
||||
export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice";
|
||||
|
||||
const sizeOf = require("image-size");
|
||||
|
||||
var dayjs = require('dayjs');
|
||||
|
|
@ -424,6 +426,36 @@ class Util {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if we should use voice mode for the given room.
|
||||
*
|
||||
* The default value is given by the room itself. If the "type" of the
|
||||
* room is set to 'im.keanu.room_type_voice' then we default to voice mode,
|
||||
* else not. The user can then override this default by flipping the "voice mode"
|
||||
* swicth on room settings (it will be persisted as a user specific tag on the room)
|
||||
*/
|
||||
useVoiceMode(roomOrNull) {
|
||||
if (roomOrNull) {
|
||||
const room = roomOrNull;
|
||||
|
||||
// Have we changed our local view mode of this room?
|
||||
const tags = room.tags;
|
||||
if (tags && tags["ui_options"]) {
|
||||
return tags["ui_options"]["voice_mode"] === 1;
|
||||
}
|
||||
|
||||
// Was the room created with a voice mode type?
|
||||
const createEvent = room.currentState.getStateEvents(
|
||||
"m.room.create",
|
||||
""
|
||||
);
|
||||
if (createEvent) {
|
||||
return createEvent.getContent().type === ROOM_TYPE_VOICE_MODE;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Generate a random user name */
|
||||
randomUser(prefix) {
|
||||
var pfx = prefix ? prefix.replace(/[^0-9a-zA-Z\-_]/gi, '') : null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue