Merge pull request #8341 from nextcloud/feature/6766/add-useful-information-to-the-topbar

⏺️ Recording part 1 - Add useful information to the top-bar
This commit is contained in:
Joas Schilling 2022-11-22 15:35:44 +01:00 коммит произвёл GitHub
Родитель 6761f84577 c18297d490
Коммит 63f60ea1eb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 328 добавлений и 61 удалений

Просмотреть файл

@ -552,6 +552,11 @@ export default {
padding: 0 !important; padding: 0 !important;
} }
::v-deep .app-navigation-toggle {
top: 8px !important;
right: -6px !important;
}
::v-deep .app-navigation__list { ::v-deep .app-navigation__list {
padding: 0 !important; padding: 0 !important;
} }

Просмотреть файл

@ -95,7 +95,7 @@
</template> </template>
<script> <script>
import { emit } from '@nextcloud/event-bus' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js' import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js' import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
import SharedItemsTab from './SharedItems/SharedItemsTab.vue' import SharedItemsTab from './SharedItems/SharedItemsTab.vue'
@ -274,6 +274,14 @@ export default {
}, },
}, },
mounted() {
subscribe('spreed:select-active-sidebar-tab', this.handleUpdateActive)
},
beforeDestroy() {
unsubscribe('spreed:select-active-sidebar-tab', this.handleUpdateActive)
},
methods: { methods: {
handleClose() { handleClose() {
this.dismissEditing() this.dismissEditing()

Просмотреть файл

@ -0,0 +1,205 @@
<!--
- @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com> -
- @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<NcPopover class="top-bar__button call-time"
close-after-click="true"
:menu-title="callTime"
:shown.sync="showPopover"
:class="{ 'call-time--wide': isWide }"
:triggers="[]"
:container="container">
<template #trigger>
<NcButton :disabled="!isRecording || !isModerator"
:wide="true"
:class="{ 'call-time__not-recording': !isRecording }"
type="tertiary"
@click="showPopover = true">
<template v-if="isRecording" #icon>
<RecordCircle :size="20"
fill-color="#e9322d" />
</template>
{{ formattedTime }}
</ncbutton>
</template>
<NcButton type="tertiary-no-background"
:wide="true"
@click="stopRecording">
<template #icon>
<StopIcon :size="20" />
</template>
{{ t('spreed', 'Stop recording') }}
</NcButton>
</NcPopover>
</template>
<script>
import RecordCircle from 'vue-material-design-icons/RecordCircle.vue'
import StopIcon from 'vue-material-design-icons/Stop.vue'
import NcPopover from '@nextcloud/vue/dist/Components/NcPopover.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import isInLobby from '../../mixins/isInLobby.js'
export default {
name: 'CallTime',
components: {
RecordCircle,
StopIcon,
NcPopover,
NcButton,
},
mixins: [isInLobby],
props: {
/**
* Unix timestamp representing the start of the call
*/
start: {
type: Number,
required: true,
},
isRecording: {
type: Boolean,
required: true,
},
canModerate: {
type: Boolean,
required: true,
},
},
data() {
return {
callTime: undefined,
showPopover: false,
timer: null,
}
},
computed: {
container() {
return this.$store.getters.getMainContainerSelector()
},
/**
* Create date object based on the unix time received from the API
*
* @return {Date} The date object
*/
callStart() {
return new Date(this.start * 1000)
},
/**
* Calculates the stopwatch string given the callTime (ms)
*
* @return {string} The formatted time
*/
formattedTime() {
if (!this.callTime) {
return '-- : --'
}
let seconds = Math.floor((this.callTime / 1000) % 60)
if (seconds < 10) {
seconds = '0' + seconds
}
let minutes = Math.floor((this.callTime / (1000 * 60)) % 60)
if (minutes < 10) {
minutes = '0' + minutes
}
const hours = Math.floor((this.callTime / (1000 * 60 * 60)) % 24)
if (hours === 0) {
return minutes + ' : ' + seconds
}
return hours + ' : ' + minutes + ' : ' + seconds
},
isWide() {
return this.formattedTime.length > 7
},
token() {
return this.$store.getters.getToken()
},
conversation() {
return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation
},
},
mounted() {
// Start the timer when mounted
this.timer = setInterval(this.computeElapsedTime, 1000)
},
beforeDestroy() {
clearInterval(this.timer)
},
methods: {
stopRecording() {
this.$emit('stop-recording')
this.showPopover = false
},
computeElapsedTime() {
if (this.start === 0) {
return
}
this.callTime = new Date() - this.callStart
},
},
}
</script>
<style lang="scss" scoped>
.call-time {
display: flex;
justify-content: center;
align-items: center;
height: var(--default-clickable-area);
font-weight: bold;
width: 116px;
&__not-recording {
padding-left: var(--default-clickable-area) !important
}
&--wide {
width: 148px;
}
}
::v-deep .button-vue {
justify-content: left !important;
color: #fff !important;
&:disabled {
opacity: 1 !important;
}
}
</style>

Просмотреть файл

@ -21,16 +21,14 @@
<template> <template>
<div class="top-bar" :class="{ 'in-call': isInCall }"> <div class="top-bar" :class="{ 'in-call': isInCall }">
<ConversationIcon v-if="!isInCall" <ConversationIcon :key="conversation.token"
:key="conversation.token"
class="conversation-icon" class="conversation-icon"
:offline="isPeerOffline" :offline="isPeerOffline"
:item="conversation" :item="conversation"
:hide-favorite="false" :hide-favorite="false"
:hide-call="false" /> :hide-call="false" />
<!-- conversation header --> <!-- conversation header -->
<a v-if="!isInCall" <a role="button"
role="button"
class="conversation-header" class="conversation-header"
@click="openConversationSettings"> @click="openConversationSettings">
<div class="conversation-header__text" <div class="conversation-header__text"
@ -42,18 +40,27 @@
class="description"> class="description">
{{ statusMessage }} {{ statusMessage }}
</p> </p>
<p v-else-if="conversation.description" <template v-if="!isInCall && conversation.description">
v-tooltip.bottom="{ <p v-tooltip.bottom="{
content: renderedDescription, content: renderedDescription,
delay: { show: 500, hide: 500 }, delay: { show: 500, hide: 500 },
autoHide: false, autoHide: false,
html: true, html: true,
}" }"
class="description"> class="description">
{{ conversation.description }} {{ conversation.description }}
</p> </p>
</template>
</div> </div>
</a> </a>
<!-- Call time -->
<CallTime v-if="isInCall"
:start="conversation.callStartTime"
:is-recording="isRecording"
@stop-recording="isRecording = false" />
<!-- Local media controls -->
<LocalMediaControls v-if="isInCall" <LocalMediaControls v-if="isInCall"
class="local-media-controls" class="local-media-controls"
:token="token" :token="token"
@ -68,36 +75,54 @@
:is-sidebar="isSidebar" :is-sidebar="isSidebar"
:model="localMediaModel" /> :model="localMediaModel" />
<div class="top-bar__buttons"> <CallButton class="top-bar__button" />
<CallButton class="top-bar__button" />
<template v-if="showOpenSidebarButton">
<!-- sidebar toggle --> <!-- sidebar toggle -->
<NcActions v-if="showOpenSidebarButton" <NcButton v-if="!isInCall"
class="top-bar__button" class="top-bar__button"
close-after-click="true" close-after-click="true"
:container="container"> type="tertiary"
<NcActionButton v-if="isInCall" @click="openSidebar">
key="openSideBarButtonMessageText" <template #icon>
@click="openSidebar"> <MenuIcon :size="20" />
<template #icon> </template>
<MessageText :size="20" </NcButton>
fill-color="#ffffff" />
</template> <!-- chat button -->
</NcActionButton> <div v-if="isInCall"
<NcActionButton v-else class="chat-button">
key="openSideBarButtonMenuPeople" <NcActions class="top-bar__button"
@click="openSidebar"> close-after-click="true"
<template #icon> :container="container">
<MenuPeople :size="20" /> <NcActionButton key="openSideBarButtonMessageText"
</template> @click="openSidebar('chat')">
</NcActionButton> <template #icon>
</NcActions> <MessageText :size="20"
</div> fill-color="#ffffff" />
<NcCounterBubble v-if="!isSidebar && showOpenSidebarButton && isInCall && unreadMessagesCounter > 0" </template>
class="unread-messages-counter" </NcActionButton>
:highlighted="hasUnreadMentions"> </NcActions>
{{ unreadMessagesCounter }} <NcCounterBubble v-if="!isSidebar && isInCall && unreadMessagesCounter > 0"
</NcCounterBubble> class="chat-button__unread-messages-counter"
:highlighted="hasUnreadMentions">
{{ unreadMessagesCounter }}
</NcCounterBubble>
</div>
<!-- participants button -->
<NcButton v-if="isInCall && !isOneToOneConversation"
class="top-bar__button"
close-after-click="true"
type="tertiary"
@click="openSidebar('participants')">
<template #icon>
<AccountMultiple :size="20"
fill-color="#ffffff" />
</template>
{{ participantsInCall }}
</NcButton>
</template>
</div> </div>
</template> </template>
@ -108,7 +133,7 @@ import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js' import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
import CallButton from './CallButton.vue' import CallButton from './CallButton.vue'
import BrowserStorage from '../../services/BrowserStorage.js' import BrowserStorage from '../../services/BrowserStorage.js'
import MenuPeople from '../missingMaterialDesignIcons/MenuPeople.vue' import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
import MessageText from 'vue-material-design-icons/MessageText.vue' import MessageText from 'vue-material-design-icons/MessageText.vue'
import { CONVERSATION } from '../../constants.js' import { CONVERSATION } from '../../constants.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
@ -121,6 +146,9 @@ import userStatus from '../../mixins/userStatus.js'
import LocalMediaControls from '../CallView/shared/LocalMediaControls.vue' import LocalMediaControls from '../CallView/shared/LocalMediaControls.vue'
import getParticipants from '../../mixins/getParticipants.js' import getParticipants from '../../mixins/getParticipants.js'
import TopBarMenu from './TopBarMenu.vue' import TopBarMenu from './TopBarMenu.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import CallTime from './CallTime.vue'
import MenuIcon from 'vue-material-design-icons/Menu.vue'
export default { export default {
name: 'TopBar', name: 'TopBar',
@ -134,11 +162,14 @@ export default {
NcActions, NcActions,
NcCounterBubble, NcCounterBubble,
CallButton, CallButton,
MenuPeople, AccountMultiple,
MessageText, MessageText,
ConversationIcon, ConversationIcon,
LocalMediaControls, LocalMediaControls,
TopBarMenu, TopBarMenu,
NcButton,
CallTime,
MenuIcon,
}, },
mixins: [ mixins: [
@ -167,7 +198,8 @@ export default {
unreadNotificationHandle: null, unreadNotificationHandle: null,
localCallParticipantModel, localCallParticipantModel,
localMediaModel, localMediaModel,
// TODO: real value
isRecording: true,
} }
}, },
@ -248,6 +280,10 @@ export default {
return !peer.sessionIds.length return !peer.sessionIds.length
} else return false } else return false
}, },
participantsInCall() {
return this.$store.getters.participantsInCall(this.token) ? this.$store.getters.participantsInCall(this.token) : ''
},
}, },
watch: { watch: {
@ -317,7 +353,10 @@ export default {
} }
}, },
openSidebar() { openSidebar(activeTab) {
if (typeof activeTab === 'string') {
emit('spreed:select-active-sidebar-tab', activeTab)
}
this.$store.dispatch('showSidebar') this.$store.dispatch('showSidebar')
BrowserStorage.setItem('sidebarOpen', 'true') BrowserStorage.setItem('sidebarOpen', 'true')
}, },
@ -325,6 +364,11 @@ export default {
openConversationSettings() { openConversationSettings() {
emit('show-conversation-settings', { token: this.token }) emit('show-conversation-settings', { token: this.token })
}, },
// TODO: implement real method
stopRecording() {
console.debug('stop recordiiing')
},
}, },
} }
</script> </script>
@ -354,12 +398,10 @@ export default {
left:0; left:0;
background-color: transparent; background-color: transparent;
display: flex; display: flex;
flex-wrap: wrap-reverse; flex-wrap: wrap;
} & * {
color: #fff;
&__buttons { }
display: flex;
margin-left: 8px;
} }
&__button { &__button {
@ -377,11 +419,14 @@ export default {
} }
} }
.unread-messages-counter { .chat-button {
position: absolute; position: relative;
top: 40px; &__unread-messages-counter {
right: 4px; position: absolute;
pointer-events: none; top: 24px;
right: 2px;
pointer-events: none;
}
} }
} }
@ -395,7 +440,8 @@ export default {
overflow-x: hidden; overflow-x: hidden;
overflow-y: clip; overflow-y: clip;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 0;
flex-grow: 1;
cursor: pointer; cursor: pointer;
&__text { &__text {
display: flex; display: flex;
@ -422,8 +468,4 @@ export default {
color: var(--color-text-lighter); color: var(--color-text-lighter);
} }
} }
.local-media-controls {
padding-left: $clickable-area;
}
</style> </style>

Просмотреть файл

@ -155,6 +155,13 @@ const getters = {
return {} return {}
}, },
participantsInCall: (state) => (token) => {
if (state.attendees[token]) {
return Object.values(state.attendees[token]).filter(attendee => attendee.inCall !== PARTICIPANT.CALL_FLAG.DISCONNECTED).length
}
return 0
},
} }
const mutations = { const mutations = {