diff options
author | matthias <matthias@butler> | 2020-03-30 23:32:37 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-07-25 09:40:05 +0200 |
commit | e84571f0037b1222549e5fe9b4ac65ee4a1285aa (patch) | |
tree | 691f701b6021e6b0db7fcf339afe0edb69e5a617 /src | |
parent | e2633171d6a0581c76fcbf9abb162b581b1d219e (diff) |
Allow for avatar downloads from social networks
Signed-off-by: call-me-matt <nextcloud@matthiasheinisch.de>
Diffstat (limited to 'src')
-rw-r--r-- | src/adminSettings.js | 34 | ||||
-rw-r--r-- | src/components/AdminSettings.vue | 35 | ||||
-rw-r--r-- | src/components/ContactDetails.vue | 4 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsAvatar.vue | 86 | ||||
-rw-r--r-- | src/store/contacts.js | 4 |
5 files changed, 152 insertions, 11 deletions
diff --git a/src/adminSettings.js b/src/adminSettings.js new file mode 100644 index 00000000..d9635b2a --- /dev/null +++ b/src/adminSettings.js @@ -0,0 +1,34 @@ +/** + * @copyright Copyright (c) 2020 Gary Kim <gary@garykim.dev> + * + * @author Gary Kim <gary@garykim.dev> + * + * @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/>. + * + */ + +import Vue from 'vue' +import AdminSettings from './components/AdminSettings' + +document.addEventListener('DOMContentLoaded', main) + +function main() { + Vue.prototype.t = t + + const View = Vue.extend(AdminSettings) + const view = new View() + view.$mount('#contacts-settings') +} diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue new file mode 100644 index 00000000..ac840085 --- /dev/null +++ b/src/components/AdminSettings.vue @@ -0,0 +1,35 @@ +<template> + <div id="contacts" class="section"> + <h2>{{ t('contacts', 'Contacts') }}</h2> + <p> + <input + id="allow-social-sync" + v-model="allowSocialSync" + type="checkbox" + class="checkbox" + @change="updateSetting('allowSocialSync')"> + <label for="allow-social-sync">{{ t('contacts', 'Allow updating avatars from social media') }}</label> + </p> + </div> +</template> + +<script> +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' +export default { + name: 'AdminSettings', + data() { + return { + 'allowSocialSync': loadState('contacts', 'allowSocialSync') === 'yes', + } + }, + methods: { + updateSetting(setting) { + axios.post(generateUrl('apps/contacts/api/v1/social/config/' + setting), { + allow: this[setting] ? 'yes' : 'no', + }) + }, + }, +} +</script> diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 9553bb1c..6d4c6fd9 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -39,7 +39,9 @@ <!-- contact header --> <header> <!-- avatar and upload photo --> - <ContactAvatar :contact="contact" /> + <ContactAvatar + :contact="contact" + @updateLocalContact="updateLocalContact" /> <!-- QUESTION: is it better to pass contact as a prop or get it from the store inside contact-avatar ? :avatar="contact.photo"--> diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue index 7b211c8c..3fc6f1ec 100644 --- a/src/components/ContactDetails/ContactDetailsAvatar.vue +++ b/src/components/ContactDetails/ContactDetailsAvatar.vue @@ -3,6 +3,7 @@ - - @author Team Popcorn <teampopcornberlin@gmail.com> - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Matthias Heinisch <nextcloud@matthiasheinisch.de> - - @license GNU AGPL version 3 or any later version - @@ -25,6 +26,7 @@ <div class="contact-header-avatar"> <div class="contact-header-avatar__wrapper"> <div class="contact-header-avatar__background" @click="toggleModal" /> + <div v-if="contact.photo" :style="{ 'backgroundImage': `url(${contact.photoUrl})` }" class="contact-header-avatar__photo" @@ -73,13 +75,20 @@ </Modal> <!-- out of the avatar__options because of the overflow hidden --> - <Actions :open="opened" class="contact-avatar-options__popovermenu"> - <ActionButton v-if="!isReadOnly" icon="icon-upload" @click="selectFileInput"> + <Actions v-if="!isReadOnly" :open="opened" class="contact-avatar-options__popovermenu"> + <ActionButton icon="icon-upload" @click="selectFileInput"> {{ t('contacts', 'Upload a new picture') }} </ActionButton> - <ActionButton v-if="!isReadOnly" icon="icon-picture" @click="selectFilePicker"> + <ActionButton icon="icon-picture" @click="selectFilePicker"> {{ t('contacts', 'Choose from files') }} </ActionButton> + <ActionButton + v-for="network in supportedSocial" + :key="network" + :icon="'icon-' + network.toLowerCase()" + @click="getSocialAvatar(network)"> + {{ t('contacts', 'Get from ' + network) }} + </ActionButton> </Actions> </div> </div> @@ -93,11 +102,14 @@ import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import Modal from '@nextcloud/vue/dist/Components/Modal' import { getFilePickerBuilder } from '@nextcloud/dialogs' -import { generateRemoteUrl } from '@nextcloud/router' +import { generateUrl, generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' +import { loadState } from '@nextcloud/initial-state' import sanitizeSVG from '@mattkrick/sanitize-svg' -const axios = () => import('axios') +import axios from '@nextcloud/axios' + +const supportedNetworks = loadState('contacts', 'supportedNetworks') export default { name: 'ContactDetailsAvatar', @@ -133,6 +145,16 @@ export default { } return false }, + supportedSocial() { + // get social networks set for the current contact + const available = this.contact.vCard.getAllProperties('x-socialprofile') + .map(a => a.jCal[1].type.toString().toLowerCase()) + // get list of social networks that allow for avatar download + const supported = supportedNetworks.map(v => v.toLowerCase()) + // return supported social networks which are set + return supported.filter(i => available.includes(i)) + .map(j => this.capitalize(j)) + }, }, mounted() { // update image size on window resize @@ -215,7 +237,15 @@ export default { this.$refs.uploadInput.value = '' this.loading = false }, - + /** + * Return the word with (only) the first letter capitalized + * + * @param {string} word the word to handle + * @returns {string} the word with the first letter capitalized + */ + capitalize(word) { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + }, /** * Return the mimetype based on the first magix byte * @@ -314,8 +344,7 @@ export default { if (file) { this.loading = true try { - const { get } = await axios() - const response = await get(`${this.root}${file}`, { + const response = await axios.get(`${this.root}${file}`, { responseType: 'arraybuffer', }) const type = response.headers['content-type'] @@ -331,6 +360,47 @@ export default { }, /** + * Downloads the Avatar from social media + * + * @param {String} network the social network to use (or 'any' for first match) + */ + async getSocialAvatar(network) { + + if (!this.loading) { + + this.loading = true + try { + const response = await axios.put(generateUrl('/apps/contacts/api/v1/social/avatar/{network}/{id}/{uid}', { + network: network.toLowerCase(), + id: this.contact.addressbook.id, + uid: this.contact.uid, + })) + if (response.status !== 200) { + throw new URIError('Download of social profile avatar failed') + } + + // Fetch newly updated contact + await this.$store.dispatch('fetchFullContact', { contact: this.contact, forceReFetch: true }) + + // Update local clone + const contact = this.$store.getters.getContact(this.contact.key) + await this.$emit('updateLocalContact', contact) + + // Notify user + OC.Notification.showTemporary(t('contacts', 'Avatar downloaded from social network')) + } catch (error) { + if (error.response.status === 304) { + OC.Notification.showTemporary(t('contacts', 'Avatar already up to date')) + } else { + OC.Notification.showTemporary(t('contacts', 'Avatar download failed')) + console.debug(error) + } + } + } + this.loading = false + }, + + /** * Menu handling */ toggleMenu() { diff --git a/src/store/contacts.js b/src/store/contacts.js index f2602f36..fed65587 100644 --- a/src/store/contacts.js +++ b/src/store/contacts.js @@ -372,11 +372,11 @@ const actions = { * @param {string} data.etag the contact etag to override in case of conflict * @returns {Promise} */ - async fetchFullContact(context, { contact, etag = '' }) { + async fetchFullContact(context, { contact, etag = '', forceReFetch = false }) { if (etag.trim() !== '') { await context.commit('updateContactEtag', { contact, etag }) } - return contact.dav.fetchCompleteData() + return contact.dav.fetchCompleteData(forceReFetch) .then((response) => { const newContact = new Contact(contact.dav.data, contact.addressbook) context.commit('updateContact', newContact) |