Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 7 additions & 42 deletions src/store/contacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,6 @@ import validate from '../services/validate.js'
ICAL.design.vcard3.param.type.multiValueSeparateDQuote = true
ICAL.design.vcard.param.type.multiValueSeparateDQuote = true

function sortData(a, b) {
const nameA = typeof a.value === 'string'
? a.value.toUpperCase() // ignore upper and lowercase
: a.value.toUnixTime() // only other sorting we support is a vCardTime
const nameB = typeof b.value === 'string'
? b.value.toUpperCase() // ignore upper and lowercase
: b.value.toUnixTime() // only other sorting we support is a vCardTime

const score = nameA.localeCompare
? nameA.localeCompare(nameB)
: nameB - nameA
// if equal, fallback to the key
return score !== 0
? score
: a.key.localeCompare(b.key)
}

function sortByFavoriteAndName(a, b) {
// favorites always on top
if (a.favorite !== b.favorite) {
Expand Down Expand Up @@ -151,31 +134,13 @@ const mutations = {
favorite: contact.favorite,
}

// Not using sort, splice has far better performances
// https://jsperf.com/sort-vs-splice-in-array
for (let i = 0, len = state.sortedContacts.length; i < len; i++) {
const other = state.sortedContacts[i]

// favorite comes before non-favorite
const differentFavStatus = other.favorite !== sortedContact.favorite
const otherShouldComeFirst = differentFavStatus && other.favorite
const sameFavAndSortedFirst = !differentFavStatus && sortData(other, sortedContact) >= 0

if (otherShouldComeFirst || sameFavAndSortedFirst) {
continue
}

if (i + 1 === len) {
state.sortedContacts.push(sortedContact)
} else {
state.sortedContacts.splice(i, 0, sortedContact)
}
break
}

if (state.sortedContacts.length === 0) {
state.sortedContacts.push(sortedContact)
}
// Keep insertion linear without re-sorting the whole contacts list.
const insertionIndex = state.sortedContacts.findIndex((other) => sortByFavoriteAndName(sortedContact, other) < 0)
state.sortedContacts.splice(
insertionIndex === -1 ? state.sortedContacts.length : insertionIndex,
0,
sortedContact,
)

state.contacts[contact.key] = contact
} else {
Expand Down
87 changes: 87 additions & 0 deletions tests/javascript/store/contacts.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import Contact from '../../../src/models/contact.js'
import contactsStore from '../../../src/store/contacts.js'

const addressbook = { id: 'default' }

function createContact(displayName, uid, favorite = false) {
const contact = new Contact(`
BEGIN:VCARD
VERSION:4.0
UID:${uid}
FN:${displayName}
END:VCARD`.replace(/\t/gm, ''), addressbook)
contact.dav = { favorite }
return contact
}

describe('contacts store', () => {
let state

beforeEach(() => {
state = {
contacts: {},
sortedContacts: [],
orderKey: 'displayName',
}
})

test('inserts a contact before all existing contacts', () => {
const nextcloudy = createContact('Nextcloudy McCloudface', 'nextcloudy')
const name = createContact('Name', 'name')
contactsStore.mutations.addContact(state, nextcloudy)

contactsStore.mutations.addContact(state, name)

expect(state.sortedContacts.map(({ key }) => key)).toEqual([
name.key,
nextcloudy.key,
])
})

test('inserts a contact between existing contacts', () => {
const aaron = createContact('Aaron', 'aaron')
const nextcloudy = createContact('Nextcloudy McCloudface', 'nextcloudy')
const name = createContact('Name', 'name')
contactsStore.mutations.addContact(state, aaron)
contactsStore.mutations.addContact(state, nextcloudy)

contactsStore.mutations.addContact(state, name)

expect(state.sortedContacts.map(({ key }) => key)).toEqual([
aaron.key,
name.key,
nextcloudy.key,
])
})

test('inserts a contact after all existing contacts', () => {
const name = createContact('Name', 'name')
const nextcloudy = createContact('Nextcloudy McCloudface', 'nextcloudy')
contactsStore.mutations.addContact(state, name)

contactsStore.mutations.addContact(state, nextcloudy)

expect(state.sortedContacts.map(({ key }) => key)).toEqual([
name.key,
nextcloudy.key,
])
})

test('inserts favorite contacts before non-favorite contacts', () => {
const aaron = createContact('Aaron', 'aaron')
const zebra = createContact('Zebra', 'zebra', true)
contactsStore.mutations.addContact(state, aaron)

contactsStore.mutations.addContact(state, zebra)

expect(state.sortedContacts.map(({ key }) => key)).toEqual([
zebra.key,
aaron.key,
])
})
})