diff options
Diffstat (limited to 'core/src/views/ContactsMenu.vue')
-rw-r--r-- | core/src/views/ContactsMenu.vue | 325 |
1 files changed, 180 insertions, 145 deletions
diff --git a/core/src/views/ContactsMenu.vue b/core/src/views/ContactsMenu.vue index f03652bb477..924ddcea56b 100644 --- a/core/src/views/ContactsMenu.vue +++ b/core/src/views/ContactsMenu.vue @@ -1,198 +1,233 @@ <!-- - - @copyright 2023 Christopher Ng <chrng8@gmail.com> - - - - @author Christopher Ng <chrng8@gmail.com> - - - - @license AGPL-3.0-or-later - - - - 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/>. - - + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later --> <template> <NcHeaderMenu id="contactsmenu" + class="contactsmenu" :aria-label="t('core', 'Search contacts')" @open="handleOpen"> <template #trigger> - <Contacts :size="20" /> + <NcIconSvgWrapper class="contactsmenu__trigger-icon" :path="mdiContacts" /> </template> - <div id="contactsmenu-menu" /> + <div class="contactsmenu__menu"> + <div class="contactsmenu__menu__input-wrapper"> + <NcTextField id="contactsmenu__menu__search" + ref="contactsMenuInput" + :value.sync="searchTerm" + trailing-button-icon="close" + :label="t('core', 'Search contacts')" + :trailing-button-label="t('core','Reset search')" + :show-trailing-button="searchTerm !== ''" + :placeholder="t('core', 'Search contacts …')" + class="contactsmenu__menu__search" + @input="onInputDebounced" + @trailing-button-click="onReset" /> + </div> + <NcEmptyContent v-if="error" :name="t('core', 'Could not load your contacts')"> + <template #icon> + <NcIconSvgWrapper :path="mdiMagnify" /> + </template> + </NcEmptyContent> + <NcEmptyContent v-else-if="loadingText" :name="loadingText"> + <template #icon> + <NcLoadingIcon /> + </template> + </NcEmptyContent> + <NcEmptyContent v-else-if="contacts.length === 0" :name="t('core', 'No contacts found')"> + <template #icon> + <NcIconSvgWrapper :path="mdiMagnify" /> + </template> + </NcEmptyContent> + <div v-else class="contactsmenu__menu__content"> + <div id="contactsmenu-contacts"> + <ul> + <Contact v-for="contact in contacts" :key="contact.id" :contact="contact" /> + </ul> + </div> + <div v-if="contactsAppEnabled" class="contactsmenu__menu__content__footer"> + <NcButton type="tertiary" :href="contactsAppURL"> + {{ t('core', 'Show all contacts') }} + </NcButton> + </div> + <div v-else-if="canInstallApp" class="contactsmenu__menu__content__footer"> + <NcButton type="tertiary" :href="contactsAppMgmtURL"> + {{ t('core', 'Install the Contacts app') }} + </NcButton> + </div> + </div> + </div> </NcHeaderMenu> </template> <script> -import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js' - -import Contacts from 'vue-material-design-icons/Contacts.vue' - -import OC from '../OC/index.js' +import { mdiContacts, mdiMagnify } from '@mdi/js' +import { generateUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' +import { t } from '@nextcloud/l10n' +import axios from '@nextcloud/axios' +import debounce from 'debounce' + +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcTextField from '@nextcloud/vue/components/NcTextField' + +import Contact from '../components/ContactsMenu/Contact.vue' +import logger from '../logger.js' +import Nextcloud from '../mixins/Nextcloud.js' export default { name: 'ContactsMenu', components: { - Contacts, + Contact, + NcButton, + NcEmptyContent, NcHeaderMenu, + NcIconSvgWrapper, + NcLoadingIcon, + NcTextField, }, - data() { + mixins: [Nextcloud], + + setup() { return { - contactsMenu: null, + mdiContacts, + mdiMagnify, } }, - mounted() { - // eslint-disable-next-line no-new - this.contactsMenu = new OC.ContactsMenu({ - el: '#contactsmenu-menu', - }) + data() { + const user = getCurrentUser() + return { + contactsAppEnabled: false, + contactsAppURL: generateUrl('/apps/contacts'), + contactsAppMgmtURL: generateUrl('/settings/apps/social/contacts'), + canInstallApp: user.isAdmin, + contacts: [], + loadingText: undefined, + error: false, + searchTerm: '', + } }, methods: { - handleOpen() { - this.contactsMenu?.loadContacts() + async handleOpen() { + await this.getContacts('') }, + async getContacts(searchTerm) { + if (searchTerm === '') { + this.loadingText = t('core', 'Loading your contacts …') + } else { + this.loadingText = t('core', 'Looking for {term} …', { + term: searchTerm, + }) + } + + // Let the user try a different query if the previous one failed + this.error = false + + try { + const { data: { contacts, contactsAppEnabled } } = await axios.post(generateUrl('/contactsmenu/contacts'), { + filter: searchTerm, + }) + this.contacts = contacts + this.contactsAppEnabled = contactsAppEnabled + this.loadingText = undefined + } catch (error) { + logger.error('could not load contacts', { + error, + searchTerm, + }) + this.error = true + } + }, + onInputDebounced: debounce(function() { + this.getContacts(this.searchTerm) + }, 500), + + /** + * Reset the search state + */ + onReset() { + this.searchTerm = '' + this.contacts = [] + this.focusInput() + }, + + /** + * Focus the search input on next tick + */ + focusInput() { + this.$nextTick(() => { + this.$refs.contactsMenuInput.focus() + this.$refs.contactsMenuInput.select() + }) + }, + }, } </script> <style lang="scss" scoped> -#contactsmenu-menu { - /* show 2.5 to 4.5 entries depending on the screen height */ - height: calc(100vh - 50px * 3); - max-height: calc(50px * 6 + 2px); - min-height: calc(50px * 3.5); - width: 350px; - - &:deep { - .emptycontent { - margin-top: 5vh !important; - margin-bottom: 1.5vh; - .icon-loading, - .icon-search { - display: inline-block; - } +.contactsmenu { + overflow-y: hidden; + + &__trigger-icon { + color: var(--color-background-plain-text) !important; + } + + &__menu { + display: flex; + flex-direction: column; + overflow: hidden; + height: calc(50px * 6 + 2px + 26px); + max-height: inherit; + + label[for="contactsmenu__menu__search"] { + font-weight: bold; + font-size: 19px; + margin-inline-start: 13px; } - #contactsmenu-search { - width: calc(100% - 16px); - margin: 8px; + &__input-wrapper { + padding: 10px; + z-index: 2; + top: 0; + } + + &__search { + width: 100%; height: 34px; + margin-top: 0!important; } - .content { - /* fixed max height of the parent container without the search input */ - height: calc(100vh - 50px * 3 - 50px); - max-height: calc(50px * 5); - min-height: calc(50px * 3.5 - 50px); + &__content { overflow-y: auto; + margin-top: 10px; + flex: 1 1 auto; - .footer { - text-align: center; - - a { - display: block; - width: 100%; - padding: 12px 0; - opacity: .5; - } + &__footer { + display: flex; + flex-direction: column; + align-items: center; } } a { - padding: 2px; - &:focus-visible { box-shadow: inset 0 0 0 2px var(--color-main-text) !important; // override rule in core/css/headers.scss #header a:focus-visible } } + } - .contact { - display: flex; - position: relative; - align-items: center; - padding: 3px 3px 3px 10px; - - .avatar { - height: 32px; - width: 32px; - display: inline-block; - } - - .body { - flex-grow: 1; - padding-left: 8px; - - div { - position: relative; - width: 100%; - } - - .full-name, .last-message { - /* TODO: don't use fixed width */ - max-width: 204px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .last-message, .email-address { - color: var(--color-text-maxcontrast); - } - } - - .top-action, .second-action, .other-actions { - width: 16px; - height: 16px; - opacity: .5; - cursor: pointer; - - &:not(button) { - padding: 14px; - } - img { - filter: var(--background-invert-if-dark); - } - - &:hover, - &:active, - &:focus { - opacity: 1; - } - } - - button.other-actions { - width: 44px; - - &:focus { - border-color: transparent; - box-shadow: 0 0 0 2px var(--color-main-text); - } - - &:focus-visible { - border-radius: var(--border-radius-pill); - } - } - - /* actions menu */ - .menu { - top: 47px; - margin-right: 13px; - } - .popovermenu::after { - right: 2px; - } - } + :deep(.empty-content) { + margin: 0 !important; } } </style> |