From 7733438a03f6b55f9e6c37c6914bf03d4efbaa93 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Fri, 17 Jan 2025 17:56:01 +0700 Subject: [PATCH 1/4] feat: default contact Signed-off-by: Hamza Mahjoubi --- appinfo/routes.php | 2 + lib/AppInfo/Application.php | 1 + lib/Controller/DefaultContactController.php | 62 +++++++++++++ lib/Controller/PageController.php | 3 + src/components/AdminSettings.vue | 96 ++++++++++++++++++++- src/services/defaultContactService.js | 14 +++ 6 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 lib/Controller/DefaultContactController.php create mode 100644 src/services/defaultContactService.js diff --git a/appinfo/routes.php b/appinfo/routes.php index 3ead33df2..cf2106550 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -15,5 +15,7 @@ ['name' => 'social_api#set_app_config', 'url' => '/api/v1/social/config/global/{key}', 'verb' => 'PUT'], ['name' => 'social_api#set_user_config', 'url' => '/api/v1/social/config/user/{key}', 'verb' => 'PUT'], ['name' => 'social_api#get_user_config', 'url' => '/api/v1/social/config/user/{key}', 'verb' => 'GET'], + ['name' => 'default_contact#set_app_config', 'url' => '/api/defaultcontact/config', 'verb' => 'PUT'], + ['name' => 'default_contact#set_default_contact', 'url' => '/api/defaultcontact/contact', 'verb' => 'PUT'], ] ]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index aebba7b70..4934626a5 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -22,6 +22,7 @@ class Application extends App implements IBootstrap { public const AVAIL_SETTINGS = [ 'allowSocialSync' => 'yes', + 'enableDefaultContact' => 'yes', ]; public function __construct() { diff --git a/lib/Controller/DefaultContactController.php b/lib/Controller/DefaultContactController.php new file mode 100644 index 000000000..da6ab5229 --- /dev/null +++ b/lib/Controller/DefaultContactController.php @@ -0,0 +1,62 @@ +config->setAppValue(Application::APP_ID, $key, $allow); + return new JSONResponse([], Http::STATUS_OK); + } + + public function setDefaultContact($contactData){ + if(!$this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'yes')){ + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + try{ + $folder = $this->appData->getFolder('defaultContact'); + } + catch(NotFoundException $e){ + $folder = $this->appData->newFolder('defaultContact'); + } + if(!$folder->fileExists('defaultContact.vcf')){ + $file = $folder->newFile('defaultContact.vcf'); + } + else { + $file = $folder->getFile('defaultContact.vcf'); + } + $file->putContent($contactData); + return new JSONResponse([], Http::STATUS_OK); + } + +} \ No newline at end of file diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index efbf03704..751a200f5 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -52,6 +52,8 @@ public function index(): TemplateResponse { $supportedNetworks = $this->socialApiService->getSupportedNetworks(); // allow users to retrieve avatars from social networks (default: yes) $syncAllowedByAdmin = $this->config->getAppValue(Application::APP_ID, 'allowSocialSync', 'yes'); + // add a default contact to the user's address book on first login (default: yes) + $enableDefaultContact = $this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'yes'); // automated background syncs for social avatars (default: no) $bgSyncEnabledByUser = $this->config->getUserValue($userId, Application::APP_ID, 'enableSocialSync', 'no'); @@ -72,6 +74,7 @@ public function index(): TemplateResponse { $this->initialStateService->provideInitialState(Application::APP_ID, 'defaultProfile', $defaultProfile); $this->initialStateService->provideInitialState(Application::APP_ID, 'supportedNetworks', $supportedNetworks); $this->initialStateService->provideInitialState(Application::APP_ID, 'allowSocialSync', $syncAllowedByAdmin); + $this->initialStateService->provideInitialState(Application::APP_ID, 'enableDefaultContact', $enableDefaultContact); $this->initialStateService->provideInitialState(Application::APP_ID, 'enableSocialSync', $bgSyncEnabledByUser); $this->initialStateService->provideInitialState(Application::APP_ID, 'isContactsInteractionEnabled', $isContactsInteractionEnabled); $this->initialStateService->provideInitialState(Application::APP_ID, 'isCirclesEnabled', $isCirclesEnabled && $isCircleVersionCompatible); diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 6975a8a53..a6eb83fda 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -11,9 +11,46 @@ v-model="allowSocialSync" type="checkbox" class="checkbox" - @change="updateSetting('allowSocialSync')"> + @change="updateAllowSocialSync">

+

+ + + + + {{ t('contacts', 'Import contact') }} + + +

+ + + + {{ t('contacts', 'Select local file') }} + +
+ +

@@ -21,19 +58,70 @@ import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' +import { NcDialog, NcButton } from '@nextcloud/vue' +import Contact from '../models/contact.js' +import validate from '../services/validate.js' +import { showError, showSuccess } from '@nextcloud/dialogs' +import IconUpload from 'vue-material-design-icons/Upload.vue' + export default { name: 'AdminSettings', + components: { + NcDialog, + NcButton, + IconUpload, + }, data() { return { allowSocialSync: loadState('contacts', 'allowSocialSync') === 'yes', + enableDefaultContact: loadState('contacts', 'enableDefaultContact') === 'yes', + isModalOpen: false, + loading: false, } }, methods: { - updateSetting(setting) { - axios.put(generateUrl('apps/contacts/api/v1/social/config/global/' + setting), { - allow: this[setting] ? 'yes' : 'no', + updateAllowSocialSync() { + axios.put(generateUrl('apps/contacts/api/v1/social/config/global/allowSocialSync'), { + allow: this.allowSocialSync ? 'yes' : 'no', + }) + }, + updateEnableDefaultContact() { + axios.put(generateUrl('apps/contacts/api/defaultcontact/config'), { + allow: this.enableDefaultContact ? 'yes' : 'no', }) }, + toggleModal() { + this.isModalOpen = !this.isModalOpen + }, + clickImportInput() { + this.$refs['contact-import-input'].click() + }, + + /** + * Process input type file change + * + * @param {Event} event the input change event + */ + processFile(event) { + this.loading = true + + const file = event.target.files[0] + const reader = new FileReader() + + reader.onload = () => { + this.isModalOpen = false + const contact = new Contact(reader.result) + /* if (!validate(contact)) { + showError(t('contacts', 'Invalid VCF file')) + event.target.value = '' + return + } */ + axios.put(generateUrl('/apps/contacts/api/defaultcontact/contact'), { contactData: reader.result }) + event.target.value = '' + } + reader.readAsText(file) + showSuccess(this.t('contacts', 'Contact imported successfully')) + }, }, } diff --git a/src/services/defaultContactService.js b/src/services/defaultContactService.js new file mode 100644 index 000000000..938386eb6 --- /dev/null +++ b/src/services/defaultContactService.js @@ -0,0 +1,14 @@ +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +export const setDefaultContact = async function(contactId) { + const request = await axios.post(generateOcsUrl('apps/contacts/api/v1/default'), { + contactId, + }) + return request.data +} From fa9ad1d349d0aca88a31f3f33141b07fa7f0dc94 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Mon, 20 Jan 2025 22:38:42 +0700 Subject: [PATCH 2/4] fixup! feat: default contact Signed-off-by: Hamza Mahjoubi --- lib/Controller/DefaultContactController.php | 43 ++++++++++++++++--- src/components/AdminSettings.vue | 46 ++++++++++++--------- src/models/contact.js | 2 +- 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/lib/Controller/DefaultContactController.php b/lib/Controller/DefaultContactController.php index da6ab5229..b69817329 100644 --- a/lib/Controller/DefaultContactController.php +++ b/lib/Controller/DefaultContactController.php @@ -10,6 +10,7 @@ use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; +use OCP\App\IAppManager; use OCP\Files\NotFoundException; use OCP\Files\IAppData; use OCP\IConfig; @@ -21,6 +22,7 @@ public function __construct( IRequest $request, private IConfig $config, private IAppData $appData, + private IAppManager $appManager ) { parent::__construct(Application::APP_ID, $request); } @@ -35,6 +37,9 @@ public function __construct( */ public function setAppConfig($allow) { $key ='enableDefaultContact'; + if($allow ==='yes' && !$this->defaultContactExists()){ + $this->setInitialDefaultContact(); + } $this->config->setAppValue(Application::APP_ID, $key, $allow); return new JSONResponse([], Http::STATUS_OK); } @@ -49,14 +54,40 @@ public function setDefaultContact($contactData){ catch(NotFoundException $e){ $folder = $this->appData->newFolder('defaultContact'); } - if(!$folder->fileExists('defaultContact.vcf')){ - $file = $folder->newFile('defaultContact.vcf'); - } - else { - $file = $folder->getFile('defaultContact.vcf'); - } + $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf'); $file->putContent($contactData); return new JSONResponse([], Http::STATUS_OK); } + private function setInitialDefaultContact(){ + $cardData = 'BEGIN:VCARD' . PHP_EOL . + 'VERSION:3.0' . PHP_EOL . + 'PRODID:-//Nextcloud Contacts v' . $this->appManager->getAppVersion('contacts') . PHP_EOL . + 'UID: janeDoe' . PHP_EOL . + 'FN:Jane Doe' . PHP_EOL . + 'ADR;TYPE=HOME:;;123 Street Street;City;State;;Country' . PHP_EOL . + 'EMAIL;TYPE=WORK:example@example.com' . PHP_EOL . + 'TEL;TYPE=HOME,VOICE:+999999999999' . PHP_EOL . + 'TITLE:Manager' . PHP_EOL . + 'ORG:Company' . PHP_EOL . + 'BDAY;VALUE=DATE:20000101' . PHP_EOL . + 'URL;VALUE=URI:https://example.com/' . PHP_EOL . + 'REV;VALUE=DATE-AND-OR-TIME:20241227T144820Z' . PHP_EOL . + 'END:VCARD'; + $folder = $this->appData->getFolder('defaultContact'); + $file = $folder->newFile('defaultContact.vcf'); + $file->putContent($cardData); + } + + private function defaultContactExists(): bool { + try{ + $folder = $this->appData->getFolder('defaultContact'); + } + catch(NotFoundException $e){ + $this->appData->newFolder('defaultContact'); + return false; + } + return $folder->fileExists('defaultContact.vcf'); + } + } \ No newline at end of file diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index a6eb83fda..76a3c2342 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -22,6 +22,7 @@ @change="updateEnableDefaultContact"> {{ t('contacts', 'Import contact') }} - -
+ :buttons="buttons"> +
+

{{ t('contacts', 'Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?') }}

- - - {{ t('contacts', 'Select local file') }} - -
+

@@ -63,6 +56,8 @@ import Contact from '../models/contact.js' import validate from '../services/validate.js' import { showError, showSuccess } from '@nextcloud/dialogs' import IconUpload from 'vue-material-design-icons/Upload.vue' +import IconCancel from '@mdi/svg/svg/cancel.svg' +import IconCheck from '@mdi/svg/svg/check.svg' export default { name: 'AdminSettings', @@ -77,6 +72,19 @@ export default { enableDefaultContact: loadState('contacts', 'enableDefaultContact') === 'yes', isModalOpen: false, loading: false, + buttons: [ + { + label: t('contacts', 'Cancel'), + icon: IconCancel, + callback: () => { this.isModalOpen = false }, + }, + { + label: t('contacts', 'Import'), + type: 'primary', + icon: IconCheck, + callback: () => { this.clickImportInput() }, + }, + ], } }, methods: { @@ -97,12 +105,7 @@ export default { this.$refs['contact-import-input'].click() }, - /** - * Process input type file change - * - * @param {Event} event the input change event - */ - processFile(event) { + processFile(event) { this.loading = true const file = event.target.files[0] @@ -125,3 +128,8 @@ export default { }, } + diff --git a/src/models/contact.js b/src/models/contact.js index 0a84440f5..cfb275eff 100644 --- a/src/models/contact.js +++ b/src/models/contact.js @@ -214,7 +214,7 @@ export default class Contact { * * @readonly * @memberof Contact - */ + */ get hasPhoto() { return this.dav && this.dav.hasphoto } From 27d815c77671798f6a8d5303f8d5706065dfb763 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Mon, 20 Jan 2025 22:41:09 +0700 Subject: [PATCH 3/4] fixup! feat: default contact Signed-off-by: Hamza Mahjoubi --- lib/Controller/DefaultContactController.php | 102 ++++++++++---------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/lib/Controller/DefaultContactController.php b/lib/Controller/DefaultContactController.php index b69817329..9e5396d61 100644 --- a/lib/Controller/DefaultContactController.php +++ b/lib/Controller/DefaultContactController.php @@ -7,12 +7,12 @@ namespace OCA\Contacts\Controller; use OCA\Contacts\AppInfo\Application; +use OCP\App\IAppManager; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; -use OCP\App\IAppManager; -use OCP\Files\NotFoundException; use OCP\Files\IAppData; +use OCP\Files\NotFoundException; use OCP\IConfig; use OCP\IRequest; @@ -21,8 +21,8 @@ class DefaultContactController extends ApiController { public function __construct( IRequest $request, private IConfig $config, - private IAppData $appData, - private IAppManager $appManager + private IAppData $appData, + private IAppManager $appManager, ) { parent::__construct(Application::APP_ID, $request); } @@ -36,58 +36,56 @@ public function __construct( * @return JSONResponse an empty JSONResponse with respective http status code */ public function setAppConfig($allow) { - $key ='enableDefaultContact'; - if($allow ==='yes' && !$this->defaultContactExists()){ - $this->setInitialDefaultContact(); - } + $key = 'enableDefaultContact'; + if ($allow === 'yes' && !$this->defaultContactExists()) { + $this->setInitialDefaultContact(); + } $this->config->setAppValue(Application::APP_ID, $key, $allow); return new JSONResponse([], Http::STATUS_OK); } - public function setDefaultContact($contactData){ - if(!$this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'yes')){ - return new JSONResponse([], Http::STATUS_FORBIDDEN); - } - try{ - $folder = $this->appData->getFolder('defaultContact'); - } - catch(NotFoundException $e){ - $folder = $this->appData->newFolder('defaultContact'); - } - $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf'); - $file->putContent($contactData); - return new JSONResponse([], Http::STATUS_OK); - } + public function setDefaultContact($contactData) { + if (!$this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'yes')) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder('defaultContact'); + } + $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf'); + $file->putContent($contactData); + return new JSONResponse([], Http::STATUS_OK); + } - private function setInitialDefaultContact(){ - $cardData = 'BEGIN:VCARD' . PHP_EOL . - 'VERSION:3.0' . PHP_EOL . - 'PRODID:-//Nextcloud Contacts v' . $this->appManager->getAppVersion('contacts') . PHP_EOL . - 'UID: janeDoe' . PHP_EOL . - 'FN:Jane Doe' . PHP_EOL . - 'ADR;TYPE=HOME:;;123 Street Street;City;State;;Country' . PHP_EOL . - 'EMAIL;TYPE=WORK:example@example.com' . PHP_EOL . - 'TEL;TYPE=HOME,VOICE:+999999999999' . PHP_EOL . - 'TITLE:Manager' . PHP_EOL . - 'ORG:Company' . PHP_EOL . - 'BDAY;VALUE=DATE:20000101' . PHP_EOL . - 'URL;VALUE=URI:https://example.com/' . PHP_EOL . - 'REV;VALUE=DATE-AND-OR-TIME:20241227T144820Z' . PHP_EOL . - 'END:VCARD'; - $folder = $this->appData->getFolder('defaultContact'); - $file = $folder->newFile('defaultContact.vcf'); - $file->putContent($cardData); - } + private function setInitialDefaultContact() { + $cardData = 'BEGIN:VCARD' . PHP_EOL . + 'VERSION:3.0' . PHP_EOL . + 'PRODID:-//Nextcloud Contacts v' . $this->appManager->getAppVersion('contacts') . PHP_EOL . + 'UID: janeDoe' . PHP_EOL . + 'FN:Jane Doe' . PHP_EOL . + 'ADR;TYPE=HOME:;;123 Street Street;City;State;;Country' . PHP_EOL . + 'EMAIL;TYPE=WORK:example@example.com' . PHP_EOL . + 'TEL;TYPE=HOME,VOICE:+999999999999' . PHP_EOL . + 'TITLE:Manager' . PHP_EOL . + 'ORG:Company' . PHP_EOL . + 'BDAY;VALUE=DATE:20000101' . PHP_EOL . + 'URL;VALUE=URI:https://example.com/' . PHP_EOL . + 'REV;VALUE=DATE-AND-OR-TIME:20241227T144820Z' . PHP_EOL . + 'END:VCARD'; + $folder = $this->appData->getFolder('defaultContact'); + $file = $folder->newFile('defaultContact.vcf'); + $file->putContent($cardData); + } - private function defaultContactExists(): bool { - try{ - $folder = $this->appData->getFolder('defaultContact'); - } - catch(NotFoundException $e){ - $this->appData->newFolder('defaultContact'); - return false; - } - return $folder->fileExists('defaultContact.vcf'); - } + private function defaultContactExists(): bool { + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + $this->appData->newFolder('defaultContact'); + return false; + } + return $folder->fileExists('defaultContact.vcf'); + } -} \ No newline at end of file +} From 206d2470a73794565ee8b5aa485cce63a7927b46 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Tue, 21 Jan 2025 21:41:22 +0700 Subject: [PATCH 4/4] fixup! feat: default contact Signed-off-by: Hamza Mahjoubi --- src/components/AdminSettings.vue | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 76a3c2342..6118b8cfb 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -52,9 +52,7 @@ import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' import { NcDialog, NcButton } from '@nextcloud/vue' -import Contact from '../models/contact.js' -import validate from '../services/validate.js' -import { showError, showSuccess } from '@nextcloud/dialogs' +import { showSuccess } from '@nextcloud/dialogs' import IconUpload from 'vue-material-design-icons/Upload.vue' import IconCancel from '@mdi/svg/svg/cancel.svg' import IconCheck from '@mdi/svg/svg/check.svg' @@ -113,12 +111,6 @@ export default { reader.onload = () => { this.isModalOpen = false - const contact = new Contact(reader.result) - /* if (!validate(contact)) { - showError(t('contacts', 'Invalid VCF file')) - event.target.value = '' - return - } */ axios.put(generateUrl('/apps/contacts/api/defaultcontact/contact'), { contactData: reader.result }) event.target.value = '' }