487 lines
12 KiB
Vue
487 lines
12 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" :class="{'icon-loading': loading}">
|
|
<!-- new-contact-button + navigation + settings -->
|
|
<AppNavigation>
|
|
<!-- new-contact-button -->
|
|
<AppNavigationNew v-if="!loading" button-id="new-contact-button" :text="t('contacts','New contact')"
|
|
button-class="icon-add" :disabled="!defaultAddressbook" @click="newContact" />
|
|
|
|
<!-- groups list -->
|
|
<ul v-if="!loading" id="groups-list">
|
|
<AppNavigationItem v-for="item in menu" :key="item.key" :item="item" />
|
|
</ul>
|
|
|
|
<!-- settings -->
|
|
<AppNavigationSettings v-if="!loading">
|
|
<SettingsSection />
|
|
</AppNavigationSettings>
|
|
</AppNavigation>
|
|
|
|
<AppContent>
|
|
<!-- go back to list when in details mode -->
|
|
<div v-if="selectedContact && isMobile" id="app-details-toggle" class="icon-confirm"
|
|
tabindex="0" @click="showList" />
|
|
|
|
<div id="app-content-wrapper">
|
|
<!-- contacts list -->
|
|
<ContactsList :list="contactsList" :contacts="contacts" :loading="loading"
|
|
:search-query="searchQuery" />
|
|
|
|
<!-- main contacts details -->
|
|
<ContactDetails :loading="loading" :contact-key="selectedContact" />
|
|
</div>
|
|
</AppContent>
|
|
<Modal v-if="isImporting" :clear-view-delay="-1" :can-close="isImportDone"
|
|
@close="closeImport">
|
|
<ImportScreen />
|
|
</Modal>
|
|
</Content>
|
|
</template>
|
|
|
|
<script>
|
|
import {
|
|
AppContent,
|
|
AppNavigation,
|
|
AppNavigationItem,
|
|
AppNavigationNew,
|
|
AppNavigationSettings,
|
|
Content,
|
|
Modal
|
|
} from 'nextcloud-vue'
|
|
import isMobile from 'nextcloud-vue/dist/Mixins/isMobile'
|
|
|
|
import moment from 'moment'
|
|
import download from 'downloadjs'
|
|
import { VCardTime } from 'ical.js'
|
|
|
|
import SettingsSection from 'Components/SettingsSection'
|
|
import ContactsList from 'Components/ContactsList'
|
|
import ContactDetails from 'Components/ContactDetails'
|
|
import ImportScreen from 'Components/ImportScreen'
|
|
|
|
import Contact from 'Models/contact'
|
|
import rfcProps from 'Models/rfcProps'
|
|
|
|
import client from 'Services/cdav'
|
|
|
|
const GROUP_ALL_CONTACTS = t('contacts', 'All contacts')
|
|
const GROUP_NO_GROUP_CONTACTS = t('contacts', 'Not grouped')
|
|
|
|
export default {
|
|
name: 'Contacts',
|
|
|
|
components: {
|
|
AppContent,
|
|
AppNavigation,
|
|
AppNavigationItem,
|
|
AppNavigationNew,
|
|
AppNavigationSettings,
|
|
ContactDetails,
|
|
ContactsList,
|
|
Content,
|
|
ImportScreen,
|
|
Modal,
|
|
SettingsSection
|
|
},
|
|
|
|
mixins: [
|
|
isMobile
|
|
],
|
|
|
|
// passed by the router
|
|
props: {
|
|
selectedGroup: {
|
|
type: String,
|
|
default: undefined,
|
|
required: true
|
|
},
|
|
selectedContact: {
|
|
type: String,
|
|
default: undefined
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
loading: true,
|
|
searchQuery: ''
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
// store getters
|
|
addressbooks() {
|
|
return this.$store.getters.getAddressbooks
|
|
},
|
|
sortedContacts() {
|
|
return this.$store.getters.getSortedContacts
|
|
},
|
|
contacts() {
|
|
return this.$store.getters.getContacts
|
|
},
|
|
groups() {
|
|
return this.$store.getters.getGroups
|
|
},
|
|
orderKey() {
|
|
return this.$store.getters.getOrderKey
|
|
},
|
|
importState() {
|
|
return this.$store.getters.getImportState
|
|
},
|
|
|
|
// importing states
|
|
isImporting() {
|
|
return this.importState.stage !== 'default'
|
|
},
|
|
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))
|
|
}
|
|
let 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)
|
|
},
|
|
|
|
// generate groups menu from groups store
|
|
groupsMenu() {
|
|
return this.groups.map(group => {
|
|
return {
|
|
id: group.name.replace(' ', '_'),
|
|
key: group.name.replace(' ', '_'),
|
|
router: {
|
|
name: 'group',
|
|
params: { selectedGroup: group.name }
|
|
},
|
|
text: group.name,
|
|
utils: {
|
|
counter: group.contacts.length,
|
|
actions: [
|
|
{
|
|
icon: 'icon-download',
|
|
text: 'Download',
|
|
action: () => this.downloadGroup(group)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}).sort(function(a, b) {
|
|
return parseInt(b.utils.counter) - parseInt(a.utils.counter)
|
|
})
|
|
},
|
|
|
|
// building the main menu
|
|
menu() {
|
|
return this.groupAllGroup.concat(this.groupNotGrouped.concat(this.groupsMenu))
|
|
},
|
|
|
|
// default group for every contacts
|
|
groupAllGroup() {
|
|
return [{
|
|
id: 'everyone',
|
|
key: 'everyone',
|
|
icon: 'icon-contacts-dark',
|
|
router: {
|
|
name: 'group',
|
|
params: { selectedGroup: GROUP_ALL_CONTACTS }
|
|
},
|
|
text: GROUP_ALL_CONTACTS,
|
|
utils: {
|
|
counter: this.sortedContacts.length
|
|
}
|
|
}]
|
|
},
|
|
|
|
// default group for every contacts
|
|
groupNotGrouped() {
|
|
if (this.ungroupedContacts.length === 0) {
|
|
return []
|
|
}
|
|
return [{
|
|
id: 'notgrouped',
|
|
key: 'notgrouped',
|
|
icon: 'icon-user',
|
|
router: {
|
|
name: 'group',
|
|
params: { selectedGroup: GROUP_NO_GROUP_CONTACTS }
|
|
},
|
|
text: GROUP_NO_GROUP_CONTACTS,
|
|
utils: {
|
|
counter: this.ungroupedContacts.length
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
// watch url change and group select
|
|
selectedGroup: function() {
|
|
if (!this.isMobile) {
|
|
this.selectFirstContactIfNone()
|
|
}
|
|
},
|
|
// watch url change and contact select
|
|
selectedContact: function() {
|
|
if (!this.isMobile) {
|
|
this.selectFirstContactIfNone()
|
|
}
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
/**
|
|
* Register search
|
|
*/
|
|
this.search = new OCA.Search(this.search, this.resetSearch)
|
|
},
|
|
|
|
beforeMount() {
|
|
// get addressbooks then get contacts
|
|
client.connect({ enableCardDAV: true }).then(() => {
|
|
console.debug('Connected to dav!', client)
|
|
this.$store.dispatch('getAddressbooks')
|
|
.then((addressbooks) => {
|
|
|
|
// No addressbooks? Create a new one!
|
|
if (addressbooks.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'))
|
|
}
|
|
})
|
|
},
|
|
|
|
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 (let name in properties) {
|
|
if (properties[name].default) {
|
|
let defaultData = properties[name].defaultValue
|
|
// add default field
|
|
let 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) {
|
|
OC.Notification.showTemporary(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.map(addressbook => {
|
|
if (addressbook.enabled) {
|
|
return this.$store.dispatch('getContactsFromAddressBook', { addressbook })
|
|
}
|
|
})).then(results => {
|
|
this.loading = false
|
|
if (!this.isMobile) {
|
|
this.selectFirstContactIfNone()
|
|
}
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Select the first contact of the list
|
|
* if none are selected already
|
|
*/
|
|
selectFirstContactIfNone() {
|
|
let inList = this.contactsList.findIndex(contact => contact.key === this.selectedContact) > -1
|
|
if (this.selectedContact === undefined || !inList) {
|
|
if (this.selectedContact && !inList) {
|
|
OC.Notification.showTemporary(t('contacts', 'Contact not found'))
|
|
}
|
|
if (Object.keys(this.contactsList).length) {
|
|
this.$router.push({
|
|
name: 'contact',
|
|
params: {
|
|
selectedGroup: this.selectedGroup,
|
|
selectedContact: Object.values(this.contactsList)[0].key
|
|
}
|
|
})
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Download vcard promise as vcard file
|
|
*
|
|
* @param {Promise} vcardPromise the full vcf file promise
|
|
*/
|
|
async downloadVcardPromise(vcardPromise) {
|
|
vcardPromise.then(response => {
|
|
const filename = moment().format('YYYY-MM-DD_HH-mm') + '_' + response.groupName + '.vcf'
|
|
download(response.data, filename, 'text/vcard')
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Download group of contacts
|
|
*
|
|
* @param {Object} group of contacts to be downloaded
|
|
*/
|
|
downloadGroup(group) {
|
|
// get grouped contacts
|
|
let groupedContacts = {}
|
|
group.contacts.map((key) => {
|
|
const id = this.contacts[key].addressbook.id
|
|
groupedContacts = Object.assign({
|
|
[id]: {
|
|
addressbook: this.contacts[key].addressbook,
|
|
contacts: []
|
|
}
|
|
}, groupedContacts)
|
|
groupedContacts[id].contacts.push(this.contacts[key].url)
|
|
})
|
|
// create vcard promise with the requested contacts
|
|
const vcardPromise = Promise.all(
|
|
Object.keys(groupedContacts).map(key =>
|
|
groupedContacts[key].addressbook.dav.addressbookMultigetExport(groupedContacts[key].contacts)))
|
|
.then(response => ({
|
|
groupName: group.name,
|
|
data: response.map(data => data.body).join('')
|
|
}))
|
|
// download vcard
|
|
this.downloadVcardPromise(vcardPromise)
|
|
},
|
|
|
|
/* SEARCH */
|
|
search(query) {
|
|
this.searchQuery = query
|
|
},
|
|
resetSearch() {
|
|
this.searchQuery = ''
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
// TODO: remove after https://github.com/nextcloud/nextcloud-vue/pull/323
|
|
if (this.isImportDone) {
|
|
this.$store.dispatch('changeStage', 'default')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|