From eb9c06d22553830a03ba370b5e3c2ba76231c137 Mon Sep 17 00:00:00 2001 From: mup Date: Mon, 26 Feb 2024 18:05:58 +0100 Subject: [PATCH] Add new fields to Contact This commit add new fields described in #6590 to the model and adapts the ContactEditor and ContactViewer to handle the new fields. --- .../tutanota/AndroidMobileContactsFacade.kt | 756 +++++++----------- .../tutao/tutanota/contacts/AndroidContact.kt | 165 +++- .../tutao/tutanota/contacts/ContactEnums.kt | 91 +++ .../generated_ipc/ContactCustomDateType.kt | 5 + .../ContactMessengerHandleType.kt | 5 + .../generated_ipc/ContactRelationshipType.kt | 5 + .../generated_ipc/ContactWebsiteType.kt | 5 + .../MobileContactsFacadeReceiveDispatcher.kt | 2 +- .../generated_ipc/StructuredContact.kt | 13 + .../generated_ipc/StructuredCustomDate.kt | 15 + .../StructuredMessengerHandle.kt | 15 + .../generated_ipc/StructuredRelationship.kt | 15 + .../generated_ipc/StructuredWebsite.kt | 15 + .../Contacts/IosMobileContactsFacade.swift | 16 +- .../Contacts/StructuredContactTypes.swift | 71 ++ .../GeneratedIpc/StructuredContact.swift | 13 + .../GeneratedIpc/StructuredCustomDate.swift | 8 + .../StructuredMessengerHandle.swift | 8 + .../GeneratedIpc/StructuredRelationship.swift | 8 + .../GeneratedIpc/StructuredWebsite.swift | 8 + ipc-schema/types/ContactCustomDateType.json | 8 + .../types/ContactMessengerHandleType.json | 8 + ipc-schema/types/ContactRelationshipType.json | 8 + ipc-schema/types/ContactWebsiteType.json | 8 + ipc-schema/types/StructuredContact.json | 15 +- ipc-schema/types/StructuredCustomDate.json | 9 + .../types/StructuredMessengerHandle.json | 9 + ipc-schema/types/StructuredRelationship.json | 9 + ipc-schema/types/StructuredWebsite.json | 9 + schemas/tutanota.json | 60 ++ src/api/common/TutanotaConstants.ts | 41 +- src/api/entities/gossip/ModelInfo.ts | 2 +- src/api/entities/tutanota/ModelInfo.ts | 4 +- src/api/entities/tutanota/TypeModels.js | 575 ++++++++++--- src/api/entities/tutanota/TypeRefs.ts | 80 ++ .../worker/offline/OfflineStorageMigrator.ts | 3 +- .../worker/offline/migrations/tutanota-v67.ts | 12 + src/calendar/view/CalendarEventBubble.ts | 4 +- src/contacts/ContactAggregateEditor.ts | 15 +- src/contacts/ContactEditor.ts | 355 +++++++- src/contacts/VCardImporter.ts | 13 + src/contacts/model/ContactUtils.ts | 96 ++- .../model/NativeContactsSyncManager.ts | 69 +- src/contacts/view/ContactGuiUtils.ts | 79 +- src/contacts/view/ContactListEntryViewer.ts | 11 + src/contacts/view/ContactMergeView.ts | 4 +- src/contacts/view/ContactView.ts | 43 +- src/contacts/view/ContactViewer.ts | 176 +++- src/gui/base/Dropdown.ts | 2 +- src/gui/base/NavButton.ts | 10 +- src/gui/base/RecipientButton.ts | 2 +- src/mail/model/MailUtils.ts | 11 + src/misc/TranslationKey.ts | 27 + .../generatedipc/ContactCustomDateType.ts | 3 + .../ContactMessengerHandleType.ts | 3 + .../generatedipc/ContactRelationshipType.ts | 3 + .../common/generatedipc/ContactWebsiteType.ts | 3 + .../common/generatedipc/StructuredContact.ts | 17 + .../generatedipc/StructuredCustomDate.ts | 8 + .../generatedipc/StructuredMessengerHandle.ts | 8 + .../generatedipc/StructuredRelationship.ts | 8 + .../common/generatedipc/StructuredWebsite.ts | 8 + src/translations/en.ts | 28 +- test/tests/desktop/DesktopContextMenuTest.ts | 4 +- 64 files changed, 2436 insertions(+), 673 deletions(-) create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactCustomDateType.kt create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactMessengerHandleType.kt create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactRelationshipType.kt create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactWebsiteType.kt create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredCustomDate.kt create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredMessengerHandle.kt create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredRelationship.kt create mode 100644 app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredWebsite.kt create mode 100644 app-ios/tutanota/Sources/GeneratedIpc/StructuredCustomDate.swift create mode 100644 app-ios/tutanota/Sources/GeneratedIpc/StructuredMessengerHandle.swift create mode 100644 app-ios/tutanota/Sources/GeneratedIpc/StructuredRelationship.swift create mode 100644 app-ios/tutanota/Sources/GeneratedIpc/StructuredWebsite.swift create mode 100644 ipc-schema/types/ContactCustomDateType.json create mode 100644 ipc-schema/types/ContactMessengerHandleType.json create mode 100644 ipc-schema/types/ContactRelationshipType.json create mode 100644 ipc-schema/types/ContactWebsiteType.json create mode 100644 ipc-schema/types/StructuredCustomDate.json create mode 100644 ipc-schema/types/StructuredMessengerHandle.json create mode 100644 ipc-schema/types/StructuredRelationship.json create mode 100644 ipc-schema/types/StructuredWebsite.json create mode 100644 src/api/worker/offline/migrations/tutanota-v67.ts create mode 100644 src/native/common/generatedipc/ContactCustomDateType.ts create mode 100644 src/native/common/generatedipc/ContactMessengerHandleType.ts create mode 100644 src/native/common/generatedipc/ContactRelationshipType.ts create mode 100644 src/native/common/generatedipc/ContactWebsiteType.ts create mode 100644 src/native/common/generatedipc/StructuredCustomDate.ts create mode 100644 src/native/common/generatedipc/StructuredMessengerHandle.ts create mode 100644 src/native/common/generatedipc/StructuredRelationship.ts create mode 100644 src/native/common/generatedipc/StructuredWebsite.ts diff --git a/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileContactsFacade.kt b/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileContactsFacade.kt index 9c4dada9ecbf..ec9c1fa510f9 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileContactsFacade.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileContactsFacade.kt @@ -13,7 +13,10 @@ import android.provider.ContactsContract.RawContacts import android.util.Log import de.tutao.tutanota.contacts.* import de.tutao.tutanota.contacts.ContactAddressType +import de.tutao.tutanota.contacts.ContactCustomDateType import de.tutao.tutanota.contacts.ContactPhoneNumberType +import de.tutao.tutanota.contacts.ContactRelationshipType +import de.tutao.tutanota.contacts.ContactWebsiteType import de.tutao.tutanota.ipc.* import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString @@ -30,22 +33,13 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo activity.getPermission(Manifest.permission.READ_CONTACTS) val selectionParam = "%$query%" - val selection = - "${ContactsContract.CommonDataKinds.Email.ADDRESS} LIKE ? OR ${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} LIKE ?" - val cursor = resolver.query( - ContactsContract.CommonDataKinds.Email.CONTENT_URI, - PROJECTION, - selection, - arrayOf(selectionParam, selectionParam), - "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} ASC " - ) ?: return listOf() + val selection = "${ContactsContract.CommonDataKinds.Email.ADDRESS} LIKE ? OR ${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} LIKE ?" + val cursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, PROJECTION, selection, arrayOf(selectionParam, selectionParam), "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} ASC ") + ?: return listOf() return cursor.use { cursor.mapTo(mutableListOf()) { - ContactSuggestion( - name = cursor.getString(1), - mailAddress = cursor.getString(2) - ) + ContactSuggestion(name = cursor.getString(1), mailAddress = cursor.getString(2)) } } } @@ -71,13 +65,10 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo override suspend fun getContactBooks(): List { checkContactPermissions() - return resolver.query( - RawContacts.CONTENT_URI, - arrayOf( - RawContacts.ACCOUNT_TYPE, - RawContacts.ACCOUNT_NAME, - ), null, null, null - ).use { cursor -> + return resolver.query(RawContacts.CONTENT_URI, arrayOf( + RawContacts.ACCOUNT_TYPE, + RawContacts.ACCOUNT_NAME, + ), null, null, null).use { cursor -> val accounts = mutableMapOf() cursor!!.forEachRow { val accountType = cursor.getString(0) @@ -110,18 +101,14 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo val query = buildQuery(accountType, accountName) val queryValues = mutableListOf() - if (accountType != null) - queryValues.add(accountType) + if (accountType != null) queryValues.add(accountType) - if (accountName != null) - queryValues.add(accountName) + if (accountName != null) queryValues.add(accountName) - return resolver.query( - RawContacts.CONTENT_URI, arrayOf( + return resolver.query(RawContacts.CONTENT_URI, arrayOf( RawContacts._ID, RawContacts.SOURCE_ID, - ), query, queryValues.toTypedArray(), null - ).use { cursor -> + ), query, queryValues.toTypedArray(), null).use { cursor -> cursor!!.mapTo(mutableListOf()) { val contactId = cursor.getLong(0) val sourceId = cursor.getString(1) @@ -151,19 +138,13 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo } private fun resetDirtyState(rawId: Long) { - val updateDirtyStateOp = ContentProviderOperation.newUpdate(RAW_CONTACT_URI) - .withSelection("${RawContacts._ID} = ?", arrayOf(rawId.toString())) - .withValue(RawContacts.DIRTY, 0) - .build() + val updateDirtyStateOp = ContentProviderOperation.newUpdate(RAW_CONTACT_URI).withSelection("${RawContacts._ID} = ?", arrayOf(rawId.toString())).withValue(RawContacts.DIRTY, 0).build() resolver.applyBatch(ContactsContract.AUTHORITY, arrayListOf(updateDirtyStateOp)) } private fun updateSourceId(rawId: Long, sourceId: String) { - val updateSourceIdOp = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI) - .withSelection("${RawContacts._ID} = ?", arrayOf(rawId.toString())) - .withValue(RawContacts.SOURCE_ID, sourceId) - .build() + val updateSourceIdOp = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI).withSelection("${RawContacts._ID} = ?", arrayOf(rawId.toString())).withValue(RawContacts.SOURCE_ID, sourceId).build() resolver.applyBatch(ContactsContract.AUTHORITY, arrayListOf(updateSourceIdOp)) } @@ -251,8 +232,7 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo } private fun deleteRawContact(rawId: Long): Int { - val uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawId).buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build() + val uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawId).buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build() return resolver.delete(uri, null, null) } @@ -265,28 +245,13 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo createSystemAccount(username) } - val rawContactUri = RawContacts.CONTENT_URI.buildUpon() - .appendQueryParameter(RawContacts.ACCOUNT_NAME, username) - .appendQueryParameter(RawContacts.ACCOUNT_TYPE, TUTA_ACCOUNT_TYPE) - .build() + val rawContactUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, username).appendQueryParameter(RawContacts.ACCOUNT_TYPE, TUTA_ACCOUNT_TYPE).build() if (sourceId != null) { - return resolver.query( - rawContactUri, - arrayOf( - RawContacts._ID, - RawContacts.SOURCE_ID - ), "${RawContacts.SOURCE_ID} = ?", arrayOf(sourceId), null - ) - } - - return resolver.query( - rawContactUri, - arrayOf( - RawContacts._ID, - RawContacts.SOURCE_ID - ), null, null, null - ) + return resolver.query(rawContactUri, arrayOf(RawContacts._ID, RawContacts.SOURCE_ID), "${RawContacts.SOURCE_ID} = ?", arrayOf(sourceId), null) + } + + return resolver.query(rawContactUri, arrayOf(RawContacts._ID, RawContacts.SOURCE_ID), null, null, null) } private fun createSystemAccount(username: String) { @@ -297,310 +262,262 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo } } - private fun checkDeletedContact(storedContact: AndroidContact, ops: ArrayList) { - if (storedContact.isDeleted) { - val updateDeletedStatusOp = ContentProviderOperation.newUpdate(RAW_CONTACT_URI) - .withSelection("${RawContacts._ID} = ?", arrayOf(storedContact.rawId.toString())) - .withValue(RawContacts.DELETED, 0) - .build() - ops += updateDeletedStatusOp - } else { - Log.d(TAG, "Contact isn't deleted, continuing...") - } - } - - private fun checkContactDetails( - storedContact: AndroidContact, - serverContact: StructuredContact, - ops: ArrayList - ) { + private fun checkContactDetails(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { if (storedContact.birthday != serverContact.birthday) { checkContactBirthday(storedContact, ops, serverContact) } - if (storedContact.company != serverContact.company) { + if (storedContact.company != serverContact.company || storedContact.role != serverContact.role || storedContact.department != serverContact.department) { checkContactCompany(storedContact, ops, serverContact) } if (storedContact.givenName != serverContact.firstName) { - val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", - arrayOf(storedContact.rawId.toString()) - ) - .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, serverContact.firstName) - .build() + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, serverContact.firstName).build() + ops += updateNameOp + } + + if (storedContact.middleName != serverContact.middleName) { + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, serverContact.middleName).build() ops += updateNameOp } if (storedContact.lastName != serverContact.lastName) { - val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", - arrayOf(storedContact.rawId.toString()) - ) - .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, serverContact.lastName) - .build() + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, serverContact.lastName).build() + ops += updateNameOp + } + + if (storedContact.title != serverContact.title) { + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, serverContact.title).build() + ops += updateNameOp + } + + if (storedContact.nameSuffix != serverContact.nameSuffix) { + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, serverContact.nameSuffix).build() + ops += updateNameOp + } + + if (storedContact.phoneticFirst != serverContact.phoneticFirst) { + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, serverContact.phoneticFirst).build() + ops += updateNameOp + } + + if (storedContact.phoneticMiddle != serverContact.phoneticMiddle) { + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, serverContact.phoneticMiddle).build() + ops += updateNameOp + } + + if (storedContact.phoneticLast != serverContact.phoneticLast) { + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, serverContact.phoneticLast).build() ops += updateNameOp } if (storedContact.nickname != serverContact.nickname) { - val updateNicknameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", - arrayOf(storedContact.rawId.toString()) - ) - .withValue(ContactsContract.CommonDataKinds.Nickname.NAME, serverContact.nickname) - .build() + val updateNicknameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.Nickname.NAME, serverContact.nickname).build() ops += updateNicknameOp } + + if (storedContact.notes != serverContact.notes) { + checkContactNote(storedContact, serverContact, ops) + } } - private fun checkContactBirthday( - storedContact: AndroidContact, - ops: ArrayList, - serverContact: StructuredContact - ) { + private fun checkContactBirthday(storedContact: AndroidContact, ops: ArrayList, serverContact: StructuredContact) { // If the birthday wasn't added during contact creation, it's // necessary to add and not just update it if (storedContact.birthday == null) { - ops.add( - ContentProviderOperation.newInsert(CONTACT_DATA_URI) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE - ) - .withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, serverContact.birthday) - .withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY) - .build() - ) + ops.add(ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Event.START_DATE, serverContact.birthday).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY).build()) } else { - ops.add( - ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", - arrayOf(storedContact.rawId.toString()) - ) - .withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY) - .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, serverContact.birthday) - .build() - ) - } - } - - private fun checkContactCompany( - storedContact: AndroidContact, - ops: ArrayList, - serverContact: StructuredContact - ) { + ops.add(ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY).withValue(ContactsContract.CommonDataKinds.Event.START_DATE, serverContact.birthday).build()) + } + } + + private fun checkContactNote(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { + if (storedContact.notes != serverContact.notes) { + if (storedContact.notes == "") { + ops.add(ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Note.NOTE, serverContact.notes).build()) + } else { + val updateNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.Note.NOTE, serverContact.notes).build() + ops += updateNameOp + } + } + } + + private fun checkContactCompany(storedContact: AndroidContact, ops: ArrayList, serverContact: StructuredContact) { // If the company wasn't added during contact creation, it's // necessary to add and not just update it if (storedContact.company == "") { - ops.add( - ContentProviderOperation.newInsert(CONTACT_DATA_URI) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE - ) - .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, serverContact.company) - .withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) - .withValue( - ContactsContract.CommonDataKinds.Organization.TYPE, - ContactsContract.CommonDataKinds.Organization.TYPE_WORK - ) - .build() - ) + ops.add(ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, serverContact.company).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.TYPE, ContactsContract.CommonDataKinds.Organization.TYPE_WORK).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, serverContact.department).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.TITLE, serverContact.role).build()) } else { - ops.add( - ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", - arrayOf(storedContact.rawId.toString()) - ) - .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, serverContact.company) - .build() - ) - } - } - - private fun checkContactMailAddresses( - storedContact: AndroidContact, - serverContact: StructuredContact, - ops: ArrayList - ) { + ops.add(ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?", arrayOf(storedContact.rawId.toString())).withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, serverContact.company).withValue(ContactsContract.CommonDataKinds.Organization.TYPE, ContactsContract.CommonDataKinds.Organization.TYPE_WORK).withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, serverContact.department).withValue(ContactsContract.CommonDataKinds.Organization.TITLE, serverContact.role).build()) + } + } + + private fun checkContactMailAddresses(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { for (serverMailAddress in serverContact.mailAddresses) { val storedAddress = storedContact.emailAddresses.find { it.address == serverMailAddress.address } if (storedAddress != null) { if (storedAddress.type != serverMailAddress.type.toAndroidType()) { - val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Email.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedAddress.address) - ) - .withValue( - ContactsContract.CommonDataKinds.Email.TYPE, - serverMailAddress.type.toAndroidType() - ) - .build() + val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Email.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedAddress.address)).withValue(ContactsContract.CommonDataKinds.Email.TYPE, serverMailAddress.type.toAndroidType()).build() ops += updateTypeOp } if (storedAddress.customTypeName != serverMailAddress.customTypeName) { - val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Email.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedAddress.address) - ) - .withValue( - ContactsContract.CommonDataKinds.Email.LABEL, - serverMailAddress.customTypeName - ) - .build() + val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Email.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedAddress.address)).withValue(ContactsContract.CommonDataKinds.Email.LABEL, serverMailAddress.customTypeName).build() ops += updateCustomTypeNameOp } } else { // it's a new mail address - val createEmailAddressOp = insertMailAddressOperation(serverMailAddress) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId) - .build() + val createEmailAddressOp = insertMailAddressOperation(serverMailAddress).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).build() ops += createEmailAddressOp } } for (storedMailAddress in storedContact.emailAddresses) { if (serverContact.mailAddresses.none { it.address == storedMailAddress.address }) { - val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Email.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedMailAddress.address) - ) - .build() + val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Email.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedMailAddress.address)).build() ops += deleteOp } } } - private fun checkContactAddresses( - storedContact: AndroidContact, - serverContact: StructuredContact, - ops: ArrayList - ) { + private fun checkContactAddresses(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { for (serverAddress in serverContact.addresses) { val storedAddress = storedContact.addresses.find { it.address == serverAddress.address } if (storedAddress != null) { if (storedAddress.type != serverAddress.type.toAndroidType()) { - val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.StructuredPostal.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedAddress.address) - ) - .withValue( - ContactsContract.CommonDataKinds.StructuredPostal.TYPE, - serverAddress.type.toAndroidType() - ) - .build() + val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.StructuredPostal.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedAddress.address)).withValue(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, serverAddress.type.toAndroidType()).build() ops += updateTypeOp } if (storedAddress.customTypeName != serverAddress.customTypeName) { - val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.StructuredPostal.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedAddress.address) - ) - .withValue( - ContactsContract.CommonDataKinds.StructuredPostal.LABEL, - serverAddress.customTypeName - ) - .build() + val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.StructuredPostal.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedAddress.address)).withValue(ContactsContract.CommonDataKinds.StructuredPostal.LABEL, serverAddress.customTypeName).build() ops += updateCustomTypeNameOp } } else { // it's a new address - val createAddressOp = insertAddressOperation(serverAddress) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId) - .build() + val createAddressOp = insertAddressOperation(serverAddress).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).build() ops += createAddressOp } } for (storedAddress in storedContact.addresses) { if (serverContact.addresses.none { it.address == storedAddress.address }) { - val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.StructuredPostal.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedAddress.address) - ) - .build() + val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.StructuredPostal.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedAddress.address)).build() ops += deleteOp } } } - private fun checkContactPhonesNumbers( - storedContact: AndroidContact, - serverContact: StructuredContact, ops: ArrayList - ) { + private fun checkContactPhonesNumbers(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { for (serverPhoneNumber in serverContact.phoneNumbers) { val storedNumber = storedContact.phoneNumbers.find { it.number == serverPhoneNumber.number } if (storedNumber != null) { if (storedNumber.type != serverPhoneNumber.type.toAndroidType()) { - val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Phone.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedNumber.number) - ) - .withValue( - ContactsContract.CommonDataKinds.Phone.TYPE, - serverPhoneNumber.type.toAndroidType() - ) - .build() + val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Phone.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedNumber.number)).withValue(ContactsContract.CommonDataKinds.Phone.TYPE, serverPhoneNumber.type.toAndroidType()).build() ops += updateTypeOp } if (storedNumber.customTypeName != serverPhoneNumber.customTypeName) { - val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Phone.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedNumber.number) - ) - .withValue( - ContactsContract.CommonDataKinds.Phone.LABEL, - serverPhoneNumber.customTypeName - ) - .build() + val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Phone.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedNumber.number)).withValue(ContactsContract.CommonDataKinds.Phone.LABEL, serverPhoneNumber.customTypeName).build() ops += updateCustomTypeNameOp } } else { // it's a new phone number - val createEmailAddressOp = insertPhoneNumberOperations(serverPhoneNumber) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId) - .build() + val createEmailAddressOp = insertPhoneNumberOperations(serverPhoneNumber).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).build() ops += createEmailAddressOp } } for (storedPhoneNumber in storedContact.phoneNumbers) { if (serverContact.phoneNumbers.none { it.number == storedPhoneNumber.number }) { - val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI) - .withSelection( - "${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Phone.DATA} = ?", - arrayOf(storedContact.rawId.toString(), storedPhoneNumber.number) - ) - .build() + val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Phone.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedPhoneNumber.number)).build() + ops += deleteOp + } + } + } + + private fun checkContactCustomDates(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { + for (serverCustomDate in serverContact.customDate) { + val storedDate = storedContact.customDate.find { it.dateIso == serverCustomDate.dateIso } + if (storedDate != null) { + if (storedDate.type != serverCustomDate.type.toAndroidType()) { + val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Event.DATA} = ?", arrayOf(storedContact.rawId.toString(), serverCustomDate.dateIso)).withValue(ContactsContract.CommonDataKinds.Event.TYPE, serverCustomDate.type.toAndroidType()).build() + ops += updateTypeOp + } + if (storedDate.customTypeName != serverCustomDate.customTypeName) { + val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Event.DATA} = ?", arrayOf(storedContact.rawId.toString(), serverCustomDate.dateIso)).withValue(ContactsContract.CommonDataKinds.Event.LABEL, serverCustomDate.customTypeName).build() + ops += updateCustomTypeNameOp + } + } else { + // it's a new custom dte number + val createCustomDate = insertCustomDateOperation(serverCustomDate).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).build() + ops += createCustomDate + } + } + + for (storedDate in storedContact.customDate) { + if (serverContact.customDate.none { it.dateIso == storedDate.dateIso }) { + val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Event.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedDate.dateIso)).build() + ops += deleteOp + } + } + } + + private fun checkContactWebsites(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { + for (serverWebsite in serverContact.websites) { + val storedWebsite = storedContact.websites.find { it.url == serverWebsite.url } + if (storedWebsite != null) { + if (storedWebsite.type != serverWebsite.type.toAndroidType()) { + val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Website.DATA} = ?", arrayOf(storedContact.rawId.toString(), serverWebsite.url)).withValue(ContactsContract.CommonDataKinds.Website.TYPE, serverWebsite.type.toAndroidType()).build() + ops += updateTypeOp + } + if (storedWebsite.customTypeName != serverWebsite.customTypeName) { + val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Website.DATA} = ?", arrayOf(storedContact.rawId.toString(), serverWebsite.url)).withValue(ContactsContract.CommonDataKinds.Website.LABEL, serverWebsite.customTypeName).build() + ops += updateCustomTypeNameOp + } + } else { + // it's a new custom website + val createWebsite = insertWebsite(serverWebsite).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).build() + ops += createWebsite + } + } + + for (storedWebsite in storedContact.websites) { + if (serverContact.websites.none { it.url == storedWebsite.url }) { + val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Website.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedWebsite.url)).build() ops += deleteOp } } } - private fun updateContact( - storedContact: AndroidContact, - serverContact: StructuredContact - ) { + private fun checkContactRelationships(storedContact: AndroidContact, serverContact: StructuredContact, ops: ArrayList) { + for (serverRelation in serverContact.relationships) { + val storedRelation = storedContact.relationships.find { it.person == serverRelation.person } + if (storedRelation != null) { + if (storedRelation.type != serverRelation.type.toAndroidType()) { + val updateTypeOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Relation.DATA} = ?", arrayOf(storedContact.rawId.toString(), serverRelation.person)).withValue(ContactsContract.CommonDataKinds.Relation.TYPE, serverRelation.type.toAndroidType()).build() + ops += updateTypeOp + } + if (storedRelation.customTypeName != serverRelation.customTypeName) { + val updateCustomTypeNameOp = ContentProviderOperation.newUpdate(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Relation.DATA} = ?", arrayOf(storedContact.rawId.toString(), serverRelation.person)).withValue(ContactsContract.CommonDataKinds.Relation.LABEL, serverRelation.customTypeName).build() + ops += updateCustomTypeNameOp + } + } else { + // it's a new custom website + val createRelation = insertRelation(serverRelation).withValue(ContactsContract.Data.RAW_CONTACT_ID, storedContact.rawId).build() + ops += createRelation + } + } + + for (storedRelation in storedContact.relationships) { + if (serverContact.relationships.none { it.person == storedRelation.person }) { + val deleteOp = ContentProviderOperation.newDelete(CONTACT_DATA_URI).withSelection("${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE}\" AND ${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.CommonDataKinds.Relation.DATA} = ?", arrayOf(storedContact.rawId.toString(), storedRelation.person)).build() + ops += deleteOp + } + } + } + + private fun updateContact(storedContact: AndroidContact, serverContact: StructuredContact) { val ops = arrayListOf() if (storedContact.isDirty) { @@ -613,123 +530,74 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo checkContactAddresses(storedContact, serverContact, ops) checkContactMailAddresses(storedContact, serverContact, ops) checkContactPhonesNumbers(storedContact, serverContact, ops) + checkContactCustomDates(storedContact, serverContact, ops) + checkContactWebsites(storedContact, serverContact, ops) + checkContactRelationships(storedContact, serverContact, ops) if (ops.isNotEmpty()) { resolver.applyBatch(ContactsContract.AUTHORITY, ops) } } - private fun createContact( - userId: String, - contact: StructuredContact - ) { + private fun createContact(userId: String, contact: StructuredContact) { val ops = ArrayList() val index = 0 - ops.add( - ContentProviderOperation.newInsert(RAW_CONTACT_URI) - .withValue(RawContacts.ACCOUNT_TYPE, TUTA_ACCOUNT_TYPE) - .withValue(RawContacts.ACCOUNT_NAME, userId) - .withValue(RawContacts.SOURCE_ID, contact.id) - .build() - ) + ops.add(ContentProviderOperation.newInsert(RAW_CONTACT_URI).withValue(RawContacts.ACCOUNT_TYPE, TUTA_ACCOUNT_TYPE).withValue(RawContacts.ACCOUNT_NAME, userId).withValue(RawContacts.SOURCE_ID, contact.id).build()) - ops.add( - ContentProviderOperation.newInsert(CONTACT_DATA_URI) - .withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE - ) - .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE - ) - .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.lastName) - .build() - ) + ops.add(ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.lastName).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, contact.phoneticFirst).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, contact.phoneticMiddle).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, contact.phoneticLast).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contact.title).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contact.nameSuffix).build()) - ops.add( - ContentProviderOperation.newInsert(CONTACT_DATA_URI) - .withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index) - .withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, contact.birthday) - .withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY) - .build() - ) + ops.add(ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Event.START_DATE, contact.birthday).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY).build()) - ops.add( - ContentProviderOperation.newInsert(CONTACT_DATA_URI) - .withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE - ) - .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company) - .build() - ) + ops.add(ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, contact.department).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contact.role).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Organization.TYPE, ContactsContract.CommonDataKinds.Organization.TYPE_WORK).build()) - ops.add( - ContentProviderOperation.newInsert(CONTACT_DATA_URI) - .withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE - ) - .withValue(ContactsContract.CommonDataKinds.Nickname.NAME, contact.nickname) - .build() - ) + ops.add(ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValueBackReference(RawContacts.Data.RAW_CONTACT_ID, index).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Nickname.NAME, contact.nickname).build()) for (mailAddress in contact.mailAddresses) { - ops.add( - insertMailAddressOperation(mailAddress) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index) - .build() - ) + ops.add(insertMailAddressOperation(mailAddress).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index).build()) } for (phoneNumber in contact.phoneNumbers) { - ops.add( - insertPhoneNumberOperations(phoneNumber) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index) - .build() - ) + ops.add(insertPhoneNumberOperations(phoneNumber).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index).build()) } for (address in contact.addresses) { - ops.add( - insertAddressOperation(address) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index) - .build() - ) + ops.add(insertAddressOperation(address).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index).build()) + } + + for (customDate in contact.customDate) { + ops.add(insertCustomDateOperation(customDate).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index).build()) + } + + for (website in contact.websites) { + ops.add(insertWebsite(website).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index).build()) + } + + for (relationship in contact.relationships) { + ops.add(insertRelation(relationship).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index).build()) } val result = resolver.applyBatch(ContactsContract.AUTHORITY, ops) Log.d(TAG, "Save result: $result") } - private fun readContact( - rawContactId: Long, - sourceId: String? - ): AndroidContact { + private fun readContact(rawContactId: Long, sourceId: String?): AndroidContact { val storedContact = AndroidContact(rawContactId, sourceId) - val entityUri = Uri.withAppendedPath( - ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), - RawContacts.Entity.CONTENT_DIRECTORY - ) - resolver.query( - entityUri, - arrayOf( - RawContacts.SOURCE_ID, - RawContacts.DELETED, - RawContacts.Entity.DATA_ID, - RawContacts.Entity.MIMETYPE, - RawContacts.Entity.DATA1, - RawContacts.Entity.DATA2, - RawContacts.Entity.DATA3, - RawContacts.DIRTY - ), null, null, null - ).use { entityCursor -> + val entityUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), RawContacts.Entity.CONTENT_DIRECTORY) + resolver.query(entityUri, arrayOf( + RawContacts.SOURCE_ID, + RawContacts.DELETED, + RawContacts.Entity.DATA_ID, + RawContacts.Entity.MIMETYPE, + RawContacts.Entity.DATA1, + RawContacts.Entity.DATA2, + RawContacts.Entity.DATA3, + RawContacts.DIRTY, + RawContacts.Entity.DATA4, + RawContacts.Entity.DATA5, + RawContacts.Entity.DATA6, + RawContacts.Entity.DATA7, + RawContacts.Entity.DATA8, + RawContacts.Entity.DATA9, + ), null, null, null).use { entityCursor -> entityCursor!!.forEachRow { if (entityCursor.getInt(1) == 1) { storedContact.isDeleted = true @@ -755,133 +623,115 @@ class AndroidMobileContactsFacade(private val activity: MainActivity) : MobileCo ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { storedContact.givenName = entityCursor.getString(5) storedContact.lastName = entityCursor.getString(6) + storedContact.title = entityCursor.getString(8) + storedContact.middleName = entityCursor.getString(9) + storedContact.nameSuffix = entityCursor.getString(10) + storedContact.phoneticFirst = entityCursor.getString(11) + storedContact.phoneticMiddle = entityCursor.getString(12) + storedContact.phoneticLast = entityCursor.getString(13) } - ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> storedContact.emailAddresses.add( - AndroidEmailAddress( - data1, - entityCursor.getInt(5), - if (!entityCursor.isNull(6)) entityCursor.getString(6) else "" - ) - ) - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> storedContact.phoneNumbers.add( - AndroidPhoneNumber( - data1, - entityCursor.getInt(5), - if (!entityCursor.isNull(6)) entityCursor.getString(6) else "" - ) - ) - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> storedContact.addresses.add( - AndroidAddress( - data1, - entityCursor.getInt(5), - if (!entityCursor.isNull(6)) entityCursor.getString(6) else "" - ) - ) + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> storedContact.emailAddresses.add(AndroidEmailAddress(data1, entityCursor.getInt(5), if (!entityCursor.isNull(6)) entityCursor.getString(6) else "")) + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> storedContact.phoneNumbers.add(AndroidPhoneNumber(data1, entityCursor.getInt(5), if (!entityCursor.isNull(6)) entityCursor.getString(6) else "")) + ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> storedContact.addresses.add(AndroidAddress(data1, entityCursor.getInt(5), if (!entityCursor.isNull(6)) entityCursor.getString(6) else "")) ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE -> storedContact.nickname = data1 ?: "" - ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> storedContact.company = data1 - ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE -> storedContact.birthday = data1 + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> { + storedContact.company = data1 + storedContact.role = entityCursor.getString(8) + storedContact.department = entityCursor.getString(9) + } + ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE -> { + val type = entityCursor.getInt(5) + if (type == ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY) { + storedContact.birthday = data1 + } else { + storedContact.customDate.add(AndroidCustomDate(data1, type, if (!entityCursor.isNull(6)) entityCursor.getString(6) else "")) + } + } + ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE -> storedContact.relationships.add(AndroidRelationship(data1, entityCursor.getInt(5), if (!entityCursor.isNull(6)) entityCursor.getString(6) else "")) + ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE -> storedContact.websites.add(AndroidWebsite(data1, entityCursor.getInt(5), if (!entityCursor.isNull(6)) entityCursor.getString(6) else "")) + ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE -> storedContact.notes = data1 } } private fun insertAddressOperation(address: StructuredAddress): ContentProviderOperation.Builder { - val contactInsert = ContentProviderOperation - .newInsert(CONTACT_DATA_URI) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE - ) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.DATA, address.address) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE - ) - .withValue( - ContactsContract.CommonDataKinds.StructuredPostal.TYPE, - address.type.toAndroidType(), - ) + val contactInsert = ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredPostal.DATA, address.address).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE).withValue( + ContactsContract.CommonDataKinds.StructuredPostal.TYPE, + address.type.toAndroidType(), + ) if (address.type == ContactAddressType.CUSTOM) { - contactInsert.withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE - ) - .withValue( - ContactsContract.CommonDataKinds.StructuredPostal.LABEL, - address.customTypeName - ) + contactInsert.withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.StructuredPostal.LABEL, address.customTypeName) } return contactInsert } private fun insertMailAddressOperation(mailAddress: StructuredMailAddress): ContentProviderOperation.Builder { - val contactInsert = ContentProviderOperation - .newInsert(CONTACT_DATA_URI) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE - ) - .withValue(ContactsContract.CommonDataKinds.Email.DATA, mailAddress.address) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE - ) - .withValue( - ContactsContract.CommonDataKinds.Email.TYPE, - mailAddress.type.toAndroidType(), - ) + val contactInsert = ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Email.DATA, mailAddress.address).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE).withValue( + ContactsContract.CommonDataKinds.Email.TYPE, + mailAddress.type.toAndroidType(), + ) if (mailAddress.type == ContactAddressType.CUSTOM) { - contactInsert.withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE - ) - .withValue( - ContactsContract.CommonDataKinds.Email.LABEL, - mailAddress.customTypeName - ) + contactInsert.withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Email.LABEL, mailAddress.customTypeName) } return contactInsert } private fun insertPhoneNumberOperations(phoneNumber: StructuredPhoneNumber): ContentProviderOperation.Builder { - val contactInsert = ContentProviderOperation - .newInsert(CONTACT_DATA_URI) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Phone.MIMETYPE - ) - .withValue(ContactsContract.CommonDataKinds.Phone.DATA, phoneNumber.number) - .withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE - ) - .withValue( - ContactsContract.CommonDataKinds.Phone.TYPE, - phoneNumber.type.toAndroidType(), - ) + val contactInsert = ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.MIMETYPE).withValue(ContactsContract.CommonDataKinds.Phone.DATA, phoneNumber.number).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE).withValue( + ContactsContract.CommonDataKinds.Phone.TYPE, + phoneNumber.type.toAndroidType(), + ) if (phoneNumber.type == ContactPhoneNumberType.CUSTOM) { - contactInsert.withValue( - RawContacts.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE - ) - .withValue( - ContactsContract.CommonDataKinds.Phone.LABEL, - phoneNumber.customTypeName - ) + contactInsert.withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Phone.LABEL, phoneNumber.customTypeName) } return contactInsert } - companion object { - private val PROJECTION = arrayOf( - ContactsContract.Contacts._ID, - ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, - ContactsContract.CommonDataKinds.Email.ADDRESS + private fun insertCustomDateOperation(customDate: StructuredCustomDate): ContentProviderOperation.Builder { + val contactInsert = ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.MIMETYPE).withValue(ContactsContract.CommonDataKinds.Event.DATA, customDate.dateIso).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE).withValue( + ContactsContract.CommonDataKinds.Event.TYPE, + customDate.type.toAndroidType(), + ) + if (customDate.type == ContactCustomDateType.CUSTOM) { + contactInsert.withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Event.LABEL, customDate.customTypeName) + } + return contactInsert + } + + private fun insertWebsite(customWebsite: StructuredWebsite): ContentProviderOperation.Builder { + val contactInsert = ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.MIMETYPE).withValue(ContactsContract.CommonDataKinds.Website.DATA, customWebsite.url).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE).withValue( + ContactsContract.CommonDataKinds.Website.TYPE, + customWebsite.type.toAndroidType(), ) + + if (customWebsite.type == ContactWebsiteType.CUSTOM) { + contactInsert.withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Website.LABEL, customWebsite.customTypeName) + } + return contactInsert + } + + private fun insertRelation(relation: StructuredRelationship): ContentProviderOperation.Builder { + val contactInsert = ContentProviderOperation.newInsert(CONTACT_DATA_URI).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Relation.MIMETYPE).withValue(ContactsContract.CommonDataKinds.Relation.DATA, relation.person).withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE).withValue( + ContactsContract.CommonDataKinds.Relation.TYPE, + relation.type.toAndroidType(), + ) + + if (relation.type == ContactRelationshipType.CUSTOM) { + contactInsert.withValue(RawContacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE).withValue(ContactsContract.CommonDataKinds.Relation.LABEL, relation.customTypeName) + } + return contactInsert + } + + + companion object { + private val PROJECTION = arrayOf(ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, ContactsContract.CommonDataKinds.Email.ADDRESS) const val TAG = "Contact" private const val TUTA_ACCOUNT_TYPE = BuildConfig.APPLICATION_ID private val RAW_CONTACT_URI = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build() private val CONTACT_DATA_URI = ContactsContract.Data.CONTENT_URI.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build() } + + private data class SaveContactsResult(val cleanContacts: Map, val dirtyContacts: List) } diff --git a/app-android/app/src/main/java/de/tutao/tutanota/contacts/AndroidContact.kt b/app-android/app/src/main/java/de/tutao/tutanota/contacts/AndroidContact.kt index 42e28672e7cc..d908d65456fa 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/contacts/AndroidContact.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/contacts/AndroidContact.kt @@ -1,10 +1,7 @@ package de.tutao.tutanota.contacts import android.provider.ContactsContract -import de.tutao.tutanota.ipc.StructuredAddress -import de.tutao.tutanota.ipc.StructuredContact -import de.tutao.tutanota.ipc.StructuredMailAddress -import de.tutao.tutanota.ipc.StructuredPhoneNumber +import de.tutao.tutanota.ipc.* data class AndroidEmailAddress( val address: String, @@ -24,22 +21,52 @@ data class AndroidPhoneNumber( val customTypeName: String ) +data class AndroidWebsite( + val url: String, + val type: Int, + val customTypeName: String +) + +data class AndroidRelationship( + val person: String, + val type: Int, + val customTypeName: String +) + +data class AndroidCustomDate( + val dateIso: String, + val type: Int, + val customTypeName: String +) + /** * Representation of RawContact + ContractsContract.Data from Android. */ data class AndroidContact( - val rawId: Long, - val sourceId: String?, - var givenName: String? = null, - var lastName: String? = null, - var company: String = "", - var nickname: String = "", - var birthday: String? = null, - val emailAddresses: MutableList = mutableListOf(), - val phoneNumbers: MutableList = mutableListOf(), - val addresses: MutableList = mutableListOf(), - var isDeleted: Boolean = false, - var isDirty: Boolean = false + val rawId: Long, + val sourceId: String?, + var givenName: String? = null, + var lastName: String? = null, + var company: String = "", + var nickname: String = "", + var birthday: String? = null, + val emailAddresses: MutableList = mutableListOf(), + val phoneNumbers: MutableList = mutableListOf(), + val addresses: MutableList = mutableListOf(), + var isDeleted: Boolean = false, + var isDirty: Boolean = false, + var department: String? = null, + var middleName: String? = null, + var nameSuffix: String? = null, + var phoneticFirst: String? = null, + var phoneticMiddle: String? = null, + var phoneticLast: String? = null, + val customDate: MutableList = mutableListOf(), + val websites: MutableList = mutableListOf(), + val relationships: MutableList = mutableListOf(), + var notes: String = "", + var title: String = "", + var role: String = "" ) { fun toStructured(): StructuredContact { return StructuredContact( @@ -53,6 +80,20 @@ data class AndroidContact( phoneNumbers = phoneNumbers.map { it.toStructured() }, addresses = addresses.map { it.toStructured() }, rawId = rawId.toString(), + department = department, + middleName = middleName, + nameSuffix = nameSuffix, + phoneticFirst = phoneticFirst, + phoneticMiddle = phoneticMiddle, + phoneticLast = phoneticLast, + customDate = customDate.map { it.toStructured() }, + messengerHandles = listOf(), // Will be deprecated on Android 15, not worth to implement now + websites = websites.map { it.toStructured() }, + relationships = relationships.map { it.toStructured() }, + pronouns = listOf(), // Not supported on Android + notes = notes, + title = title, + role = role ) } } @@ -74,17 +115,91 @@ fun ContactPhoneNumberType.toAndroidType(): Int = when (this) { } fun AndroidEmailAddress.toStructured() = StructuredMailAddress( - address = address, - type = addressTypeFromAndroid(type), - customTypeName = customTypeName + address = address, + type = addressTypeFromAndroid(type), + customTypeName = customTypeName ) fun AndroidPhoneNumber.toStructured() = StructuredPhoneNumber( - number = number, - type = phoneNumberTypeFromAndroid(type), - customTypeName = customTypeName + number = number, + type = phoneNumberTypeFromAndroid(type), + customTypeName = customTypeName +) + +fun ContactRelationshipType.toAndroidType(): Int = when (this) { + ContactRelationshipType.PARENT -> ContactsContract.CommonDataKinds.Relation.TYPE_PARENT + ContactRelationshipType.BROTHER -> ContactsContract.CommonDataKinds.Relation.TYPE_BROTHER + ContactRelationshipType.SISTER -> ContactsContract.CommonDataKinds.Relation.TYPE_SISTER + ContactRelationshipType.CHILD -> ContactsContract.CommonDataKinds.Relation.TYPE_CHILD + ContactRelationshipType.FRIEND -> ContactsContract.CommonDataKinds.Relation.TYPE_FRIEND + ContactRelationshipType.RELATIVE -> ContactsContract.CommonDataKinds.Relation.TYPE_RELATIVE + ContactRelationshipType.SPOUSE -> ContactsContract.CommonDataKinds.Relation.TYPE_SPOUSE + ContactRelationshipType.PARTNER -> ContactsContract.CommonDataKinds.Relation.TYPE_PARTNER + ContactRelationshipType.ASSISTANT -> ContactsContract.CommonDataKinds.Relation.TYPE_ASSISTANT + ContactRelationshipType.MANAGER -> ContactsContract.CommonDataKinds.Relation.TYPE_MANAGER + ContactRelationshipType.CUSTOM -> ContactsContract.CommonDataKinds.Relation.TYPE_CUSTOM + else -> ContactsContract.CommonDataKinds.Relation.TYPE_CUSTOM +} + +fun ContactWebsiteType.toAndroidType(): Int = when (this) { + ContactWebsiteType.PRIVATE -> ContactsContract.CommonDataKinds.Website.TYPE_HOME + ContactWebsiteType.WORK -> ContactsContract.CommonDataKinds.Website.TYPE_WORK + ContactWebsiteType.CUSTOM -> ContactsContract.CommonDataKinds.Website.TYPE_CUSTOM + else -> ContactsContract.CommonDataKinds.Website.TYPE_OTHER +} + +fun ContactCustomDateType.toAndroidType(): Int = when (this) { + ContactCustomDateType.ANNIVERSARY -> ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY + ContactCustomDateType.CUSTOM -> ContactsContract.CommonDataKinds.Event.TYPE_CUSTOM + else -> ContactsContract.CommonDataKinds.Event.TYPE_OTHER +} + +fun AndroidCustomDate.toStructured() = StructuredCustomDate( + dateIso = dateIso, + type = dateTypeFromAndroid(type), + customTypeName = customTypeName +) + +fun AndroidWebsite.toStructured() = StructuredWebsite( + url = url, + type = websiteTypeFromAndroid(type), + customTypeName = customTypeName ) +fun AndroidRelationship.toStructured() = StructuredRelationship( + person = person, + type = relationshipTypeFromAndroid(type), + customTypeName = customTypeName +) + +fun relationshipTypeFromAndroid(androidType: Int): ContactRelationshipType = when (androidType) { + ContactsContract.CommonDataKinds.Relation.TYPE_PARENT -> ContactRelationshipType.PARENT + ContactsContract.CommonDataKinds.Relation.TYPE_BROTHER -> ContactRelationshipType.BROTHER + ContactsContract.CommonDataKinds.Relation.TYPE_SISTER -> ContactRelationshipType.SISTER + ContactsContract.CommonDataKinds.Relation.TYPE_CHILD -> ContactRelationshipType.CHILD + ContactsContract.CommonDataKinds.Relation.TYPE_FRIEND -> ContactRelationshipType.FRIEND + ContactsContract.CommonDataKinds.Relation.TYPE_RELATIVE -> ContactRelationshipType.RELATIVE + ContactsContract.CommonDataKinds.Relation.TYPE_SPOUSE -> ContactRelationshipType.SPOUSE + ContactsContract.CommonDataKinds.Relation.TYPE_PARTNER -> ContactRelationshipType.PARTNER + ContactsContract.CommonDataKinds.Relation.TYPE_ASSISTANT -> ContactRelationshipType.ASSISTANT + ContactsContract.CommonDataKinds.Relation.TYPE_MANAGER -> ContactRelationshipType.MANAGER + ContactsContract.CommonDataKinds.Relation.TYPE_CUSTOM -> ContactRelationshipType.CUSTOM + else -> ContactRelationshipType.OTHER +} + +fun websiteTypeFromAndroid(androidType: Int): ContactWebsiteType = when (androidType) { + ContactsContract.CommonDataKinds.Website.TYPE_HOME -> ContactWebsiteType.PRIVATE + ContactsContract.CommonDataKinds.Website.TYPE_WORK -> ContactWebsiteType.WORK + ContactsContract.CommonDataKinds.Website.TYPE_CUSTOM -> ContactWebsiteType.CUSTOM + else -> ContactWebsiteType.WORK +} + +fun dateTypeFromAndroid(androidType: Int): ContactCustomDateType = when (androidType) { + ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY -> ContactCustomDateType.ANNIVERSARY + ContactsContract.CommonDataKinds.Event.TYPE_CUSTOM -> ContactCustomDateType.CUSTOM + else -> ContactCustomDateType.OTHER +} + fun addressTypeFromAndroid(androidType: Int): ContactAddressType = when (androidType) { ContactsContract.CommonDataKinds.Email.TYPE_HOME -> ContactAddressType.PRIVATE ContactsContract.CommonDataKinds.Email.TYPE_WORK -> ContactAddressType.WORK @@ -104,7 +219,7 @@ fun phoneNumberTypeFromAndroid(androidType: Int): ContactPhoneNumberType = when } fun AndroidAddress.toStructured() = StructuredAddress( - address = address, - type = addressTypeFromAndroid(type), - customTypeName = customTypeName + address = address, + type = addressTypeFromAndroid(type), + customTypeName = customTypeName ) \ No newline at end of file diff --git a/app-android/app/src/main/java/de/tutao/tutanota/contacts/ContactEnums.kt b/app-android/app/src/main/java/de/tutao/tutanota/contacts/ContactEnums.kt index 985f16dddf20..1590f8d7c01e 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/contacts/ContactEnums.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/contacts/ContactEnums.kt @@ -39,4 +39,95 @@ enum class ContactPhoneNumberType { @SerialName("5") CUSTOM, +} + +/** Mirror of ContactCustomDateType from TutanotaConstants */ +@Serializable +enum class ContactCustomDateType { + @SerialName("0") + ANNIVERSARY, + + @SerialName("1") + OTHER, + + @SerialName("2") + CUSTOM, +} + +/** Mirror of ContactWebsiteType from TutanotaConstants */ +@Serializable +enum class ContactWebsiteType { + @SerialName("0") + PRIVATE, + + @SerialName("1") + WORK, + + @SerialName("2") + OTHER, + + @SerialName("3") + CUSTOM, +} + +/** Mirror of ContactWebsiteType from TutanotaConstants */ +@Serializable +enum class ContactMessengerHandleType { + @SerialName("0") + SIGNAL, + + @SerialName("1") + WHATSAPP, + + @SerialName("2") + TELEGRAM, + + @SerialName("3") + DISCORD, + + @SerialName("4") + OTHER, + + @SerialName("5") + CUSTOM +} + +/** Mirror of ContactWebsiteType from TutanotaConstants */ +@Serializable +enum class ContactRelationshipType { + @SerialName("0") + PARENT, + + @SerialName("1") + BROTHER, + + @SerialName("2") + SISTER, + + @SerialName("3") + CHILD, + + @SerialName("4") + FRIEND, + + @SerialName("5") + RELATIVE, + + @SerialName("6") + SPOUSE, + + @SerialName("7") + PARTNER, + + @SerialName("8") + ASSISTANT, + + @SerialName("9") + MANAGER, + + @SerialName("10") + OTHER, + + @SerialName("11") + CUSTOM, } \ No newline at end of file diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactCustomDateType.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactCustomDateType.kt new file mode 100644 index 000000000000..22c5b44f1ae8 --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactCustomDateType.kt @@ -0,0 +1,5 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc +typealias ContactCustomDateType = de.tutao.tutanota.contacts.ContactCustomDateType diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactMessengerHandleType.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactMessengerHandleType.kt new file mode 100644 index 000000000000..ca7b3213f648 --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactMessengerHandleType.kt @@ -0,0 +1,5 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc +typealias ContactMessengerHandleType = de.tutao.tutanota.contacts.ContactMessengerHandleType diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactRelationshipType.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactRelationshipType.kt new file mode 100644 index 000000000000..bd63d1c638a5 --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactRelationshipType.kt @@ -0,0 +1,5 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc +typealias ContactRelationshipType = de.tutao.tutanota.contacts.ContactRelationshipType diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactWebsiteType.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactWebsiteType.kt new file mode 100644 index 000000000000..15a274a1889e --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/ContactWebsiteType.kt @@ -0,0 +1,5 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc +typealias ContactWebsiteType = de.tutao.tutanota.contacts.ContactWebsiteType diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/MobileContactsFacadeReceiveDispatcher.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/MobileContactsFacadeReceiveDispatcher.kt index 43788e384d0d..03b148ecffc4 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/MobileContactsFacadeReceiveDispatcher.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/MobileContactsFacadeReceiveDispatcher.kt @@ -11,7 +11,7 @@ class MobileContactsFacadeReceiveDispatcher( private val json: Json, private val facade: MobileContactsFacade, ) { - + suspend fun dispatch(method: String, arg: List): String { when (method) { "findSuggestions" -> { diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredContact.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredContact.kt index 9f448e94c4a6..c7b9f6d535da 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredContact.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredContact.kt @@ -19,4 +19,17 @@ data class StructuredContact( val phoneNumbers: List, val addresses: List, val rawId: String?, + val customDate: List, + val department: String?, + val messengerHandles: List, + val middleName: String?, + val nameSuffix: String?, + val phoneticFirst: String?, + val phoneticLast: String?, + val phoneticMiddle: String?, + val relationships: List, + val websites: List, + val notes: String, + val title: String, + val role: String, ) diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredCustomDate.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredCustomDate.kt new file mode 100644 index 000000000000..732d4212b206 --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredCustomDate.kt @@ -0,0 +1,15 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + + +@Serializable +data class StructuredCustomDate( + val dateIso: String, + val type: ContactCustomDateType, + val customTypeName: String, +) diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredMessengerHandle.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredMessengerHandle.kt new file mode 100644 index 000000000000..72fe695cfb15 --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredMessengerHandle.kt @@ -0,0 +1,15 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + + +@Serializable +data class StructuredMessengerHandle( + val handle: String, + val type: ContactMessengerHandleType, + val customTypeName: String, +) diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredRelationship.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredRelationship.kt new file mode 100644 index 000000000000..967be99a31a9 --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredRelationship.kt @@ -0,0 +1,15 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + + +@Serializable +data class StructuredRelationship( + val person: String, + val type: ContactRelationshipType, + val customTypeName: String, +) diff --git a/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredWebsite.kt b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredWebsite.kt new file mode 100644 index 000000000000..a60bff3c0ece --- /dev/null +++ b/app-android/app/src/main/java/de/tutao/tutanota/generated_ipc/StructuredWebsite.kt @@ -0,0 +1,15 @@ +/* generated file, don't edit. */ + + +package de.tutao.tutanota.ipc + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + + +@Serializable +data class StructuredWebsite( + val url: String, + val type: ContactWebsiteType, + val customTypeName: String, +) diff --git a/app-ios/tutanota/Sources/Contacts/IosMobileContactsFacade.swift b/app-ios/tutanota/Sources/Contacts/IosMobileContactsFacade.swift index 0d34aa436192..0c45889943f5 100644 --- a/app-ios/tutanota/Sources/Contacts/IosMobileContactsFacade.swift +++ b/app-ios/tutanota/Sources/Contacts/IosMobileContactsFacade.swift @@ -571,7 +571,21 @@ private extension CNContact { mailAddresses: emailAddresses.map { $0.toStructuredMailAddress() }, phoneNumbers: phoneNumbers.map { $0.toStructuredPhoneNumber() }, addresses: postalAddresses.map { $0.toStructuredAddress() }, - rawId: identifier + rawId: identifier, + + customDate: dates.map { $0.toStructuredCustomDate() }, + department: departmentName, + messengerHandles: instantMessageAddresses.map { $0.toStructuredMessengerHandle() }, + middleName: middleName, + nameSuffix: nameSuffix, + phoneticFirst: phoneticGivenName, + phoneticLast: phoneticFamilyName, + phoneticMiddle: phoneticMiddleName, + relationships: contactRelations.map { $0.toStructuredRelationship() }, + websites: urlAddresses.map { $0.toStructuredWebsite() }, + notes: "", // FIXME: not apple approved + title: namePrefix, + role: jobTitle ) } } diff --git a/app-ios/tutanota/Sources/Contacts/StructuredContactTypes.swift b/app-ios/tutanota/Sources/Contacts/StructuredContactTypes.swift index 55ad343f39e1..8dbbff0e5140 100644 --- a/app-ios/tutanota/Sources/Contacts/StructuredContactTypes.swift +++ b/app-ios/tutanota/Sources/Contacts/StructuredContactTypes.swift @@ -48,6 +48,58 @@ extension StructuredPhoneNumber: Hashable { } } +extension StructuredWebsite: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.type == rhs.type && lhs.customTypeName == rhs.customTypeName && lhs.url == rhs.url } +} + +extension StructuredWebsite: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(url) + hasher.combine(type) + hasher.combine(customTypeName) + } +} + +extension StructuredCustomDate: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.type == rhs.type && lhs.customTypeName == rhs.customTypeName && lhs.dateIso == rhs.dateIso } +} + +extension StructuredCustomDate: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(dateIso) + hasher.combine(type) + hasher.combine(customTypeName) + } +} + +extension StructuredMessengerHandle: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.type == rhs.type && lhs.customTypeName == rhs.customTypeName && lhs.handle == rhs.handle } +} + +extension StructuredMessengerHandle: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(handle) + hasher.combine(type) + hasher.combine(customTypeName) + } +} + +extension StructuredRelationship: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.type == rhs.type && lhs.customTypeName == rhs.customTypeName && lhs.person == rhs.person } +} + +extension StructuredPronouns: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.pronouns == rhs.pronouns && lhs.language == rhs.language } +} + +extension StructuredRelationship: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(person) + hasher.combine(type) + hasher.combine(customTypeName) + } +} + extension StructuredPhoneNumber: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { lhs.number == rhs.number && lhs.type == rhs.type && lhs.customTypeName == rhs.customTypeName } } @@ -56,6 +108,11 @@ extension StructuredContact: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id && lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName && lhs.nickname == rhs.nickname && lhs.company == rhs.company && lhs.birthday == rhs.birthday && lhs.mailAddresses == rhs.mailAddresses && lhs.phoneNumbers == rhs.phoneNumbers && lhs.addresses == rhs.addresses + && lhs.customDate == rhs.customDate && lhs.department == rhs.department && lhs.messengerHandles == rhs.messengerHandles + && lhs.middleName == rhs.middleName && lhs.nameSuffix == rhs.nameSuffix && lhs.phoneticFirst == rhs.phoneticFirst + && lhs.phoneticLast == rhs.phoneticLast && lhs.phoneticMiddle == rhs.phoneticMiddle + && lhs.relationships == rhs.relationships && lhs.websites == rhs.websites && lhs.notes == rhs.notes && lhs.title == rhs.title + && lhs.role == rhs.role } } @@ -70,5 +127,19 @@ extension StructuredContact: Hashable { hasher.combine(mailAddresses) hasher.combine(phoneNumbers) hasher.combine(addresses) + // hasher.combine(rawId) // no need + hasher.combine(customDate) + hasher.combine(department) + hasher.combine(messengerHandles) + hasher.combine(middleName) + hasher.combine(nameSuffix) + hasher.combine(phoneticFirst) + hasher.combine(phoneticLast) + hasher.combine(phoneticMiddle) + hasher.combine(relationships) + hasher.combine(websites) + hasher.combine(notes) + hasher.combine(title) + hasher.combine(role) } } diff --git a/app-ios/tutanota/Sources/GeneratedIpc/StructuredContact.swift b/app-ios/tutanota/Sources/GeneratedIpc/StructuredContact.swift index 80dbdf8c6263..9be570e03363 100644 --- a/app-ios/tutanota/Sources/GeneratedIpc/StructuredContact.swift +++ b/app-ios/tutanota/Sources/GeneratedIpc/StructuredContact.swift @@ -12,4 +12,17 @@ public struct StructuredContact : Codable { let phoneNumbers: [StructuredPhoneNumber] let addresses: [StructuredAddress] let rawId: String? + let customDate: [StructuredCustomDate] + let department: String? + let messengerHandles: [StructuredMessengerHandle] + let middleName: String? + let nameSuffix: String? + let phoneticFirst: String? + let phoneticLast: String? + let phoneticMiddle: String? + let relationships: [StructuredRelationship] + let websites: [StructuredWebsite] + let notes: String + let title: String + let role: String } diff --git a/app-ios/tutanota/Sources/GeneratedIpc/StructuredCustomDate.swift b/app-ios/tutanota/Sources/GeneratedIpc/StructuredCustomDate.swift new file mode 100644 index 000000000000..ff3e856161fd --- /dev/null +++ b/app-ios/tutanota/Sources/GeneratedIpc/StructuredCustomDate.swift @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + + +public struct StructuredCustomDate : Codable { + let dateIso: String + let type: ContactCustomDateType + let customTypeName: String +} diff --git a/app-ios/tutanota/Sources/GeneratedIpc/StructuredMessengerHandle.swift b/app-ios/tutanota/Sources/GeneratedIpc/StructuredMessengerHandle.swift new file mode 100644 index 000000000000..17fb3a4ff67c --- /dev/null +++ b/app-ios/tutanota/Sources/GeneratedIpc/StructuredMessengerHandle.swift @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + + +public struct StructuredMessengerHandle : Codable { + let handle: String + let type: ContactMessengerHandleType + let customTypeName: String +} diff --git a/app-ios/tutanota/Sources/GeneratedIpc/StructuredRelationship.swift b/app-ios/tutanota/Sources/GeneratedIpc/StructuredRelationship.swift new file mode 100644 index 000000000000..cda514e68cea --- /dev/null +++ b/app-ios/tutanota/Sources/GeneratedIpc/StructuredRelationship.swift @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + + +public struct StructuredRelationship : Codable { + let person: String + let type: ContactRelationshipType + let customTypeName: String +} diff --git a/app-ios/tutanota/Sources/GeneratedIpc/StructuredWebsite.swift b/app-ios/tutanota/Sources/GeneratedIpc/StructuredWebsite.swift new file mode 100644 index 000000000000..eaf3b0e7be38 --- /dev/null +++ b/app-ios/tutanota/Sources/GeneratedIpc/StructuredWebsite.swift @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + + +public struct StructuredWebsite : Codable { + let url: String + let type: ContactWebsiteType + let customTypeName: String +} diff --git a/ipc-schema/types/ContactCustomDateType.json b/ipc-schema/types/ContactCustomDateType.json new file mode 100644 index 000000000000..167b16d3130b --- /dev/null +++ b/ipc-schema/types/ContactCustomDateType.json @@ -0,0 +1,8 @@ +{ + "name": "ContactCustomDateType", + "type": "typeref", + "location": { + "typescript": "../src/api/common/TutanotaConstants.js", + "kotlin": "de.tutao.tutanota.contacts.ContactCustomDateType" + } +} diff --git a/ipc-schema/types/ContactMessengerHandleType.json b/ipc-schema/types/ContactMessengerHandleType.json new file mode 100644 index 000000000000..47eb73b74774 --- /dev/null +++ b/ipc-schema/types/ContactMessengerHandleType.json @@ -0,0 +1,8 @@ +{ + "name": "ContactMessengerHandleType", + "type": "typeref", + "location": { + "typescript": "../src/api/common/TutanotaConstants.js", + "kotlin": "de.tutao.tutanota.contacts.ContactMessengerHandleType" + } +} diff --git a/ipc-schema/types/ContactRelationshipType.json b/ipc-schema/types/ContactRelationshipType.json new file mode 100644 index 000000000000..ad35a2f053f1 --- /dev/null +++ b/ipc-schema/types/ContactRelationshipType.json @@ -0,0 +1,8 @@ +{ + "name": "ContactRelationshipType", + "type": "typeref", + "location": { + "typescript": "../src/api/common/TutanotaConstants.js", + "kotlin": "de.tutao.tutanota.contacts.ContactRelationshipType" + } +} diff --git a/ipc-schema/types/ContactWebsiteType.json b/ipc-schema/types/ContactWebsiteType.json new file mode 100644 index 000000000000..1313c86c6395 --- /dev/null +++ b/ipc-schema/types/ContactWebsiteType.json @@ -0,0 +1,8 @@ +{ + "name": "ContactWebsiteType", + "type": "typeref", + "location": { + "typescript": "../src/api/common/TutanotaConstants.js", + "kotlin": "de.tutao.tutanota.contacts.ContactWebsiteType" + } +} diff --git a/ipc-schema/types/StructuredContact.json b/ipc-schema/types/StructuredContact.json index 3dc02a689740..60bcf880ccba 100644 --- a/ipc-schema/types/StructuredContact.json +++ b/ipc-schema/types/StructuredContact.json @@ -11,6 +11,19 @@ "mailAddresses": "List", "phoneNumbers": "List", "addresses": "List", - "rawId": "string?" + "rawId": "string?", + "customDate": "List", + "department": "string?", + "messengerHandles": "List", + "middleName": "string?", + "nameSuffix": "string?", + "phoneticFirst": " string?", + "phoneticLast": "string?", + "phoneticMiddle": "string?", + "relationships": "List", + "websites": "List", + "notes": "string", + "title": "string", + "role": "string" } } diff --git a/ipc-schema/types/StructuredCustomDate.json b/ipc-schema/types/StructuredCustomDate.json new file mode 100644 index 000000000000..8f49b818756e --- /dev/null +++ b/ipc-schema/types/StructuredCustomDate.json @@ -0,0 +1,9 @@ +{ + "name": "StructuredCustomDate", + "type": "struct", + "fields": { + "dateIso": "string", + "type": "ContactCustomDateType", + "customTypeName": "string" + } +} diff --git a/ipc-schema/types/StructuredMessengerHandle.json b/ipc-schema/types/StructuredMessengerHandle.json new file mode 100644 index 000000000000..b1aa588c73ec --- /dev/null +++ b/ipc-schema/types/StructuredMessengerHandle.json @@ -0,0 +1,9 @@ +{ + "name": "StructuredMessengerHandle", + "type": "struct", + "fields": { + "handle": "string", + "type": "ContactMessengerHandleType", + "customTypeName": "string" + } +} diff --git a/ipc-schema/types/StructuredRelationship.json b/ipc-schema/types/StructuredRelationship.json new file mode 100644 index 000000000000..fc31fb6b9fc8 --- /dev/null +++ b/ipc-schema/types/StructuredRelationship.json @@ -0,0 +1,9 @@ +{ + "name": "StructuredRelationship", + "type": "struct", + "fields": { + "person": "string", + "type": "ContactRelationshipType", + "customTypeName": "string" + } +} diff --git a/ipc-schema/types/StructuredWebsite.json b/ipc-schema/types/StructuredWebsite.json new file mode 100644 index 000000000000..752abab2dc01 --- /dev/null +++ b/ipc-schema/types/StructuredWebsite.json @@ -0,0 +1,9 @@ +{ + "name": "StructuredWebsite", + "type": "struct", + "fields": { + "url": "string", + "type": "ContactWebsiteType", + "customTypeName": "string" + } +} diff --git a/schemas/tutanota.json b/schemas/tutanota.json index 6741f113bbc5..87d7c43d9d4a 100644 --- a/schemas/tutanota.json +++ b/schemas/tutanota.json @@ -95,6 +95,66 @@ "info": "AddValue InternalRecipientKeyData/protocolVersion/1352." } ] + }, + { + "version": 67, + "changes": [ + { + "name": "AddValue", + "sourceType": "Contact", + "info": "AddValue Contact/middleName/1380." + }, + { + "name": "AddValue", + "sourceType": "Contact", + "info": "AddValue Contact/nameSuffix/1381." + }, + { + "name": "AddValue", + "sourceType": "Contact", + "info": "AddValue Contact/phoneticFirst/1382." + }, + { + "name": "AddValue", + "sourceType": "Contact", + "info": "AddValue Contact/phoneticMiddle/1383." + }, + { + "name": "AddValue", + "sourceType": "Contact", + "info": "AddValue Contact/phoneticLast/1384." + }, + { + "name": "AddValue", + "sourceType": "Contact", + "info": "AddValue Contact/department/1385." + }, + { + "name": "AddAssociation", + "sourceType": "Contact", + "info": "AddAssociation Contact/customDate/AGGREGATION/1386." + }, + { + "name": "AddAssociation", + "sourceType": "Contact", + "info": "AddAssociation Contact/websites/AGGREGATION/1387." + }, + { + "name": "AddAssociation", + "sourceType": "Contact", + "info": "AddAssociation Contact/relationships/AGGREGATION/1388." + }, + { + "name": "AddAssociation", + "sourceType": "Contact", + "info": "AddAssociation Contact/messengerHandles/AGGREGATION/1389." + }, + { + "name": "AddAssociation", + "sourceType": "Contact", + "info": "AddAssociation Contact/pronouns/AGGREGATION/1390." + } + ] } ] } diff --git a/src/api/common/TutanotaConstants.ts b/src/api/common/TutanotaConstants.ts index ee4a16e95fd6..57f327a0571b 100644 --- a/src/api/common/TutanotaConstants.ts +++ b/src/api/common/TutanotaConstants.ts @@ -3,7 +3,7 @@ import { DAY_IN_MILLIS, downcast } from "@tutao/tutanota-utils" import type { CertificateInfo, CreditCard, EmailSenderListElement, GroupMembership } from "../entities/sys/TypeRefs.js" import { AccountingInfo, Customer } from "../entities/sys/TypeRefs.js" -import type { CalendarEventAttendee, UserSettingsGroupRoot } from "../entities/tutanota/TypeRefs.js" +import type { CalendarEventAttendee, ContactCustomDate, ContactRelationship, UserSettingsGroupRoot } from "../entities/tutanota/TypeRefs.js" import { ContactSocialId, MailFolder } from "../entities/tutanota/TypeRefs.js" import { isApp, isElectronClient } from "./Env" import type { Country } from "./CountryList" @@ -125,7 +125,46 @@ export const enum ContactSocialType { CUSTOM = "5", } +export const enum ContactRelationshipType { + PARENT = "0", + BROTHER = "1", + SISTER = "2", + CHILD = "3", + FRIEND = "4", + RELATIVE = "5", + SPOUSE = "6", + PARTNER = "7", + ASSISTANT = "8", + MANAGER = "9", + OTHER = "10", + CUSTOM = "11", +} + +export const enum ContactMessengerHandleType { + SIGNAL = "0", + WHATSAPP = "1", + TELEGRAM = "2", + DISCORD = "3", + OTHER = "4", + CUSTOM = "5", +} + +export const enum ContactWebsiteType { + PRIVATE = "0", + WORK = "1", + OTHER = "2", + CUSTOM = "3", +} + +export const enum ContactCustomDateType { + ANNIVERSARY = "0", + OTHER = "1", + CUSTOM = "2", +} + export const getContactSocialType = (contactSocialId: ContactSocialId): ContactSocialType => downcast(contactSocialId.type) +export const getCustomDateType = (customDate: ContactCustomDate): ContactCustomDateType => downcast(customDate.type) +export const getRelationshipType = (relationship: ContactRelationship): ContactRelationshipType => downcast(relationship.type) export const enum OperationType { CREATE = "0", diff --git a/src/api/entities/gossip/ModelInfo.ts b/src/api/entities/gossip/ModelInfo.ts index 2a9a746ae733..ae21ccab109a 100644 --- a/src/api/entities/gossip/ModelInfo.ts +++ b/src/api/entities/gossip/ModelInfo.ts @@ -1,5 +1,5 @@ const modelInfo = { - version: 13, + version: 14, compatibleSince: 0, } diff --git a/src/api/entities/tutanota/ModelInfo.ts b/src/api/entities/tutanota/ModelInfo.ts index 66ebd42033a0..3d6f0a84a9af 100644 --- a/src/api/entities/tutanota/ModelInfo.ts +++ b/src/api/entities/tutanota/ModelInfo.ts @@ -1,6 +1,6 @@ const modelInfo = { - version: 66, - compatibleSince: 66, + version: 67, + compatibleSince: 67, } export default modelInfo \ No newline at end of file diff --git a/src/api/entities/tutanota/TypeModels.js b/src/api/entities/tutanota/TypeModels.js index 447031d9989a..5ba1beb50f86 100644 --- a/src/api/entities/tutanota/TypeModels.js +++ b/src/api/entities/tutanota/TypeModels.js @@ -56,7 +56,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "Birthday": { "name": "Birthday", @@ -106,7 +106,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "Body": { "name": "Body", @@ -147,7 +147,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarDeleteData": { "name": "CalendarDeleteData", @@ -181,7 +181,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarEvent": { "name": "CalendarEvent", @@ -371,7 +371,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarEventAttendee": { "name": "CalendarEventAttendee", @@ -414,7 +414,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarEventIndexRef": { "name": "CalendarEventIndexRef", @@ -448,7 +448,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarEventUidIndex": { "name": "CalendarEventUidIndex", @@ -519,7 +519,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarEventUpdate": { "name": "CalendarEventUpdate", @@ -598,7 +598,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarEventUpdateList": { "name": "CalendarEventUpdateList", @@ -632,7 +632,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarGroupRoot": { "name": "CalendarGroupRoot", @@ -722,7 +722,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CalendarRepeatRule": { "name": "CalendarRepeatRule", @@ -801,7 +801,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "Contact": { "name": "Contact", @@ -911,6 +911,15 @@ export const typeModels = { "cardinality": "One", "encrypted": true }, + "department": { + "final": false, + "name": "department", + "id": 1385, + "since": 67, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, "firstName": { "final": false, "name": "firstName", @@ -929,6 +938,24 @@ export const typeModels = { "cardinality": "One", "encrypted": true }, + "middleName": { + "final": false, + "name": "middleName", + "id": 1380, + "since": 67, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, + "nameSuffix": { + "final": false, + "name": "nameSuffix", + "id": 1381, + "since": 67, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, "nickname": { "final": false, "name": "nickname", @@ -947,6 +974,33 @@ export const typeModels = { "cardinality": "ZeroOrOne", "encrypted": true }, + "phoneticFirst": { + "final": false, + "name": "phoneticFirst", + "id": 1382, + "since": 67, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, + "phoneticLast": { + "final": false, + "name": "phoneticLast", + "id": 1384, + "since": 67, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, + "phoneticMiddle": { + "final": false, + "name": "phoneticMiddle", + "id": 1383, + "since": 67, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, "presharedPassword": { "final": false, "name": "presharedPassword", @@ -986,6 +1040,16 @@ export const typeModels = { "refType": "ContactAddress", "dependency": null }, + "customDate": { + "final": false, + "name": "customDate", + "id": 1386, + "since": 67, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ContactCustomDate", + "dependency": null + }, "mailAddresses": { "final": false, "name": "mailAddresses", @@ -996,6 +1060,16 @@ export const typeModels = { "refType": "ContactMailAddress", "dependency": null }, + "messengerHandles": { + "final": false, + "name": "messengerHandles", + "id": 1389, + "since": 67, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ContactMessengerHandle", + "dependency": null + }, "oldBirthdayAggregate": { "final": false, "name": "oldBirthdayAggregate", @@ -1026,6 +1100,26 @@ export const typeModels = { "refType": "File", "dependency": null }, + "pronouns": { + "final": false, + "name": "pronouns", + "id": 1390, + "since": 67, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ContactPronouns", + "dependency": null + }, + "relationships": { + "final": false, + "name": "relationships", + "id": 1388, + "since": 67, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ContactRelationship", + "dependency": null + }, "socialIds": { "final": false, "name": "socialIds", @@ -1035,10 +1129,20 @@ export const typeModels = { "cardinality": "Any", "refType": "ContactSocialId", "dependency": null + }, + "websites": { + "final": false, + "name": "websites", + "id": 1387, + "since": 67, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ContactWebsite", + "dependency": null } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ContactAddress": { "name": "ContactAddress", @@ -1088,7 +1192,57 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" + }, + "ContactCustomDate": { + "name": "ContactCustomDate", + "since": 67, + "type": "AGGREGATED_TYPE", + "id": 1356, + "rootId": "CHR1dGFub3RhAAVM", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1357, + "since": 67, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "customTypeName": { + "final": false, + "name": "customTypeName", + "id": 1359, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "dateIso": { + "final": false, + "name": "dateIso", + "id": 1360, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "type": { + "final": false, + "name": "type", + "id": 1358, + "since": 67, + "type": "Number", + "cardinality": "One", + "encrypted": true + } + }, + "associations": {}, + "app": "tutanota", + "version": "67" }, "ContactList": { "name": "ContactList", @@ -1168,7 +1322,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ContactListEntry": { "name": "ContactListEntry", @@ -1236,7 +1390,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "ContactListGroupRoot": { "name": "ContactListGroupRoot", @@ -1306,7 +1460,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ContactMailAddress": { "name": "ContactMailAddress", @@ -1356,7 +1510,57 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" + }, + "ContactMessengerHandle": { + "name": "ContactMessengerHandle", + "since": 67, + "type": "AGGREGATED_TYPE", + "id": 1371, + "rootId": "CHR1dGFub3RhAAVb", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1372, + "since": 67, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "customTypeName": { + "final": false, + "name": "customTypeName", + "id": 1374, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "handle": { + "final": false, + "name": "handle", + "id": 1375, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "type": { + "final": false, + "name": "type", + "id": 1373, + "since": 67, + "type": "Number", + "cardinality": "One", + "encrypted": true + } + }, + "associations": {}, + "app": "tutanota", + "version": "67" }, "ContactPhoneNumber": { "name": "ContactPhoneNumber", @@ -1406,7 +1610,98 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" + }, + "ContactPronouns": { + "name": "ContactPronouns", + "since": 67, + "type": "AGGREGATED_TYPE", + "id": 1376, + "rootId": "CHR1dGFub3RhAAVg", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1377, + "since": 67, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "language": { + "final": false, + "name": "language", + "id": 1378, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "pronouns": { + "final": false, + "name": "pronouns", + "id": 1379, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + } + }, + "associations": {}, + "app": "tutanota", + "version": "67" + }, + "ContactRelationship": { + "name": "ContactRelationship", + "since": 67, + "type": "AGGREGATED_TYPE", + "id": 1366, + "rootId": "CHR1dGFub3RhAAVW", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1367, + "since": 67, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "customTypeName": { + "final": false, + "name": "customTypeName", + "id": 1369, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "person": { + "final": false, + "name": "person", + "id": 1370, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "type": { + "final": false, + "name": "type", + "id": 1368, + "since": 67, + "type": "Number", + "cardinality": "One", + "encrypted": true + } + }, + "associations": {}, + "app": "tutanota", + "version": "67" }, "ContactSocialId": { "name": "ContactSocialId", @@ -1456,7 +1751,57 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" + }, + "ContactWebsite": { + "name": "ContactWebsite", + "since": 67, + "type": "AGGREGATED_TYPE", + "id": 1361, + "rootId": "CHR1dGFub3RhAAVR", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1362, + "since": 67, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "customTypeName": { + "final": false, + "name": "customTypeName", + "id": 1364, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "type": { + "final": false, + "name": "type", + "id": 1363, + "since": 67, + "type": "Number", + "cardinality": "One", + "encrypted": true + }, + "url": { + "final": false, + "name": "url", + "id": 1365, + "since": 67, + "type": "String", + "cardinality": "One", + "encrypted": true + } + }, + "associations": {}, + "app": "tutanota", + "version": "67" }, "ConversationEntry": { "name": "ConversationEntry", @@ -1545,7 +1890,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CreateExternalUserGroupData": { "name": "CreateExternalUserGroupData", @@ -1595,7 +1940,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "CreateGroupPostReturn": { "name": "CreateGroupPostReturn", @@ -1629,7 +1974,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CreateLocalAdminGroupData": { "name": "CreateLocalAdminGroupData", @@ -1672,7 +2017,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CreateMailFolderData": { "name": "CreateMailFolderData", @@ -1733,7 +2078,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CreateMailFolderReturn": { "name": "CreateMailFolderReturn", @@ -1767,7 +2112,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CreateMailGroupData": { "name": "CreateMailGroupData", @@ -1828,7 +2173,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "CustomerAccountCreateData": { "name": "CustomerAccountCreateData", @@ -1982,7 +2327,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DeleteGroupData": { "name": "DeleteGroupData", @@ -2025,7 +2370,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DeleteMailData": { "name": "DeleteMailData", @@ -2069,7 +2414,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DeleteMailFolderData": { "name": "DeleteMailFolderData", @@ -2103,7 +2448,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DraftAttachment": { "name": "DraftAttachment", @@ -2156,7 +2501,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DraftCreateData": { "name": "DraftCreateData", @@ -2226,7 +2571,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DraftCreateReturn": { "name": "DraftCreateReturn", @@ -2260,7 +2605,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DraftData": { "name": "DraftData", @@ -2407,7 +2752,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DraftRecipient": { "name": "DraftRecipient", @@ -2448,7 +2793,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "DraftUpdateData": { "name": "DraftUpdateData", @@ -2492,7 +2837,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "DraftUpdateReturn": { "name": "DraftUpdateReturn", @@ -2526,7 +2871,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "EmailTemplate": { "name": "EmailTemplate", @@ -2614,7 +2959,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "EmailTemplateContent": { "name": "EmailTemplateContent", @@ -2655,7 +3000,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "EncryptTutanotaPropertiesData": { "name": "EncryptTutanotaPropertiesData", @@ -2698,7 +3043,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "EncryptedMailAddress": { "name": "EncryptedMailAddress", @@ -2739,7 +3084,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "EntropyData": { "name": "EntropyData", @@ -2771,7 +3116,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "ExternalUserData": { "name": "ExternalUserData", @@ -2904,7 +3249,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "File": { "name": "File", @@ -3048,7 +3393,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "FileSystem": { "name": "FileSystem", @@ -3118,7 +3463,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "GroupInvitationDeleteData": { "name": "GroupInvitationDeleteData", @@ -3152,7 +3497,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "GroupInvitationPostData": { "name": "GroupInvitationPostData", @@ -3196,7 +3541,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "GroupInvitationPostReturn": { "name": "GroupInvitationPostReturn", @@ -3250,7 +3595,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "GroupInvitationPutData": { "name": "GroupInvitationPutData", @@ -3302,7 +3647,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "GroupSettings": { "name": "GroupSettings", @@ -3354,7 +3699,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "Header": { "name": "Header", @@ -3395,7 +3740,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "ImapFolder": { "name": "ImapFolder", @@ -3456,7 +3801,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ImapSyncConfiguration": { "name": "ImapSyncConfiguration", @@ -3526,7 +3871,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ImapSyncState": { "name": "ImapSyncState", @@ -3587,7 +3932,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "InboxRule": { "name": "InboxRule", @@ -3639,7 +3984,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "InternalGroupData": { "name": "InternalGroupData", @@ -3745,7 +4090,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "InternalRecipientKeyData": { "name": "InternalRecipientKeyData", @@ -3804,7 +4149,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "KnowledgeBaseEntry": { "name": "KnowledgeBaseEntry", @@ -3892,7 +4237,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "KnowledgeBaseEntryKeyword": { "name": "KnowledgeBaseEntryKeyword", @@ -3924,7 +4269,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "ListUnsubscribeData": { "name": "ListUnsubscribeData", @@ -3976,7 +4321,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "Mail": { "name": "Mail", @@ -4301,7 +4646,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailAddress": { "name": "MailAddress", @@ -4353,7 +4698,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailAddressProperties": { "name": "MailAddressProperties", @@ -4394,7 +4739,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "MailBody": { "name": "MailBody", @@ -4489,7 +4834,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "MailBox": { "name": "MailBox", @@ -4627,7 +4972,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailDetails": { "name": "MailDetails", @@ -4709,7 +5054,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailDetailsBlob": { "name": "MailDetailsBlob", @@ -4779,7 +5124,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailDetailsDraft": { "name": "MailDetailsDraft", @@ -4849,7 +5194,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailDetailsDraftsRef": { "name": "MailDetailsDraftsRef", @@ -4883,7 +5228,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailFolder": { "name": "MailFolder", @@ -4991,7 +5336,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailFolderRef": { "name": "MailFolderRef", @@ -5025,7 +5370,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailHeaders": { "name": "MailHeaders", @@ -5102,7 +5447,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "MailboxGroupRoot": { "name": "MailboxGroupRoot", @@ -5223,7 +5568,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailboxProperties": { "name": "MailboxProperties", @@ -5302,7 +5647,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "MailboxServerProperties": { "name": "MailboxServerProperties", @@ -5361,7 +5706,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "MoveMailData": { "name": "MoveMailData", @@ -5405,7 +5750,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "NewDraftAttachment": { "name": "NewDraftAttachment", @@ -5466,7 +5811,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "NewsId": { "name": "NewsId", @@ -5507,7 +5852,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "NewsIn": { "name": "NewsIn", @@ -5539,7 +5884,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "NewsOut": { "name": "NewsOut", @@ -5573,7 +5918,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "NotificationMail": { "name": "NotificationMail", @@ -5641,7 +5986,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "OutOfOfficeNotification": { "name": "OutOfOfficeNotification", @@ -5729,7 +6074,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "OutOfOfficeNotificationMessage": { "name": "OutOfOfficeNotificationMessage", @@ -5779,7 +6124,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "OutOfOfficeNotificationRecipientList": { "name": "OutOfOfficeNotificationRecipientList", @@ -5813,7 +6158,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "PasswordAutoAuthenticationReturn": { "name": "PasswordAutoAuthenticationReturn", @@ -5836,7 +6181,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "PasswordChannelPhoneNumber": { "name": "PasswordChannelPhoneNumber", @@ -5868,7 +6213,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "PasswordChannelReturn": { "name": "PasswordChannelReturn", @@ -5902,7 +6247,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "PasswordMessagingData": { "name": "PasswordMessagingData", @@ -5952,7 +6297,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "PasswordMessagingReturn": { "name": "PasswordMessagingReturn", @@ -5984,7 +6329,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "PasswordRetrievalData": { "name": "PasswordRetrievalData", @@ -6016,7 +6361,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "PasswordRetrievalReturn": { "name": "PasswordRetrievalReturn", @@ -6048,7 +6393,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "PhishingMarkerWebsocketData": { "name": "PhishingMarkerWebsocketData", @@ -6091,7 +6436,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "PhotosRef": { "name": "PhotosRef", @@ -6125,7 +6470,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ReceiveInfoServiceData": { "name": "ReceiveInfoServiceData", @@ -6157,7 +6502,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "Recipients": { "name": "Recipients", @@ -6211,7 +6556,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "RemoteImapSyncInfo": { "name": "RemoteImapSyncInfo", @@ -6281,7 +6626,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ReportMailPostData": { "name": "ReportMailPostData", @@ -6333,7 +6678,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "ReportedMailFieldMarker": { "name": "ReportedMailFieldMarker", @@ -6374,7 +6719,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "SecureExternalRecipientKeyData": { "name": "SecureExternalRecipientKeyData", @@ -6489,7 +6834,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "SendDraftData": { "name": "SendDraftData", @@ -6617,7 +6962,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "SendDraftReturn": { "name": "SendDraftReturn", @@ -6679,7 +7024,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "SharedGroupData": { "name": "SharedGroupData", @@ -6774,7 +7119,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "SpamResults": { "name": "SpamResults", @@ -6808,7 +7153,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "Subfiles": { "name": "Subfiles", @@ -6842,7 +7187,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "SymEncInternalRecipientKeyData": { "name": "SymEncInternalRecipientKeyData", @@ -6894,7 +7239,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "TemplateGroupRoot": { "name": "TemplateGroupRoot", @@ -6974,7 +7319,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "TutanotaProperties": { "name": "TutanotaProperties", @@ -7145,7 +7490,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "UpdateMailFolderData": { "name": "UpdateMailFolderData", @@ -7189,7 +7534,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "UserAccountCreateData": { "name": "UserAccountCreateData", @@ -7242,7 +7587,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "UserAccountUserData": { "name": "UserAccountUserData", @@ -7463,7 +7808,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "66" + "version": "67" }, "UserAreaGroupData": { "name": "UserAreaGroupData", @@ -7542,7 +7887,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "UserAreaGroupDeleteData": { "name": "UserAreaGroupDeleteData", @@ -7576,7 +7921,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "UserAreaGroupPostData": { "name": "UserAreaGroupPostData", @@ -7610,7 +7955,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" }, "UserSettingsGroupRoot": { "name": "UserSettingsGroupRoot", @@ -7707,6 +8052,6 @@ export const typeModels = { } }, "app": "tutanota", - "version": "66" + "version": "67" } } \ No newline at end of file diff --git a/src/api/entities/tutanota/TypeRefs.ts b/src/api/entities/tutanota/TypeRefs.ts index 605b87014eb8..f3107932d33a 100644 --- a/src/api/entities/tutanota/TypeRefs.ts +++ b/src/api/entities/tutanota/TypeRefs.ts @@ -227,20 +227,31 @@ export type Contact = { birthdayIso: null | string; comment: string; company: string; + department: null | string; firstName: string; lastName: string; + middleName: null | string; + nameSuffix: null | string; nickname: null | string; oldBirthdayDate: null | Date; + phoneticFirst: null | string; + phoneticLast: null | string; + phoneticMiddle: null | string; presharedPassword: null | string; role: string; title: null | string; addresses: ContactAddress[]; + customDate: ContactCustomDate[]; mailAddresses: ContactMailAddress[]; + messengerHandles: ContactMessengerHandle[]; oldBirthdayAggregate: null | Birthday; phoneNumbers: ContactPhoneNumber[]; photo: null | IdTuple; + pronouns: ContactPronouns[]; + relationships: ContactRelationship[]; socialIds: ContactSocialId[]; + websites: ContactWebsite[]; } export const ContactAddressTypeRef: TypeRef = new TypeRef("tutanota", "ContactAddress") @@ -256,6 +267,20 @@ export type ContactAddress = { customTypeName: string; type: NumberString; } +export const ContactCustomDateTypeRef: TypeRef = new TypeRef("tutanota", "ContactCustomDate") + +export function createContactCustomDate(values: StrippedEntity): ContactCustomDate { + return Object.assign(create(typeModels.ContactCustomDate, ContactCustomDateTypeRef), values) +} + +export type ContactCustomDate = { + _type: TypeRef; + + _id: Id; + customTypeName: string; + dateIso: string; + type: NumberString; +} export const ContactListTypeRef: TypeRef = new TypeRef("tutanota", "ContactList") export function createContactList(values: StrippedEntity): ContactList { @@ -324,6 +349,20 @@ export type ContactMailAddress = { customTypeName: string; type: NumberString; } +export const ContactMessengerHandleTypeRef: TypeRef = new TypeRef("tutanota", "ContactMessengerHandle") + +export function createContactMessengerHandle(values: StrippedEntity): ContactMessengerHandle { + return Object.assign(create(typeModels.ContactMessengerHandle, ContactMessengerHandleTypeRef), values) +} + +export type ContactMessengerHandle = { + _type: TypeRef; + + _id: Id; + customTypeName: string; + handle: string; + type: NumberString; +} export const ContactPhoneNumberTypeRef: TypeRef = new TypeRef("tutanota", "ContactPhoneNumber") export function createContactPhoneNumber(values: StrippedEntity): ContactPhoneNumber { @@ -338,6 +377,33 @@ export type ContactPhoneNumber = { number: string; type: NumberString; } +export const ContactPronounsTypeRef: TypeRef = new TypeRef("tutanota", "ContactPronouns") + +export function createContactPronouns(values: StrippedEntity): ContactPronouns { + return Object.assign(create(typeModels.ContactPronouns, ContactPronounsTypeRef), values) +} + +export type ContactPronouns = { + _type: TypeRef; + + _id: Id; + language: string; + pronouns: string; +} +export const ContactRelationshipTypeRef: TypeRef = new TypeRef("tutanota", "ContactRelationship") + +export function createContactRelationship(values: StrippedEntity): ContactRelationship { + return Object.assign(create(typeModels.ContactRelationship, ContactRelationshipTypeRef), values) +} + +export type ContactRelationship = { + _type: TypeRef; + + _id: Id; + customTypeName: string; + person: string; + type: NumberString; +} export const ContactSocialIdTypeRef: TypeRef = new TypeRef("tutanota", "ContactSocialId") export function createContactSocialId(values: StrippedEntity): ContactSocialId { @@ -352,6 +418,20 @@ export type ContactSocialId = { socialId: string; type: NumberString; } +export const ContactWebsiteTypeRef: TypeRef = new TypeRef("tutanota", "ContactWebsite") + +export function createContactWebsite(values: StrippedEntity): ContactWebsite { + return Object.assign(create(typeModels.ContactWebsite, ContactWebsiteTypeRef), values) +} + +export type ContactWebsite = { + _type: TypeRef; + + _id: Id; + customTypeName: string; + type: NumberString; + url: string; +} export const ConversationEntryTypeRef: TypeRef = new TypeRef("tutanota", "ConversationEntry") export function createConversationEntry(values: StrippedEntity): ConversationEntry { diff --git a/src/api/worker/offline/OfflineStorageMigrator.ts b/src/api/worker/offline/OfflineStorageMigrator.ts index a218a8d37c8d..e56ead364f7f 100644 --- a/src/api/worker/offline/OfflineStorageMigrator.ts +++ b/src/api/worker/offline/OfflineStorageMigrator.ts @@ -11,6 +11,7 @@ import { tutanota65 } from "./migrations/tutanota-v65.js" import { sys91 } from "./migrations/sys-v91.js" import { sys90 } from "./migrations/sys-v90.js" import { tutanota64 } from "./migrations/tutanota-v64.js" +import { tutanota67 } from "./migrations/tutanota-v67.js" export interface OfflineMigration { readonly app: VersionMetadataBaseKey @@ -25,7 +26,7 @@ export interface OfflineMigration { * Normally you should only add them to the end of the list but with offline ones it can be a bit tricky since they change the db structure itself so sometimes * they should rather be in the beginning. */ -export const OFFLINE_STORAGE_MIGRATIONS: ReadonlyArray = [sys90, tutanota64, sys91, tutanota65, sys92, tutanota66, sys94] +export const OFFLINE_STORAGE_MIGRATIONS: ReadonlyArray = [sys90, tutanota64, sys91, tutanota65, sys92, tutanota66, sys94, tutanota67] const CURRENT_OFFLINE_VERSION = 1 diff --git a/src/api/worker/offline/migrations/tutanota-v67.ts b/src/api/worker/offline/migrations/tutanota-v67.ts new file mode 100644 index 000000000000..c3c5a0e0ccfe --- /dev/null +++ b/src/api/worker/offline/migrations/tutanota-v67.ts @@ -0,0 +1,12 @@ +import { OfflineMigration } from "../OfflineStorageMigrator.js" +import { OfflineStorage } from "../OfflineStorage.js" +import { deleteInstancesOfType } from "../StandardMigrations.js" +import { ContactTypeRef } from "../../../entities/tutanota/TypeRefs.js" + +export const tutanota67: OfflineMigration = { + app: "tutanota", + version: 67, + async migrate(storage: OfflineStorage) { + await deleteInstancesOfType(storage, ContactTypeRef) + }, +} diff --git a/src/calendar/view/CalendarEventBubble.ts b/src/calendar/view/CalendarEventBubble.ts index 3c92c69dfc98..7e7159bcdaab 100644 --- a/src/calendar/view/CalendarEventBubble.ts +++ b/src/calendar/view/CalendarEventBubble.ts @@ -50,9 +50,9 @@ export class CalendarEventBubble implements Component opacity: attrs.opacity, pointerEvents: enablePointerEvents ? "auto" : "none", }, - onclick: (e: MouseEvent) => { + onclick: (e: MouseEvent, dom: HTMLElement) => { e.stopPropagation() - attrs.click(e, e.target as HTMLElement) + attrs.click(e, dom) }, }, [ diff --git a/src/contacts/ContactAggregateEditor.ts b/src/contacts/ContactAggregateEditor.ts index a95c82f30737..ba9464809ae9 100644 --- a/src/contacts/ContactAggregateEditor.ts +++ b/src/contacts/ContactAggregateEditor.ts @@ -1,4 +1,4 @@ -import type { TextFieldType } from "../gui/base/TextField.js" +import { TextFieldAttrs, TextFieldType } from "../gui/base/TextField.js" import { TextField } from "../gui/base/TextField.js" import type { TranslationKey } from "../misc/LanguageViewModel" import { lang } from "../misc/LanguageViewModel" @@ -9,6 +9,8 @@ import { attachDropdown } from "../gui/base/Dropdown.js" import { IconButton } from "../gui/base/IconButton.js" import { BootIcons } from "../gui/base/icons/BootIcons.js" import { ButtonSize } from "../gui/base/ButtonSize.js" +import { lazy } from "@tutao/tutanota-utils" +import type { TranslationKeyType } from "../misc/TranslationKey.js" export type AggregateEditorAttrs = { value: string @@ -20,7 +22,7 @@ export type AggregateEditorAttrs = { fieldType: TextFieldType onUpdate: (newValue: string) => unknown label: string - helpLabel: TranslationKey + helpLabel: TranslationKey | lazy typeLabels: ReadonlyArray<[AggregateType, TranslationKey]> onTypeSelected: (arg0: AggregateType) => unknown } @@ -37,12 +39,19 @@ export class ContactAggregateEditor implements Component>): Children { const attrs = vnode.attrs + const helpLabel = () => { + if (typeof attrs.helpLabel === "function") { + return attrs.helpLabel() + } + + return lang.get(attrs.helpLabel) + } return m(".flex.items-center.child-grow", [ m(TextField, { value: attrs.value, label: () => attrs.label, type: attrs.fieldType, - helpLabel: () => lang.get(attrs.helpLabel), + helpLabel: () => helpLabel(), injectionsRight: () => this._moreButtonFor(attrs), oninput: (value) => attrs.onUpdate(value), }), diff --git a/src/contacts/ContactEditor.ts b/src/contacts/ContactEditor.ts index 86dd96f54cb8..f0164c52e762 100644 --- a/src/contacts/ContactEditor.ts +++ b/src/contacts/ContactEditor.ts @@ -3,16 +3,40 @@ import { Dialog } from "../gui/base/Dialog" import type { TranslationKey } from "../misc/LanguageViewModel" import { lang } from "../misc/LanguageViewModel" import { isMailAddress } from "../misc/FormatValidator" -import { formatBirthdayNumeric, formatBirthdayOfContact } from "./model/ContactUtils" -import { ContactAddressType, ContactPhoneNumberType, ContactSocialType, GroupType, Keys } from "../api/common/TutanotaConstants" -import type { Contact, ContactAddress, ContactMailAddress, ContactPhoneNumber, ContactSocialId } from "../api/entities/tutanota/TypeRefs.js" +import { formatBirthdayNumeric, formatContactDate } from "./model/ContactUtils" import { + ContactAddressType, + ContactCustomDateType, + ContactMessengerHandleType, + ContactPhoneNumberType, + ContactRelationshipType, + ContactSocialType, + ContactWebsiteType, + GroupType, + Keys, +} from "../api/common/TutanotaConstants" +import { + Contact, + ContactAddress, + ContactCustomDate, + ContactMailAddress, + ContactMessengerHandle, + ContactPhoneNumber, + ContactPronouns, + ContactRelationship, + ContactSocialId, + ContactWebsite, createBirthday, createContact, createContactAddress, + createContactCustomDate, createContactMailAddress, + createContactMessengerHandle, createContactPhoneNumber, + createContactPronouns, + createContactRelationship, createContactSocialId, + createContactWebsite, } from "../api/entities/tutanota/TypeRefs.js" import { assertNotNull, clone, downcast, findAndRemove, lastIndex, lastThrow, noOp, typedEntries } from "@tutao/tutanota-utils" import { assertMainOrNode } from "../api/common/Env" @@ -22,11 +46,19 @@ import type { ButtonAttrs } from "../gui/base/Button.js" import { ButtonType } from "../gui/base/Button.js" import { birthdayToIsoDate } from "../api/common/utils/BirthdayUtils" import { + ContactCustomDateTypeToLabel, + ContactCustomWebsiteTypeToLabel, ContactMailAddressTypeToLabel, + ContactMessengerHandleTypeToLabel, ContactPhoneNumberTypeToLabel, + ContactRelationshipTypeToLabel, ContactSocialTypeToLabel, getContactAddressTypeLabel, + getContactCustomDateTypeToLabel, + getContactCustomWebsiteTypeToLabel, + getContactMessengerHandleTypeToLabel, getContactPhoneNumberTypeLabel, + getContactRelationshipTypeToLabel, getContactSocialTypeLabel, } from "./view/ContactGuiUtils" import { parseBirthday } from "../misc/DateParser" @@ -34,7 +66,7 @@ import type { TextFieldAttrs } from "../gui/base/TextField.js" import { Autocomplete, TextField, TextFieldType } from "../gui/base/TextField.js" import { EntityClient } from "../api/common/EntityClient" import { timestampToGeneratedId } from "../api/common/utils/EntityUtils" -import { ContactAggregateEditor } from "./ContactAggregateEditor" +import { AggregateEditorAttrs, ContactAggregateEditor } from "./ContactAggregateEditor" import { DefaultAnimationTime } from "../gui/animation/Animations" import { DialogHeaderBarAttrs } from "../gui/base/DialogHeaderBar" import { ProgrammingError } from "../api/common/error/ProgrammingError.js" @@ -48,6 +80,11 @@ assertMainOrNode() const TAG = "[ContactEditor]" +interface CompleteCustomDate extends ContactCustomDate { + date: string + isValid: boolean +} + export class ContactEditor { private readonly dialog: Dialog private hasInvalidBirthday: boolean @@ -55,6 +92,11 @@ export class ContactEditor { private readonly phoneNumbers: Array<[ContactPhoneNumber, Id]> private readonly addresses: Array<[ContactAddress, Id]> private readonly socialIds: Array<[ContactSocialId, Id]> + private readonly websites: Array<[ContactWebsite, Id]> + private readonly relationships: Array<[ContactRelationship, Id]> + private readonly messengerHandles: Array<[ContactMessengerHandle, Id]> + private readonly pronouns: Array<[ContactPronouns, Id]> + private readonly customDates: Array<[CompleteCustomDate, Id]> private birthday: string private isPasswordRevealed: boolean = false windowCloseUnsubscribe: () => unknown @@ -97,6 +139,17 @@ export class ContactEditor { addresses: [], autoTransmitPassword: "", oldBirthdayAggregate: null, + department: null, + middleName: null, + nameSuffix: null, + phoneticFirst: null, + phoneticLast: null, + phoneticMiddle: null, + customDate: [], + messengerHandles: [], + pronouns: [], + relationships: [], + websites: [], }) this.isNewContact = contact?._id == null @@ -116,8 +169,20 @@ export class ContactEditor { this.addresses.push(this.newAddress()) this.socialIds = this.contact.socialIds.map((socialId) => [socialId, id(socialId)]) this.socialIds.push(this.newSocialId()) + + this.websites = this.contact.websites.map((website) => [website, id(website)]) + this.websites.push(this.newWebsite()) + this.relationships = this.contact.relationships.map((relation) => [relation, id(relation)]) + this.relationships.push(this.newRelationship()) + this.messengerHandles = this.contact.messengerHandles.map((handler) => [handler, id(handler)]) + this.messengerHandles.push(this.newMessengerHandler()) + this.pronouns = this.contact.pronouns.map((pronoun) => [pronoun, id(pronoun)]) + this.pronouns.push(this.newPronoun()) + this.customDates = this.contact.customDate.map((date) => [{ ...date, date: formatContactDate(date.dateIso), isValid: true }, id(date)]) + this.customDates.push(this.newCustomDate()) + this.hasInvalidBirthday = false - this.birthday = formatBirthdayOfContact(this.contact) || "" + this.birthday = formatContactDate(this.contact.birthdayIso) || "" this.dialog = this.createDialog() this.windowCloseUnsubscribe = noOp } @@ -133,9 +198,29 @@ export class ContactEditor { view(): Children { return m("#contact-editor", [ m(".wrapping-row", [this.renderFirstNameField(), this.renderLastNameField()]), - m(".wrapping-row", [this.renderTitleField(), this.renderBirthdayField()]), - m(".wrapping-row", [this.renderRoleField(), this.renderCompanyField(), this.renderNickNameField(), this.renderCommentField()]), + m(".wrapping-row", [this.renderField("middleName", "middleName_placeholder"), this.renderTitleField()]), + m(".wrapping-row", [this.renderField("nameSuffix", "nameSuffix_placeholder"), this.renderField("phoneticFirst", "phoneticFirst_placeholder")]), + m(".wrapping-row", [ + this.renderField("phoneticMiddle", "phoneticMiddle_placeholder"), + this.renderField("phoneticLast", "phoneticLast_placeholder"), + ]), + m(".wrapping-row", [this.renderField("nickname", "nickname_placeholder"), this.renderBirthdayField()]), + m(".wrapping-row", [ + this.renderRoleField(), + this.renderField("department", "department_placeholder"), + this.renderCompanyField(), + this.renderCommentField(), + ]), m(".wrapping-row", [ + m(".custom-dates.mt-xl", [ + m(".h4", lang.get("dates_label")), + m(".aggregateEditors", [ + this.customDates.map(([date, id], index) => { + const lastEditor = index === lastIndex(this.customDates) + return this.renderCustomDatesEditor(id, !lastEditor, date) + }), + ]), + ]), m(".mail.mt-xl", [ m(".h4", lang.get("email_label")), m(".aggregateEditors", [ @@ -154,8 +239,15 @@ export class ContactEditor { }), ]), ]), - ]), - m(".wrapping-row", [ + m(".relationship.mt-xl", [ + m(".h4", lang.get("relationships_label")), + m(".aggregateEditors", [ + this.relationships.map(([relationship, id], index) => { + const lastEditor = index === lastIndex(this.relationships) + return this.renderRelationshipsEditor(id, !lastEditor, relationship) + }), + ]), + ]), m(".address.mt-xl", [ m(".h4", lang.get("address_label")), m(".aggregateEditors", [ @@ -165,6 +257,17 @@ export class ContactEditor { }), ]), ]), + ]), + m(".wrapping-row", [ + m(".pronouns.mt-xl", [ + m(".h4", lang.get("pronouns_label")), + m(".aggregateEditors", [ + this.pronouns.map(([pronouns, id], index) => { + const lastEditor = index === lastIndex(this.pronouns) + return this.renderPronounsEditor(id, !lastEditor, pronouns) + }), + ]), + ]), m(".social.mt-xl", [ m(".h4", lang.get("social_label")), m(".aggregateEditors", [ @@ -174,6 +277,24 @@ export class ContactEditor { }), ]), ]), + m(".website.mt-xl", [ + m(".h4", lang.get("websites_label")), + m(".aggregateEditors", [ + this.websites.map(([website, id], index) => { + const lastEditor = index === lastIndex(this.websites) + return this.renderWebsitesEditor(id, !lastEditor, website) + }), + ]), + ]), + m(".instant-message.mt-xl", [ + m(".h4", lang.get("messenger_handles_label")), + m(".aggregateEditors", [ + this.messengerHandles.map(([handle, id], index) => { + const lastEditor = index === lastIndex(this.messengerHandles) + return this.renderMessengerHandleEditor(id, !lastEditor, handle) + }), + ]), + ]), ]), this.renderPresharedPasswordField(), m(".pb"), @@ -211,6 +332,11 @@ export class ContactEditor { this.contact.phoneNumbers = this.phoneNumbers.map((e) => e[0]).filter((e) => e.number.trim().length > 0) this.contact.addresses = this.addresses.map((e) => e[0]).filter((e) => e.address.trim().length > 0) this.contact.socialIds = this.socialIds.map((e) => e[0]).filter((e) => e.socialId.trim().length > 0) + this.contact.customDate = this.customDates.map((e) => e[0] as ContactCustomDate).filter((e) => e.dateIso.trim().length > 0) + this.contact.relationships = this.relationships.map((e) => e[0]).filter((e) => e.person.trim().length > 0) + this.contact.websites = this.websites.map((e) => e[0]).filter((e) => e.url.length > 0) + this.contact.messengerHandles = this.messengerHandles.map((e) => e[0]).filter((e) => e.handle.length > 0) + this.contact.pronouns = this.pronouns.map((e) => e[0]).filter((e) => e.pronouns.length > 0) try { if (this.isNewContact) { await this.saveNewContact() @@ -254,6 +380,57 @@ export class ContactEditor { } } + private renderCustomDatesEditor(id: Id, allowCancel: boolean, date: CompleteCustomDate): Children { + let dateHelpText = () => { + let bday = createBirthday({ + day: "22", + month: "9", + year: "2000", + }) + return !date.isValid + ? lang.get("invalidDateFormat_msg", { + "{1}": formatBirthdayNumeric(bday), + }) + : "" + } + + const typeLabels: Array<[ContactCustomDateType, TranslationKey]> = typedEntries(ContactCustomDateTypeToLabel) + return m(ContactAggregateEditor, { + value: date.date, + fieldType: TextFieldType.Text, + label: getContactCustomDateTypeToLabel(downcast(date.type), date.customTypeName), + helpLabel: () => dateHelpText(), + cancelAction: () => { + findAndRemove(this.mailAddresses, (t) => t[1] === id) + }, + onUpdate: (value) => { + date.date = value + if (value.trim().length > 0) { + let parsedDate = parseBirthday(value, (referenceDate) => formatDate(referenceDate)) + + if (parsedDate) { + try { + date.dateIso = birthdayToIsoDate(parsedDate) + if (date === lastThrow(this.customDates)[0]) this.customDates.push(this.newCustomDate()) + date.isValid = true + } catch (e) { + date.isValid = false + } + } else { + date.isValid = false + } + } else { + date.isValid = true + } + }, + animateCreate: !date.dateIso, + allowCancel, + key: id, + typeLabels, + onTypeSelected: (type) => this.onTypeSelected(type === ContactCustomDateType.CUSTOM, type, date), + } satisfies AggregateEditorAttrs) + } + private renderMailAddressesEditor(id: Id, allowCancel: boolean, mailAddress: ContactMailAddress): Children { let helpLabel: TranslationKey @@ -350,6 +527,94 @@ export class ContactEditor { }) } + private renderWebsitesEditor(id: Id, allowCancel: boolean, website: ContactWebsite): Children { + const typeLabels = typedEntries(ContactCustomWebsiteTypeToLabel) + return m(ContactAggregateEditor, { + value: website.url, + fieldType: TextFieldType.Text, + label: getContactCustomWebsiteTypeToLabel(downcast(website.type), website.customTypeName), + helpLabel: "emptyString_msg", + cancelAction: () => { + findAndRemove(this.websites, (t) => t[1] === id) + }, + onUpdate: (value) => { + website.url = value + if (website === lastThrow(this.websites)[0]) this.websites.push(this.newWebsite()) + }, + animateCreate: !website.url, + allowCancel, + key: id, + typeLabels, + onTypeSelected: (type) => this.onTypeSelected(type === ContactWebsiteType.CUSTOM, type, website), + }) + } + + private renderRelationshipsEditor(id: Id, allowCancel: boolean, relationship: ContactRelationship): Children { + const typeLabels = typedEntries(ContactRelationshipTypeToLabel) + return m(ContactAggregateEditor, { + value: relationship.person, + fieldType: TextFieldType.Text, + label: getContactRelationshipTypeToLabel(downcast(relationship.type), relationship.customTypeName), + helpLabel: "emptyString_msg", + cancelAction: () => { + findAndRemove(this.relationships, (t) => t[1] === id) + }, + onUpdate: (value) => { + relationship.person = value + if (relationship === lastThrow(this.relationships)[0]) this.relationships.push(this.newRelationship()) + }, + animateCreate: !relationship.person, + allowCancel, + key: id, + typeLabels, + onTypeSelected: (type) => this.onTypeSelected(type === ContactRelationshipType.CUSTOM, type, relationship), + }) + } + + private renderMessengerHandleEditor(id: Id, allowCancel: boolean, messengerHandle: ContactMessengerHandle): Children { + const typeLabels = typedEntries(ContactMessengerHandleTypeToLabel) + return m(ContactAggregateEditor, { + value: messengerHandle.handle, + fieldType: TextFieldType.Text, + label: getContactMessengerHandleTypeToLabel(downcast(messengerHandle.type), messengerHandle.customTypeName), + helpLabel: "emptyString_msg", + cancelAction: () => { + findAndRemove(this.messengerHandles, (t) => t[1] === id) + }, + onUpdate: (value) => { + messengerHandle.handle = value + if (messengerHandle === lastThrow(this.messengerHandles)[0]) this.messengerHandles.push(this.newMessengerHandler()) + }, + animateCreate: !messengerHandle.handle, + allowCancel, + key: id, + typeLabels, + onTypeSelected: (type) => this.onTypeSelected(type === ContactMessengerHandleType.CUSTOM, type, messengerHandle), + }) + } + + private renderPronounsEditor(id: Id, allowCancel: boolean, pronouns: ContactPronouns): Children { + const typeLabels = typedEntries({ "0": "language_label" } as Record) + return m(ContactAggregateEditor, { + value: pronouns.pronouns, + fieldType: TextFieldType.Text, + label: pronouns.language, + helpLabel: "emptyString_msg", + cancelAction: () => { + findAndRemove(this.messengerHandles, (t) => t[1] === id) + }, + onUpdate: (value) => { + pronouns.pronouns = value + if (pronouns === lastThrow(this.pronouns)[0]) this.pronouns.push(this.newPronoun()) + }, + animateCreate: !pronouns.pronouns, + allowCancel, + key: id, + typeLabels, + onTypeSelected: () => this.onLanguageSelect(pronouns), + }) + } + private renderCommentField(): Children { return m(StandaloneField, { label: "comment_label", @@ -367,12 +632,16 @@ export class ContactEditor { }) } - private renderNickNameField(): Children { + private renderField(fieldName: keyof Contact, label: TranslationKey): Children { return m(StandaloneField, { - label: "nickname_placeholder", - value: this.contact.nickname ?? "", - oninput: (value) => (this.contact.nickname = value), - }) + label, + value: (this.contact[fieldName] ?? "") as string, + oninput: (value: string) => { + if (typeof value === "string") { + this.contact[fieldName] = downcast(value) + } + }, + } satisfies TextFieldAttrs) } private renderLastNameField(): Children { @@ -472,7 +741,7 @@ export class ContactEditor { private createCloseButtonAttrs(): ButtonAttrs { return { label: "close_alt", - click: (e, dom) => this.close(), + click: () => this.close(), type: ButtonType.Secondary, } } @@ -513,6 +782,50 @@ export class ContactEditor { return [socialId, this.newId()] } + private newRelationship(): [ContactRelationship, Id] { + const relationship = createContactRelationship({ + person: "", + type: ContactRelationshipType.ASSISTANT, + customTypeName: "", + }) + return [relationship, this.newId()] + } + + private newMessengerHandler(): [ContactMessengerHandle, Id] { + const messengerHandler = createContactMessengerHandle({ + handle: "", + type: ContactMessengerHandleType.SIGNAL, + customTypeName: "", + }) + return [messengerHandler, this.newId()] + } + + private newPronoun(): [ContactPronouns, Id] { + const contactPronouns = createContactPronouns({ + language: "", + pronouns: "", + }) + return [contactPronouns, this.newId()] + } + + private newCustomDate(): [CompleteCustomDate, Id] { + const contactDate = createContactCustomDate({ + dateIso: "", + type: ContactCustomDateType.ANNIVERSARY, + customTypeName: "", + }) + return [{ ...contactDate, date: "", isValid: true }, this.newId()] + } + + private newWebsite(): [ContactWebsite, Id] { + const website = createContactWebsite({ + type: ContactWebsiteType.PRIVATE, + url: "", + customTypeName: "", + }) + return [website, this.newId()] + } + private newId(): Id { return timestampToGeneratedId(Date.now()) } @@ -534,6 +847,18 @@ export class ContactEditor { } } + private onLanguageSelect(pronouns: ContactPronouns): void { + setTimeout(() => { + Dialog.showTextInputDialog({ + title: "language_label", + label: "language_label", + defaultValue: pronouns.language.length > 0 ? pronouns.language : "", + }).then((name) => { + pronouns.language = name + }) + }, DefaultAnimationTime) // wait till the dropdown is hidden + } + private renderRevealIcon(): Children { return m(ToggleButton, { title: "revealPassword_action", diff --git a/src/contacts/VCardImporter.ts b/src/contacts/VCardImporter.ts index e2b69782842d..8862062d11d8 100644 --- a/src/contacts/VCardImporter.ts +++ b/src/contacts/VCardImporter.ts @@ -315,6 +315,19 @@ export function vCardListToContacts(vCardList: string[], ownerGroupId: Id): Cont photo: null, oldBirthdayDate: null, oldBirthdayAggregate: null, + + // FIXME: see if anything in here can be imported from the vcard + department: null, + middleName: null, + nameSuffix: null, + phoneticFirst: null, + phoneticLast: null, + phoneticMiddle: null, + customDate: [], + messengerHandles: [], + pronouns: [], + relationships: [], + websites: [], }) } diff --git a/src/contacts/model/ContactUtils.ts b/src/contacts/model/ContactUtils.ts index 58894cb97beb..044f7ab70343 100644 --- a/src/contacts/model/ContactUtils.ts +++ b/src/contacts/model/ContactUtils.ts @@ -1,13 +1,35 @@ import { lang } from "../../misc/LanguageViewModel" -import { Birthday, Contact, ContactAddress, ContactMailAddress, ContactPhoneNumber, ContactSocialId } from "../../api/entities/tutanota/TypeRefs.js" +import { + Birthday, + Contact, + ContactAddress, + ContactCustomDate, + ContactMailAddress, + ContactMessengerHandle, + ContactPhoneNumber, + ContactRelationship, + ContactSocialId, + ContactWebsite, +} from "../../api/entities/tutanota/TypeRefs.js" import { formatDate } from "../../misc/Formatter" import { isoDateToBirthday } from "../../api/common/utils/BirthdayUtils" import { assertMainOrNode } from "../../api/common/Env" -import { ContactAddressType, ContactPhoneNumberType, ContactSocialType } from "../../api/common/TutanotaConstants" +import { + ContactAddressType, + ContactCustomDateType, + ContactMessengerHandleType, + ContactPhoneNumberType, + ContactRelationshipType, + ContactSocialType, + ContactWebsiteType, +} from "../../api/common/TutanotaConstants" import { StructuredMailAddress } from "../../native/common/generatedipc/StructuredMailAddress.js" import { StructuredPhoneNumber } from "../../native/common/generatedipc/StructuredPhoneNumber.js" import { StructuredAddress } from "../../native/common/generatedipc/StructuredAddress.js" import { StructuredContact } from "../../native/common/generatedipc/StructuredContact.js" +import { StructuredCustomDate } from "../../native/common/generatedipc/StructuredCustomDate.js" +import { StructuredWebsite } from "../../native/common/generatedipc/StructuredWebsite.js" +import { StructuredRelationship } from "../../native/common/generatedipc/StructuredRelationship.js" assertMainOrNode() @@ -41,14 +63,12 @@ export function formatBirthdayNumeric(birthday: Birthday): string { } /** - * Returns the birthday of the contact as formatted string using default date formatter including date, month and year. - * If birthday contains no year only month and day will be included. - * If there is no birthday or an invalid birthday format an empty string returns. + * Returns the given date of the contact as formatted string using default date formatter including date, month and year. + * If date contains no year only month and day will be included. + * If there is no date or an invalid birthday format an empty string returns. */ -export function formatBirthdayOfContact(contact: Contact): string { - if (contact.birthdayIso) { - const isoDate = contact.birthdayIso - +export function formatContactDate(isoDate: string | null): string { + if (isoDate) { try { return formatBirthdayNumeric(isoDateToBirthday(isoDate)) } catch (e) { @@ -97,6 +117,40 @@ export function getSocialUrl(contactId: ContactSocialId): string { return `${http}${worldwidew}${socialUrlType}${contactId.socialId.trim()}` } +export function getWebsiteUrl(websiteUrl: string): string { + let http = "https://" + let worldwidew = "www." + + const isSchemePrefixed = websiteUrl.indexOf("http") !== -1 + const isWwwDotPrefixed = websiteUrl.indexOf(worldwidew) !== -1 + + if (isSchemePrefixed) { + http = "" + } + + if (isSchemePrefixed || isWwwDotPrefixed) { + worldwidew = "" + } + + return `${http}${worldwidew}${websiteUrl}` +} + +export function getMessengerHandleUrl(handle: ContactMessengerHandle): string { + const replaceNumberExp = new RegExp(/[^0-9+]/g) + switch (handle.type) { + case ContactMessengerHandleType.SIGNAL: + return `sgnl://signal.me/#p/${handle.handle.replaceAll(replaceNumberExp, "")}` + case ContactMessengerHandleType.WHATSAPP: + return `whatsapp://send?phone=${handle.handle.replaceAll(replaceNumberExp, "")}` + case ContactMessengerHandleType.TELEGRAM: + return `tg://resolve?domain=${handle.handle}` + case ContactMessengerHandleType.DISCORD: + return `discord://-/users/${handle.handle}` + default: + return "" + } +} + export function extractStructuredMailAddresses(addresses: ContactMailAddress[]): ReadonlyArray { return addresses.map((address) => ({ address: address.address, @@ -121,6 +175,30 @@ export function extractStructuredPhoneNumbers(numbers: ContactPhoneNumber[]): Re })) } +export function extractStructuredCustomDates(dates: ContactCustomDate[]): ReadonlyArray { + return dates.map((date) => ({ + dateIso: date.dateIso, + type: date.type as ContactCustomDateType, + customTypeName: date.customTypeName, + })) +} + +export function extractStructuredWebsites(websites: ContactWebsite[]): ReadonlyArray { + return websites.map((website) => ({ + url: website.url, + type: website.type as ContactWebsiteType, + customTypeName: website.customTypeName, + })) +} + +export function extractStructuredRelationships(relationships: ContactRelationship[]): ReadonlyArray { + return relationships.map((relation) => ({ + person: relation.person, + type: relation.type as ContactRelationshipType, + customTypeName: relation.customTypeName, + })) +} + export function validateBirthdayOfContact(contact: StructuredContact) { if (contact.birthday != null) { try { diff --git a/src/contacts/model/NativeContactsSyncManager.ts b/src/contacts/model/NativeContactsSyncManager.ts index ba334ebf928b..dc0fe030e101 100644 --- a/src/contacts/model/NativeContactsSyncManager.ts +++ b/src/contacts/model/NativeContactsSyncManager.ts @@ -4,14 +4,24 @@ import { ContactTypeRef, createContact, createContactAddress, + createContactCustomDate, createContactMailAddress, createContactPhoneNumber, + createContactRelationship, + createContactWebsite, } from "../../api/entities/tutanota/TypeRefs.js" import { GroupType, OperationType } from "../../api/common/TutanotaConstants.js" import { defer, getFirstOrThrow, getFromMap, ofClass } from "@tutao/tutanota-utils" import { StructuredContact } from "../../native/common/generatedipc/StructuredContact.js" import { elementIdPart, getElementId, StrippedEntity } from "../../api/common/utils/EntityUtils.js" -import { extractStructuredAddresses, extractStructuredMailAddresses, extractStructuredPhoneNumbers } from "./ContactUtils.js" +import { + extractStructuredAddresses, + extractStructuredCustomDates, + extractStructuredMailAddresses, + extractStructuredPhoneNumbers, + extractStructuredRelationships, + extractStructuredWebsites, +} from "./ContactUtils.js" import { LoginController } from "../../api/main/LoginController.js" import { EntityClient } from "../../api/common/EntityClient.js" import { EventController } from "../../api/main/EventController.js" @@ -20,6 +30,7 @@ import { DeviceConfig } from "../../misc/DeviceConfig.js" import { PermissionError } from "../../api/common/error/PermissionError.js" import { MobileContactsFacade } from "../../native/common/generatedipc/MobileContactsFacade.js" import { ContactSyncResult } from "../../native/common/generatedipc/ContactSyncResult.js" +import { isIOSApp } from "../../api/common/Env.js" export class NativeContactsSyncManager { private entityUpdateLock: Promise = Promise.resolve() @@ -80,6 +91,19 @@ export class NativeContactsSyncManager { phoneNumbers: extractStructuredPhoneNumbers(contact.phoneNumbers), addresses: extractStructuredAddresses(contact.addresses), rawId: null, + customDate: extractStructuredCustomDates(contact.customDate), + department: contact.department, + messengerHandles: [], + middleName: contact.middleName, + nameSuffix: contact.nameSuffix, + phoneticFirst: contact.phoneticFirst, + phoneticLast: contact.phoneticLast, + phoneticMiddle: contact.phoneticMiddle, + relationships: extractStructuredRelationships(contact.relationships), + websites: extractStructuredWebsites(contact.websites), + notes: contact.comment, + title: contact.title ?? "", + role: contact.role, }) }) } @@ -113,6 +137,19 @@ export class NativeContactsSyncManager { addresses: extractStructuredAddresses(contact.addresses), rawId: null, deleted: false, + customDate: extractStructuredCustomDates(contact.customDate), + department: contact.department, + messengerHandles: [], + middleName: contact.middleName, + nameSuffix: contact.nameSuffix, + phoneticFirst: contact.phoneticFirst, + phoneticLast: contact.phoneticLast, + phoneticMiddle: contact.phoneticMiddle, + relationships: extractStructuredRelationships(contact.relationships), + websites: extractStructuredWebsites(contact.websites), + notes: contact.comment, + title: contact.title ?? "", + role: contact.role, } }) @@ -193,11 +230,8 @@ export class NativeContactsSyncManager { ).group, _owner: this.loginController.getUserController().user._id, autoTransmitPassword: "", - comment: "", oldBirthdayDate: null, presharedPassword: null, - role: "", - title: null, oldBirthdayAggregate: null, photo: null, socialIds: [], @@ -209,6 +243,20 @@ export class NativeContactsSyncManager { company: contact.company, birthdayIso: contact.birthday, addresses: contact.addresses.map((address) => createContactAddress(address)), + customDate: contact.customDate.map((date) => createContactCustomDate(date)), + department: contact.department, + messengerHandles: [], + middleName: contact.middleName, + nameSuffix: contact.nameSuffix, + phoneticFirst: contact.phoneticFirst, + phoneticLast: contact.phoneticLast, + phoneticMiddle: contact.phoneticMiddle, + pronouns: [], + relationships: contact.relationships.map((relation) => createContactRelationship(relation)), + websites: contact.websites.map((website) => createContactWebsite(website)), + comment: contact.notes, + title: contact.title ?? "", + role: contact.role, } } @@ -223,6 +271,19 @@ export class NativeContactsSyncManager { company: contact.company, birthdayIso: contact.birthday, addresses: contact.addresses.map((address) => createContactAddress(address)), + customDate: contact.customDate.map((date) => createContactCustomDate(date)), + department: contact.department, + messengerHandles: [], + middleName: contact.middleName, + nameSuffix: contact.nameSuffix, + phoneticFirst: contact.phoneticFirst, + phoneticLast: contact.phoneticLast, + phoneticMiddle: contact.phoneticMiddle, + relationships: contact.relationships.map((relation) => createContactRelationship(relation)), + websites: contact.websites.map((website) => createContactWebsite(website)), + comment: contact.notes, + title: contact.title ?? "", + role: contact.role, } } } diff --git a/src/contacts/view/ContactGuiUtils.ts b/src/contacts/view/ContactGuiUtils.ts index cdf5f25c4bc1..61eeaa2f04c1 100644 --- a/src/contacts/view/ContactGuiUtils.ts +++ b/src/contacts/view/ContactGuiUtils.ts @@ -1,4 +1,12 @@ -import { ContactAddressType, ContactPhoneNumberType, ContactSocialType } from "../../api/common/TutanotaConstants" +import { + ContactAddressType, + ContactCustomDateType, + ContactMessengerHandleType, + ContactPhoneNumberType, + ContactRelationshipType, + ContactSocialType, + ContactWebsiteType, +} from "../../api/common/TutanotaConstants" import type { TranslationKey } from "../../misc/LanguageViewModel" import { lang } from "../../misc/LanguageViewModel" import type { Contact } from "../../api/entities/tutanota/TypeRefs.js" @@ -53,6 +61,75 @@ export function getContactSocialTypeLabel(type: ContactSocialType, custom: strin } } +export const ContactRelationshipTypeToLabel: Record = { + [ContactRelationshipType.PARENT]: "parent_label", + [ContactRelationshipType.BROTHER]: "brother_label", + [ContactRelationshipType.SISTER]: "sister_label", + [ContactRelationshipType.CHILD]: "child_label", + [ContactRelationshipType.FRIEND]: "friend_label", + [ContactRelationshipType.RELATIVE]: "relative_label", + [ContactRelationshipType.SPOUSE]: "spouse_label", + [ContactRelationshipType.PARTNER]: "partner_label", + [ContactRelationshipType.ASSISTANT]: "assistant_label", + [ContactRelationshipType.MANAGER]: "manager_label", + [ContactRelationshipType.OTHER]: "other_label", + [ContactRelationshipType.CUSTOM]: "custom_label", +} + +export function getContactRelationshipTypeToLabel(type: ContactRelationshipType, custom: string): string { + if (type === ContactRelationshipType.CUSTOM) { + return custom + } else { + return lang.get(ContactRelationshipTypeToLabel[type]) + } +} + +export const ContactMessengerHandleTypeToLabel: Record = { + [ContactMessengerHandleType.SIGNAL]: "signal_label", + [ContactMessengerHandleType.WHATSAPP]: "whatsapp_label", + [ContactMessengerHandleType.TELEGRAM]: "telegram_label", + [ContactMessengerHandleType.DISCORD]: "discord_label", + [ContactMessengerHandleType.OTHER]: "other_label", + [ContactMessengerHandleType.CUSTOM]: "custom_label", +} + +export function getContactMessengerHandleTypeToLabel(type: ContactMessengerHandleType, custom: string): string { + if (type === ContactMessengerHandleType.CUSTOM) { + return custom + } else { + return lang.get(ContactMessengerHandleTypeToLabel[type]) + } +} + +export const ContactCustomDateTypeToLabel: Record = { + [ContactCustomDateType.ANNIVERSARY]: "anniversary_label", + [ContactCustomDateType.OTHER]: "other_label", + [ContactCustomDateType.CUSTOM]: "custom_label", +} + +export function getContactCustomDateTypeToLabel(type: ContactCustomDateType, custom: string): string { + if (type === ContactCustomDateType.CUSTOM) { + return custom + } else { + return lang.get(ContactCustomDateTypeToLabel[type]) + } +} + +export const ContactCustomWebsiteTypeToLabel: Record = { + [ContactWebsiteType.PRIVATE]: "private_label", + [ContactWebsiteType.WORK]: "work_label", + [ContactWebsiteType.OTHER]: "other_label", + [ContactWebsiteType.CUSTOM]: "custom_label", +} + +export function getContactCustomWebsiteTypeToLabel(type: ContactWebsiteType, custom: string): string { + if (type === ContactWebsiteType.CUSTOM) { + return custom + } else { + return lang.get(ContactCustomWebsiteTypeToLabel[type]) + } +} + export type ContactComparator = (arg0: Contact, arg1: Contact) => number /** diff --git a/src/contacts/view/ContactListEntryViewer.ts b/src/contacts/view/ContactListEntryViewer.ts index 754d8e8a1e72..c675faac7e8a 100644 --- a/src/contacts/view/ContactListEntryViewer.ts +++ b/src/contacts/view/ContactListEntryViewer.ts @@ -79,6 +79,17 @@ export class ContactListEntryViewer implements Component 0 ? "***" : "", diff --git a/src/contacts/view/ContactView.ts b/src/contacts/view/ContactView.ts index 73f5bb7e634b..ee6bc3df8a8c 100644 --- a/src/contacts/view/ContactView.ts +++ b/src/contacts/view/ContactView.ts @@ -8,9 +8,11 @@ import { Contact, ContactTypeRef, createContact, - createContactAddress, - createContactMailAddress, + createContactAddress, createContactCustomDate, + createContactMailAddress, createContactMessengerHandle, createContactPhoneNumber, + createContactRelationship, + createContactWebsite, } from "../../api/entities/tutanota/TypeRefs.js" import { ContactListView } from "./ContactListView" import { lang } from "../../misc/LanguageViewModel" @@ -419,7 +421,7 @@ export class ContactView extends BaseTopLevelView implements TopLevelView this.contactListViewModel.listModel?.selectNone(), }) - : undefined, + : null, backgroundColor: theme.navigation_bg, }) : m(ContactListEntryViewer, { @@ -988,6 +990,41 @@ export async function importContacts() { export function contactFromStructuredContact(ownerGroupId: Id, contact: StructuredContact): Contact { const userId = locator.logins.getUserController().userId return createContact({ + customDate: contact.customDate.map((date) => + createContactCustomDate({ + type: date.type, + dateIso: date.dateIso, + customTypeName: date.customTypeName, + }), + ), + department: contact.department, + messengerHandles: contact.messengerHandles.map((handle) => + createContactMessengerHandle({ + type: handle.type, + handle: handle.handle, + customTypeName: handle.customTypeName, + }), + ), + middleName: contact.middleName, + nameSuffix: contact.nameSuffix, + phoneticFirst: contact.phoneticFirst, + phoneticLast: contact.phoneticLast, + phoneticMiddle: contact.phoneticMiddle, + pronouns: [], + relationships: contact.relationships.map((relation) => + createContactRelationship({ + type: relation.type, + person: relation.person, + customTypeName: relation.customTypeName, + }), + ), + websites: contact.websites.map((website) => + createContactWebsite({ + type: website.type, + url: website.url, + customTypeName: website.customTypeName, + }), + ), _owner: userId, _ownerGroup: ownerGroupId, nickname: contact.nickname, diff --git a/src/contacts/view/ContactViewer.ts b/src/contacts/view/ContactViewer.ts index 522e0f0343d6..fa2e76787ac6 100644 --- a/src/contacts/view/ContactViewer.ts +++ b/src/contacts/view/ContactViewer.ts @@ -2,12 +2,26 @@ import m, { Children, ClassComponent, Vnode } from "mithril" import { lang } from "../../misc/LanguageViewModel" import { TextField, TextFieldType } from "../../gui/base/TextField.js" import { Icons } from "../../gui/base/icons/Icons" -import type { ContactAddressType } from "../../api/common/TutanotaConstants" -import { ContactPhoneNumberType, getContactSocialType } from "../../api/common/TutanotaConstants" -import type { Contact, ContactAddress, ContactPhoneNumber, ContactSocialId } from "../../api/entities/tutanota/TypeRefs.js" +import { ContactAddressType, ContactPhoneNumberType, getContactSocialType, getCustomDateType, getRelationshipType } from "../../api/common/TutanotaConstants" +import type { + Contact, + ContactAddress, + ContactMessengerHandle, + ContactPhoneNumber, + ContactSocialId, + ContactWebsite, +} from "../../api/entities/tutanota/TypeRefs.js" import { assertNotNull, downcast, memoized, NBSP, noOp } from "@tutao/tutanota-utils" -import { getContactAddressTypeLabel, getContactPhoneNumberTypeLabel, getContactSocialTypeLabel } from "./ContactGuiUtils" -import { formatBirthdayOfContact, getSocialUrl } from "../model/ContactUtils" +import { + getContactAddressTypeLabel, + getContactCustomDateTypeToLabel, + getContactCustomWebsiteTypeToLabel, + getContactMessengerHandleTypeToLabel, + getContactPhoneNumberTypeLabel, + getContactRelationshipTypeToLabel, + getContactSocialTypeLabel, +} from "./ContactGuiUtils" +import { formatContactDate, getMessengerHandleUrl, getSocialUrl, getWebsiteUrl } from "../model/ContactUtils" import { assertMainOrNode } from "../../api/common/Env" import { IconButton } from "../../gui/base/IconButton.js" import { ButtonSize } from "../../gui/base/ButtonSize.js" @@ -29,13 +43,24 @@ export interface ContactViewerAttrs { export class ContactViewer implements ClassComponent { private readonly contactAppellation = memoized((contact: Contact) => { const title = contact.title ? `${contact.title} ` : "" - // const nickname = contact.nickname ? ` | "${contact.nickname}"` : "" - const fullName = `${contact.firstName} ${contact.lastName}` - return (title + fullName).trim() + const middleName = contact.middleName != null ? ` ${contact.middleName} ` : " " + const fullName = `${contact.firstName}${middleName}${contact.lastName} ` + const suffix = contact.nameSuffix ?? "" + return (title + fullName + suffix).trim() + }) + + private readonly contactPhoneticName = memoized((contact: Contact): string | null => { + const firstName = contact.phoneticFirst ?? "" + const middleName = contact.phoneticMiddle ? ` ${contact.phoneticMiddle}` : "" + const lastName = contact.phoneticLast ? ` ${contact.phoneticLast}` : "" + + const phoneticName = (firstName + middleName + lastName).trim() + + return phoneticName.length > 0 ? phoneticName : null }) private readonly formattedBirthday = memoized((contact: Contact) => { - return this.hasBirthday(contact) ? formatBirthdayOfContact(contact) : null + return this.hasBirthday(contact) ? formatContactDate(contact.birthdayIso) : null }) private hasBirthday(contact: Contact): boolean { @@ -44,6 +69,9 @@ export class ContactViewer implements ClassComponent { view({ attrs }: Vnode): Children { const { contact, onWriteMail } = attrs + + const phoneticName = this.contactPhoneticName(attrs.contact) + return m(".plr-l.pb-floating.mlr-safe-inset", [ m("", [ m( @@ -53,21 +81,10 @@ export class ContactViewer implements ClassComponent { this.contactAppellation(contact), NBSP, // alignment in case nothing is present here ]), + phoneticName ? m("", phoneticName) : null, + contact.pronouns.length > 0 ? this.renderPronounsInfo(contact) : null, contact.nickname ? m("", `"${contact.nickname}"`) : null, - m( - "", - insertBetween([contact.role ? m("span", contact.role) : null, contact.company ? m("span", contact.company) : null], () => - m( - "span.plr-s", - { - style: { - fontWeight: "900", - }, - }, - " · ", - ), - ), - ), + m("", this.renderJobInformation(contact)), this.hasBirthday(contact) ? m("", this.formattedBirthday(contact)) : null, ]), contact && (attrs.editAction || attrs.deleteAction) @@ -109,12 +126,54 @@ export class ContactViewer implements ClassComponent { ), m("hr.hr.mt.mb"), ]), + this.renderCustomDatesAndRelationships(contact), this.renderMailAddressesAndPhones(contact, onWriteMail), this.renderAddressesAndSocialIds(contact), + this.renderWebsitesAndInstantMessengers(contact), this.renderComment(contact), ]) } + private renderJobInformation(contact: Contact): Children { + const spacerFunction = () => + m( + "span.plr-s", + { + style: { + fontWeight: "900", + }, + }, + " · ", + ) + + return insertBetween( + [ + contact.role ? m("span", contact.role) : null, + contact.department ? m("span", contact.department) : null, + contact.company ? m("span", contact.company) : null, + ], + spacerFunction, + ) + } + + private renderPronounsInfo(contact: Contact): Children { + const spacerFunction = () => + m( + "span.plr-s", + { + style: { + fontWeight: "900", + }, + }, + " · ", + ) + + return insertBetween( + contact.pronouns.map((pronouns) => m("span", `${pronouns.language}: ${pronouns.pronouns}`)), + spacerFunction, + ) + } + private renderAddressesAndSocialIds(contact: Contact): Children { const addresses = contact.addresses.map((element) => this.renderAddress(element)) const socials = contact.socialIds.map((element) => this.renderSocialId(element)) @@ -126,6 +185,47 @@ export class ContactViewer implements ClassComponent { : null } + private renderWebsitesAndInstantMessengers(contact: Contact): Children { + const websites = contact.websites.map((element) => this.renderWebsite(element)) + const instantMessengers = contact.messengerHandles.map((element) => this.renderMessengerHandle(element)) + return websites.length > 0 || instantMessengers.length > 0 + ? m(".wrapping-row", [ + m(".website.mt-l", websites.length > 0 ? [m(".h4", lang.get("websites_label")), m(".aggregateEditors", websites)] : null), + m( + ".messenger-handles.mt-l", + instantMessengers.length > 0 ? [m(".h4", lang.get("messenger_handles_label")), m(".aggregateEditors", instantMessengers)] : null, + ), + ]) + : null + } + + private renderCustomDatesAndRelationships(contact: Contact): Children { + const dates = contact.customDate.map((element) => + m(TextField, { + label: () => getContactCustomDateTypeToLabel(getCustomDateType(element), element.customTypeName), + value: formatContactDate(element.dateIso), + isReadOnly: true, + }), + ) + const relationships = contact.relationships.map((element) => + m(TextField, { + label: () => getContactRelationshipTypeToLabel(getRelationshipType(element), element.customTypeName), + value: element.person, + isReadOnly: true, + }), + ) + + return dates.length > 0 || relationships.length > 0 + ? m(".wrapping-row", [ + m(".dates.mt-l", dates.length > 0 ? [m(".h4", lang.get("dates_label")), m(".aggregateEditors", dates)] : null), + m( + ".relationships.mt-l", + relationships.length > 0 ? [m(".h4", lang.get("relationships_label")), m(".aggregateEditors", relationships)] : null, + ), + ]) + : null + } + private renderMailAddressesAndPhones(contact: Contact, onWriteMail: ContactViewerAttrs["onWriteMail"]): Children { const mailAddresses = contact.mailAddresses.map((element) => this.renderMailAddress(contact, element, onWriteMail)) const phones = contact.phoneNumbers.map((element) => this.renderPhoneNumber(element)) @@ -158,6 +258,36 @@ export class ContactViewer implements ClassComponent { }) } + private renderWebsite(website: ContactWebsite): Children { + const showButton = m(IconButton, { + title: "showURL_alt", + click: noOp, + icon: Icons.ArrowForward, + size: ButtonSize.Compact, + }) + return m(TextField, { + label: () => getContactCustomWebsiteTypeToLabel(downcast(website.type), website.customTypeName), + value: website.url, + isReadOnly: true, + injectionsRight: () => m(`a[href=${getWebsiteUrl(website.url)}][target=_blank]`, showButton), + }) + } + + private renderMessengerHandle(messengerHandle: ContactMessengerHandle): Children { + const showButton = m(IconButton, { + title: "showURL_alt", + click: noOp, + icon: Icons.ArrowForward, + size: ButtonSize.Compact, + }) + return m(TextField, { + label: () => getContactMessengerHandleTypeToLabel(downcast(messengerHandle.type), messengerHandle.customTypeName), + value: messengerHandle.handle, + isReadOnly: true, + injectionsRight: () => m(`a[href=${getMessengerHandleUrl(messengerHandle)}][target=_blank]`, showButton), + }) + } + private renderMailAddress(contact: Contact, address: ContactAddress, onWriteMail: ContactViewerAttrs["onWriteMail"]): Children { const newMailButton = m(IconButton, { title: "sendMail_alt", diff --git a/src/gui/base/Dropdown.ts b/src/gui/base/Dropdown.ts index 476ae4dc9d6f..8deb3a7d663a 100644 --- a/src/gui/base/Dropdown.ts +++ b/src/gui/base/Dropdown.ts @@ -577,7 +577,7 @@ class DropdownButton implements Component { role: "menuitem", "aria-selected": String(attrs.selected ?? false), oncreate: (vnode) => (this.dom = vnode.dom as HTMLElement), - onclick: (e: MouseEvent) => attrs.click?.(e, neverNull(this.dom)), + onclick: (e: MouseEvent, dom: HTMLElement) => attrs.click?.(e, dom), }, [ attrs.icon && attrs.showingIcons diff --git a/src/gui/base/NavButton.ts b/src/gui/base/NavButton.ts index 0ec5e1c6161a..5313e8767b0d 100644 --- a/src/gui/base/NavButton.ts +++ b/src/gui/base/NavButton.ts @@ -124,10 +124,10 @@ export class NavButton implements Component { title: this.getLabel(a.label), target: this._isExternalUrl(a.href) ? "_blank" : undefined, selector: this._getNavButtonClass(a), - onclick: (e: MouseEvent) => this.click(e, a), - onkeyup: (e: KeyboardEvent) => { + onclick: (e: MouseEvent, dom: HTMLElement) => this.click(e, a, dom), + onkeyup: (e: KeyboardEvent, dom: HTMLElement) => { if (isKeyPressed(e.key, Keys.SPACE)) { - this.click(e, a) + this.click(e, a, dom) } }, onfocus: a.onfocus, @@ -171,13 +171,13 @@ export class NavButton implements Component { return attr } - click(event: Event, a: NavButtonAttrs) { + click(event: Event, a: NavButtonAttrs, dom: HTMLElement) { if (!this._isExternalUrl(a.href)) { m.route.set(this._getUrl(a.href)) try { if (a.click != null) { - a.click(event, this._domButton) + a.click(event, dom) } event.preventDefault() diff --git a/src/gui/base/RecipientButton.ts b/src/gui/base/RecipientButton.ts index 10ac30bd5d80..09963b36a801 100644 --- a/src/gui/base/RecipientButton.ts +++ b/src/gui/base/RecipientButton.ts @@ -19,7 +19,7 @@ export class RecipientButton implements Component { }, attrs.style, ), - onclick: (e: MouseEvent) => attrs.click(e, e.target as HTMLElement), + onclick: (e: MouseEvent, dom: HTMLElement) => attrs.click(e, dom), }, [attrs.label], ) diff --git a/src/mail/model/MailUtils.ts b/src/mail/model/MailUtils.ts index 6b7848b9525d..7c4c2fbf7057 100644 --- a/src/mail/model/MailUtils.ts +++ b/src/mail/model/MailUtils.ts @@ -86,6 +86,17 @@ export function createNewContact(user: User, mailAddress: string, name: string): phoneNumbers: [], photo: null, socialIds: [], + department: null, + middleName: null, + nameSuffix: null, + phoneticFirst: null, + phoneticLast: null, + phoneticMiddle: null, + customDate: [], + messengerHandles: [], + pronouns: [], + relationships: [], + websites: [], }) return contact } diff --git a/src/misc/TranslationKey.ts b/src/misc/TranslationKey.ts index 6bfd6567a7c3..c8fbc2319cc7 100644 --- a/src/misc/TranslationKey.ts +++ b/src/misc/TranslationKey.ts @@ -1651,6 +1651,33 @@ export type TranslationKeyType = | "you_label" | "emptyString_msg" | "mailAddressInfoLegacy_msg" + | "websites_label" + | "middleName_placeholder" + | "namePrefix_placeholder" + | "nameSuffix_placeholder" + | "department_placeholder" + | "phoneticFirst_placeholder" + | "phoneticMiddle_placeholder" + | "phoneticLast_placeholder" + | "dates_label" + | "relationships_label" + | "parent_label" + | "brother_label" + | "sister_label" + | "child_label" + | "friend_label" + | "relative_label" + | "spouse_label" + | "partner_label" + | "assistant_label" + | "manager_label" + | "messenger_handles_label" + | "signal_label" + | "whatsapp_label" + | "telegram_label" + | "discord_label" + | "anniversary_label" + | "pronouns_label" | "vcardInSharingFiles_msg" | "importFromContactBook_label" | "importContacts_label" diff --git a/src/native/common/generatedipc/ContactCustomDateType.ts b/src/native/common/generatedipc/ContactCustomDateType.ts new file mode 100644 index 000000000000..9358ad43b46a --- /dev/null +++ b/src/native/common/generatedipc/ContactCustomDateType.ts @@ -0,0 +1,3 @@ +/* generated file, don't edit. */ + +export { ContactCustomDateType } from "../../../api/common/TutanotaConstants.js" diff --git a/src/native/common/generatedipc/ContactMessengerHandleType.ts b/src/native/common/generatedipc/ContactMessengerHandleType.ts new file mode 100644 index 000000000000..1a9402deec5d --- /dev/null +++ b/src/native/common/generatedipc/ContactMessengerHandleType.ts @@ -0,0 +1,3 @@ +/* generated file, don't edit. */ + +export { ContactMessengerHandleType } from "../../../api/common/TutanotaConstants.js" diff --git a/src/native/common/generatedipc/ContactRelationshipType.ts b/src/native/common/generatedipc/ContactRelationshipType.ts new file mode 100644 index 000000000000..503c1b9cbceb --- /dev/null +++ b/src/native/common/generatedipc/ContactRelationshipType.ts @@ -0,0 +1,3 @@ +/* generated file, don't edit. */ + +export { ContactRelationshipType } from "../../../api/common/TutanotaConstants.js" diff --git a/src/native/common/generatedipc/ContactWebsiteType.ts b/src/native/common/generatedipc/ContactWebsiteType.ts new file mode 100644 index 000000000000..fd2ed12e7376 --- /dev/null +++ b/src/native/common/generatedipc/ContactWebsiteType.ts @@ -0,0 +1,3 @@ +/* generated file, don't edit. */ + +export { ContactWebsiteType } from "../../../api/common/TutanotaConstants.js" diff --git a/src/native/common/generatedipc/StructuredContact.ts b/src/native/common/generatedipc/StructuredContact.ts index 30c8f7afb494..c38fec5d2c47 100644 --- a/src/native/common/generatedipc/StructuredContact.ts +++ b/src/native/common/generatedipc/StructuredContact.ts @@ -3,6 +3,10 @@ import { StructuredMailAddress } from "./StructuredMailAddress.js" import { StructuredPhoneNumber } from "./StructuredPhoneNumber.js" import { StructuredAddress } from "./StructuredAddress.js" +import { StructuredCustomDate } from "./StructuredCustomDate.js" +import { StructuredMessengerHandle } from "./StructuredMessengerHandle.js" +import { StructuredRelationship } from "./StructuredRelationship.js" +import { StructuredWebsite } from "./StructuredWebsite.js" export interface StructuredContact { readonly id: string | null readonly firstName: string @@ -14,4 +18,17 @@ export interface StructuredContact { readonly phoneNumbers: ReadonlyArray readonly addresses: ReadonlyArray readonly rawId: string | null + readonly customDate: ReadonlyArray + readonly department: string | null + readonly messengerHandles: ReadonlyArray + readonly middleName: string | null + readonly nameSuffix: string | null + readonly phoneticFirst: string | null + readonly phoneticLast: string | null + readonly phoneticMiddle: string | null + readonly relationships: ReadonlyArray + readonly websites: ReadonlyArray + readonly notes: string + readonly title: string + readonly role: string } diff --git a/src/native/common/generatedipc/StructuredCustomDate.ts b/src/native/common/generatedipc/StructuredCustomDate.ts new file mode 100644 index 000000000000..f37b0878f421 --- /dev/null +++ b/src/native/common/generatedipc/StructuredCustomDate.ts @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + +import { ContactCustomDateType } from "./ContactCustomDateType.js" +export interface StructuredCustomDate { + readonly dateIso: string + readonly type: ContactCustomDateType + readonly customTypeName: string +} diff --git a/src/native/common/generatedipc/StructuredMessengerHandle.ts b/src/native/common/generatedipc/StructuredMessengerHandle.ts new file mode 100644 index 000000000000..d3cca7b052ac --- /dev/null +++ b/src/native/common/generatedipc/StructuredMessengerHandle.ts @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + +import { ContactMessengerHandleType } from "./ContactMessengerHandleType.js" +export interface StructuredMessengerHandle { + readonly handle: string + readonly type: ContactMessengerHandleType + readonly customTypeName: string +} diff --git a/src/native/common/generatedipc/StructuredRelationship.ts b/src/native/common/generatedipc/StructuredRelationship.ts new file mode 100644 index 000000000000..7a38bf094339 --- /dev/null +++ b/src/native/common/generatedipc/StructuredRelationship.ts @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + +import { ContactRelationshipType } from "./ContactRelationshipType.js" +export interface StructuredRelationship { + readonly person: string + readonly type: ContactRelationshipType + readonly customTypeName: string +} diff --git a/src/native/common/generatedipc/StructuredWebsite.ts b/src/native/common/generatedipc/StructuredWebsite.ts new file mode 100644 index 000000000000..35de53034670 --- /dev/null +++ b/src/native/common/generatedipc/StructuredWebsite.ts @@ -0,0 +1,8 @@ +/* generated file, don't edit. */ + +import { ContactWebsiteType } from "./ContactWebsiteType.js" +export interface StructuredWebsite { + readonly url: string + readonly type: ContactWebsiteType + readonly customTypeName: string +} diff --git a/src/translations/en.ts b/src/translations/en.ts index 0a3eac3ee702..4d40ac7171a0 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -1665,9 +1665,35 @@ export default { "yourFolders_action": "YOUR FOLDERS", "yourMessage_label": "Your message", "you_label": "You", - "mailAddressInfoLegacy_msg": "Disabled email addresses can be reassigned to another user or mailbox within your account.", "vcardInSharingFiles_msg": "We detected one or more contact files, do you want to import or attach them?", "importFromContactBook_label": "Import contacts from your device", "importContacts_label": "Import contacts", + "websites_label": "Websites", + "dates_label": "Dates", + "relationships_label": "Relationships", + "middleName_placeholder": "Middle Name", + "namePrefix_placeholder": "Name Prefix", + "nameSuffix_placeholder": "Name Suffix", + "department_placeholder": "Department", + "phoneticFirst_placeholder": "Phonetic First Name", + "phoneticMiddle_placeholder": "Phonetic Middle Name", + "phoneticLast_placeholder": "Phonetic Last Name", + "parent_label": "Parent", + "brother_label": "Brother", + "sister_label": "Sister", + "child_label": "Child", + "friend_label": "Friend", + "relative_label": "Relative", + "spouse_label": "Spouse", + "partner_label": "Partner", + "assistant_label": "Assistant", + "manager_label": "Manager", + "messenger_handles_label": "Instant Messengers", + "signal_label": "Signal", + "whatsapp_label": "WhatsApp", + "telegram_label": "Telegram", + "discord_label": "Discord", + "anniversary_label": "Anniversary", + "pronouns_label": "Pronouns" } } diff --git a/test/tests/desktop/DesktopContextMenuTest.ts b/test/tests/desktop/DesktopContextMenuTest.ts index 91cde28e038f..53df188e107e 100644 --- a/test/tests/desktop/DesktopContextMenuTest.ts +++ b/test/tests/desktop/DesktopContextMenuTest.ts @@ -57,10 +57,10 @@ o.spec("DesktopContextMenu Test", () => { } contextMenu.open(contextMenuParams as ContextMenuParams) for (const i of downcast(electronMock.MenuItem).mockedInstances) { - i.click?.(undefined, undefined) + i.click?.(undefined) } for (const i of downcast(electronMock.MenuItem).mockedInstances) { - i.click?.(undefined, "nowebcontents") + i.click?.(undefined) } }) })