nextcloud-contacts/src/views/Contacts.vue

428 lines
11 KiB
Vue

<!--
- @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
- @author Charismatic Claire <charismatic.claire@noservice.noreply>
-
- @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>
<Content app-name="contacts">
<!-- go back to list when in details mode -->
<a v-if="selectedContact && isMobile"
class="app-details-toggle icon-confirm"
href="#"
@click.stop.prevent="showList" />
<!-- new-contact-button + navigation + settings -->
<RootNavigation
:contacts-list="contactsList"
:loading="loadingContacts || loadingCircles"
:selected-group="selectedGroup"
:selected-contact="selectedContact">
<!-- new-contact-button -->
<AppNavigationNew v-if="!loadingContacts"
button-id="new-contact-button"
:text="t('contacts','New contact')"
button-class="icon-add"
:disabled="!defaultAddressbook"
@click="newContact" />
</RootNavigation>
<!-- Main content: circle or contacts -->
<CircleContent v-if="selectedCircle"
:loading="loadingCircles" />
<ContactsContent v-else
:contacts-list="contactsList"
:loading="loadingContacts"
@newContact="newContact" />
<!-- Import modal -->
<Modal v-if="isImporting"
:clear-view-delay="-1"
:can-close="isImportDone"
@close="closeImport">
<ImportView @close="closeImport" />
</Modal>
<!-- Select contacts group modal -->
<ContactsPicker />
</Content>
</template>
<script>
import { GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, ROUTE_CIRCLE } from '../models/constants.ts'
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
import Content from '@nextcloud/vue/dist/Components/Content'
import isMobile from '@nextcloud/vue/dist/Mixins/isMobile'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import { showError } from '@nextcloud/dialogs'
import { VCardTime } from 'ical.js'
import CircleContent from '../components/AppContent/CircleContent'
import ContactsContent from '../components/AppContent/ContactsContent'
import ContactsPicker from '../components/EntityPicker/ContactsPicker'
import ImportView from './Processing/ImportView'
import RootNavigation from '../components/AppNavigation/RootNavigation'
import Contact from '../models/contact'
import rfcProps from '../models/rfcProps'
import client from '../services/cdav'
export default {
name: 'Contacts',
components: {
AppNavigationNew,
CircleContent,
ContactsContent,
ContactsPicker,
Content,
ImportView,
Modal,
RootNavigation,
},
mixins: [
isMobile,
],
// passed by the router
props: {
selectedCircle: {
type: String,
default: undefined,
},
selectedGroup: {
type: String,
default: undefined,
},
selectedContact: {
type: String,
default: undefined,
},
},
data() {
return {
loadingCircles: true,
loadingContacts: true,
}
},
computed: {
// store getters
addressbooks() {
return this.$store.getters.getAddressbooks
},
contacts() {
return this.$store.getters.getContacts
},
sortedContacts() {
return this.$store.getters.getSortedContacts
},
groups() {
return this.$store.getters.getGroups
},
orderKey() {
return this.$store.getters.getOrderKey
},
importState() {
return this.$store.getters.getImportState
},
/**
* Are we importing contacts ?
* @returns {boolean}
*/
isImporting() {
return this.importState.stage !== 'default'
},
/**
* Are we done importing contacts ?
* @returns {boolean}
*/
isImportDone() {
return this.importState.stage === 'done'
},
// first enabled addressbook of the list
defaultAddressbook() {
return this.addressbooks.find(addressbook => !addressbook.readOnly && addressbook.enabled)
},
/**
* Contacts list based on the selected group.
* Those filters are pretty fast, so let's only
* intersect the groups contacts and the full
* sorted contacts List.
*
* @returns {Array}
*/
contactsList() {
if (this.selectedGroup === GROUP_ALL_CONTACTS) {
return this.sortedContacts
} else if (this.selectedGroup === GROUP_NO_GROUP_CONTACTS) {
return this.ungroupedContacts.map(contact => this.sortedContacts.find(item => item.key === contact.key))
}
const group = this.groups.filter(group => group.name === this.selectedGroup)[0]
if (group) {
return this.sortedContacts.filter(contact => group.contacts.indexOf(contact.key) >= 0)
}
return []
},
ungroupedContacts() {
return this.sortedContacts.filter(contact => this.contacts[contact.key].groups && this.contacts[contact.key].groups.length === 0)
},
},
watch: {
// watch url change and group select
selectedGroup() {
if (!this.isMobile) {
this.selectFirstContactIfNone()
}
},
// watch url change and contact select
selectedContact() {
if (!this.isMobile) {
this.selectFirstContactIfNone()
}
},
},
mounted() {
// Register search
this.search = new OCA.Search(this.search, this.resetSearch)
},
async beforeMount() {
// get addressbooks then get contacts
client.connect({ enableCardDAV: true }).then(() => {
console.debug('Connected to dav!', client)
this.$store.dispatch('getAddressbooks')
.then((addressbooks) => {
const writeableAddressBooks = addressbooks.filter(addressbook => !addressbook.readOnly)
// No writeable addressbooks? Create a new one!
if (writeableAddressBooks.length === 0) {
this.$store.dispatch('appendAddressbook', { displayName: t('contacts', 'Contacts') })
.then(() => {
this.fetchContacts()
})
// else, let's get those contacts!
} else {
this.fetchContacts()
}
})
// check local storage for orderKey
if (localStorage.getItem('orderKey')) {
// run setOrder mutation with local storage key
this.$store.commit('setOrder', localStorage.getItem('orderKey'))
}
})
// Get circles
this.$store.dispatch('getCircles').then(() => {
this.loadingCircles = false
})
},
methods: {
async newContact() {
const rev = new VCardTime()
const contact = new Contact(`
BEGIN:VCARD
VERSION:4.0
PRODID:-//Nextcloud Contacts v${appVersion}
END:VCARD
`.trim().replace(/\t/gm, ''),
this.defaultAddressbook)
contact.fullName = t('contacts', 'New contact')
rev.fromUnixTime(Date.now() / 1000)
contact.rev = rev
// itterate over all properties (filter is not usable on objects and we need the key of the property)
const properties = rfcProps.properties
for (const name in properties) {
if (properties[name].default) {
const defaultData = properties[name].defaultValue
// add default field
const property = contact.vCard.addPropertyWithValue(name, defaultData.value)
// add default type
if (defaultData.type) {
property.setParameter('type', defaultData.type)
}
}
}
// set group if it's selected already
// BUT NOT if it's the _fake_ groups like all contacts and not grouped
if ([GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS].indexOf(this.selectedGroup) === -1) {
contact.groups = [this.selectedGroup]
}
try {
// this will trigger the proper commits to groups, contacts and addressbook
await this.$store.dispatch('addContact', contact)
await this.$router.push({
name: 'contact',
params: {
selectedGroup: this.selectedGroup,
selectedContact: contact.key,
},
})
} catch (error) {
showError(t('contacts', 'Unable to create the contact.'))
console.error(error)
}
},
/**
* Dispatch sorting update request to the store
*
* @param {string} orderKey the object key to order by
*/
updateSorting(orderKey = 'displayName') {
this.$store.commit('setOrder', orderKey)
this.$store.commit('sortContacts')
},
/**
* Fetch the contacts of each addressbooks
*/
fetchContacts() {
// wait for all addressbooks to have fetch their contacts
Promise.all(this.addressbooks
.filter(addressbook => addressbook.enabled)
.map(addressbook => {
return this.$store.dispatch('getContactsFromAddressBook', { addressbook })
})
).then(results => {
this.loadingContacts = false
if (!this.isMobile) {
this.selectFirstContactIfNone()
}
})
},
/**
* Select the first contact of the list
* if none are selected already
*/
selectFirstContactIfNone() {
// Do not redirect if pending import
if (this.$route.name === 'import') {
return
}
const inList = this.contactsList.findIndex(contact => contact.key === this.selectedContact) > -1
if (this.selectedContact === undefined || !inList) {
// Unknown contact
if (this.selectedContact && !inList) {
showError(t('contacts', 'Contact not found'))
this.$router.push({
name: 'group',
params: {
selectedGroup: this.selectedGroup,
},
})
}
// Unknown group
if (!this.selectedCircle
&& !this.groups.find(group => group.name === this.selectedGroup)
&& GROUP_ALL_CONTACTS !== this.selectedGroup
&& GROUP_NO_GROUP_CONTACTS !== this.selectedGroup
&& ROUTE_CIRCLE !== this.selectedGroup) {
showError(t('contacts', 'Group {group} not found', { group: this.selectedGroup }))
console.error('Group not found', this.selectedGroup)
this.$router.push({
name: 'root',
})
return
}
if (Object.keys(this.contactsList).length) {
this.$router.push({
name: 'contact',
params: {
selectedGroup: this.selectedGroup,
selectedContact: Object.values(this.contactsList)[0].key,
},
})
}
}
},
/**
* Show the list and deselect contact
*/
showList() {
// Reset the selected contact
this.$router.push({
name: 'contact',
params: {
selectedGroup: this.selectedGroup,
selectedContact: undefined,
},
})
},
/**
* Done importing, the user closed the import status screen
*/
closeImport() {
this.$store.dispatch('changeStage', 'default')
},
},
}
</script>
<style lang="scss" scoped>
.app-details-toggle {
position: absolute;
width: 44px;
height: 44px;
padding: 14px;
cursor: pointer;
opacity: .6;
font-size: 16px;
line-height: 17px;
transform: rotate(180deg);
background-color: var(--color-main-background);
z-index: 2000;
&:active,
&:hover,
&:focus {
opacity: 1;
}
// Hide app-navigation toggle if shown
&::v-deep + .app-navigation .app-navigation-toggle {
display: none;
}
}
</style>