Skip to content

Commit

Permalink
Fix importing contacts from Device book on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
murilopereirame committed Mar 1, 2024
1 parent c807012 commit 21bdf25
Show file tree
Hide file tree
Showing 9 changed files with 470 additions and 444 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ import kotlinx.serialization.json.*
@Serializable
data class ContactBook(
val id: String,
val name: String,
val name: String?,
)
2 changes: 1 addition & 1 deletion app-ios/tutanota/Sources/GeneratedIpc/ContactBook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
*/
public struct ContactBook : Codable {
let id: String
let name: String
let name: String?
}
2 changes: 1 addition & 1 deletion ipc-schema/types/ContactBook.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"doc": "Represents an account/list from the device's phonebook.",
"fields": {
"id": "string",
"name": "string"
"name": "string?"
}
}
16 changes: 15 additions & 1 deletion src/contacts/model/ContactUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { lang } from "../../misc/LanguageViewModel"
import type { Birthday, Contact, ContactAddress, ContactMailAddress, ContactPhoneNumber, ContactSocialId } from "../../api/entities/tutanota/TypeRefs.js"
import { Birthday, Contact, ContactAddress, ContactMailAddress, ContactPhoneNumber, ContactSocialId } 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 { 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"

assertMainOrNode()

Expand Down Expand Up @@ -119,3 +120,16 @@ export function extractStructuredPhoneNumbers(numbers: ContactPhoneNumber[]): Re
customTypeName: number.customTypeName,
}))
}

export function validateBirthdayOfContact(contact: StructuredContact) {
if (contact.birthday != null) {
try {
isoDateToBirthday(contact.birthday)
return contact.birthday
} catch (_) {
return null
}
} else {
return null
}
}
91 changes: 87 additions & 4 deletions src/contacts/view/ContactView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import { ColumnType, ViewColumn } from "../../gui/base/ViewColumn"
import { AppHeaderAttrs, Header } from "../../gui/Header.js"
import { Button, ButtonColor, ButtonType } from "../../gui/base/Button.js"
import { ContactEditor } from "../ContactEditor"
import type { Contact } from "../../api/entities/tutanota/TypeRefs.js"
import { ContactTypeRef } from "../../api/entities/tutanota/TypeRefs.js"
import {
Contact,
ContactTypeRef,
createContact,
createContactAddress,
createContactMailAddress,
createContactPhoneNumber,
} from "../../api/entities/tutanota/TypeRefs.js"
import { ContactListView } from "./ContactListView"
import { lang } from "../../misc/LanguageViewModel"
import { assertNotNull, clear, getFirstOrThrow, noOp, ofClass } from "@tutao/tutanota-utils"
import { assert, assertNotNull, clear, getFirstOrThrow, noOp, ofClass, promiseMap } from "@tutao/tutanota-utils"
import { ContactMergeAction, Keys } from "../../api/common/TutanotaConstants"
import { assertMainOrNode, isApp } from "../../api/common/Env"
import type { Shortcut } from "../../misc/KeyManager"
Expand Down Expand Up @@ -65,6 +71,9 @@ import { showPlanUpgradeRequiredDialog } from "../../misc/SubscriptionDialogs.js
import ColumnEmptyMessageBox from "../../gui/base/ColumnEmptyMessageBox.js"
import { ContactListInfo } from "../model/ContactModel.js"
import { CONTACTLIST_PREFIX } from "../../misc/RouteChange.js"
import { showContactImportDialog } from "../ContactImporter.js"
import { StructuredContact } from "../../native/common/generatedipc/StructuredContact.js"
import { validateBirthdayOfContact } from "../model/ContactUtils.js"

assertMainOrNode()

Expand Down Expand Up @@ -564,7 +573,13 @@ export class ContactView extends BaseTopLevelView implements TopLevelView<Contac
},
childAttrs: () => {
const vcardButtons: Array<DropdownButtonAttrs> = isApp()
? []
? [
{
label: "importContacts_label",
click: () => importContacts(),
icon: Icons.ContactImport,
},
]
: [
{
label: "importVCard_action",
Expand Down Expand Up @@ -944,3 +959,71 @@ export function confirmMerge(keptContact: Contact, goodbyeContact: Contact): Pro
return Dialog.message("presharedPasswordsUnequal_msg")
}
}

export async function importContacts() {
assert(isApp(), "isApp")
const { ImportNativeContactBooksDialog } = await import("../../settings/ImportNativeContactBooksDialog.js")
const contactBooks = await showProgressDialog("pleaseWait_msg", locator.mobileContactsFacade.getContactBooks())
const importDialog = new ImportNativeContactBooksDialog(contactBooks)
const books = await importDialog.show()
if (books == null || books.length === 0) return

const contactListId = await locator.contactModel.getContactListId()
const contactGroupId = await locator.contactModel.getContactGroupId()
const contactsToImport: Contact[] = (
await promiseMap(books, async (book) => {
const structuredContacts = await locator.mobileContactsFacade.getContactsInContactBook(book.id)
return structuredContacts.map((contact) => contactFromStructuredContact(contactGroupId, contact))
})
).flat()

const importer = await locator.contactImporter()

showContactImportDialog(contactsToImport, (dialog) => {
dialog.close()
importer.importContacts(contactsToImport, assertNotNull(contactListId))
})
}

export function contactFromStructuredContact(ownerGroupId: Id, contact: StructuredContact): Contact {
const userId = locator.logins.getUserController().userId
return createContact({
_owner: userId,
_ownerGroup: ownerGroupId,
nickname: contact.nickname,
firstName: contact.firstName,
lastName: contact.lastName,
company: contact.company,
addresses: contact.addresses.map((address) =>
createContactAddress({
type: address.type,
address: address.address,
customTypeName: address.customTypeName,
}),
),
mailAddresses: contact.mailAddresses.map((address) =>
createContactMailAddress({
type: address.type,
address: address.address,
customTypeName: address.customTypeName,
}),
),
phoneNumbers: contact.phoneNumbers.map((number) =>
createContactPhoneNumber({
type: number.type,
number: number.number,
customTypeName: number.customTypeName,
}),
),
role: "",
oldBirthdayAggregate: null,
oldBirthdayDate: null,
photo: null,
presharedPassword: null,
socialIds: [],
birthdayIso: validateBirthdayOfContact(contact),
autoTransmitPassword: "",
title: null,
comment: "",
})
}
2 changes: 1 addition & 1 deletion src/native/common/generatedipc/ContactBook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
*/
export interface ContactBook {
readonly id: string
readonly name: string
readonly name: string | null
}
99 changes: 3 additions & 96 deletions src/settings/ContactsSettingsViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,11 @@ import type { UpdatableSettingsViewer } from "./SettingsView"
import { EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/EntityUpdateUtils.js"
import { locator } from "../api/main/MainLocator.js"
import { FeatureType, OperationType } from "../api/common/TutanotaConstants.js"
import {
Contact,
createContact,
createContactAddress,
createContactMailAddress,
createContactPhoneNumber,
TutanotaProperties,
TutanotaPropertiesTypeRef,
} from "../api/entities/tutanota/TypeRefs.js"
import { TutanotaProperties, TutanotaPropertiesTypeRef } from "../api/entities/tutanota/TypeRefs.js"
import { deviceConfig } from "../misc/DeviceConfig.js"
import { Button, ButtonType } from "../gui/base/Button.js"
import { ImportNativeContactBooksDialog } from "./ImportNativeContactBooksDialog.js"
import { showProgressDialog } from "../gui/dialogs/ProgressDialog.js"
import { Dialog } from "../gui/base/Dialog.js"
import { StructuredContact } from "../native/common/generatedipc/StructuredContact.js"
import { isoDateToBirthday } from "../api/common/utils/BirthdayUtils.js"
import { assert, assertNotNull, promiseMap } from "@tutao/tutanota-utils"
import { showContactImportDialog } from "../contacts/ContactImporter.js"
import { importContacts } from "../contacts/view/ContactView.js"

assertMainOrNode()

Expand Down Expand Up @@ -86,92 +73,12 @@ export class ContactsSettingsViewer implements UpdatableSettingsViewer {
lang.get("importFromContactBook_label"),
m(Button, {
label: "import_action",
click: () => this.importContacts(),
click: () => importContacts(),
type: ButtonType.Primary,
}),
])
}

private async importContacts() {
assert(isApp(), "isApp")
const contactBooks = await showProgressDialog("pleaseWait_msg", locator.mobileContactsFacade.getContactBooks())
const importDialog = new ImportNativeContactBooksDialog(contactBooks)
const books = await importDialog.show()
if (books == null || books.length === 0) return

const contactListId = await locator.contactModel.getContactListId()
const contactGroupId = await locator.contactModel.getContactGroupId()
const contactsToImport: Contact[] = (
await promiseMap(books, async (book) => {
const structuredContacts = await locator.mobileContactsFacade.getContactsInContactBook(book.id)
return structuredContacts.map((contact) => this.contactFromStructuredContact(contactGroupId, contact))
})
).flat()

const importer = await locator.contactImporter()

showContactImportDialog(contactsToImport, (dialog) => {
dialog.close()
importer.importContacts(contactsToImport, assertNotNull(contactListId))
})
}

private contactFromStructuredContact(ownerGroupId: Id, contact: StructuredContact): Contact {
const userId = locator.logins.getUserController().userId
return createContact({
_owner: userId,
_ownerGroup: ownerGroupId,
nickname: contact.nickname,
firstName: contact.firstName,
lastName: contact.lastName,
company: contact.company,
addresses: contact.addresses.map((address) =>
createContactAddress({
type: address.type,
address: address.address,
customTypeName: address.customTypeName,
}),
),
mailAddresses: contact.mailAddresses.map((address) =>
createContactMailAddress({
type: address.type,
address: address.address,
customTypeName: address.customTypeName,
}),
),
phoneNumbers: contact.phoneNumbers.map((number) =>
createContactPhoneNumber({
type: number.type,
number: number.number,
customTypeName: number.customTypeName,
}),
),
role: "",
oldBirthdayAggregate: null,
oldBirthdayDate: null,
photo: null,
presharedPassword: null,
socialIds: [],
birthdayIso: this.validateBirthdayOfContact(contact),
autoTransmitPassword: "",
title: null,
comment: "",
})
}

private validateBirthdayOfContact(contact: StructuredContact) {
if (contact.birthday != null) {
try {
isoDateToBirthday(contact.birthday)
return contact.birthday
} catch (_) {
return null
}
} else {
return null
}
}

private renderContactsSyncDropdown(): Child {
if (!isApp()) return null

Expand Down
2 changes: 1 addition & 1 deletion src/settings/ImportNativeContactBooksDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ImportNativeContactBooksDialog {
".flex.items-center",
m(Checkbox, {
checked,
label: () => book.name,
label: () => book.name ?? lang.get("pushIdentifierCurrentDevice_label"),
onChecked: () => {
if (checked) {
this.selectedContactBooks.delete(book.id)
Expand Down

0 comments on commit 21bdf25

Please sign in to comment.