summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authormatthias <matthias@butler>2020-03-30 23:32:37 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-07-25 09:40:05 +0200
commite84571f0037b1222549e5fe9b4ac65ee4a1285aa (patch)
tree691f701b6021e6b0db7fcf339afe0edb69e5a617 /src
parente2633171d6a0581c76fcbf9abb162b581b1d219e (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.js34
-rw-r--r--src/components/AdminSettings.vue35
-rw-r--r--src/components/ContactDetails.vue4
-rw-r--r--src/components/ContactDetails/ContactDetailsAvatar.vue86
-rw-r--r--src/store/contacts.js4
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)