aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/views/ContactsMenu.vue
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/views/ContactsMenu.vue')
-rw-r--r--core/src/views/ContactsMenu.vue325
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>