diff --git a/Makefile b/Makefile index c9883d569..9184cbcb6 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,10 @@ # SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc. # SPDX-License-Identifier: AGPL-3.0-or-later -app_name=$(notdir $(CURDIR)) -project_directory=$(CURDIR)/../$(app_name) +app_name="contacts" +project_folder="nextcloud-contacts" + +project_directory=$(CURDIR)/../$(project_folder) appstore_build_directory=$(CURDIR)/build/artifacts appstore_package_name=$(appstore_build_directory)/$(app_name) @@ -20,3 +22,55 @@ clean-dev: # Builds the source package for the app store, ignores php and js tests appstore: krankerl package + +# Builds the source package for the app store, ignores php and js tests +# command: make version={version_number} buildapp +# concatenate cd, ls and tar commands with '&&' otherwise the script context will remain the root instead of build +.PHONY: buildapp +buildapp: + make check-version + + version=$(version) + + make clean-buildapp + + mkdir -p $(appstore_build_directory) + cd build && \ + ln -s ../ $(app_name) && \ + tar cvzfh $(appstore_build_directory)/$(app_name)_$(version).tar.gz \ + --exclude="$(app_name)/build" \ + --exclude="$(app_name)/release" \ + --exclude="$(app_name)/tests" \ + --exclude="$(app_name)/src" \ + --exclude="$(app_name)/tests" \ + --exclude="$(app_name)/vite.config.js" \ + --exclude="$(app_name)/*.log" \ + --exclude="$(app_name)/phpunit*xml" \ + --exclude="$(app_name)/composer.*" \ + --exclude="$(app_name)/node_modules" \ + --exclude="$(app_name)/js/node_modules" \ + --exclude="$(app_name)/js/tests" \ + --exclude="$(app_name)/js/test" \ + --exclude="$(app_name)/js/*.log" \ + --exclude="$(app_name)/js/package.json" \ + --exclude="$(app_name)/js/bower.json" \ + --exclude="$(app_name)/js/karma.*" \ + --exclude="$(app_name)/js/protractor.*" \ + --exclude="$(app_name)/package.json" \ + --exclude="$(app_name)/bower.json" \ + --exclude="$(app_name)/karma.*" \ + --exclude="$(app_name)/protractor\.*" \ + --exclude="$(app_name)/.*" \ + --exclude="$(app_name)/js/.*" \ + --exclude-vcs \ + $(app_name) && \ + rm $(app_name) + +clean-buildapp: + rm -rf ${appstore_build_directory} + +check-version: + @if [ "${version}" = "" ]; then\ + echo "Error: You must set version, eg. make -e version=v0.0.1 buildapp";\ + exit 1;\ + fi \ No newline at end of file diff --git a/appinfo/info.xml b/appinfo/info.xml index 2073c751a..8d4c22d3f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -49,6 +49,7 @@ OCA\Contacts\Cron\SocialUpdateRegistration + OCA\Contacts\Cron\UpdateOcmProviders @@ -67,4 +68,10 @@ OCA\Contacts\ContactsMenu\Providers\DetailsProvider + + + OCA\Contacts\Command\EnableOcmInvites + OCA\Contacts\Command\DisableOcmInvites + OCA\Contacts\Command\SetMeshProvidersService + diff --git a/appinfo/routes.php b/appinfo/routes.php index 635003352..4c2d0c7be 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -5,10 +5,23 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +use OCA\Contacts\WayfProvider; +use OCA\Contacts\Service\FederatedInvitesService; + return [ 'routes' => [ ['name' => 'contacts#direct', 'url' => '/direct/contact/{contact}', 'verb' => 'GET'], ['name' => 'contacts#directcircle', 'url' => '/direct/circle/{singleId}', 'verb' => 'GET'], + + ['name' => 'federated_invites#get_invites', 'url' => '/ocm/invitations', 'verb' => 'GET'], + ['name' => 'federated_invites#delete_invite', 'url' => '/ocm/invitations/{token}', 'verb' => 'DELETE'], + ['name' => 'federated_invites#create_invite', 'url' => '/ocm/invitations', 'verb' => 'POST'], + ['name' => 'federated_invites#accept_invite', 'url' => '/ocm/invitations/{token}/accept', 'verb' => 'PATCH'], + ['name' => 'federated_invites#resend_invite', 'url' => '/ocm/invitations/{token}/resend', 'verb' => 'PATCH'], + ['name' => 'federated_invites#invite_accept_dialog', 'url' => FederatedInvitesService::OCM_INVITE_ACCEPT_DIALOG_ROUTE, 'verb' => 'GET'], + ['name' => 'federated_invites#wayf', 'url' => WayfProvider::WAYF_ROUTE, 'verb' => 'GET'], + ['name' => 'federated_invites#discover', 'url' => WayfProvider::DISCOVERY_ROUTE, 'verb' => 'GET'], + ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], ['name' => 'page#index', 'url' => '/{group}', 'verb' => 'GET', 'postfix' => 'group'], ['name' => 'page#index', 'url' => '/{group}/{contact}', 'verb' => 'GET', 'postfix' => 'group.contact'], diff --git a/l10n/bg.js b/l10n/bg.js index fcb928526..f5dc4c687 100644 --- a/l10n/bg.js +++ b/l10n/bg.js @@ -243,4 +243,4 @@ OC.L10N.register( "Add to {circle}" : "Добавяне към {circle}", "Select chart …" : "Избор на диаграма ..." }, -"nplurals=2; plural=(n != 1);"); +"nplurals=2; plural=(n != 1);"); \ No newline at end of file diff --git a/l10n/br.js b/l10n/br.js index 117bd784a..e6f2e6b8a 100644 --- a/l10n/br.js +++ b/l10n/br.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Munudoù", "Contacts" : "Darempredoù", "Add contacts" : "Ouzhpennañ darempredoù", + "General settings" : "Stummoù hollek", "Rename" : "Adenvel", "Send email" : "Kas postel", "Delete" : "Dilemel", @@ -12,13 +13,10 @@ OC.L10N.register( "Share with users or groups" : "Rannañ gant implijourienn pe strolladoù", "No users or groups" : "Implijourienn pe strodadoù ebet", "can edit" : "posuple eo embann", - "Cancel" : "Nullañ", "Add" : "Ouzhpennañ", "Display name" : "Anv ardivink", "Save" : "Enrollañ", - "Close" : "Seriñ", - "Calendar" : "Deiziataer", - "Create" : "Krouiñ", + "Cancel" : "Nullañ", "Invalid image" : "N'eo ket aotreet ar skeudenn", "Name" : "Anv", "Title" : "Titl", @@ -28,6 +26,7 @@ OC.L10N.register( "No results" : "Disoc'h ebet", "Pending" : "O c'hortoz", "None" : "Hini ebet", + "Close" : "Seriñ", "Import" : "Emporzhiañ ", "User" : "Implijer", "Group" : "Stollad", @@ -50,7 +49,6 @@ OC.L10N.register( "Sister" : "C'hoar", "Unknown" : "Dianv", "Loading …" : "O Kargañ ...", - "General settings" : "Stummoù hollek", "Search contacts …" : "Klask darempred ..." }, "nplurals=5; plural=((n%10 == 1) && (n%100 != 11) && (n%100 !=71) && (n%100 !=91) ? 0 :(n%10 == 2) && (n%100 != 12) && (n%100 !=72) && (n%100 !=92) ? 1 :(n%10 ==3 || n%10==4 || n%10==9) && (n%100 < 10 || n% 100 > 19) && (n%100 < 70 || n%100 > 79) && (n%100 < 90 || n%100 > 99) ? 2 :(n != 0 && n % 1000000 == 0) ? 3 : 4);"); diff --git a/l10n/br.json b/l10n/br.json index 37308b12e..69d75c155 100644 --- a/l10n/br.json +++ b/l10n/br.json @@ -2,6 +2,7 @@ "Details" : "Munudoù", "Contacts" : "Darempredoù", "Add contacts" : "Ouzhpennañ darempredoù", + "General settings" : "Stummoù hollek", "Rename" : "Adenvel", "Send email" : "Kas postel", "Delete" : "Dilemel", @@ -10,13 +11,10 @@ "Share with users or groups" : "Rannañ gant implijourienn pe strolladoù", "No users or groups" : "Implijourienn pe strodadoù ebet", "can edit" : "posuple eo embann", - "Cancel" : "Nullañ", "Add" : "Ouzhpennañ", "Display name" : "Anv ardivink", "Save" : "Enrollañ", - "Close" : "Seriñ", - "Calendar" : "Deiziataer", - "Create" : "Krouiñ", + "Cancel" : "Nullañ", "Invalid image" : "N'eo ket aotreet ar skeudenn", "Name" : "Anv", "Title" : "Titl", @@ -26,6 +24,7 @@ "No results" : "Disoc'h ebet", "Pending" : "O c'hortoz", "None" : "Hini ebet", + "Close" : "Seriñ", "Import" : "Emporzhiañ ", "User" : "Implijer", "Group" : "Stollad", @@ -48,7 +47,6 @@ "Sister" : "C'hoar", "Unknown" : "Dianv", "Loading …" : "O Kargañ ...", - "General settings" : "Stummoù hollek", "Search contacts …" : "Klask darempred ..." },"pluralForm" :"nplurals=5; plural=((n%10 == 1) && (n%100 != 11) && (n%100 !=71) && (n%100 !=91) ? 0 :(n%10 == 2) && (n%100 != 12) && (n%100 !=72) && (n%100 !=92) ? 1 :(n%10 ==3 || n%10==4 || n%10==9) && (n%100 < 10 || n% 100 > 19) && (n%100 < 70 || n%100 > 79) && (n%100 < 90 || n%100 > 99) ? 2 :(n != 0 && n % 1000000 == 0) ? 3 : 4);" } \ No newline at end of file diff --git a/l10n/bs.js b/l10n/bs.js index d8072ea25..a7c65d18a 100644 --- a/l10n/bs.js +++ b/l10n/bs.js @@ -22,7 +22,9 @@ OC.L10N.register( "Leave team" : "Leave team", "Delete team" : "Delete team", "Contacts settings" : "Contacts settings", + "General settings" : "General settings", "Update avatars from social media" : "Update avatars from social media", + "(refreshed once per week)" : "(refreshed once per week)", "Address books" : "Address books", "Rename" : "Preimenuj", "Export" : "Export", @@ -74,7 +76,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "Importing is disabled because there are no address books available", "An error occurred, unable to create the address book" : "An error occurred, unable to create the address book", "Add new address book" : "Add new address book", - "Cancel" : "Otkaži", "Add" : "Dodaj", "First name" : "Ime", "Last name" : "Prezime", @@ -82,6 +83,7 @@ OC.L10N.register( "Phonetic last name" : "Phonetic last name", "Display name" : "Display name", "Last modified" : "Last modified", + "Sort by {sorting}" : "Sort by {sorting}", "Manages" : "Manages", "Oversees" : "Oversees", "An error happened during the config change" : "An error happened during the config change", @@ -90,14 +92,13 @@ OC.L10N.register( "Save" : "Spremi", "Change unique password" : "Change unique password", "Failed to save password. Please try again later." : "Failed to save password. Please try again later.", - "Close" : "Zatvori", "There is no description for this team" : "There is no description for this team", "Enter a description for the team" : "Enter a description for the team", "Folder" : "Folder", "Team name" : "Team name", "Edit" : "Edit", + "Cancel" : "Otkaži", "Request to join" : "Request to join", - "Create" : "Ustvari", "Your request to join this team is pending approval" : "Your request to join this team is pending approval", "You are not a member of {circle}" : "You are not a member of {circle}", "Confirm" : "Confirm", @@ -194,6 +195,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contact added to {name}","{success} contacts added to {name}","{success} contacts added to {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Adding {success} contact to {name}","Adding {success} contacts to {name}","Adding {success} contacts to {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errors","{count} errors"], + "Close" : "Zatvori", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importing %n contact into {addressbook}","Importing %n contacts into {addressbook}","Importing %n contacts into {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Done importing %n contact into {addressbook}","Done importing %n contacts into {addressbook}","Done importing %n contacts into {addressbook}"], "No data for this contact" : "No data for this contact", @@ -303,8 +305,6 @@ OC.L10N.register( "Unable to delete contact" : "Unable to delete contact", "Loading contacts …" : "Loading contacts …", "Loading …" : "Loading …", - "General settings" : "General settings", - "(refreshed once per week)" : "(refreshed once per week)", "{addressbookname} (Disabled)" : "{addressbookname} (Disabled)", "Unique password …" : "Unique password …", "Search contacts …" : "Search contacts …", diff --git a/l10n/bs.json b/l10n/bs.json index 3b3de0415..950998f30 100644 --- a/l10n/bs.json +++ b/l10n/bs.json @@ -20,7 +20,9 @@ "Leave team" : "Leave team", "Delete team" : "Delete team", "Contacts settings" : "Contacts settings", + "General settings" : "General settings", "Update avatars from social media" : "Update avatars from social media", + "(refreshed once per week)" : "(refreshed once per week)", "Address books" : "Address books", "Rename" : "Preimenuj", "Export" : "Export", @@ -72,7 +74,6 @@ "Importing is disabled because there are no address books available" : "Importing is disabled because there are no address books available", "An error occurred, unable to create the address book" : "An error occurred, unable to create the address book", "Add new address book" : "Add new address book", - "Cancel" : "Otkaži", "Add" : "Dodaj", "First name" : "Ime", "Last name" : "Prezime", @@ -80,6 +81,7 @@ "Phonetic last name" : "Phonetic last name", "Display name" : "Display name", "Last modified" : "Last modified", + "Sort by {sorting}" : "Sort by {sorting}", "Manages" : "Manages", "Oversees" : "Oversees", "An error happened during the config change" : "An error happened during the config change", @@ -88,14 +90,13 @@ "Save" : "Spremi", "Change unique password" : "Change unique password", "Failed to save password. Please try again later." : "Failed to save password. Please try again later.", - "Close" : "Zatvori", "There is no description for this team" : "There is no description for this team", "Enter a description for the team" : "Enter a description for the team", "Folder" : "Folder", "Team name" : "Team name", "Edit" : "Edit", + "Cancel" : "Otkaži", "Request to join" : "Request to join", - "Create" : "Ustvari", "Your request to join this team is pending approval" : "Your request to join this team is pending approval", "You are not a member of {circle}" : "You are not a member of {circle}", "Confirm" : "Confirm", @@ -192,6 +193,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contact added to {name}","{success} contacts added to {name}","{success} contacts added to {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Adding {success} contact to {name}","Adding {success} contacts to {name}","Adding {success} contacts to {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errors","{count} errors"], + "Close" : "Zatvori", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importing %n contact into {addressbook}","Importing %n contacts into {addressbook}","Importing %n contacts into {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Done importing %n contact into {addressbook}","Done importing %n contacts into {addressbook}","Done importing %n contacts into {addressbook}"], "No data for this contact" : "No data for this contact", @@ -301,8 +303,6 @@ "Unable to delete contact" : "Unable to delete contact", "Loading contacts …" : "Loading contacts …", "Loading …" : "Loading …", - "General settings" : "General settings", - "(refreshed once per week)" : "(refreshed once per week)", "{addressbookname} (Disabled)" : "{addressbookname} (Disabled)", "Unique password …" : "Unique password …", "Search contacts …" : "Search contacts …", diff --git a/l10n/ca.js b/l10n/ca.js index 53b3ef00c..f945e896c 100644 --- a/l10n/ca.js +++ b/l10n/ca.js @@ -22,7 +22,9 @@ OC.L10N.register( "Leave team" : "Abandonar l'equip", "Delete team" : "Suprimeix l'equip", "Contacts settings" : "Paràmetres de Contactes", + "General settings" : "Paràmetres generals", "Update avatars from social media" : "Actualitza els avatars des de les xarxes socials", + "(refreshed once per week)" : "(s'actualitza una vegada la setmana)", "Address books" : "Llibretes d'adreces", "Rename" : "Canvia el nom", "Export" : "Exporta", @@ -74,7 +76,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "La importació està inhabilitada perquè no hi ha cap llibreta d'adreces disponible", "An error occurred, unable to create the address book" : "S'ha produït un error, no s'ha pogut crear la llibreta d'adreces", "Add new address book" : "Afegeix una llibreta d'adreces", - "Cancel" : "Cancel·la", "Add" : "Afegeix", "First name" : "Nom", "Last name" : "Cognoms", @@ -82,6 +83,7 @@ OC.L10N.register( "Phonetic last name" : "Cognoms fonètics", "Display name" : "Nom de visualització", "Last modified" : "Darrera modificació", + "Sort by {sorting}" : "Ordena per {sorting}", "Manages" : "Gestiona", "Oversees" : "Supervisa", "An error happened during the config change" : "S'ha produït un error durant el canvi de configuració", @@ -90,12 +92,12 @@ OC.L10N.register( "Save" : "Desa", "Change unique password" : "Canvia la contrasenya única", "Failed to save password. Please try again later." : "No s'ha pogut desar la contrasenya. Torneu-ho a provar més tard.", - "Close" : "Tanca", "There is no description for this team" : "No hi ha cap descripció per a aquest equip", "Enter a description for the team" : "Introduïu una descripció per a l'equip", "Folder" : "Folder", "Team name" : "Nom de l'equip", "Edit" : "Edita", + "Cancel" : "Cancel·la", "Request to join" : "Sol·licita unir-m'hi", "Your request to join this team is pending approval" : "La teva sol·licitud per unir-te a aquest equip està pendent d'aprovació", "You are not a member of {circle}" : "No formeu part del cercle {circle}", @@ -194,6 +196,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["S'ha afegit {success} contacte a {name}","S'han afegit {success} contactes a {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["S'està afegint {success} contacte a {name}","S'estan afegint {success} contactes a {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errors"], + "Close" : "Tanca", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["S'està important %n contacte a {addressbook}","S'estan important %n contactes a {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["La importació d'%n contacte a {addressbook} ha finalitzat","La importació de %n contactes a {addressbook} ha finalitzat"], "No data for this contact" : "No hi ha dades per a aquest contacte", @@ -303,8 +306,6 @@ OC.L10N.register( "Unable to delete contact" : "No s'ha pogut suprimir el contacte", "Loading contacts …" : "S'estan carregant els contactes …", "Loading …" : "S'està carregant…", - "General settings" : "Paràmetres generals", - "(refreshed once per week)" : "(s'actualitza una vegada la setmana)", "{addressbookname} (Disabled)" : "{addressbookname} (desactivat)", "Unique password …" : "Contrasenya única …", "Search contacts …" : "Cerqueu contactes…", diff --git a/l10n/ca.json b/l10n/ca.json index bbe08e980..845e55c40 100644 --- a/l10n/ca.json +++ b/l10n/ca.json @@ -20,7 +20,9 @@ "Leave team" : "Abandonar l'equip", "Delete team" : "Suprimeix l'equip", "Contacts settings" : "Paràmetres de Contactes", + "General settings" : "Paràmetres generals", "Update avatars from social media" : "Actualitza els avatars des de les xarxes socials", + "(refreshed once per week)" : "(s'actualitza una vegada la setmana)", "Address books" : "Llibretes d'adreces", "Rename" : "Canvia el nom", "Export" : "Exporta", @@ -72,7 +74,6 @@ "Importing is disabled because there are no address books available" : "La importació està inhabilitada perquè no hi ha cap llibreta d'adreces disponible", "An error occurred, unable to create the address book" : "S'ha produït un error, no s'ha pogut crear la llibreta d'adreces", "Add new address book" : "Afegeix una llibreta d'adreces", - "Cancel" : "Cancel·la", "Add" : "Afegeix", "First name" : "Nom", "Last name" : "Cognoms", @@ -80,6 +81,7 @@ "Phonetic last name" : "Cognoms fonètics", "Display name" : "Nom de visualització", "Last modified" : "Darrera modificació", + "Sort by {sorting}" : "Ordena per {sorting}", "Manages" : "Gestiona", "Oversees" : "Supervisa", "An error happened during the config change" : "S'ha produït un error durant el canvi de configuració", @@ -88,12 +90,12 @@ "Save" : "Desa", "Change unique password" : "Canvia la contrasenya única", "Failed to save password. Please try again later." : "No s'ha pogut desar la contrasenya. Torneu-ho a provar més tard.", - "Close" : "Tanca", "There is no description for this team" : "No hi ha cap descripció per a aquest equip", "Enter a description for the team" : "Introduïu una descripció per a l'equip", "Folder" : "Folder", "Team name" : "Nom de l'equip", "Edit" : "Edita", + "Cancel" : "Cancel·la", "Request to join" : "Sol·licita unir-m'hi", "Your request to join this team is pending approval" : "La teva sol·licitud per unir-te a aquest equip està pendent d'aprovació", "You are not a member of {circle}" : "No formeu part del cercle {circle}", @@ -192,6 +194,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["S'ha afegit {success} contacte a {name}","S'han afegit {success} contactes a {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["S'està afegint {success} contacte a {name}","S'estan afegint {success} contactes a {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errors"], + "Close" : "Tanca", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["S'està important %n contacte a {addressbook}","S'estan important %n contactes a {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["La importació d'%n contacte a {addressbook} ha finalitzat","La importació de %n contactes a {addressbook} ha finalitzat"], "No data for this contact" : "No hi ha dades per a aquest contacte", @@ -301,8 +304,6 @@ "Unable to delete contact" : "No s'ha pogut suprimir el contacte", "Loading contacts …" : "S'estan carregant els contactes …", "Loading …" : "S'està carregant…", - "General settings" : "Paràmetres generals", - "(refreshed once per week)" : "(s'actualitza una vegada la setmana)", "{addressbookname} (Disabled)" : "{addressbookname} (desactivat)", "Unique password …" : "Contrasenya única …", "Search contacts …" : "Cerqueu contactes…", diff --git a/l10n/el.js b/l10n/el.js index 9d7817b99..592497ff6 100644 --- a/l10n/el.js +++ b/l10n/el.js @@ -22,7 +22,9 @@ OC.L10N.register( "Leave team" : "Αποχώρηση από την ομάδα", "Delete team" : "Διαγραφή ομάδας", "Contacts settings" : "Ρυθμίσεις επαφών", + "General settings" : "Γενικές ρυθμίσεις", "Update avatars from social media" : "Ενημέρωση των άβαταρ από τα κοινωνικά δίκτυα", + "(refreshed once per week)" : "(ανανεώνεται μία φορά την εβδομάδα)", "Address books" : "Βιβλία διευθύνσεων", "Rename" : "Μετονομασία", "Export" : "Εξαγωγή", @@ -74,7 +76,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "Η λειτουργία εισαγωγής απενεργοποιήθηκε, καθώς δεν βρέθηκε κάποιο βιβλίο διευθύνσεων διαθέσιμο", "An error occurred, unable to create the address book" : "Παρουσιάστηκε σφάλμα, δεν ήταν δυνατή η δημιουργία του βιβλίου διευθύνσεων", "Add new address book" : "Προσθήκη νέου βιβλίο διευθύνσεων", - "Cancel" : "Ακύρωση", "Add" : "Προσθήκη", "First name" : "Όνομα", "Last name" : "Επώνυμο", @@ -82,6 +83,7 @@ OC.L10N.register( "Phonetic last name" : "Φωνητικό επίθετο", "Display name" : "Εμφανιζόμενο όνομα", "Last modified" : "Τελευταία τροποίηση", + "Sort by {sorting}" : "Ταξινόμηση κατά {sorting}", "Manages" : "Διαχειρίζεται", "Oversees" : "Επιβλέπει", "An error happened during the config change" : "Παρουσιάστηκε σφάλμα κατά την αλλαγή της ρύθμισης", @@ -90,15 +92,13 @@ OC.L10N.register( "Save" : "Αποθήκευση", "Change unique password" : "Αλλαγή μοναδικού κωδικού πρόσβασης", "Failed to save password. Please try again later." : "Αποτυχία αποθήκευσης κωδικού πρόσβασης. Παρακαλώ προσπαθήστε ξανά αργότερα.", - "Close" : "Κλείσιμο", "There is no description for this team" : "Δεν υπάρχει περιγραφή για αυτήν την ομάδα", "Enter a description for the team" : "Εισάγετε μια περιγραφή για την ομάδα", - "Conversation name" : "Όνομα συνομιλίας", - "Calendar" : "Ημερολόγιο", "An error happened while saving {fields}" : "Παρουσιάστηκε σφάλμα κατά την αποθήκευση {fields}", "Team name" : "Όνομα ομάδας", "Team owner" : "Ιδιοκτήτης ομάδας", "Edit" : "Επεξεργασία", + "Cancel" : "Ακύρωση", "Request to join" : "Αίτημα συμμετοχής", "Create" : "Δημιουργία", "Your request to join this team is pending approval" : "Το αίτημά σας για συμμετοχή σε αυτήν την ομάδα εκκρεμεί έγκρισης", @@ -216,6 +216,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["Προστέθηκε {success} επαφή σε {name}","Προστέθηκαν {success} επαφές σε {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Προσθήκη {success} επαφής σε {name}","Προσθήκη {success} επαφών σε {name}"], "_{count} error_::_{count} errors_" : ["{count} σφάλμα","{count} σφάλματα"], + "Close" : "Κλείσιμο", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Εισαγωγή %n επαφής σε {addressbook}","Εισαγωγή %n επαφών σε {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Ολοκληρώθηκε η εισαγωγή %n επαφής σε {addressbook}","Ολοκληρώθηκε η εισαγωγή %n επαφών σε {addressbook}"], "No data for this contact" : "Δεν υπάρχουν δεδομένα για αυτήν την επαφή", @@ -327,8 +328,6 @@ OC.L10N.register( "Unable to delete contact" : "Δεν είναι δυνατή η διαγραφή της επαφής", "Loading contacts …" : "Φόρτωση επαφών …", "Loading …" : "Φόρτωση…", - "General settings" : "Γενικές ρυθμίσεις", - "(refreshed once per week)" : "(ανανεώνεται μία φορά την εβδομάδα)", "{addressbookname} (Disabled)" : "{addressbookname} (Απενεργοποιημένο)", "Unique password …" : "Μοναδικός συνθηματικό…", "Search contacts …" : "Αναζήτηση επαφών …", diff --git a/l10n/el.json b/l10n/el.json index 362d10f39..b87ca7e03 100644 --- a/l10n/el.json +++ b/l10n/el.json @@ -20,7 +20,9 @@ "Leave team" : "Αποχώρηση από την ομάδα", "Delete team" : "Διαγραφή ομάδας", "Contacts settings" : "Ρυθμίσεις επαφών", + "General settings" : "Γενικές ρυθμίσεις", "Update avatars from social media" : "Ενημέρωση των άβαταρ από τα κοινωνικά δίκτυα", + "(refreshed once per week)" : "(ανανεώνεται μία φορά την εβδομάδα)", "Address books" : "Βιβλία διευθύνσεων", "Rename" : "Μετονομασία", "Export" : "Εξαγωγή", @@ -72,7 +74,6 @@ "Importing is disabled because there are no address books available" : "Η λειτουργία εισαγωγής απενεργοποιήθηκε, καθώς δεν βρέθηκε κάποιο βιβλίο διευθύνσεων διαθέσιμο", "An error occurred, unable to create the address book" : "Παρουσιάστηκε σφάλμα, δεν ήταν δυνατή η δημιουργία του βιβλίου διευθύνσεων", "Add new address book" : "Προσθήκη νέου βιβλίο διευθύνσεων", - "Cancel" : "Ακύρωση", "Add" : "Προσθήκη", "First name" : "Όνομα", "Last name" : "Επώνυμο", @@ -80,6 +81,7 @@ "Phonetic last name" : "Φωνητικό επίθετο", "Display name" : "Εμφανιζόμενο όνομα", "Last modified" : "Τελευταία τροποίηση", + "Sort by {sorting}" : "Ταξινόμηση κατά {sorting}", "Manages" : "Διαχειρίζεται", "Oversees" : "Επιβλέπει", "An error happened during the config change" : "Παρουσιάστηκε σφάλμα κατά την αλλαγή της ρύθμισης", @@ -88,15 +90,13 @@ "Save" : "Αποθήκευση", "Change unique password" : "Αλλαγή μοναδικού κωδικού πρόσβασης", "Failed to save password. Please try again later." : "Αποτυχία αποθήκευσης κωδικού πρόσβασης. Παρακαλώ προσπαθήστε ξανά αργότερα.", - "Close" : "Κλείσιμο", "There is no description for this team" : "Δεν υπάρχει περιγραφή για αυτήν την ομάδα", "Enter a description for the team" : "Εισάγετε μια περιγραφή για την ομάδα", - "Conversation name" : "Όνομα συνομιλίας", - "Calendar" : "Ημερολόγιο", "An error happened while saving {fields}" : "Παρουσιάστηκε σφάλμα κατά την αποθήκευση {fields}", "Team name" : "Όνομα ομάδας", "Team owner" : "Ιδιοκτήτης ομάδας", "Edit" : "Επεξεργασία", + "Cancel" : "Ακύρωση", "Request to join" : "Αίτημα συμμετοχής", "Create" : "Δημιουργία", "Your request to join this team is pending approval" : "Το αίτημά σας για συμμετοχή σε αυτήν την ομάδα εκκρεμεί έγκρισης", @@ -214,6 +214,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["Προστέθηκε {success} επαφή σε {name}","Προστέθηκαν {success} επαφές σε {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Προσθήκη {success} επαφής σε {name}","Προσθήκη {success} επαφών σε {name}"], "_{count} error_::_{count} errors_" : ["{count} σφάλμα","{count} σφάλματα"], + "Close" : "Κλείσιμο", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Εισαγωγή %n επαφής σε {addressbook}","Εισαγωγή %n επαφών σε {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Ολοκληρώθηκε η εισαγωγή %n επαφής σε {addressbook}","Ολοκληρώθηκε η εισαγωγή %n επαφών σε {addressbook}"], "No data for this contact" : "Δεν υπάρχουν δεδομένα για αυτήν την επαφή", @@ -325,8 +326,6 @@ "Unable to delete contact" : "Δεν είναι δυνατή η διαγραφή της επαφής", "Loading contacts …" : "Φόρτωση επαφών …", "Loading …" : "Φόρτωση…", - "General settings" : "Γενικές ρυθμίσεις", - "(refreshed once per week)" : "(ανανεώνεται μία φορά την εβδομάδα)", "{addressbookname} (Disabled)" : "{addressbookname} (Απενεργοποιημένο)", "Unique password …" : "Μοναδικός συνθηματικό…", "Search contacts …" : "Αναζήτηση επαφών …", diff --git a/l10n/es_CO.js b/l10n/es_CO.js index 52379490e..31b51a5b3 100644 --- a/l10n/es_CO.js +++ b/l10n/es_CO.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -11,15 +12,13 @@ OC.L10N.register( "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -29,6 +28,7 @@ OC.L10N.register( "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -91,7 +91,6 @@ OC.L10N.register( "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/l10n/es_CO.json b/l10n/es_CO.json index c5bd97808..5b49477de 100644 --- a/l10n/es_CO.json +++ b/l10n/es_CO.json @@ -2,6 +2,7 @@ "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -9,15 +10,13 @@ "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -27,6 +26,7 @@ "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -89,7 +89,6 @@ "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/l10n/es_EC.js b/l10n/es_EC.js index 1a039a583..3d813409a 100644 --- a/l10n/es_EC.js +++ b/l10n/es_EC.js @@ -17,7 +17,9 @@ OC.L10N.register( "Create contacts" : "Crear contactos", "Add contacts" : "Añadir contactos", "Contacts settings" : "Configuración de contactos", + "General settings" : "Configuraciones generales", "Update avatars from social media" : "Actualizar avatares desde redes sociales", + "(refreshed once per week)" : "(actualizado una vez por semana)", "Address books" : "Libretas de direcciones", "Rename" : "Renombrar", "Export" : "Exportar", @@ -56,7 +58,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "La importación está desactivada porque no hay libretas de direcciones disponibles", "An error occurred, unable to create the address book" : "Ocurrió un error, no se pudo crear la libreta de direcciones", "Add new address book" : "Añadir nueva libreta de direcciones", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", @@ -64,16 +65,16 @@ OC.L10N.register( "Phonetic last name" : "Apellido fonético", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", + "Sort by {sorting}" : "Ordenar por {sorting}", "Manages" : "Gestiona", "Oversees" : "Supervisa", "An error happened during the config change" : "Ocurrió un error durante el cambio de configuración", "Save" : "Guardar", "Change unique password" : "Cambiar contraseña única", "Failed to save password. Please try again later." : "Error al guardar la contraseña. Por favor, inténtalo de nuevo más tarde.", - "Close" : "Cerrar", "Edit" : "Editar", + "Cancel" : "Cancelar", "Request to join" : "Solicitud para unirse", - "Create" : "Crear", "You are not a member of {circle}" : "No eres miembro de {circle}", "Add more info" : "Añadir más información", "More fields" : "Más campos", @@ -150,6 +151,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contacto añadido a {name}","{success} contactos añadidos a {name}","{success} contactos añadidos a {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Añadiendo {success} contacto a {name}","Añadiendo {success} contactos a {name}","Añadiendo {success} contactos a {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errores","{count} errores"], + "Close" : "Cerrar", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importando %n contacto en {addressbook}","Importando %n contactos en {addressbook}","Importando %n contactos en {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Importación de %n contacto en {addressbook} finalizada","Importación de %n contactos en {addressbook} finalizada","Importación de %n contactos en {addressbook} finalizada"], "Import" : "Importar", @@ -236,8 +238,6 @@ OC.L10N.register( "Unable to delete contact" : "No se pudo eliminar el contacto", "Loading contacts …" : "Cargando contactos ...", "Loading …" : "Cargando...", - "General settings" : "Configuraciones generales", - "(refreshed once per week)" : "(actualizado una vez por semana)", "{addressbookname} (Disabled)" : "{addressbookname} (Desactivado)", "Unique password …" : "Contraseña única...", "Search contacts …" : "Buscar contactos ...", diff --git a/l10n/es_EC.json b/l10n/es_EC.json index 20f4c6d90..184ec638e 100644 --- a/l10n/es_EC.json +++ b/l10n/es_EC.json @@ -15,7 +15,9 @@ "Create contacts" : "Crear contactos", "Add contacts" : "Añadir contactos", "Contacts settings" : "Configuración de contactos", + "General settings" : "Configuraciones generales", "Update avatars from social media" : "Actualizar avatares desde redes sociales", + "(refreshed once per week)" : "(actualizado una vez por semana)", "Address books" : "Libretas de direcciones", "Rename" : "Renombrar", "Export" : "Exportar", @@ -54,7 +56,6 @@ "Importing is disabled because there are no address books available" : "La importación está desactivada porque no hay libretas de direcciones disponibles", "An error occurred, unable to create the address book" : "Ocurrió un error, no se pudo crear la libreta de direcciones", "Add new address book" : "Añadir nueva libreta de direcciones", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", @@ -62,16 +63,16 @@ "Phonetic last name" : "Apellido fonético", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", + "Sort by {sorting}" : "Ordenar por {sorting}", "Manages" : "Gestiona", "Oversees" : "Supervisa", "An error happened during the config change" : "Ocurrió un error durante el cambio de configuración", "Save" : "Guardar", "Change unique password" : "Cambiar contraseña única", "Failed to save password. Please try again later." : "Error al guardar la contraseña. Por favor, inténtalo de nuevo más tarde.", - "Close" : "Cerrar", "Edit" : "Editar", + "Cancel" : "Cancelar", "Request to join" : "Solicitud para unirse", - "Create" : "Crear", "You are not a member of {circle}" : "No eres miembro de {circle}", "Add more info" : "Añadir más información", "More fields" : "Más campos", @@ -148,6 +149,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contacto añadido a {name}","{success} contactos añadidos a {name}","{success} contactos añadidos a {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Añadiendo {success} contacto a {name}","Añadiendo {success} contactos a {name}","Añadiendo {success} contactos a {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errores","{count} errores"], + "Close" : "Cerrar", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importando %n contacto en {addressbook}","Importando %n contactos en {addressbook}","Importando %n contactos en {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Importación de %n contacto en {addressbook} finalizada","Importación de %n contactos en {addressbook} finalizada","Importación de %n contactos en {addressbook} finalizada"], "Import" : "Importar", @@ -234,8 +236,6 @@ "Unable to delete contact" : "No se pudo eliminar el contacto", "Loading contacts …" : "Cargando contactos ...", "Loading …" : "Cargando...", - "General settings" : "Configuraciones generales", - "(refreshed once per week)" : "(actualizado una vez por semana)", "{addressbookname} (Disabled)" : "{addressbookname} (Desactivado)", "Unique password …" : "Contraseña única...", "Search contacts …" : "Buscar contactos ...", diff --git a/l10n/es_HN.js b/l10n/es_HN.js index e207fa5f5..a59320932 100644 --- a/l10n/es_HN.js +++ b/l10n/es_HN.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -11,15 +12,13 @@ OC.L10N.register( "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -29,6 +28,7 @@ OC.L10N.register( "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -88,7 +88,6 @@ OC.L10N.register( "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/l10n/es_HN.json b/l10n/es_HN.json index e3803ad63..51a5f2783 100644 --- a/l10n/es_HN.json +++ b/l10n/es_HN.json @@ -2,6 +2,7 @@ "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -9,15 +10,13 @@ "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -27,6 +26,7 @@ "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -86,7 +86,6 @@ "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/l10n/es_MX.js b/l10n/es_MX.js index d953c9b79..e22388b12 100644 --- a/l10n/es_MX.js +++ b/l10n/es_MX.js @@ -22,7 +22,9 @@ OC.L10N.register( "Leave team" : "Abandonar equipo", "Delete team" : "Eliminar equipo", "Contacts settings" : "Configuración de contactos", + "General settings" : "Configuraciones generales", "Update avatars from social media" : "Actualizar avatares desde redes sociales", + "(refreshed once per week)" : "(actualizado una vez por semana)", "Address books" : "Libretas de direcciones", "Rename" : "Renombrar", "Export" : "Exportar", @@ -72,7 +74,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "La importación está deshabilitada porque no hay libretas de direcciones disponibles", "An error occurred, unable to create the address book" : "Ocurrió un error, no se pudo crear la libreta de direcciones", "Add new address book" : "Añadir una nueva libreta de direcciones", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", @@ -80,6 +81,7 @@ OC.L10N.register( "Phonetic last name" : "Apellido fonético", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", + "Sort by {sorting}" : "Ordenar por {sorting}", "Manages" : "Administra", "Oversees" : "Supervisa", "An error happened during the config change" : "Ocurrió un error durante el cambio de configuración", @@ -88,12 +90,12 @@ OC.L10N.register( "Save" : "Guardar", "Change unique password" : "Cambiar la contraseña única", "Failed to save password. Please try again later." : "No se pudo guardar la contraseña. Por favor, intente de nuevo más tarde.", - "Close" : "Cerrar", "There is no description for this team" : "No hay descripción para este equipo", "Enter a description for the team" : "Ingresar una descripción para el equipo", "Folder" : "Folder", "Team name" : "Nombre del equipo", "Edit" : "Editar", + "Cancel" : "Cancelar", "Request to join" : "Solicitar unirse", "Your request to join this team is pending approval" : "Su solicitud para unirse a este equipo está pendiente de aprobación", "You are not a member of {circle}" : "No es miembro de {circle}", @@ -190,6 +192,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contacto añadido a {name}","{success} contactos añadidos a {name}","{success} contactos añadidos a {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Añadiendo {success} contacto a {name}","Añadiendo {success} contactos a {name}","Añadiendo {success} contactos a {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errores","{count} errores"], + "Close" : "Cerrar", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importando %n contacto a {addressbook}","Importando %n contactos a {addressbook}","Importando %n contactos a {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Importación de %n contacto a {addressbook} completada","Importación de %n contactos a {addressbook} completada","Importación de %n contactos a {addressbook} completada"], "Import" : "Importar", @@ -293,8 +296,6 @@ OC.L10N.register( "Unable to delete contact" : "No se pudo eliminar el contacto", "Loading contacts …" : "Cargando contactos ...", "Loading …" : "Cargando …", - "General settings" : "Configuraciones generales", - "(refreshed once per week)" : "(actualizado una vez por semana)", "{addressbookname} (Disabled)" : "{addressbookname} (Deshabilitado)", "Unique password …" : "Contraseña única ...", "Search contacts …" : "Buscar contactos ...", diff --git a/l10n/es_MX.json b/l10n/es_MX.json index 977350a02..800dea520 100644 --- a/l10n/es_MX.json +++ b/l10n/es_MX.json @@ -20,7 +20,9 @@ "Leave team" : "Abandonar equipo", "Delete team" : "Eliminar equipo", "Contacts settings" : "Configuración de contactos", + "General settings" : "Configuraciones generales", "Update avatars from social media" : "Actualizar avatares desde redes sociales", + "(refreshed once per week)" : "(actualizado una vez por semana)", "Address books" : "Libretas de direcciones", "Rename" : "Renombrar", "Export" : "Exportar", @@ -70,7 +72,6 @@ "Importing is disabled because there are no address books available" : "La importación está deshabilitada porque no hay libretas de direcciones disponibles", "An error occurred, unable to create the address book" : "Ocurrió un error, no se pudo crear la libreta de direcciones", "Add new address book" : "Añadir una nueva libreta de direcciones", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", @@ -78,6 +79,7 @@ "Phonetic last name" : "Apellido fonético", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", + "Sort by {sorting}" : "Ordenar por {sorting}", "Manages" : "Administra", "Oversees" : "Supervisa", "An error happened during the config change" : "Ocurrió un error durante el cambio de configuración", @@ -86,12 +88,12 @@ "Save" : "Guardar", "Change unique password" : "Cambiar la contraseña única", "Failed to save password. Please try again later." : "No se pudo guardar la contraseña. Por favor, intente de nuevo más tarde.", - "Close" : "Cerrar", "There is no description for this team" : "No hay descripción para este equipo", "Enter a description for the team" : "Ingresar una descripción para el equipo", "Folder" : "Folder", "Team name" : "Nombre del equipo", "Edit" : "Editar", + "Cancel" : "Cancelar", "Request to join" : "Solicitar unirse", "Your request to join this team is pending approval" : "Su solicitud para unirse a este equipo está pendiente de aprobación", "You are not a member of {circle}" : "No es miembro de {circle}", @@ -188,6 +190,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contacto añadido a {name}","{success} contactos añadidos a {name}","{success} contactos añadidos a {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Añadiendo {success} contacto a {name}","Añadiendo {success} contactos a {name}","Añadiendo {success} contactos a {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errores","{count} errores"], + "Close" : "Cerrar", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importando %n contacto a {addressbook}","Importando %n contactos a {addressbook}","Importando %n contactos a {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Importación de %n contacto a {addressbook} completada","Importación de %n contactos a {addressbook} completada","Importación de %n contactos a {addressbook} completada"], "Import" : "Importar", @@ -291,8 +294,6 @@ "Unable to delete contact" : "No se pudo eliminar el contacto", "Loading contacts …" : "Cargando contactos ...", "Loading …" : "Cargando …", - "General settings" : "Configuraciones generales", - "(refreshed once per week)" : "(actualizado una vez por semana)", "{addressbookname} (Disabled)" : "{addressbookname} (Deshabilitado)", "Unique password …" : "Contraseña única ...", "Search contacts …" : "Buscar contactos ...", diff --git a/l10n/es_NI.js b/l10n/es_NI.js index e207fa5f5..a59320932 100644 --- a/l10n/es_NI.js +++ b/l10n/es_NI.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -11,15 +12,13 @@ OC.L10N.register( "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -29,6 +28,7 @@ OC.L10N.register( "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -88,7 +88,6 @@ OC.L10N.register( "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/l10n/es_NI.json b/l10n/es_NI.json index e3803ad63..51a5f2783 100644 --- a/l10n/es_NI.json +++ b/l10n/es_NI.json @@ -2,6 +2,7 @@ "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -9,15 +10,13 @@ "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -27,6 +26,7 @@ "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -86,7 +86,6 @@ "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/l10n/es_PA.js b/l10n/es_PA.js index e207fa5f5..a59320932 100644 --- a/l10n/es_PA.js +++ b/l10n/es_PA.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -11,15 +12,13 @@ OC.L10N.register( "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -29,6 +28,7 @@ OC.L10N.register( "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -88,7 +88,6 @@ OC.L10N.register( "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/l10n/es_PA.json b/l10n/es_PA.json index e3803ad63..51a5f2783 100644 --- a/l10n/es_PA.json +++ b/l10n/es_PA.json @@ -2,6 +2,7 @@ "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -9,15 +10,13 @@ "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -27,6 +26,7 @@ "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -86,7 +86,6 @@ "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/l10n/es_PR.js b/l10n/es_PR.js index e207fa5f5..a59320932 100644 --- a/l10n/es_PR.js +++ b/l10n/es_PR.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -11,15 +12,13 @@ OC.L10N.register( "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -29,6 +28,7 @@ OC.L10N.register( "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -88,7 +88,6 @@ OC.L10N.register( "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/l10n/es_PR.json b/l10n/es_PR.json index e3803ad63..51a5f2783 100644 --- a/l10n/es_PR.json +++ b/l10n/es_PR.json @@ -2,6 +2,7 @@ "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -9,15 +10,13 @@ "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -27,6 +26,7 @@ "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -86,7 +86,6 @@ "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/l10n/es_PY.js b/l10n/es_PY.js index e207fa5f5..a59320932 100644 --- a/l10n/es_PY.js +++ b/l10n/es_PY.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -11,15 +12,13 @@ OC.L10N.register( "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -29,6 +28,7 @@ OC.L10N.register( "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -88,7 +88,6 @@ OC.L10N.register( "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/l10n/es_PY.json b/l10n/es_PY.json index e3803ad63..51a5f2783 100644 --- a/l10n/es_PY.json +++ b/l10n/es_PY.json @@ -2,6 +2,7 @@ "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -9,15 +10,13 @@ "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -27,6 +26,7 @@ "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -86,7 +86,6 @@ "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/l10n/es_UY.js b/l10n/es_UY.js index 7450b1b68..07a17161e 100644 --- a/l10n/es_UY.js +++ b/l10n/es_UY.js @@ -4,6 +4,7 @@ OC.L10N.register( "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -11,15 +12,13 @@ OC.L10N.register( "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -30,6 +29,7 @@ OC.L10N.register( "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -89,7 +89,6 @@ OC.L10N.register( "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/l10n/es_UY.json b/l10n/es_UY.json index ac0b24c09..635defba1 100644 --- a/l10n/es_UY.json +++ b/l10n/es_UY.json @@ -2,6 +2,7 @@ "Details" : "Detalles", "All contacts" : "Todos los contactos", "Contacts" : "Contactos", + "General settings" : "Configuraciones generales", "Rename" : "Renombrar", "Send email" : "Enviar correo electrónico", "Delete" : "Borrar", @@ -9,15 +10,13 @@ "Download" : "Descargar", "Share with users or groups" : "Compartir con otros usuarios o grupos", "can edit" : "puede editar", - "Cancel" : "Cancelar", "Add" : "Agregar", "First name" : "Nombre", "Last name" : "Apellido", "Display name" : "Nombre a desplegar", "Last modified" : "Última modificación", "Save" : "Guardar", - "Close" : "Cerrar", - "Create" : "Crear", + "Cancel" : "Cancelar", "Invalid image" : "Imagen inválida", "Address book" : "Libreta de direcciones", "Name" : "Nombre", @@ -28,6 +27,7 @@ "None" : "Ninguno", "Member" : "Miembro", "New contact" : "Nuevo contacto", + "Close" : "Cerrar", "Import" : "Importar", "Not grouped" : "No agrupado", "User" : "Usuario", @@ -87,7 +87,6 @@ "Male" : "Masculino", "Unknown" : "Desconocido", "Loading contacts …" : "Cargando contactos ...", - "General settings" : "Configuraciones generales", "Search contacts …" : "Buscar contactos ..." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/l10n/is.js b/l10n/is.js index 6f2070a00..dda6371a7 100644 --- a/l10n/is.js +++ b/l10n/is.js @@ -315,4 +315,4 @@ OC.L10N.register( "Add to {circle}" : "Bæta við {circle}", "Select chart …" : "Veldu graf …" }, -"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"); +"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"); \ No newline at end of file diff --git a/l10n/it.js b/l10n/it.js index 1d0148bda..3539bffce 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -362,4 +362,4 @@ OC.L10N.register( "Add to {circle}" : "Aggiungi a {circle}", "Select chart …" : "Seleziona grafico..." }, -"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); \ No newline at end of file diff --git a/l10n/ka.js b/l10n/ka.js index 8bba85741..ba541fe22 100644 --- a/l10n/ka.js +++ b/l10n/ka.js @@ -17,8 +17,9 @@ OC.L10N.register( "Create contacts" : "Create contacts", "Add contacts" : "Add contacts", "Contacts settings" : "Contacts settings", - "General" : "General", + "General settings" : "General settings", "Update avatars from social media" : "Update avatars from social media", + "(refreshed once per week)" : "(refreshed once per week)", "Address books" : "Address books", "Rename" : "Rename", "Export" : "Export", @@ -58,7 +59,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "Importing is disabled because there are no address books available", "An error occurred, unable to create the address book" : "An error occurred, unable to create the address book", "Add new address book" : "Add new address book", - "Cancel" : "Cancel", "Add" : "Add", "First name" : "First name", "Last name" : "Last name", @@ -66,14 +66,15 @@ OC.L10N.register( "Phonetic last name" : "Phonetic last name", "Display name" : "Display name", "Last modified" : "Last modified", + "Sort by {sorting}" : "Sort by {sorting}", "Manages" : "Manages", "Oversees" : "Oversees", "An error happened during the config change" : "An error happened during the config change", "Save" : "Save", "Change unique password" : "Change unique password", "Failed to save password. Please try again later." : "Failed to save password. Please try again later.", - "Close" : "Close", "Edit" : "Edit", + "Cancel" : "Cancel", "Request to join" : "Request to join", "You are not a member of {circle}" : "You are not a member of {circle}", "Add more info" : "Add more info", @@ -158,6 +159,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contact added to {name}","{success} contacts added to {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Adding {success} contact to {name}","Adding {success} contacts to {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errors"], + "Close" : "Close", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importing %n contact into {addressbook}","Importing %n contacts into {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Done importing %n contact into {addressbook}","Done importing %n contacts into {addressbook}"], "Import" : "Import", @@ -243,8 +245,6 @@ OC.L10N.register( "Unable to delete contact" : "Unable to delete contact", "Loading contacts …" : "Loading contacts …", "Loading …" : "Loading …", - "General settings" : "General settings", - "(refreshed once per week)" : "(refreshed once per week)", "{addressbookname} (Disabled)" : "{addressbookname} (Disabled)", "Unique password …" : "Unique password …", "Search contacts …" : "Search contacts …", diff --git a/l10n/ka.json b/l10n/ka.json index 6f61bc0fb..df711ac83 100644 --- a/l10n/ka.json +++ b/l10n/ka.json @@ -15,8 +15,9 @@ "Create contacts" : "Create contacts", "Add contacts" : "Add contacts", "Contacts settings" : "Contacts settings", - "General" : "General", + "General settings" : "General settings", "Update avatars from social media" : "Update avatars from social media", + "(refreshed once per week)" : "(refreshed once per week)", "Address books" : "Address books", "Rename" : "Rename", "Export" : "Export", @@ -56,7 +57,6 @@ "Importing is disabled because there are no address books available" : "Importing is disabled because there are no address books available", "An error occurred, unable to create the address book" : "An error occurred, unable to create the address book", "Add new address book" : "Add new address book", - "Cancel" : "Cancel", "Add" : "Add", "First name" : "First name", "Last name" : "Last name", @@ -64,14 +64,15 @@ "Phonetic last name" : "Phonetic last name", "Display name" : "Display name", "Last modified" : "Last modified", + "Sort by {sorting}" : "Sort by {sorting}", "Manages" : "Manages", "Oversees" : "Oversees", "An error happened during the config change" : "An error happened during the config change", "Save" : "Save", "Change unique password" : "Change unique password", "Failed to save password. Please try again later." : "Failed to save password. Please try again later.", - "Close" : "Close", "Edit" : "Edit", + "Cancel" : "Cancel", "Request to join" : "Request to join", "You are not a member of {circle}" : "You are not a member of {circle}", "Add more info" : "Add more info", @@ -156,6 +157,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contact added to {name}","{success} contacts added to {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Adding {success} contact to {name}","Adding {success} contacts to {name}"], "_{count} error_::_{count} errors_" : ["{count} error","{count} errors"], + "Close" : "Close", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importing %n contact into {addressbook}","Importing %n contacts into {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Done importing %n contact into {addressbook}","Done importing %n contacts into {addressbook}"], "Import" : "Import", @@ -241,8 +243,6 @@ "Unable to delete contact" : "Unable to delete contact", "Loading contacts …" : "Loading contacts …", "Loading …" : "Loading …", - "General settings" : "General settings", - "(refreshed once per week)" : "(refreshed once per week)", "{addressbookname} (Disabled)" : "{addressbookname} (Disabled)", "Unique password …" : "Unique password …", "Search contacts …" : "Search contacts …", diff --git a/l10n/lt_LT.js b/l10n/lt_LT.js index dc1f4ba0b..f18b5c507 100644 --- a/l10n/lt_LT.js +++ b/l10n/lt_LT.js @@ -22,7 +22,9 @@ OC.L10N.register( "Leave team" : "Išeiti iš komandos", "Delete team" : "Ištrinti komandą", "Contacts settings" : "Adresatų nustatymai", + "General settings" : "Bendri nustatymai", "Update avatars from social media" : "Atnaujinti avatarus iš socialinių tinklų", + "(refreshed once per week)" : "(įkeliama iš naujo kartą per savaitę)", "Address books" : "Adresų knygos", "Rename" : "Pervadinti", "Export" : "Eksportuoti", @@ -70,7 +72,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "Importavimas yra išjungtas, nes nėra prieinamų adresų knygų", "An error occurred, unable to create the address book" : "Įvyko klaida, nepavyko sukurti adresų knygos", "Add new address book" : "Pridėti naują adresų knygą", - "Cancel" : "Atsisakyti", "Add" : "Add", "First name" : "Vardas", "Last name" : "Pavardė", @@ -78,19 +79,19 @@ OC.L10N.register( "Phonetic last name" : "Fonetinė pavardė", "Display name" : "Rodomas vardas", "Last modified" : "Paskutinis modifikavimas", + "Sort by {sorting}" : "Rikiuoti pagal {sorting}", "Manages" : "Tvarko", "Oversees" : "Prižiūri", "An error happened during the config change" : "Keičiant konfigūraciją įvyko klaida", "Save" : "Įrašyti", "Failed to save password. Please try again later." : "Nepavyko įrašyti slaptažodžio. Vėliau bandykite dar kartą.", - "Close" : "Užverti", "There is no description for this team" : "Ši komanda neturi aprašo", "Enter a description for the team" : "Įveskite komandos aprašą", "Folder" : "Folder", "Team name" : "Komandos pavadinimas", "Edit" : "Taisyt", + "Cancel" : "Atsisakyti", "Request to join" : "Prašyti prisijungti", - "Create" : "Sukurti", "Your request to join this team is pending approval" : "Jūsų užklausa prisijungti prie šios komandos laukia patvirtinimo", "You are not a member of {circle}" : "Jūs nesate rato {circle} narys", "Members" : "Nariai", @@ -169,6 +170,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} adresatas sėkmingai pridėtas į {name}","{success} adresatai sėkmingai pridėti į {name}","{success} adresatų sėkmingai pridėta į {name}","{success} adresatas sėkmingai pridėtas į {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Pridedamas {success} adresatas į {name}","Pridedami {success} adresatai į {name}","Pridedama {success} adresatų į {name}","Pridedamas {success} adresatas į {name}"], "_{count} error_::_{count} errors_" : ["{count} klaida","{count} klaidos","{count} klaidų","{count} klaida"], + "Close" : "Užverti", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["%n adresatas importuojamas į {addressbook}","%n adresatai importuojami į {addressbook}","%n adresatų importuojama į {addressbook}","%n adresatas importuojamas į {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Atliktas %n adresato importavimas į {addressbook}","Atliktas %n adresatų importavimas į {addressbook}","Atliktas %n adresatų importavimas į {addressbook}","Atliktas %n adresato importavimas į {addressbook}"], "Import" : "Importuoti", @@ -267,8 +269,6 @@ OC.L10N.register( "Unable to delete contact" : "Nepavyko ištrinti adresato", "Loading contacts …" : "Įkeliami adresatai…", "Loading …" : "Įkeliama…", - "General settings" : "Bendri nustatymai", - "(refreshed once per week)" : "(įkeliama iš naujo kartą per savaitę)", "{addressbookname} (Disabled)" : "{addressbookname} (Išjungta)", "Search contacts …" : "Ieškoti adresatų…", "Loading members list …" : "Įkeliamas narių sąrašas…", diff --git a/l10n/lt_LT.json b/l10n/lt_LT.json index 9cd08fb68..01979b29b 100644 --- a/l10n/lt_LT.json +++ b/l10n/lt_LT.json @@ -20,7 +20,9 @@ "Leave team" : "Išeiti iš komandos", "Delete team" : "Ištrinti komandą", "Contacts settings" : "Adresatų nustatymai", + "General settings" : "Bendri nustatymai", "Update avatars from social media" : "Atnaujinti avatarus iš socialinių tinklų", + "(refreshed once per week)" : "(įkeliama iš naujo kartą per savaitę)", "Address books" : "Adresų knygos", "Rename" : "Pervadinti", "Export" : "Eksportuoti", @@ -68,7 +70,6 @@ "Importing is disabled because there are no address books available" : "Importavimas yra išjungtas, nes nėra prieinamų adresų knygų", "An error occurred, unable to create the address book" : "Įvyko klaida, nepavyko sukurti adresų knygos", "Add new address book" : "Pridėti naują adresų knygą", - "Cancel" : "Atsisakyti", "Add" : "Add", "First name" : "Vardas", "Last name" : "Pavardė", @@ -76,19 +77,19 @@ "Phonetic last name" : "Fonetinė pavardė", "Display name" : "Rodomas vardas", "Last modified" : "Paskutinis modifikavimas", + "Sort by {sorting}" : "Rikiuoti pagal {sorting}", "Manages" : "Tvarko", "Oversees" : "Prižiūri", "An error happened during the config change" : "Keičiant konfigūraciją įvyko klaida", "Save" : "Įrašyti", "Failed to save password. Please try again later." : "Nepavyko įrašyti slaptažodžio. Vėliau bandykite dar kartą.", - "Close" : "Užverti", "There is no description for this team" : "Ši komanda neturi aprašo", "Enter a description for the team" : "Įveskite komandos aprašą", "Folder" : "Folder", "Team name" : "Komandos pavadinimas", "Edit" : "Taisyt", + "Cancel" : "Atsisakyti", "Request to join" : "Prašyti prisijungti", - "Create" : "Sukurti", "Your request to join this team is pending approval" : "Jūsų užklausa prisijungti prie šios komandos laukia patvirtinimo", "You are not a member of {circle}" : "Jūs nesate rato {circle} narys", "Members" : "Nariai", @@ -167,6 +168,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} adresatas sėkmingai pridėtas į {name}","{success} adresatai sėkmingai pridėti į {name}","{success} adresatų sėkmingai pridėta į {name}","{success} adresatas sėkmingai pridėtas į {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Pridedamas {success} adresatas į {name}","Pridedami {success} adresatai į {name}","Pridedama {success} adresatų į {name}","Pridedamas {success} adresatas į {name}"], "_{count} error_::_{count} errors_" : ["{count} klaida","{count} klaidos","{count} klaidų","{count} klaida"], + "Close" : "Užverti", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["%n adresatas importuojamas į {addressbook}","%n adresatai importuojami į {addressbook}","%n adresatų importuojama į {addressbook}","%n adresatas importuojamas į {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Atliktas %n adresato importavimas į {addressbook}","Atliktas %n adresatų importavimas į {addressbook}","Atliktas %n adresatų importavimas į {addressbook}","Atliktas %n adresato importavimas į {addressbook}"], "Import" : "Importuoti", @@ -265,8 +267,6 @@ "Unable to delete contact" : "Nepavyko ištrinti adresato", "Loading contacts …" : "Įkeliami adresatai…", "Loading …" : "Įkeliama…", - "General settings" : "Bendri nustatymai", - "(refreshed once per week)" : "(įkeliama iš naujo kartą per savaitę)", "{addressbookname} (Disabled)" : "{addressbookname} (Išjungta)", "Search contacts …" : "Ieškoti adresatų…", "Loading members list …" : "Įkeliamas narių sąrašas…", diff --git a/l10n/lv.js b/l10n/lv.js index 7fc48a299..b353c9826 100644 --- a/l10n/lv.js +++ b/l10n/lv.js @@ -9,6 +9,7 @@ OC.L10N.register( "Copy to full name" : "Kopēt uz pilnu vārdu", "Omit year" : "Izlaiduma gads", "Contacts settings" : "Kontaktpersonu iestatījumi", + "General settings" : "Vispārīgi iestatījumi", "Address books" : "Adrešu grāmatas", "Rename" : "Pārdēvēt", "Send email" : "Sūtīt e-pasta ziņojumu", @@ -32,16 +33,15 @@ OC.L10N.register( "Import into the {addressbookName} address book" : "Ievietot adrešu grāmatā \"{addressbookName}\"", "Import from files" : "Ievietot no datnēm", "Importing is disabled because there are no address books available" : "Ievietošana ir atspējota, jo nav pieejama neviena adrešu grāmata", - "Cancel" : "Atcelt", "Add" : "Pievienot", "First name" : "Vārds", "Last name" : "Uzvārds", "Display name" : "Ekrāna vārds", "Last modified" : "Pēdējoreiz izmainīts", + "Sort by {sorting}" : "Kārtot pēc {sorting}", "Save" : "Saglabāt", - "Close" : "Aizvērt", "Edit" : "Labot", - "Create" : "Izveidot", + "Cancel" : "Atcelt", "More fields" : "Vairāk lauku", "Invalid image" : "Nederīgs attēls", "Pick an avatar" : "Izvēlēties avataru", @@ -69,6 +69,7 @@ OC.L10N.register( "Unable to create the contact." : "Nevar izveidot kontaktpersonu.", "Contact not found" : "Kontaktpersona nav atrasta!", "New contact" : "Jauna kontaktpersona", + "Close" : "Aizvērt", "Import" : "Ievieto", "Not grouped" : "Negrupēts", "Teams are groups of people that you can create yourself and with whom you can share data. They can be made up of other accounts or groups of accounts of the Nextcloud instance, but also of contacts from your address book or even external people by simply entering their e-mail addresses." : "Komandas ir cilvēku kopas, kuras vari izveidot un ar kurām vari kopīgot datus· Tās var būt veidotas no citiem Nextcloud servera kontiem vai kontu kopām, kā arī kontaktpersonām no Tavas adrešu grāmatas vai pat ārējiem cilvēkiem, vienkārši ievadot to e-pasta adresi.", @@ -136,7 +137,6 @@ OC.L10N.register( "_{failed} contact failed to be read_::_{failed} contacts failed to be read_" : ["{failed} kontaktpersonas neizdevās nolasīt","{failed} kontaktpersonu neizdevās nolasīt","{failed} kontaktpersonas neizdevās nolasīt"], "An error has occurred in team(s). Check the console for more details." : "Atgadījās kļūda komandā(s). Jāpārbauda konsole, lai iegūtu vairāk informācijas.", "Loading …" : "Notiek ielāde ...", - "General settings" : "Vispārīgi iestatījumi", "{addressbookname} (Disabled)" : "{addressbookname} (atspējota)", "Search contacts …" : "Meklēt kontaktpersonas…" }, diff --git a/l10n/lv.json b/l10n/lv.json index 02bd237d0..ecb0ebc22 100644 --- a/l10n/lv.json +++ b/l10n/lv.json @@ -7,6 +7,7 @@ "Copy to full name" : "Kopēt uz pilnu vārdu", "Omit year" : "Izlaiduma gads", "Contacts settings" : "Kontaktpersonu iestatījumi", + "General settings" : "Vispārīgi iestatījumi", "Address books" : "Adrešu grāmatas", "Rename" : "Pārdēvēt", "Send email" : "Sūtīt e-pasta ziņojumu", @@ -30,16 +31,15 @@ "Import into the {addressbookName} address book" : "Ievietot adrešu grāmatā \"{addressbookName}\"", "Import from files" : "Ievietot no datnēm", "Importing is disabled because there are no address books available" : "Ievietošana ir atspējota, jo nav pieejama neviena adrešu grāmata", - "Cancel" : "Atcelt", "Add" : "Pievienot", "First name" : "Vārds", "Last name" : "Uzvārds", "Display name" : "Ekrāna vārds", "Last modified" : "Pēdējoreiz izmainīts", + "Sort by {sorting}" : "Kārtot pēc {sorting}", "Save" : "Saglabāt", - "Close" : "Aizvērt", "Edit" : "Labot", - "Create" : "Izveidot", + "Cancel" : "Atcelt", "More fields" : "Vairāk lauku", "Invalid image" : "Nederīgs attēls", "Pick an avatar" : "Izvēlēties avataru", @@ -67,6 +67,7 @@ "Unable to create the contact." : "Nevar izveidot kontaktpersonu.", "Contact not found" : "Kontaktpersona nav atrasta!", "New contact" : "Jauna kontaktpersona", + "Close" : "Aizvērt", "Import" : "Ievieto", "Not grouped" : "Negrupēts", "Teams are groups of people that you can create yourself and with whom you can share data. They can be made up of other accounts or groups of accounts of the Nextcloud instance, but also of contacts from your address book or even external people by simply entering their e-mail addresses." : "Komandas ir cilvēku kopas, kuras vari izveidot un ar kurām vari kopīgot datus· Tās var būt veidotas no citiem Nextcloud servera kontiem vai kontu kopām, kā arī kontaktpersonām no Tavas adrešu grāmatas vai pat ārējiem cilvēkiem, vienkārši ievadot to e-pasta adresi.", @@ -134,7 +135,6 @@ "_{failed} contact failed to be read_::_{failed} contacts failed to be read_" : ["{failed} kontaktpersonas neizdevās nolasīt","{failed} kontaktpersonu neizdevās nolasīt","{failed} kontaktpersonas neizdevās nolasīt"], "An error has occurred in team(s). Check the console for more details." : "Atgadījās kļūda komandā(s). Jāpārbauda konsole, lai iegūtu vairāk informācijas.", "Loading …" : "Notiek ielāde ...", - "General settings" : "Vispārīgi iestatījumi", "{addressbookname} (Disabled)" : "{addressbookname} (atspējota)", "Search contacts …" : "Meklēt kontaktpersonas…" },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" diff --git a/l10n/ms_MY.js b/l10n/ms_MY.js index 55aefd6c7..4093e8d90 100644 --- a/l10n/ms_MY.js +++ b/l10n/ms_MY.js @@ -6,14 +6,13 @@ OC.L10N.register( "Delete" : "Padam", "Download" : "Muat turun", "can edit" : "boleh mengubah", - "Cancel" : "Batal", "Add" : "Tambah", "Save" : "Simpan", - "Close" : "Tutup", - "Create" : "Buat", + "Cancel" : "Batal", "Name" : "Nama", "Title" : "Judul", "Pending" : "Dalam proses", + "Close" : "Tutup", "Import" : "Import", "User" : "User", "Group" : "Group", diff --git a/l10n/ms_MY.json b/l10n/ms_MY.json index f3331c55c..fc516654d 100644 --- a/l10n/ms_MY.json +++ b/l10n/ms_MY.json @@ -4,14 +4,13 @@ "Delete" : "Padam", "Download" : "Muat turun", "can edit" : "boleh mengubah", - "Cancel" : "Batal", "Add" : "Tambah", "Save" : "Simpan", - "Close" : "Tutup", - "Create" : "Buat", + "Cancel" : "Batal", "Name" : "Nama", "Title" : "Judul", "Pending" : "Dalam proses", + "Close" : "Tutup", "Import" : "Import", "User" : "User", "Group" : "Group", diff --git a/l10n/nb.js b/l10n/nb.js index a5b0d41b8..b254931f8 100644 --- a/l10n/nb.js +++ b/l10n/nb.js @@ -22,7 +22,9 @@ OC.L10N.register( "Leave team" : "Forlat lag", "Delete team" : "Slett lag", "Contacts settings" : "Kontaktinnstillinger", + "General settings" : "Generelle innstillinger", "Update avatars from social media" : "Oppdater avatarer fra sosiale medier", + "(refreshed once per week)" : "(oppdateres en gang i uken)", "Address books" : "Adressebøker", "Rename" : "Gi nytt navn", "Export" : "Eksporter", @@ -72,7 +74,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "Import er deaktivert fordi det ikke finnes noen tilgjengelige adressebøker", "An error occurred, unable to create the address book" : "Det oppstod en feil, kan ikke opprette adresseboken", "Add new address book" : "Legg til ny adressebok", - "Cancel" : "Avbryt", "Add" : "Legg til", "First name" : "Fornavn", "Last name" : "Etternavn", @@ -80,6 +81,7 @@ OC.L10N.register( "Phonetic last name" : "Fonetisk etternavn", "Display name" : "Visningsnavn", "Last modified" : "Sist endret", + "Sort by {sorting}" : "Sorter etter {sorting}", "Manages" : "Håndterer", "Oversees" : "Overvåker", "An error happened during the config change" : "Det oppstod en feil under konfigurasjonsendringen", @@ -88,14 +90,13 @@ OC.L10N.register( "Save" : "Lagre", "Change unique password" : "Endre unikt passord", "Failed to save password. Please try again later." : "Lagring av passord feilet. Forsøk igjen senere.", - "Close" : "Lukk", "There is no description for this team" : "Det er ingen beskrivelse for dette laget", "Enter a description for the team" : "Angi en beskrivelse for laget", "Folder" : "Folder", "Team name" : "Lagnavn", "Edit" : "Redigere", + "Cancel" : "Avbryt", "Request to join" : "Spør om å bli med", - "Create" : "Ny", "Your request to join this team is pending approval" : "Forespørselen din om å bli med i dette laget venter på godkjenning", "You are not a member of {circle}" : "Du er ikke medlem av {circle}", "Members" : "Medlemmer", @@ -191,6 +192,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} kontakt lagt til {name}","{success} kontakter lagt til {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Legger til {success} kontakt til {name}","Legger til {success} kontakter til {name}"], "_{count} error_::_{count} errors_" : ["{count} feil","{count} feil"], + "Close" : "Lukk", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importerer %n kontakt til {addressbook}","Importerer %n kontakter til {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Ferdig med å importere %n kontakt til {addressbook}","Ferdig med å importere %n kontakter til {addressbook}"], "Import" : "Importer", @@ -295,8 +297,6 @@ OC.L10N.register( "Unable to delete contact" : "Ikke i stand til å slette kontakt", "Loading contacts …" : "Laster inn kontakter…", "Loading …" : "Laster...", - "General settings" : "Generelle innstillinger", - "(refreshed once per week)" : "(oppdateres en gang i uken)", "{addressbookname} (Disabled)" : "{addressbookname} (Deaktivert)", "Unique password …" : "Unikt passord...", "Search contacts …" : "Søk etter kontakter…", diff --git a/l10n/nb.json b/l10n/nb.json index 02c042b7d..c86a5abb1 100644 --- a/l10n/nb.json +++ b/l10n/nb.json @@ -20,7 +20,9 @@ "Leave team" : "Forlat lag", "Delete team" : "Slett lag", "Contacts settings" : "Kontaktinnstillinger", + "General settings" : "Generelle innstillinger", "Update avatars from social media" : "Oppdater avatarer fra sosiale medier", + "(refreshed once per week)" : "(oppdateres en gang i uken)", "Address books" : "Adressebøker", "Rename" : "Gi nytt navn", "Export" : "Eksporter", @@ -70,7 +72,6 @@ "Importing is disabled because there are no address books available" : "Import er deaktivert fordi det ikke finnes noen tilgjengelige adressebøker", "An error occurred, unable to create the address book" : "Det oppstod en feil, kan ikke opprette adresseboken", "Add new address book" : "Legg til ny adressebok", - "Cancel" : "Avbryt", "Add" : "Legg til", "First name" : "Fornavn", "Last name" : "Etternavn", @@ -78,6 +79,7 @@ "Phonetic last name" : "Fonetisk etternavn", "Display name" : "Visningsnavn", "Last modified" : "Sist endret", + "Sort by {sorting}" : "Sorter etter {sorting}", "Manages" : "Håndterer", "Oversees" : "Overvåker", "An error happened during the config change" : "Det oppstod en feil under konfigurasjonsendringen", @@ -86,14 +88,13 @@ "Save" : "Lagre", "Change unique password" : "Endre unikt passord", "Failed to save password. Please try again later." : "Lagring av passord feilet. Forsøk igjen senere.", - "Close" : "Lukk", "There is no description for this team" : "Det er ingen beskrivelse for dette laget", "Enter a description for the team" : "Angi en beskrivelse for laget", "Folder" : "Folder", "Team name" : "Lagnavn", "Edit" : "Redigere", + "Cancel" : "Avbryt", "Request to join" : "Spør om å bli med", - "Create" : "Ny", "Your request to join this team is pending approval" : "Forespørselen din om å bli med i dette laget venter på godkjenning", "You are not a member of {circle}" : "Du er ikke medlem av {circle}", "Members" : "Medlemmer", @@ -189,6 +190,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} kontakt lagt til {name}","{success} kontakter lagt til {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["Legger til {success} kontakt til {name}","Legger til {success} kontakter til {name}"], "_{count} error_::_{count} errors_" : ["{count} feil","{count} feil"], + "Close" : "Lukk", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importerer %n kontakt til {addressbook}","Importerer %n kontakter til {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Ferdig med å importere %n kontakt til {addressbook}","Ferdig med å importere %n kontakter til {addressbook}"], "Import" : "Importer", @@ -293,8 +295,6 @@ "Unable to delete contact" : "Ikke i stand til å slette kontakt", "Loading contacts …" : "Laster inn kontakter…", "Loading …" : "Laster...", - "General settings" : "Generelle innstillinger", - "(refreshed once per week)" : "(oppdateres en gang i uken)", "{addressbookname} (Disabled)" : "{addressbookname} (Deaktivert)", "Unique password …" : "Unikt passord...", "Search contacts …" : "Søk etter kontakter…", diff --git a/l10n/nl.js b/l10n/nl.js index 5c0407867..e9bb85523 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -23,7 +23,9 @@ OC.L10N.register( "Leave team" : "Team verlaten", "Delete team" : "Team verwijderen", "Contacts settings" : "Contacten instellingen", + "General settings" : "Algemene instellingen", "Update avatars from social media" : "Werk de avatars van sociale media bij", + "(refreshed once per week)" : "(wordt eens per week bijgewerkt)", "Address books" : "Adresboeken", "Rename" : "Hernoemen", "Export" : "Export", @@ -67,7 +69,6 @@ OC.L10N.register( "Importing is disabled because there are no address books available" : "Import is uitgeschakeld omdat er geen adresboek beschikbaar is", "An error occurred, unable to create the address book" : "Er trad een fout op, kon het adresboek niet creëren", "Add new address book" : "Nieuw adresboek toevoegen", - "Cancel" : "Annuleren", "Add" : "Toevoegen", "First name" : "Voornaam", "Last name" : "Achternaam", @@ -75,16 +76,16 @@ OC.L10N.register( "Phonetic last name" : "Fonetische achternaam", "Display name" : "Weergavenaam", "Last modified" : "Laatst gewijzigd", + "Sort by {sorting}" : "Sorteren op {sorting}", "Manages" : "Leid", "Oversees" : "Ziet toe op", "An error happened during the config change" : "Er trad een fout op bij de config-wijziging", "Save" : "Opslaan", "Change unique password" : "Wijzig uniek wachtwoord", "Failed to save password. Please try again later." : "Niet gelukt om het wachtwoord op te slaan. Probeer aub later nogmaals.", - "Close" : "Sluit", - "Conversation name" : "Gespreksnaam", "Team name" : "Team naam", "Edit" : "Bewerken", + "Cancel" : "Annuleren", "Request to join" : "Aanvraag voor deelname", "Your request to join this team is pending approval" : "Je verzoek tot deelname aan het team wacht op bevestiging", "You are not a member of {circle}" : "U bent geen lid van [circle]", @@ -176,6 +177,7 @@ OC.L10N.register( "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contactpersoon toegevoegd aan {name}","{success} contactpersonen toegevoegd aan {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["{success} contactpersonen aan het toevoegen aan {name}","{success} contactpersonen aan het toevoegen aan {name}"], "_{count} error_::_{count} errors_" : ["{count} fout","{count} fouten"], + "Close" : "Sluit", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importeren %n contactpersoon in {addressbook}","Importeren %n contactpersonen in {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Klaar met het importeren van %n contactpersoon in {addressbook}","Klaar met het importeren van %n contactpersonen in {addressbook}"], "Import" : "Importeren", @@ -262,8 +264,6 @@ OC.L10N.register( "Unable to delete contact" : "Kan contactpersoon niet verwijderen", "Loading contacts …" : "Contacten laden ...", "Loading …" : "Laden ...", - "General settings" : "Algemene instellingen", - "(refreshed once per week)" : "(wordt eens per week bijgewerkt)", "{addressbookname} (Disabled)" : "{addressbookname} (Uitgeschakeld)", "Unique password …" : "Uniek wachtwoord ...", "Search contacts …" : "Zoek contacten ...", diff --git a/l10n/nl.json b/l10n/nl.json index 3e3a7bde6..c789fbdb0 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -21,7 +21,9 @@ "Leave team" : "Team verlaten", "Delete team" : "Team verwijderen", "Contacts settings" : "Contacten instellingen", + "General settings" : "Algemene instellingen", "Update avatars from social media" : "Werk de avatars van sociale media bij", + "(refreshed once per week)" : "(wordt eens per week bijgewerkt)", "Address books" : "Adresboeken", "Rename" : "Hernoemen", "Export" : "Export", @@ -65,7 +67,6 @@ "Importing is disabled because there are no address books available" : "Import is uitgeschakeld omdat er geen adresboek beschikbaar is", "An error occurred, unable to create the address book" : "Er trad een fout op, kon het adresboek niet creëren", "Add new address book" : "Nieuw adresboek toevoegen", - "Cancel" : "Annuleren", "Add" : "Toevoegen", "First name" : "Voornaam", "Last name" : "Achternaam", @@ -73,16 +74,16 @@ "Phonetic last name" : "Fonetische achternaam", "Display name" : "Weergavenaam", "Last modified" : "Laatst gewijzigd", + "Sort by {sorting}" : "Sorteren op {sorting}", "Manages" : "Leid", "Oversees" : "Ziet toe op", "An error happened during the config change" : "Er trad een fout op bij de config-wijziging", "Save" : "Opslaan", "Change unique password" : "Wijzig uniek wachtwoord", "Failed to save password. Please try again later." : "Niet gelukt om het wachtwoord op te slaan. Probeer aub later nogmaals.", - "Close" : "Sluit", - "Conversation name" : "Gespreksnaam", "Team name" : "Team naam", "Edit" : "Bewerken", + "Cancel" : "Annuleren", "Request to join" : "Aanvraag voor deelname", "Your request to join this team is pending approval" : "Je verzoek tot deelname aan het team wacht op bevestiging", "You are not a member of {circle}" : "U bent geen lid van [circle]", @@ -174,6 +175,7 @@ "_{success} contact added to {name}_::_{success} contacts added to {name}_" : ["{success} contactpersoon toegevoegd aan {name}","{success} contactpersonen toegevoegd aan {name}"], "_Adding {success} contact to {name}_::_Adding {success} contacts to {name}_" : ["{success} contactpersonen aan het toevoegen aan {name}","{success} contactpersonen aan het toevoegen aan {name}"], "_{count} error_::_{count} errors_" : ["{count} fout","{count} fouten"], + "Close" : "Sluit", "_Importing %n contact into {addressbook}_::_Importing %n contacts into {addressbook}_" : ["Importeren %n contactpersoon in {addressbook}","Importeren %n contactpersonen in {addressbook}"], "_Done importing %n contact into {addressbook}_::_Done importing %n contacts into {addressbook}_" : ["Klaar met het importeren van %n contactpersoon in {addressbook}","Klaar met het importeren van %n contactpersonen in {addressbook}"], "Import" : "Importeren", @@ -260,8 +262,6 @@ "Unable to delete contact" : "Kan contactpersoon niet verwijderen", "Loading contacts …" : "Contacten laden ...", "Loading …" : "Laden ...", - "General settings" : "Algemene instellingen", - "(refreshed once per week)" : "(wordt eens per week bijgewerkt)", "{addressbookname} (Disabled)" : "{addressbookname} (Uitgeschakeld)", "Unique password …" : "Uniek wachtwoord ...", "Search contacts …" : "Zoek contacten ...", diff --git a/l10n/pl.js b/l10n/pl.js index 095b0c3a6..4a870cdda 100644 --- a/l10n/pl.js +++ b/l10n/pl.js @@ -392,4 +392,4 @@ OC.L10N.register( "Select chart …" : "Wybierz wykres…", "_Imported %n contact (skipped %d)_::_Imported %n contacts (skipped %d)_" : ["Zaimportowano %n kontakt (pominięto %d)","Zaimportowano %n kontaktów (pominięto %d)","Zaimportowano %n kontaktów (pominięto %d)","Zaimportowano %n kontakty (pominięto %d)"] }, -"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"); +"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"); \ No newline at end of file diff --git a/l10n/sc.js b/l10n/sc.js index 740db9a25..811ef3555 100644 --- a/l10n/sc.js +++ b/l10n/sc.js @@ -218,4 +218,4 @@ OC.L10N.register( "Loading members list …" : "Carrighende s'elencu de is membros ... ", "Add to {circle}" : "Agiunghe a {circle}" }, -"nplurals=2; plural=(n != 1);"); +"nplurals=2; plural=(n != 1);"); \ No newline at end of file diff --git a/l10n/si.js b/l10n/si.js index 2352b62ec..730e7e391 100644 --- a/l10n/si.js +++ b/l10n/si.js @@ -5,11 +5,10 @@ OC.L10N.register( "Rename" : "නැවත නම් කරන්න", "Copy link" : "සබැඳිය පිටපත් කරන්න", "Download" : "බාගන්න", - "Cancel" : "අවලංගු කරන්න", "First name" : "මුල් නම", "Last name" : "අවසන් නම", "Save" : "සුරකින්න", - "Close" : "වසන්න", + "Cancel" : "අවලංගු කරන්න", "Invalid image" : "වලංගු නොවන පින්තූරයකි", "Name" : "නම", "Company" : "සමාගම", @@ -20,6 +19,7 @@ OC.L10N.register( "Select Date" : "දිනය තෝරන්න", "Member" : "සාමාජික", "_{count} error_::_{count} errors_" : ["දෝෂ {count}","දෝෂ {count}"], + "Close" : "වසන්න", "User" : "පරිශීලක", "Group" : "සමූහය", "Email" : "විද්‍යුත් තැපෑල", diff --git a/l10n/si.json b/l10n/si.json index 7d6409e0f..bb11dc6d5 100644 --- a/l10n/si.json +++ b/l10n/si.json @@ -3,11 +3,10 @@ "Rename" : "නැවත නම් කරන්න", "Copy link" : "සබැඳිය පිටපත් කරන්න", "Download" : "බාගන්න", - "Cancel" : "අවලංගු කරන්න", "First name" : "මුල් නම", "Last name" : "අවසන් නම", "Save" : "සුරකින්න", - "Close" : "වසන්න", + "Cancel" : "අවලංගු කරන්න", "Invalid image" : "වලංගු නොවන පින්තූරයකි", "Name" : "නම", "Company" : "සමාගම", @@ -18,6 +17,7 @@ "Select Date" : "දිනය තෝරන්න", "Member" : "සාමාජික", "_{count} error_::_{count} errors_" : ["දෝෂ {count}","දෝෂ {count}"], + "Close" : "වසන්න", "User" : "පරිශීලක", "Group" : "සමූහය", "Email" : "විද්‍යුත් තැපෑල", diff --git a/l10n/sv.js b/l10n/sv.js index 6a2a4a81f..8a716ab84 100644 --- a/l10n/sv.js +++ b/l10n/sv.js @@ -390,4 +390,4 @@ OC.L10N.register( "Select chart …" : "Välj schema ...", "_Imported %n contact (skipped %d)_::_Imported %n contacts (skipped %d)_" : ["Importerade %n kontakt (skippade %d)","Importerade %n kontakter (skippade %d)"] }, -"nplurals=2; plural=(n != 1);"); +"nplurals=2; plural=(n != 1);"); \ No newline at end of file diff --git a/l10n/ta.js b/l10n/ta.js index faa4addf6..5f23028fc 100644 --- a/l10n/ta.js +++ b/l10n/ta.js @@ -7,16 +7,15 @@ OC.L10N.register( "Delete" : "நீக்குக", "Download" : "பதிவிறக்குக", "can edit" : "தொகுக்க முடியும்", - "Cancel" : "இரத்து செய்க", "First name" : "முதல் பெயர்", "Last name" : "கடைசிப் பெயர்", "Save" : "சேமிக்க ", - "Close" : "மூடுக", - "Create" : "உருவாக்குக", + "Cancel" : "இரத்து செய்க", "Name" : "பெயர்", "Title" : "தலைப்பு", "Pending" : "நிலுவையிலுள்ள", "None" : "ஒன்றுமில்லை", + "Close" : "மூடுக", "Import" : "இறக்குமதி", "User" : "User", "Group" : "Group", diff --git a/l10n/ta.json b/l10n/ta.json index a977f91bb..8c5573f21 100644 --- a/l10n/ta.json +++ b/l10n/ta.json @@ -5,16 +5,15 @@ "Delete" : "நீக்குக", "Download" : "பதிவிறக்குக", "can edit" : "தொகுக்க முடியும்", - "Cancel" : "இரத்து செய்க", "First name" : "முதல் பெயர்", "Last name" : "கடைசிப் பெயர்", "Save" : "சேமிக்க ", - "Close" : "மூடுக", - "Create" : "உருவாக்குக", + "Cancel" : "இரத்து செய்க", "Name" : "பெயர்", "Title" : "தலைப்பு", "Pending" : "நிலுவையிலுள்ள", "None" : "ஒன்றுமில்லை", + "Close" : "மூடுக", "Import" : "இறக்குமதி", "User" : "User", "Group" : "Group", diff --git a/l10n/uz.js b/l10n/uz.js index 5d60ab156..786e67090 100644 --- a/l10n/uz.js +++ b/l10n/uz.js @@ -2,20 +2,19 @@ OC.L10N.register( "contacts", { "Contacts" : "Contacts", - "General" : "Umumiy", "Rename" : "Nomini o'zgartirish", "Delete" : "O'chirish", "Download" : "Yuklab olish", - "Cancel" : "Bekor qilish", "Add" : "Qo'shish", "Save" : "Saqlash", - "Close" : "Yopish", + "Cancel" : "Bekor qilish", "Invalid image" : "Invalid image", "Name" : "Name", "Title" : "Sarlavha", "Pending" : "Pending", "None" : "Yo'q", "Member" : "Member", + "Close" : "Yopish", "Admin" : "Admin", "Users" : "Users", "Notes" : "Notes", diff --git a/l10n/uz.json b/l10n/uz.json index 3b6a350d0..38354d426 100644 --- a/l10n/uz.json +++ b/l10n/uz.json @@ -1,19 +1,18 @@ { "translations": { "Contacts" : "Contacts", - "General" : "Umumiy", "Rename" : "Nomini o'zgartirish", "Delete" : "O'chirish", "Download" : "Yuklab olish", - "Cancel" : "Bekor qilish", "Add" : "Qo'shish", "Save" : "Saqlash", - "Close" : "Yopish", + "Cancel" : "Bekor qilish", "Invalid image" : "Invalid image", "Name" : "Name", "Title" : "Sarlavha", "Pending" : "Pending", "None" : "Yo'q", "Member" : "Member", + "Close" : "Yopish", "Admin" : "Admin", "Users" : "Users", "Notes" : "Notes", diff --git a/l10n/zh_CN.js b/l10n/zh_CN.js index f59b454bc..1eaba2563 100644 --- a/l10n/zh_CN.js +++ b/l10n/zh_CN.js @@ -392,4 +392,4 @@ OC.L10N.register( "Select chart …" : "选择图表…", "_Imported %n contact (skipped %d)_::_Imported %n contacts (skipped %d)_" : ["已导入 %n 位联系人(跳过 %d 位)"] }, -"nplurals=1; plural=0;"); +"nplurals=1; plural=0;"); \ No newline at end of file diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index ac43828a3..18f25b1c6 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -6,11 +6,14 @@ */ namespace OCA\Contacts\AppInfo; +use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent; use OCA\Contacts\Capabilities; use OCA\Contacts\Dav\PatchPlugin; use OCA\Contacts\Event\LoadContactsOcaApiEvent; +use OCA\Contacts\Listener\FederatedInviteAcceptedListener; use OCA\Contacts\Listener\LoadContactsFilesActions; use OCA\Contacts\Listener\LoadContactsOcaApi; +use OCA\Contacts\Listener\OcmDiscoveryListener; use OCA\DAV\Events\SabrePluginAddEvent; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCP\AppFramework\App; @@ -18,6 +21,8 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\EventDispatcher\IEventDispatcher; +use OCP\OCM\Events\LocalOCMDiscoveryEvent; +use OCP\OCM\Events\OCMEndpointRequestEvent; class Application extends App implements IBootstrap { public const APP_ID = 'contacts'; @@ -33,8 +38,11 @@ public function __construct() { #[\Override] public function register(IRegistrationContext $context): void { $context->registerCapability(Capabilities::class); + $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadContactsFilesActions::class); $context->registerEventListener(LoadContactsOcaApiEvent::class, LoadContactsOcaApi::class); + $context->registerEventListener(OCMEndpointRequestEvent::class, FederatedInviteAcceptedListener::class); + $context->registerEventListener(LocalOCMDiscoveryEvent::class, OcmDiscoveryListener::class); } #[\Override] diff --git a/lib/Command/DisableOcmInvites.php b/lib/Command/DisableOcmInvites.php new file mode 100644 index 000000000..26efe61d6 --- /dev/null +++ b/lib/Command/DisableOcmInvites.php @@ -0,0 +1,41 @@ +setName('contacts:disable-ocm-invites') + ->setDescription('Disable OCM Invites.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $isEnabled = $this->appConfig->getValueBool('contacts', 'ocm_invites_enabled'); + if (!$isEnabled) { + $output->writeln('OCM Invites already disabled.'); + return self::SUCCESS; + } + + $this->appConfig->setValueBool('contacts', 'ocm_invites_enabled', false); + $this->appConfig->deleteKey('core', ConfigLexicon::OCM_INVITE_ACCEPT_DIALOG); + $output->writeln('OCM Invites successfully disabled.'); + return self::SUCCESS; + } +} diff --git a/lib/Command/EnableOcmInvites.php b/lib/Command/EnableOcmInvites.php new file mode 100644 index 000000000..0d2dfb383 --- /dev/null +++ b/lib/Command/EnableOcmInvites.php @@ -0,0 +1,42 @@ +setName('contacts:enable-ocm-invites') + ->setDescription('Enable OCM Invites.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $isAlreadyEnabled = $this->appConfig->getValueBool('contacts', 'ocm_invites_enabled'); + + if ($isAlreadyEnabled) { + $output->writeln('OCM Invites already enabled.'); + return self::SUCCESS; + } + + $this->appConfig->setValueBool('contacts', 'ocm_invites_enabled', true); + $this->appConfig->setValueString('core', 'ocm_invite_accept_dialog', 'contacts.federated_invites.invite_accept_dialog'); + + $output->writeln('OCM Invites successfully enabled.'); + return self::SUCCESS; + } +} diff --git a/lib/Command/SetMeshProvidersService.php b/lib/Command/SetMeshProvidersService.php new file mode 100644 index 000000000..5e6763083 --- /dev/null +++ b/lib/Command/SetMeshProvidersService.php @@ -0,0 +1,41 @@ +setName('contacts:set-mesh-providers-service'); + $this->addArgument( + 'mesh-providers-service', + InputArgument::REQUIRED, + 'The URL to the OCM Discovery Service' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $disco = $input->getArgument('mesh-providers-service'); + $this->config->setAppValue('contacts', 'mesh_providers_service', $disco); + $output->writeln('OCM Discovery Service successfully configured.'); + return self::SUCCESS; + } +} diff --git a/lib/Controller/FederatedInvitesController.php b/lib/Controller/FederatedInvitesController.php new file mode 100644 index 000000000..3994224ca --- /dev/null +++ b/lib/Controller/FederatedInvitesController.php @@ -0,0 +1,485 @@ +federatedInviteMapper->findOpenInvitesByUid($this->userSession->getUser()->getUID()); + $invites = []; + foreach ($_invites as $invite) { + if ($invite instanceof FederatedInvite) { + array_push( + $invites, + $invite->jsonSerialize() + ); + } + } + return new JSONResponse($invites, Http::STATUS_OK); + } + + /** + * Deletes the invite with the specified token. + * + * @param string $token the token of the invite to delete + * @return JSONResponse with data signature ['token' | 'message'] - the token of the deleted invitation or an error message in case of error + */ + #[NoAdminRequired] + public function deleteInvite(string $token): JSONResponse { + try { + $uid = $this->userSession->getUser()->getUID(); + $invite = $this->federatedInviteMapper->findInviteByTokenAndUidd($token, $uid); + $this->federatedInviteMapper->delete($invite); + return new JSONResponse(['token' => $token], Http::STATUS_OK); + } catch (DoesNotExistException $e) { + $this->logger->error("Could not find invite with token=$token for user with uid=$uid . Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'An unexpected error occurred trying to delete the invite'], Http::STATUS_NOT_FOUND); + } catch (Exception $e) { + $this->logger->error("An unexpected error occurred deleting invite with token=$token. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'An unexpected error occurred trying to delete the invite'], Http::STATUS_NOT_FOUND); + } + } + + /** + * Results in displaying the invite accept dialog upon following the invite link. + * + * @param string $token + * @param string $providerDomain + * @return TemplateResponse + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function inviteAcceptDialog(string $token = '', string $providerDomain = ''): TemplateResponse { + $this->initialStateService->provideInitialState(Application::APP_ID, 'inviteToken', $token); + $this->initialStateService->provideInitialState(Application::APP_ID, 'inviteProvider', $providerDomain); + $this->initialStateService->provideInitialState(Application::APP_ID, 'acceptInviteDialogUrl', FederatedInvitesService::OCM_INVITE_ACCEPT_DIALOG_ROUTE); + + return $this->index(); + } + + /** + * Creates an invitation to exchange contact info with the remote user. + * + * @param string $emailAddress the recipient's (remote user's) email address to send the invitation to. + * @param string $message optional message to send with the invitation. + * @return JSONResponse with data signature ['invite' | 'message'] - the invite url or an error message in case of error. + */ + #[NoAdminRequired] + public function createInvite(string $email, ?string $message): JSONResponse { + if (!isset($email)) { + return new JSONResponse(['message' => 'Recipient email is required'], Http::STATUS_BAD_REQUEST); + } + + // check for existing open invite for the specified email and return 'invite exists' + $uid = $this->userSession->getUser()->getUID(); + $existingInvites = $this->federatedInviteMapper->findOpenInvitesByRecipientEmail( + $uid, + $email, + ); + if (count($existingInvites) > 0) { + $this->logger->error("An open invite already exists for user with uid $uid and for recipient email $email", ['app' => Application::APP_ID]); + return new JSONResponse(['message' => $this->il10->t('An open invite already exists.')], Http::STATUS_CONFLICT); + } + + $invite = new FederatedInvite(); + $invite->setUserId($uid); + $token = UUIDUtil::getUUID(); + $invite->setToken($token); + // created-/expiredAt in seconds + $invite->setCreatedAt($this->timeFactory->now()->getTimestamp()); + $invite->setExpiredAt($this->federatedInvitesService->getInviteExpirationDate($invite->getCreatedAt())); + $invite->setRecipientEmail($email); + $invite->setAccepted(false); + try { + $this->federatedInviteMapper->insert($invite); + } catch (Exception $e) { + $this->logger->error('An unexpected error occurred saving a new invite. Stacktrace: ' . $e->getTraceAsString(), ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'An unexpected error occurred creating the invite.'], Http::STATUS_NOT_FOUND); + } + + $senderProvider = $this->federatedInvitesService->getProviderFQDN(); + /** @var JSONResponse */ + $response = $this->sendEmail($token, $senderProvider, $email, $message); + if ($response->getStatus() !== Http::STATUS_OK) { + // delete invite in case sending the email has failed + try { + $this->federatedInviteMapper->delete($invite); + } catch (Exception $e) { + $this->logger->error("An unexpected error occurred deleting invite with token $token. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'An unexpected error occurred creating the invite.'], Http::STATUS_NOT_FOUND); + } + return $response; + } + + // the new invite url + $inviteUrl = $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('contacts.page.index') . 'ocm-invites/' . $email + ); + return new JSONResponse(['invite' => $inviteUrl], Http::STATUS_OK); + } + + /** + * Accepts the invite and creates a new contact from the inviter. + * On success the user is redirected to the new contact url. + * + * @param string $token the token of the invite + * @param string $provider the provider of the sender of the invite + * @return JSONResponse with data signature ['contact' | 'message'] - the new contact url or an error message in case of error + */ + #[NoAdminRequired] + public function acceptInvite(string $token = '', string $provider = ''): JSONResponse { + if ($token === '' || $provider === '') { + $this->logger->error("Both token and provider must be specified. Received: token=$token, provider=$provider", ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'Both token and provider must be specified.'], Http::STATUS_NOT_FOUND); + } + $localUser = $this->userSession->getUser(); + $recipientProvider = $this->federatedInvitesService->getProviderFQDN(); + $userId = $localUser->getUID(); + $email = $localUser->getEMailAddress(); + $name = $localUser->getDisplayName(); + if ($recipientProvider === '' || $userId === '' || $email === '' || $name === '') { + $this->logger->error("All of these must be set: recipientProvider: $recipientProvider, email: $email, userId: $userId, name: $name", ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'Could not accept invite, user data is incomplete.'], Http::STATUS_NOT_FOUND); + } + try { + // accept the invite by calling provider OCM /invite-accepted + // this returns a response with the following data signature: ['userID', 'email', 'name'] + // @link https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post + $client = $this->httpClient->newClient(); + /** + * @var IOCMProvider $discovered + * + */ + $discovered = $this->discovery->discover($provider); + $capabilities = $discovered->getCapabilities(); + if (in_array('invite-accepted', $capabilities)) { + + $response = $this->discovery->requestRemoteOcmEndpoint( + null, + $provider, + '/invite-accepted', + [ + 'recipientProvider' => $recipientProvider, + 'token' => $token, + 'userID' => $userId, + 'email' => $email, + 'name' => $name + ], + 'POST', + $client + ); + $responseData = $response->getBody(); + $data = json_decode($responseData, true); + + $cloudId = $data['userID'] . '@' . $this->addressHandler->removeProtocolFromUrl($provider); + + $contactRef = $this->federatedInvitesService->createNewContact( + $cloudId, + $data['email'], + $data['name'], + null + ); + if(!isset($contactRef)) { + return new JSONResponse(['message' => 'An unexpected error occurred trying to accept invite.'], Http::STATUS_NOT_FOUND); + } + $key = base64_encode($contactRef); + $contactUrl = $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('contacts.page.index') . $this->il10->t('All contacts') . '/' . $key + ); + return new JSONResponse(['contact' => $contactUrl], Http::STATUS_OK); + } else { + $this->logger->error('Provider: ' . $provider . ' does not support invites.', ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'Provider: ' . $provider . ' does not support invites.'], Http::STATUS_NOT_FOUND); + } + } catch (ContactExistsException $e) { + return new JSONResponse(['message' => 'Contact with cloudID ' . $cloudId . ' already exists.'], Http::STATUS_CONFLICT); + } catch (RequestException $e) { // this should catch OCM API request exceptions + $this->logger->error('/invite-accepted returned an error: ' . $e->getMessage(), ['app' => Application::APP_ID]); + /** + * 400: Invalid or non existing token + * 409: Invite already accepted + */ + $statusCode = $e->getCode(); + switch ($statusCode) { + case Http::STATUS_BAD_REQUEST: + return new JSONResponse(['message' => 'Invalid, non existing or expired token'], $e->getCode()); + case Http::STATUS_CONFLICT: + return new JSONResponse(['message' => 'Invite already accepted'], $e->getCode()); + } + $this->logger->error("An unexpected error occurred accepting invite with token=$token and provider=$provider. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'An unexpected error occurred trying to accept invite.'], Http::STATUS_NOT_FOUND); + } catch (OCMProviderException|OCMRequestException|Exception $e) { + $this->logger->error("An unexpected error occurred accepting invite with token=$token and provider=$provider. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'An unexpected error occurred trying to accept invite'], Http::STATUS_NOT_FOUND); + } + } + + /** + * Resets the creation and expiration dates, and sends a new invite to the recipient. + * + * + */ + #[NoAdminRequired] + public function resendInvite(string $token): JSONResponse { + $invite = $this->federatedInviteMapper->findByToken($token); + $sendDate = date('Y-m-d', $invite->getCreatedAt()); + $invite->setCreatedAt($this->timeFactory->now()->getTimestamp()); + $invite->setExpiredAt($this->federatedInvitesService->getInviteExpirationDate($invite->getCreatedAt())); + $this->federatedInviteMapper->update($invite); + $initiatorDisplayName = $this->userSession->getUser()->getDisplayName(); + // a resend notification that refers to the previously sent invite + $message = $this->il10->t( + 'This is a copy of an invite send to you previously by %1$s on %2$s', + [ + $initiatorDisplayName, + $sendDate + ] + ); + $senderProvider = $this->federatedInvitesService->getProviderFQDN(); + /** @var JSONResponse */ + $response = $this->sendEmail($token, $senderProvider, $invite->getRecipientEmail(), $message); + if ($response->getStatus() !== Http::STATUS_OK) { + $this->logger->error("An unexpected error occurred resending the invite with token $token. HTTP response status: " . $response->getStatus(), ['app' => Application::APP_ID]); + return $response; + } + + // the invite url + $inviteUrl = $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('contacts.page.index') . 'ocm-invites/' . $invite->getRecipientEmail() + ); + return new JSONResponse(['invite' => $inviteUrl], Http::STATUS_OK); + } + + /** + * Do OCM discovery on behalf of VUE frontend to avoid CSRF issues + * @param string $base base url to discover + * @return DataResponse + */ + #[PublicPage] + public function discover(string $base): DataResponse { + $base = trim($base); + if ($base === '') { + return new DataResponse(['error' => 'empty base'], 400); + } + if (!preg_match('#^https?://#i', $base)) { + $base = 'https://' . $base; + } + $base = rtrim($base, '/'); + + /** + * @var OCP\OCM\ICapabilityAwareOCMProvider $provider + * + */ + $provider = $this->discovery->discover($base); + $dialog = $provider->getInviteAcceptDialog(); + if (!empty($dialog)) { + $absolute = preg_match('#^https?://#i', $dialog) ? $dialog : $base . $dialog; + return new DataResponse([ + 'base' => $base, + 'inviteAcceptDialog' => $dialog, + 'inviteAcceptDialogAbsolute' => $absolute, + 'raw' => $provider->jsonSerialize(), + ]); + } elseif (empty($dialog)) { + // We can not check and see, because we have to be logged in here + // so we will just risk it. + $dialog = $base . $this->wayfProvider->getInviteAcceptDialogPath(); + $absolute = preg_match('#^https?://#i', $dialog) ? $dialog : $base . $dialog; + return new DataResponse([ + 'base' => $base, + 'inviteAcceptDialog' => $dialog, + 'inviteAcceptDialogAbsolute' => $absolute, + 'raw' => $provider->jsonSerialize(), + ]); + } + return new DataResponse(['error' => 'OCM discovery failed', 'base' => $base], 404); + } + + /** + * Accepts the invite and creates a new contact from the inviter. + * On success the user is redirected to the new contact url. + * + * @param string $token the token of the invite + * @param string $provider the provider of the sender of the invite + * @return TemplateResponse the WAYF page + */ + #[PublicPage] + #[NoCSRFRequired] + public function wayf(string $token = ''): TemplateResponse { + Util::addScript(Application::APP_ID, 'contacts-wayf'); + Util::addStyle(Application::APP_ID, 'contacts-wayf'); + try { + $federations = $this->wayfProvider->getMeshProvidersFromCache(); + $providerDomain = parse_url($this->urlGenerator->getBaseUrl(), PHP_URL_HOST); + $this->initialStateService->provideInitialState(Application::APP_ID, 'wayf', [ + 'federations' => $federations, + 'providerDomain' => $providerDomain, + 'token' => $token, + ]); + } catch (Exception $e) { + $this->logger->error($e->getMessage() . ' Trace: ' . $e->getTraceAsString(), ['app' => Application::APP_ID]); + } + $template = new TemplateResponse('contacts', 'wayf', [], TemplateResponse::RENDER_AS_GUEST); + return $template; + } + + /** + * @param string $token the invite token + * @param string $senderProvider this provider + * @param string $address the recipient email address to send the invitation to + * @param string $message the optional message to send with the invitation + * @return JSONResponse + */ + private function sendEmail(string $token, string $senderProvider, string $address, string $message): JSONResponse { + /** @var IMessage */ + $email = $this->mailer->createMessage(); + if (!$this->mailer->validateMailAddress($address)) { + $this->logger->error("Could not sent invite, invalid email address '$address'", ['app' => Application::APP_ID]); + return new JSONResponse(['message' => 'Recipient email address is invalid'], Http::STATUS_NOT_FOUND); + } + $email->setTo([$address]); + + $instanceName = $this->defaults->getName(); + $initiatorDisplayName = $this->userSession->getUser()->getDisplayName(); + $senderName = $this->il10->t( + '%1$s via %2$s', + [ + $initiatorDisplayName, + $instanceName + ] + ); + $email->setFrom([Util::getDefaultEmailAddress($instanceName) => $senderName]); + $subject = $this->il10->t('%1$s invites you to exchange cloud IDs', [$initiatorDisplayName]); + $email->setSubject($subject); + + $wayfEndpoint = $this->wayfProvider->getWayfEndpoint(); + if(empty($wayfEndpoint)) { + $this->logger->error("Invalid WAYF endpoint (null).", ['app' => Application::APP_ID]); + return new JSONResponse(['message' => "Could not sent invite."], Http::STATUS_NOT_FOUND); + } + $inviteLink = "$wayfEndpoint?token=$token"; + + $this->logger->debug("message: $message : " . print_r($message, true)); + + $header = $this->il10->t('Hi there,

%1$s invites you to exchange cloud IDs.
', [$initiatorDisplayName]); + $inviteLinkNote = $this->il10->t('
To accept this invite use the following invite link: %1$s
There you will be requested to sign in at your Cloud Provider.
', [$inviteLink]); + $encoded = base64_encode("$token@$senderProvider"); + $inviteDetails = $this->il10->t('
Details:
Invite string: %1$s
token: %2$s
provider: %3$s
', [$encoded, $token, $senderProvider]); + + $messageLineBreaksToHtml = str_replace("\n", "
", $message); + $message = trim($message) === '' ? '' : "
---
$messageLineBreaksToHtml
---
"; + + $body = "$header$message$inviteLinkNote$inviteDetails"; + $email->setHtmlBody($body); + $email->setPlainBody(strip_tags(str_replace(['
', '
', '
'], "\n", $body))); + + /** @var string[] */ + $failedRecipients = $this->mailer->send($email); + if (!empty($failedRecipients)) { + $this->logger->error("Could not sent invite to '$address'", ['app' => Application::APP_ID]); + return new JSONResponse(['message' => "Could not sent invite to '$address'"], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse([], Http::STATUS_OK); + } +} diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 93b7da979..101787feb 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -9,6 +9,7 @@ use OC\App\CompareVersion; use OCA\Contacts\AppInfo\Application; +use OCA\Contacts\Service\FederatedInvitesService; use OCA\Contacts\Service\GroupSharingService; use OCA\Contacts\Service\SocialApiService; use OCP\App\IAppManager; @@ -25,6 +26,7 @@ class PageController extends Controller { public function __construct( IRequest $request, + private FederatedInvitesService $federatedInvitesService, private IConfig $config, private IInitialStateService $initialStateService, private IFactory $languageFactory, @@ -67,6 +69,7 @@ public function index(): TemplateResponse { $isTalkEnabled = $this->appManager->isEnabledForUser('spreed') === true; $isTalkVersionCompatible = $this->compareVersion->isCompatible($talkVersion ? $talkVersion : '0.0.0', 2); + $isOcmInvitesEnabled = $this->federatedInvitesService->isOcmInvitesEnabled(); $this->initialStateService->provideInitialState(Application::APP_ID, 'isGroupSharingEnabled', $isGroupSharingEnabled); $this->initialStateService->provideInitialState(Application::APP_ID, 'locales', $locales); @@ -77,6 +80,7 @@ public function index(): TemplateResponse { $this->initialStateService->provideInitialState(Application::APP_ID, 'isContactsInteractionEnabled', $isContactsInteractionEnabled); $this->initialStateService->provideInitialState(Application::APP_ID, 'isCirclesEnabled', $isCirclesEnabled && $isCircleVersionCompatible); $this->initialStateService->provideInitialState(Application::APP_ID, 'isTalkEnabled', $isTalkEnabled && $isTalkVersionCompatible); + $this->initialStateService->provideInitialState(Application::APP_ID, 'isOcmInvitesEnabled', $isOcmInvitesEnabled); Util::addStyle(Application::APP_ID, 'contacts-main'); Util::addScript(Application::APP_ID, 'contacts-main'); diff --git a/lib/Cron/UpdateOcmProviders.php b/lib/Cron/UpdateOcmProviders.php new file mode 100644 index 000000000..c2d8db3dc --- /dev/null +++ b/lib/Cron/UpdateOcmProviders.php @@ -0,0 +1,36 @@ +setInterval($this->expire_time); + } + + protected function run($argument) { + $data = $this->wayfProvider->getMeshProviders(); + $data['expires'] = time() + $this->expire_time; + $json = json_encode($data); + $this->appConfig->setValueArray(Application::APP_ID, 'federations_cache', $data, true); + } +} diff --git a/lib/Db/FederatedInvite.php b/lib/Db/FederatedInvite.php new file mode 100644 index 000000000..3bf0ed0b5 --- /dev/null +++ b/lib/Db/FederatedInvite.php @@ -0,0 +1,78 @@ +addType('accepted', Types::BOOLEAN); + $this->addType('acceptedAt', Types::BIGINT); + $this->addType('createdAt', Types::BIGINT); + $this->addType('expiredAt', Types::BIGINT); + $this->addType('recipientEmail', Types::STRING); + $this->addType('recipientName', Types::STRING); + $this->addType('recipientProvider', Types::STRING); + $this->addType('recipientUserId', Types::STRING); + $this->addType('token', Types::STRING); + $this->addType('userId', Types::STRING); + } + + public function jsonSerialize(): array { + return [ + 'accepted' => $this->accepted, + 'acceptedAt' => $this->acceptedAt, + 'createdAt' => $this->createdAt, + 'expiredAt' => $this->expiredAt, + 'recipientEmail' => $this->recipientEmail, + 'recipientName' => $this->recipientName, + 'recipientProvider' => $this->recipientProvider, + 'recipientUserId' => $this->recipientUserId, + 'token' => $this->token, + 'userId' => $this->userId, + ]; + } + +} diff --git a/lib/Db/FederatedInviteMapper.php b/lib/Db/FederatedInviteMapper.php new file mode 100644 index 000000000..b630a96eb --- /dev/null +++ b/lib/Db/FederatedInviteMapper.php @@ -0,0 +1,82 @@ + + */ +class FederatedInviteMapper extends QBMapper { + public const TABLE_NAME = 'federated_invites'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::TABLE_NAME); + } + + /** + * Returns the federated invite with the specified token + * + * @return FederatedInvite + */ + public function findByToken(string $token): FederatedInvite { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('federated_invites') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); + return $this->findEntity($qb); + } + + /** + * Returns all open federated invites for the user with the specified user id + * + * @return array a list of FederatedInvite objects + */ + public function findOpenInvitesByUid(string $userId):array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from(self::TABLE_NAME) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))); + return $this->findEntities($qb); + } + + /** + * Returns all open federated invites for the user with the specified user id and for the specified recipient email + * + * @return array a list of FederatedInvite objects + */ + public function findOpenInvitesByRecipientEmail(string $userId, string $email):array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from(self::TABLE_NAME) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('recipient_email', $qb->createNamedParameter($email))) + ->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))); + return $this->findEntities($qb); + } + + /** + * Returns the federated invite with the specified token for the user with the specified user id + * + * @return FederatedInvite a list of FederatedInvite objects + */ + public function findInviteByTokenAndUidd(string $token, string $userId):FederatedInvite { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from(self::TABLE_NAME) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR))); + return $this->findEntity($qb); + } + +} diff --git a/lib/Exception/ContactExistsException.php b/lib/Exception/ContactExistsException.php new file mode 100644 index 000000000..98b7fd0ad --- /dev/null +++ b/lib/Exception/ContactExistsException.php @@ -0,0 +1,15 @@ + */ +class FederatedInviteAcceptedListener implements IEventListener { + + public function __construct( + private AddressHandler $addressHandler, + private FederatedInvitesService $federatedInvitesService, + private SocialApiService $socialApiService, + private LoggerInterface $logger, + ) { + } + + /** + * Handles the OCMEndpointRequestEvent that is dispatched by the OCMRequestController as response to an OCM request. + * This handler will handle the capability: invite-accepted + * + * @param Event $event an event of type OCMEndpointRequestEvent + * @return void + */ + public function handle(Event $event): void { + if ($event instanceof \OCP\OCM\Events\OCMEndpointRequestEvent + && $event->getRequestedCapability() === 'invite-accepted') { + + /** @var JSONResponse */ + $response = null; + + $recipientProvider = $event->getPayload()['recipientProvider']; + $token = $event->getPayload()['token']; + $userID = $event->getPayload()['userID']; + $email = $event->getPayload()['email']; + $name = $event->getPayload()['name']; + + if ($recipientProvider === '' || $userID === '' || $email === '' || $name === '') { + $this->logger->error("All of these must be set: recipientProvider: $recipientProvider, email: $email, userId: $userID, name: $name", ['app' => Application::APP_ID]); + $response = new JSONResponse(['message' => 'Could not accept invite, user data is incomplete.'], Http::STATUS_NOT_FOUND); + } + + $response = $this->federatedInvitesService->inviteAccepted( + $recipientProvider, + $token, + $userID, + $email, + $name + ); + + $event->setResponse($response); + } + return; + } +} diff --git a/lib/Listener/OcmDiscoveryListener.php b/lib/Listener/OcmDiscoveryListener.php new file mode 100644 index 000000000..782cc2b44 --- /dev/null +++ b/lib/Listener/OcmDiscoveryListener.php @@ -0,0 +1,42 @@ + */ +class OcmDiscoveryListener implements IEventListener { + + public function __construct( + private IAppConfig $appConfig, + private IURLGenerator $urlGenerator, + ) {} + + /** + * This handler will register the capability invite-accepted + * and set the invite accept dialog url. + * + * @param Event $event an event of type LocalOCMDiscoveryEvent + * @return void + */ + public function handle(Event $event): void { + if ($event instanceof LocalOCMDiscoveryEvent) { + $event->addCapability('invite-accepted'); + $inviteAcceptDialog = $this->appConfig->getValueString('core', ConfigLexicon::OCM_INVITE_ACCEPT_DIALOG); + if ($inviteAcceptDialog !== '') { + $event->getProvider()->setInviteAcceptDialog($this->urlGenerator->linkToRouteAbsolute($inviteAcceptDialog)); + } + } + } +} \ No newline at end of file diff --git a/lib/Migration/Version8004Date20260130131217.php b/lib/Migration/Version8004Date20260130131217.php new file mode 100644 index 000000000..b605d8b23 --- /dev/null +++ b/lib/Migration/Version8004Date20260130131217.php @@ -0,0 +1,91 @@ +hasTable($table_name)) { + $table = $schema->createTable($table_name); + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ]); + $table->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + + ]); + // https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/#maximum-url-length-in-different-browsers + // We use the least common denominator, the minimum length supported by browsers + $table->addColumn('recipient_provider', Types::STRING, [ + 'notnull' => false, + 'length' => 2083, + ]); + $table->addColumn('recipient_user_id', Types::STRING, [ + 'notnull' => false, + 'length' => 1024, + ]); + $table->addColumn('recipient_name', Types::STRING, [ + 'notnull' => false, + 'length' => 1024, + ]); + // https://www.directedignorance.com/blog/maximum-length-of-email-address + $table->addColumn('recipient_email', Types::STRING, [ + 'notnull' => false, + 'length' => 320, + ]); + $table->addColumn('token', Types::STRING, [ + 'notnull' => true, + 'length' => 60, + ]); + $table->addColumn('accepted', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false + ]); + $table->addColumn('created_at', Types::BIGINT, [ + 'notnull' => true, + ]); + + $table->addColumn('expired_at', Types::BIGINT, [ + 'notnull' => false, + ]); + + $table->addColumn('accepted_at', Types::BIGINT, [ + 'notnull' => false, + ]); + + $table->addUniqueConstraint(['token']); + $table->setPrimaryKey(['id']); + return $schema; + } + + return null; + } +} diff --git a/lib/Service/FederatedInvitesService.php b/lib/Service/FederatedInvitesService.php new file mode 100644 index 000000000..6e5ec7e28 --- /dev/null +++ b/lib/Service/FederatedInvitesService.php @@ -0,0 +1,183 @@ +appConfig->getValueBool(Application::APP_ID, 'ocm_invites_enabled', FederatedInvitesService::OCM_INVITES_ENABLED_BY_DEFAULT); + } + + /** + * Returns the provider's server FQDN. + * @return string the FQDN + */ + public function getProviderFQDN(): string { + $serverUrl = $this->urlGenerator->getAbsoluteURL('/'); + $fqdn = parse_url($serverUrl)['host']; + return $fqdn; + } + + /** + * Returns the expiration date. + * @param int $creationDate + * @return int the expiration date + */ + public function getInviteExpirationDate(int $creationDate): int { + return $creationDate + self::INVITE_EXPIRATION_PERIOD_SECONDS; + } + + /** + * This is the invite-accepted capability implementation. + */ + public function inviteAccepted(string $recipientProvider, string $token, string $userID, string $email, string $name): JSONResponse { + $this->logger->debug('Processing share invitation for ' . $userID . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name); + + $updated = $this->timeFactory->getTime(); + + if ($token === '') { + $response = new JSONResponse(['message' => 'Invalid or non existing token', 'error' => true], Http::STATUS_BAD_REQUEST); + $response->throttle(); + return $response; + } + + try { + $invitation = $this->federatedInviteMapper->findByToken($token); + } catch (DoesNotExistException) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + + if ($invitation->isAccepted() === true) { + $response = ['message' => 'Invite already accepted', 'error' => true]; + $status = Http::STATUS_CONFLICT; + return new JSONResponse($response, $status); + } + + if ($invitation->getExpiredAt() !== null && $updated > $invitation->getExpiredAt()) { + $response = ['message' => 'Invitation expired', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + return new JSONResponse($response, $status); + } + // Note that there is no user session; local user is the sender of the invite + $localUser = $this->userManager->get($invitation->getUserId()); + if ($localUser === null) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + + $sharedFromEmail = $localUser->getEMailAddress(); + if ($sharedFromEmail === null) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + $sharedFromDisplayName = $localUser->getDisplayName(); + + $response = ['userID' => $localUser->getUID(), 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName]; + $status = Http::STATUS_OK; + + $invitation->setAccepted(true); + $invitation->setRecipientEmail($email); + $invitation->setRecipientName($name); + $invitation->setRecipientProvider($recipientProvider); + $invitation->setRecipientUserId($userID); + $invitation->setAcceptedAt($updated); + $invitation = $this->federatedInviteMapper->update($invitation); + + // now create contact based on the supplied parameters (by the receiver of the invite) + try { + // the ocm address: nextcloud cloud id format + $cloudId = $invitation->getRecipientUserId() . '@' . $this->addressHandler->removeProtocolFromUrl($invitation->getRecipientProvider()); + $contactRef = $this->createNewContact( + $cloudId, + $email, + $name, + $localUser->getUID() + ); + } catch(ContactExistsException $e) { + // this is not an OCM exception + $this->logger->info("Contact with cloud id $cloudId already exists. "); + } + return new JSONResponse($response, $status); + } + + /** + * Creates a new contact and adds it to the address book of the user with the specified userId or, + * if null, the current logged-in user. + * + * @param string cloudId + * @param string email + * @param string name + * @param ?string userId id of the user for which to create the new contact. + * If null, this is the current logged-in user. + * + * @return string the ref of the new contact in the form 'contactURI~PERSONAL_ADDRESSBOOK_URI' + * @throws ContactExistsException + */ + public function createNewContact(string $cloudId, string $email, string $name, ?string $userId): string | null { + $localUserId = $userId ? $userId : $this->userSession->getUser()->getUID(); + $newContact = $this->socialApiService->createContact( + $cloudId, + $email, + $name, + $localUserId, + ); + if (!isset($newContact)) { + $this->logger->error("Error creating contact .", ['app' => Application::APP_ID]); + return null; + } + $this->logger->info('Created new contact with UID: ' . $newContact['UID'] . ' for user with UID: ' . $localUserId, ['app' => Application::APP_ID]); + $contactRef = $newContact['UID'] . '~' . CardDavBackend::PERSONAL_ADDRESSBOOK_URI; + return $contactRef; + } +} diff --git a/lib/Service/SocialApiService.php b/lib/Service/SocialApiService.php index 4b2356bec..a787c8a4a 100644 --- a/lib/Service/SocialApiService.php +++ b/lib/Service/SocialApiService.php @@ -9,7 +9,9 @@ namespace OCA\Contacts\Service; +use Exception; use OCA\Contacts\AppInfo\Application; +use OCA\Contacts\Exception\ContactExistsException; use OCA\Contacts\Service\Social\CompositeSocialProvider; use OCA\DAV\CardDAV\ContactsManager; use OCP\AppFramework\Http; @@ -22,6 +24,7 @@ use OCP\IL10N; use OCP\IURLGenerator; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; class SocialApiService { private $appName; @@ -36,6 +39,7 @@ public function __construct( private IURLGenerator $urlGen, private ITimeFactory $timeFactory, private ImageResizer $imageResizer, + private LoggerInterface $logger, ) { $this->appName = Application::APP_ID; } @@ -215,6 +219,64 @@ public function updateContact(string $addressbookId, string $contactId, ?string return new JSONResponse([], Http::STATUS_OK); } + /** + * Creates a contact and adds it to the address book of the local user with the specified userId, + * unless a contact with the specified cloudId already exists for that local user. + * + * @param {string} cloudId the cloud id of the contact + * @param {string} email the email of the contact + * @param {string} name the name of the contact + * @param {string} userId the uid of the local user + * @throws ContactExistsException + */ + public function createContact(string $cloudId, string $email, string $name, string $userId): ?array { + try { + // Set up the contacts provider for the user with the specified uid + $cm = $this->serverContainer->get(ContactsManager::class); + $cm->setupContactsProvider($this->manager, $userId, $this->urlGen); + + // if contact already exists we throw ContactExistsException + $searchResult = $this->manager->search($cloudId, ['CLOUD']); + if (count($searchResult) > 0) { + $this->logger->info('Contact with cloud id ' . $cloudId . ' already exists.', ['app' => Application::APP_ID]); + throw new ContactExistsException('Contact with cloud id ' . $cloudId . ' already exists.'); + } + + /** @var \OCP\IAddressBook */ + $addressBook = null; + $addressBooks = $this->manager->getUserAddressBooks(); + foreach ($addressBooks as $_addressBook) { + // TODO properly resolve the correct addressbook to add the contact to + // Resolve by uri seems a bit risky ... can we be sure the uri equals 'contacts' ? + // Perhaps add to the first 'non system' addressbook we find ? + // (although we still would like to add to the 'Contacts' addressbook I guess) + if ($_addressBook->getUri() === 'contacts') { + $addressBook = $_addressBook; + break; + } + } + if (!isset($addressBook)) { + $this->logger->error('Contacts address book not found. Unable to add the new contact on invite accepted.', ['app' => Application::APP_ID]); + return null; + } + + $newContact = $this->manager->createOrUpdate( + [ + 'FN' => $name, + 'EMAIL' => $email, + 'CLOUD' => $cloudId, + ], + $addressBook->getKey() + ); + return $newContact; + } catch (ContactExistsException $e) { + throw $e; + } catch (Exception $e) { + $this->logger->error('An exception occurred creating a new contact: ' . $e->getTraceAsString(), ['app' => Application::APP_ID]); + } + return null; + } + /** * checks an addressbook is existing * diff --git a/lib/WayfProvider.php b/lib/WayfProvider.php new file mode 100644 index 000000000..89a2b3a22 --- /dev/null +++ b/lib/WayfProvider.php @@ -0,0 +1,131 @@ +appConfig->getValueString(Application::APP_ID, 'mesh_providers_service'))); + $federations = []; + + $found = []; + foreach ($urls as $url) { + if ($url === '') { + continue; + } + try { + $res = $this->httpClient->newClient()->get($url); + $code = $res->getStatusCode(); + if (!($code >= 200 && $code < 400)) { + continue; + } + $data = json_decode($res->getBody(), true); + $fed = $data['federation'] ?? 'Unknown'; + $federations[$fed] = $federations[$fed] ?? []; + + foreach ($data['servers'] as $prov) { + $fqdn = parse_url($prov['url'], PHP_URL_HOST); + $our_fqdn = parse_url($this->urlGenerator->getAbsoluteUrl('/'))['host']; + if (($our_fqdn == $fqdn) || in_array($fqdn, $found)) { + continue; + } + try { + $disc = $this->discovery->discover($prov['url'], true); + $inviteAcceptDialog = $disc->getInviteAcceptDialog(); + } catch (Exception $e) { + $this->logger->error('Discovery failed for ' . $prov['url'] . ': ' . $e->getMessage(), ['app' => Application::APP_ID]); + continue; + } + if ($inviteAcceptDialog === '') { + // We fall back on Nextcloud default path + $inviteAcceptDialogPath = self::getInviteAcceptDialogPath(); + $inviteAcceptDialog = rtrim($prov['url'], '/') . $inviteAcceptDialogPath; + } + $federations[$fed][] = [ + 'provider' => $disc->getProvider(), + 'name' => $prov['displayName'], + 'fqdn' => $fqdn, + 'inviteAcceptDialog' => $inviteAcceptDialog, + ]; + array_push($found, $fqdn); + } + usort($federations[$fed], fn ($a, $b) => strcmp($a['name'], $b['name'])); + } catch (Exception $e) { + $this->logger->error('Fetch failed for ' . $url . ': ' . $e->getMessage(), ['app' => Application::APP_ID]); + } + } + return $federations; + } + + /** + * Returns all mesh providers from cache if possible. + * + * @return array an array containing all mesh providers + */ + public function getMeshProvidersFromCache(): array { + $data = $this->appConfig->getValueArray(Application::APP_ID, 'federations_cache', [], true); + if (isset($data) && array_key_exists('expires', $data)) { + $this->logger->debug('Cache hit, expires at: ' . $data['expires'], ['app' => Application::APP_ID]); + unset($data['expires']); + } else { + $this->logger->debug('Cache miss: cron job should update providers.', ['app' => Application::APP_ID]); + $data = $this->getMeshProviders(); + } + return $data; + } + + /** + * Returns the WAYF (Where Are You From) login page endpoint to be used in the invitation link. + * Can be read from the app config key 'wayf_endpoint'. + * If not set the endpoint the WAYF page implementation of this app is returned. + * Note that the invitation link still needs the token and provider parameters, eg. "https://?token=$token&provider=$provider" + * @return string|null the WAYF login page endpoint or null if it could not be created + */ + public function getWayfEndpoint(): string|null { + // default wayf endpoint + $defaultWayfEndpoint = $this->urlGenerator->linkToRouteAbsolute(Application::APP_ID . '.federated_invites.wayf'); + return $this->appConfig->getValueString(Application::APP_ID, 'wayf_endpoint', $defaultWayfEndpoint); + } + + /** + * Returns the path of the invite accept dialog route. + * + * @return string + */ + public function getInviteAcceptDialogPath(): string { + return $this->urlGenerator->linkToRoute(Application::APP_ID . '.federated_invites.invite_accept_dialog'); + } +} diff --git a/src/components/AppContent/OcmInvitesContent.vue b/src/components/AppContent/OcmInvitesContent.vue new file mode 100644 index 000000000..5016acbd1 --- /dev/null +++ b/src/components/AppContent/OcmInvitesContent.vue @@ -0,0 +1,123 @@ + + + + + + diff --git a/src/components/AppNavigation/ContactsSettings.vue b/src/components/AppNavigation/ContactsSettings.vue index 5e2099613..e001c990d 100644 --- a/src/components/AppNavigation/ContactsSettings.vue +++ b/src/components/AppNavigation/ContactsSettings.vue @@ -9,30 +9,36 @@ v-model:open="showSettings" :name="t('contacts', 'Contacts settings')" :show-navigation="true"> - - - - - - - - + + + - -
    - -
- +
+
+
    + +
+
+ + +
@@ -40,31 +46,25 @@ - diff --git a/src/components/AppNavigation/RootNavigation.vue b/src/components/AppNavigation/RootNavigation.vue index 5bd9bc3b7..c4e1ea161 100644 --- a/src/components/AppNavigation/RootNavigation.vue +++ b/src/components/AppNavigation/RootNavigation.vue @@ -94,6 +94,24 @@ + + + + + + import { showError } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' +import { CHART_ALL_CONTACTS, CIRCLE_DESC, CONTACTS_SETTINGS, ELLIPSIS_COUNT, GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, GROUP_RECENTLY_CONTACTED, GROUP_ALL_OCM_INVITES, ROUTE_NAME_ALL_OCM_INVITES } from '../../models/constants.ts' + import { NcActionInput as ActionInput, NcActionText as ActionText, @@ -227,10 +247,11 @@ import CircleNavigationItem from './CircleNavigationItem.vue' import ContactsSettings from './ContactsSettings.vue' import GroupNavigationItem from './GroupNavigationItem.vue' import RouterMixin from '../../mixins/RouterMixin.js' -import { CHART_ALL_CONTACTS, CIRCLE_DESC, CONTACTS_SETTINGS, ELLIPSIS_COUNT, GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, GROUP_RECENTLY_CONTACTED } from '../../models/constants.ts' import isCirclesEnabled from '../../services/isCirclesEnabled.js' import isContactsInteractionEnabled from '../../services/isContactsInteractionEnabled.js' import useUserGroupStore from '../../store/userGroup.ts' +import IconAccountSwitchOutline from 'vue-material-design-icons/AccountSwitchOutline.vue' +import isOcmInvitesEnabled from '../../services/isOcmInvitesEnabled.js' export default { name: 'RootNavigation', @@ -247,6 +268,7 @@ export default { Cog, ContactsSettings, GroupNavigationItem, + IconAccountSwitchOutline, IconContact, IconContactFilled, IconUser, @@ -277,6 +299,8 @@ export default { CHART_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, GROUP_RECENTLY_CONTACTED, + GROUP_ALL_OCM_INVITES, + ROUTE_NAME_ALL_OCM_INVITES, // create group isNewGroupMenuOpen: false, @@ -296,6 +320,7 @@ export default { showSettings: false, routeState: 'all', + isOcmInvitesEnabled, } }, @@ -326,6 +351,9 @@ export default { userGroups() { return this.userGroupStore.userGroupList }, + ocmInvites() { + return this.$store.getters.getSortedOcmInvites + }, // list all the contacts that doesn't have a group ungroupedContacts() { diff --git a/src/components/ContactsList.vue b/src/components/ContactsList.vue index bf43f2258..afebfbe92 100644 --- a/src/components/ContactsList.vue +++ b/src/components/ContactsList.vue @@ -234,7 +234,6 @@ export default { contact.isMultiSelected = this.multiSelectedContacts.has(index) } }) - return contactsList }, @@ -391,14 +390,7 @@ export default { */ matchSearch(contact) { if (this.query.trim() !== '') { - try { - return contact.searchData.toString().toLowerCase().search(this.query.trim().toLowerCase()) !== -1 - } catch (e) { - if (e instanceof SyntaxError) { - // this.query likely is an invalid regex (i.e. just `+`) - return contact.searchData.toString().toLowerCase().includes(this.query.trim().toLowerCase()) - } - } + return contact.searchData.toString().toLowerCase().search(this.query.trim().toLowerCase()) !== -1 } return true }, diff --git a/src/components/Ocm/OcmAcceptForm.vue b/src/components/Ocm/OcmAcceptForm.vue new file mode 100644 index 000000000..2013fa487 --- /dev/null +++ b/src/components/Ocm/OcmAcceptForm.vue @@ -0,0 +1,129 @@ + + + + diff --git a/src/components/Ocm/OcmInviteAccept.vue b/src/components/Ocm/OcmInviteAccept.vue new file mode 100644 index 000000000..7e9c8362f --- /dev/null +++ b/src/components/Ocm/OcmInviteAccept.vue @@ -0,0 +1,53 @@ + + + + + + diff --git a/src/components/Ocm/OcmInviteDetails.vue b/src/components/Ocm/OcmInviteDetails.vue new file mode 100644 index 000000000..f0bc2b5aa --- /dev/null +++ b/src/components/Ocm/OcmInviteDetails.vue @@ -0,0 +1,161 @@ + + + + + + + diff --git a/src/components/Ocm/OcmInviteForm.vue b/src/components/Ocm/OcmInviteForm.vue new file mode 100644 index 000000000..abbcfa79e --- /dev/null +++ b/src/components/Ocm/OcmInviteForm.vue @@ -0,0 +1,64 @@ + + + + + + \ No newline at end of file diff --git a/src/components/Ocm/OcmInvitesList.vue b/src/components/Ocm/OcmInvitesList.vue new file mode 100644 index 000000000..135bac8cf --- /dev/null +++ b/src/components/Ocm/OcmInvitesList.vue @@ -0,0 +1,175 @@ + + + + + + + diff --git a/src/components/Ocm/OcmInvitesListItem.vue b/src/components/Ocm/OcmInvitesListItem.vue new file mode 100644 index 000000000..1f7776234 --- /dev/null +++ b/src/components/Ocm/OcmInvitesListItem.vue @@ -0,0 +1,96 @@ + + + + + + diff --git a/src/components/Ocm/Wayf.vue b/src/components/Ocm/Wayf.vue new file mode 100644 index 000000000..9dafc080a --- /dev/null +++ b/src/components/Ocm/Wayf.vue @@ -0,0 +1,129 @@ + + + diff --git a/src/css/wayf.scss b/src/css/wayf.scss new file mode 100644 index 000000000..f94011cc6 --- /dev/null +++ b/src/css/wayf.scss @@ -0,0 +1,37 @@ +#contacts-wayf { + background: var(--color-background-plain); + color: var(--color-background-plain-text); + border-radius: 8px; +} + +.wayf-list { + text-align: start; + margin: 0; + padding: 0; + overflow: hidden; +} + +.wayf-list > li { + align-items: center; + justify-content: space-between; + gap: 0.5rem; + + padding: 0.75rem 1rem; + margin: 0.25rem 0; + + background-color: var(--color-background-dark); + color: var(--color-main-text); + + cursor: pointer; + text-decoration: none; + transition: background-color 0.15s; +} + +.wayf-list > li:hover { + background-color: var(--color-background-darker); +} + +.wayf-list > li:active { + background-color: var(--color-primary); + color: var(--color-primary-text); +} diff --git a/src/mixins/PropertyMixin.js b/src/mixins/PropertyMixin.js index aa116fb6d..95579e937 100644 --- a/src/mixins/PropertyMixin.js +++ b/src/mixins/PropertyMixin.js @@ -107,6 +107,9 @@ export default { deleteProperty() { this.$emit('delete') }, + saveInvite() { + this.$emit('saveInvite') + }, /** * Debounce and send update event to parent diff --git a/src/models/constants.ts b/src/models/constants.ts index 4da974e51..1dcce6f5b 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -4,6 +4,7 @@ */ /// +import { loadState } from '@nextcloud/initial-state' import { translate as t } from '@nextcloud/l10n' import { ShareType } from '@nextcloud/sharing' @@ -29,6 +30,14 @@ export const ROUTE_CIRCLE = 'circle' export const ROUTE_CHART = 'chart' export const ROUTE_USER_GROUP = 'user_group' +const acceptInviteDialogUrl = loadState('contacts', 'acceptInviteDialogUrl', '') +export const ROUTE_INVITE_ACCEPT_DIALOG = acceptInviteDialogUrl +export const ROUTE_NAME_INVITE_ACCEPT_DIALOG = 'invite_accept_dialog' +export const ROUTE_ALL_OCM_INVITES = 'ocm-invites' +export const ROUTE_NAME_ALL_OCM_INVITES = 'all_ocm_invites' +export const ROUTE_NAME_OCM_INVITE = 'ocm_invite' +export const GROUP_ALL_OCM_INVITES = t('contacts', 'All invites') + // Contact settings export const CONTACTS_SETTINGS: DefaultGroup = t('contacts', 'Contacts settings') diff --git a/src/models/contact.js b/src/models/contact.js index c2af2fe80..d7752540c 100644 --- a/src/models/contact.js +++ b/src/models/contact.js @@ -25,7 +25,7 @@ function isEmpty(value) { export const ContactKindProperties = ['KIND', 'X-ADDRESSBOOKSERVER-KIND'] export const MinimalContactProperties = [ - 'EMAIL', 'UID', 'TEL', 'CATEGORIES', 'FN', 'ORG', 'N', 'X-PHONETIC-FIRST-NAME', 'X-PHONETIC-LAST-NAME', 'X-MANAGERSNAME', 'TITLE', 'NOTE', 'RELATED', + 'EMAIL', 'UID', 'CATEGORIES', 'FN', 'ORG', 'N', 'X-PHONETIC-FIRST-NAME', 'X-PHONETIC-LAST-NAME', 'X-MANAGERSNAME', 'TITLE', 'NOTE', 'RELATED', ].concat(ContactKindProperties) export default class Contact { @@ -585,21 +585,7 @@ export default class Contact { * @return {string[]} */ get searchData() { - const MinimalContactPropertiesLower = MinimalContactProperties.map((prop) => prop.toLowerCase()) - const filtered = this.jCal[1] - .filter((x) => MinimalContactPropertiesLower.includes(x[0].toLowerCase())) - .map((x) => { - if (x[0].toLowerCase() === 'tel') { - return this.normalizedTels(x[3]) - } - return x[3].toString() - }) - return filtered - } - - // support numbers in weird formats for searching e.g. +49 (0) 123 456-789 - normalizedTels(number) { - return number.replace(/[^0-9+#]/g, '') + return this.jCal[1].map((x) => x[0] + ':' + x[3]) } /** diff --git a/src/models/ocminvite.ts b/src/models/ocminvite.ts new file mode 100644 index 000000000..cdcd5b6b5 --- /dev/null +++ b/src/models/ocminvite.ts @@ -0,0 +1,59 @@ +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export default class OcmInvite { + + _data: any = {}; + + /** + * Creates an instance of Invitation + * + * @param data + */ + constructor(data: any) { + if (typeof data !== 'object') { + throw new Error('Invalid invitation') + } + + this._data = data + } + + get key(): string { + return this._data.recipientEmail + } + + get token(): string { + return this._data.token + } + + get accepted(): boolean { + return this._data.accepted + } + + get recipientEmail(): string { + return this._data.recipientEmail + } + + get createdAt(): number { + return this._data.createdAt + } + + get expiredAt(): number { + return this._data.expiredAt + } + + /** + * Return the property for the search + * + * @readonly + * @memberof OcmInvite + * @return string + */ + get searchData() { + return this.recipientEmail + } +} diff --git a/src/router/index.js b/src/router/index.js index 491393302..0b61b5e0f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -6,7 +6,7 @@ import { generateUrl } from '@nextcloud/router' import { createRouter, createWebHistory } from 'vue-router' import Contacts from '../views/Contacts.vue' -import { ROUTE_CHART, ROUTE_CIRCLE, ROUTE_USER_GROUP } from '../models/constants.ts' +import { ROUTE_CHART, ROUTE_CIRCLE, ROUTE_USER_GROUP, GROUP_ALL_OCM_INVITES, ROUTE_ALL_OCM_INVITES, ROUTE_NAME_OCM_INVITE, ROUTE_NAME_ALL_OCM_INVITES, ROUTE_NAME_INVITE_ACCEPT_DIALOG, ROUTE_INVITE_ACCEPT_DIALOG } from '../models/constants.ts' // if index.php is in the url AND we got this far, then it's working: // let's keep using index.php in the url @@ -27,6 +27,23 @@ export default createRouter({ params: { selectedGroup: t('contacts', 'All contacts') }, }, children: [ + { + path: `/${ROUTE_ALL_OCM_INVITES}`, + name: ROUTE_NAME_ALL_OCM_INVITES, + component: Contacts, + meta: { selectedGroup: GROUP_ALL_OCM_INVITES }, + }, + { + path: `/${ROUTE_ALL_OCM_INVITES}/:selectedInvite`, + name: ROUTE_NAME_OCM_INVITE, + component: Contacts, + meta: { selectedGroup: GROUP_ALL_OCM_INVITES }, + }, + { + path: ROUTE_INVITE_ACCEPT_DIALOG, + name: ROUTE_NAME_INVITE_ACCEPT_DIALOG, + component: Contacts, + }, { path: `/${ROUTE_CHART}/:selectedChart`, name: 'chart', diff --git a/src/services/isOcmInvitesEnabled.js b/src/services/isOcmInvitesEnabled.js new file mode 100644 index 000000000..555d7479b --- /dev/null +++ b/src/services/isOcmInvitesEnabled.js @@ -0,0 +1,9 @@ +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { loadState } from '@nextcloud/initial-state' + +const isOcmInvitesEnabled = loadState('contacts', 'isOcmInvitesEnabled', false) +export default isOcmInvitesEnabled diff --git a/src/store/index.js b/src/store/index.js index ef220836e..4b2bb705f 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -10,6 +10,7 @@ import circles from './circles.js' import contacts from './contacts.js' import groups from './groups.js' import importState from './importState.js' +import ocminvites from './ocminvites.js' const mutations = {} @@ -18,6 +19,7 @@ const modules = { contacts, groups, importState, + ocminvites, } // If circles is enabled let's init the store diff --git a/src/store/ocminvites.js b/src/store/ocminvites.js new file mode 100644 index 000000000..df310ee1b --- /dev/null +++ b/src/store/ocminvites.js @@ -0,0 +1,123 @@ +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' +import logger from '../services/logger.js' +import OcmInvite from '../models/ocminvite.ts' + +const sortData = (a, b) => { + return a.key.localeCompare(b.key) +} + +const state = { + // Using objects for performance + // https://codepen.io/skjnldsv/pen/ZmKvQo + ocmInvites: {}, + sortedOcmInvites: [], + orderKey: 'recipientEmail', +} + +const getters = { + getOcmInvite: (state) => (key) => state.ocmInvites[key], + getOcmInvites: state => state.ocmInvites, + getSortedOcmInvites: state => state.sortedOcmInvites, +} + +const actions = { + fetchOcmInvites(context) { + axios.get(generateUrl('/apps/contacts/ocm/invitations')) + .then(response => { + context.commit('appendInvites', response.data) + context.commit('sortInvites', response.data) + }) + .catch((error) => { + logger.error('Error fetching OCM invites: ' + error) + }) + }, + async deleteOcmInvite(context, invite) { + const token = invite.token + const url = generateUrl('/apps/contacts/ocm/invitations/{token}', { token: token }) + axios.delete(url) + .then(response => { + context.commit('deleteOcmInvite', invite.key) + }) + .catch((error) => { + logger.error('Error deleting OCM invite with token ' + token) + }) + }, + async resendOcmInvite(context, invite) { + const token = invite.token + const url = generateUrl('/apps/contacts/ocm/invitations/{token}/resend', { token: token }) + const response = await axios.patch(url) + .then(response => { + return response + }) + .catch((error) => { + logger.error('Error resending OCM invite with token ' + token) + throw error + }) + return response + }, + async newOcmInvite(context, invite) { + const url = generateUrl('/apps/contacts/ocm/invitations') + const response = await axios.post(url, invite) + .then(response => { + return response + }) + .catch((error) => { + logger.error('Error creating a new OCM invite for ' + invite.email) + throw error + }) + return response + } +} + +const mutations = { + + /** + * Store raw OCM invites into state + * Used by the first invite fetch + * + * @param {object} state Default state + * @param {Array} invites OCM invites + */ + appendInvites(state, invites = [] ) { + state.ocmInvites = invites.reduce(function(list, _invite) { + const invite = new OcmInvite(_invite) + if (invite.token) { // we should at least have a token + list[invite.key] = invite + } else { + console.error('Invalid invite object', invite) + } + return list + }, state.ocmInvites) + }, + /** + * Sort the OCM invites list. Filters have terrible performances. + * We do not want to run the sorting function every time. + * Let's only run it on additions and create an index + * + * @param {object} state the store data + */ + sortInvites(state) { + state.sortedOcmInvites = Object.values(state.ocmInvites) + .map(invite => { return { key: invite.key, value: invite[state.orderKey] } }) + .sort(sortData) + }, + + /** + * Deletes the invite with the specified key from the OCM invites list + * + * @param {object} state + * @param {string} key + */ + deleteOcmInvite(state, key) { + const index = state.sortedOcmInvites.findIndex(search => search.key === key) + state.sortedOcmInvites.splice(index, 1) + delete state.ocmInvites[key] + } +} + +export default { state, getters, actions, mutations } diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue index befd9f7dc..9ae5e3be1 100644 --- a/src/views/Contacts.vue +++ b/src/views/Contacts.vue @@ -6,13 +6,15 @@