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:
N Pex 2023-02-17 21:39:37 +00:00
commit b7d3a1dc95
11 changed files with 424 additions and 252 deletions

View file

@ -39,5 +39,6 @@
"siteId": "25"
}
}
]
],
"experimental_voice_mode": true
}

View file

@ -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;

View 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>

View 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>

View file

@ -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",

View file

@ -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>

View file

@ -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);

View file

@ -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;
});

View file

@ -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>

View file

@ -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>

View file

@ -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;