@@ -42,6 +42,24 @@ function sortData(a, b) {
4242 : a . key . localeCompare ( b . key )
4343}
4444
45+ function sortByFavoriteAndName ( a , b ) {
46+ // favorites always on top
47+ if ( a . favorite !== b . favorite ) {
48+ return a . favorite ? - 1 : 1
49+ }
50+ // alphabetical within each group
51+ if ( ! a . value && ! b . value ) {
52+ return 0
53+ }
54+ if ( ! a . value ) {
55+ return 1
56+ }
57+ if ( ! b . value ) {
58+ return - 1
59+ }
60+ return a . value . localeCompare ( b . value )
61+ }
62+
4563const state = {
4664 // Using objects for performance
4765 // https://codepen.io/skjnldsv/pen/ZmKvQo
@@ -51,7 +69,6 @@ const state = {
5169}
5270
5371const mutations = {
54-
5572 /**
5673 * Store raw contacts into state
5774 * Used by the first contact fetch
@@ -70,6 +87,36 @@ const mutations = {
7087 } , state . contacts )
7188 } ,
7289
90+ /**
91+ * Store favorite state into store
92+ *
93+ * @param {object } state Default state
94+ * @param {Contact } contact Contact
95+ */
96+ updateContactFavorite ( state , contact ) {
97+ if ( ! state . contacts [ contact . key ] || ! ( contact instanceof Contact ) ) {
98+ console . error ( 'Invalid contact update' , contact )
99+ return
100+ }
101+
102+ if ( state . contacts [ contact . key ] . dav ) {
103+ state . contacts [ contact . key ] . dav . favorite = contact . dav . favorite
104+ }
105+
106+ const sortedContact = state . sortedContacts . find ( ( c ) => c . key === contact . key )
107+ if ( sortedContact ) {
108+ sortedContact . favorite = contact . favorite || false
109+ }
110+
111+ state . sortedContacts = Object . values ( state . contacts )
112+ . filter ( ( c ) => c . kind !== 'group' )
113+ . map ( ( c ) => ( {
114+ key : c . key ,
115+ value : ( c [ state . orderKey ] || '' ) . toString ( ) . toLowerCase ( ) ,
116+ favorite : c . favorite || false ,
117+ } ) )
118+ . sort ( sortByFavoriteAndName )
119+ } ,
73120 /**
74121 * Delete a contact from the global contacts list
75122 *
@@ -93,33 +140,42 @@ const mutations = {
93140 * @param {Contact } contact the contact to add
94141 */
95142 addContact ( state , contact ) {
143+ // Checking contact validity 🔍🙈
96144 if ( contact instanceof Contact ) {
97- // Checking contact validity 🔍🙈
98145 validate ( contact )
99146
100147 const sortedContact = {
101148 key : contact . key ,
102- value : contact [ state . orderKey ] ,
149+ value : ( contact [ state . orderKey ] || '' ) . toString ( ) . toLowerCase ( ) ,
150+ favorite : contact . favorite ,
103151 }
104152
105153 // Not using sort, splice has far better performances
106154 // https://jsperf.com/sort-vs-splice-in-array
107155 for ( let i = 0 , len = state . sortedContacts . length ; i < len ; i ++ ) {
108- if ( sortData ( state . sortedContacts [ i ] , sortedContact ) >= 0 ) {
109- state . sortedContacts . splice ( i , 0 , sortedContact )
110- break
111- } else if ( i + 1 === len ) {
112- // we reached the end insert it now
156+ const other = state . sortedContacts [ i ]
157+
158+ // favorite comes before non-favorite
159+ const differentFavStatus = other . favorite !== sortedContact . favorite
160+ const otherShouldComeFirst = differentFavStatus && other . favorite
161+ const sameFavAndSortedFirst = ! differentFavStatus && sortData ( other , sortedContact ) >= 0
162+
163+ if ( otherShouldComeFirst || sameFavAndSortedFirst ) {
164+ continue
165+ }
166+
167+ if ( i + 1 === len ) {
113168 state . sortedContacts . push ( sortedContact )
169+ } else {
170+ state . sortedContacts . splice ( i , 0 , sortedContact )
114171 }
172+ break
115173 }
116174
117- // sortedContact is empty, just push it
118175 if ( state . sortedContacts . length === 0 ) {
119176 state . sortedContacts . push ( sortedContact )
120177 }
121178
122- // default contacts list
123179 state . contacts [ contact . key ] = contact
124180 } else {
125181 console . error ( 'Error while adding the following contact' , contact )
@@ -134,17 +190,29 @@ const mutations = {
134190 */
135191 updateContact ( state , contact ) {
136192 if ( state . contacts [ contact . key ] && contact instanceof Contact ) {
137- // replace contact object data
193+ const existingFavorite = state . contacts [ contact . key ] . dav ?. favorite || false
138194 state . contacts [ contact . key ] . updateContact ( contact . jCal )
195+
196+ // restore favorite on dav if it was lost during the update
197+ if ( state . contacts [ contact . key ] . dav && state . contacts [ contact . key ] . dav . favorite === undefined ) {
198+ state . contacts [ contact . key ] . dav . favorite = existingFavorite
199+ }
200+
139201 const sortedContact = state . sortedContacts . find ( ( search ) => search . key === contact . key )
140202
141- // has the sort key changed for this contact ?
142- const hasChanged = sortedContact . value !== contact [ state . orderKey ]
143- if ( hasChanged ) {
144- // then update the new data
203+ if ( ! sortedContact ) {
204+ console . warn ( 'sortedContact not found for' , contact . key )
205+ return
206+ }
207+
208+ const hasValueChanged = sortedContact . value !== contact [ state . orderKey ]
209+ const hasFavoriteChanged = sortedContact . favorite !== ( state . contacts [ contact . key ] . dav ?. favorite || false )
210+
211+ if ( hasValueChanged || hasFavoriteChanged ) {
145212 sortedContact . value = contact [ state . orderKey ]
146- // and then we sort again
147- state . sortedContacts . sort ( sortData )
213+ sortedContact . favorite = state . contacts [ contact . key ] . dav ?. favorite || false
214+
215+ state . sortedContacts . sort ( sortByFavoriteAndName )
148216 }
149217 } else {
150218 console . error ( 'Error while replacing the following contact' , contact )
@@ -215,10 +283,13 @@ const mutations = {
215283 */
216284 sortContacts ( state ) {
217285 state . sortedContacts = Object . values ( state . contacts )
218- // exclude groups
219286 . filter ( ( contact ) => contact . kind !== 'group' )
220- . map ( ( contact ) => { return { key : contact . key , value : contact [ state . orderKey ] } } )
221- . sort ( sortData )
287+ . map ( ( contact ) => ( {
288+ key : contact . key ,
289+ value : contact [ state . orderKey ] ,
290+ favorite : contact . favorite || false ,
291+ } ) )
292+ . sort ( sortByFavoriteAndName )
222293 } ,
223294
224295 /**
@@ -274,6 +345,33 @@ const getters = {
274345
275346const actions = {
276347
348+ /**
349+ * Toggle the favorite state of a contact.
350+ * Updates the store
351+ *
352+ * @param {object } context the store mutations
353+ * @param {object } contact the contact key to toggle
354+ */
355+ async toggleFavorite ( context , contact ) {
356+ if ( ! contact . dav ) {
357+ throw new Error ( `Missing DAV object for contact ${ contact . key } ` )
358+ }
359+
360+ const oldValue = contact . dav . favorite || false
361+ const newValue = ! oldValue
362+
363+ try {
364+ contact . dav . favorite = newValue
365+ await contact . dav . updateProperties ( )
366+ context . commit ( 'updateContactFavorite' , contact )
367+ } catch ( error ) {
368+ contact . dav . favorite = oldValue
369+ context . commit ( 'updateContactFavorite' , contact )
370+ showError ( t ( 'contacts' , 'Could not update favorite state' ) )
371+ console . error ( 'Could not toggle favorite state' , error )
372+ }
373+ } ,
374+
277375 /**
278376 * Delete a contact from the list and from the associated addressbook
279377 *
@@ -375,9 +473,17 @@ const actions = {
375473 if ( etag . trim ( ) !== '' ) {
376474 await context . commit ( 'updateContactEtag' , { contact, etag } )
377475 }
378- return contact . dav . fetchCompleteData ( forceReFetch )
476+
477+ const storeContact = context . getters . getContact ( contact . key )
478+ const davObject = storeContact ?. dav || contact . dav
479+
480+ const savedFavorite = davObject . favorite
481+
482+ return davObject . fetchCompleteData ( forceReFetch )
379483 . then ( ( ) => {
380- const newContact = new Contact ( contact . dav . data , contact . addressbook )
484+ const newContact = new Contact ( davObject . data , contact . addressbook )
485+ newContact . dav = davObject
486+ newContact . dav . favorite = savedFavorite
381487 context . commit ( 'updateContact' , newContact )
382488 } )
383489 . catch ( ( error ) => { throw error } )
0 commit comments