Skip to content

Commit 24e0f54

Browse files
GretaDbackportbot[bot]
authored andcommitted
feat: sort favorite contact first in list
Signed-off-by: greta <gretadoci@gmail.com>
1 parent 3c875dd commit 24e0f54

3 files changed

Lines changed: 223 additions & 22 deletions

File tree

src/components/ContactsList/ContactsListItem.vue

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@
3636
v-if="(source.isMultiSelected || hoveringAvatar) && !isStatic"
3737
:size="28"
3838
:class="{ 'contacts-list__item-avatar-selected': source.isMultiSelected, 'contacts-list__item-avatar-hovered': !source.isMultiSelected }" />
39+
<div
40+
class="favorite-star"
41+
:class="{ favorite: isFavorite }"
42+
@click.stop="toggleFavorite">
43+
<StarIcon
44+
v-if="isFavorite"
45+
:size="16"
46+
class="favorite-icon" />
47+
<StarOutlineIcon
48+
v-else
49+
:size="16"
50+
class="favorite-icon" />
51+
</div>
3952
</div>
4053
</template>
4154
<template #subname>
@@ -50,16 +63,36 @@
5063
</span>
5164
</div>
5265
</template>
66+
<template #actions>
67+
<NcActionButton
68+
v-if="!isStatic"
69+
@click="toggleFavorite">
70+
<template #icon>
71+
<StarIcon
72+
v-if="isFavorite"
73+
:size="20"
74+
class="favorite-icon" />
75+
<StarOutlineIcon
76+
v-else
77+
:size="20" />
78+
</template>
79+
{{ isFavorite ? t('contacts', 'Remove from favorites') : t('contacts', 'Add to favorites') }}
80+
</NcActionButton>
81+
</template>
5382
</ListItem>
5483
</div>
5584
</template>
5685

5786
<script>
87+
import { showError } from '@nextcloud/dialogs'
5888
import {
5989
NcListItem as ListItem,
90+
NcActionButton,
6091
NcAvatar,
6192
} from '@nextcloud/vue'
6293
import CheckIcon from 'vue-material-design-icons/Check.vue'
94+
import StarIcon from 'vue-material-design-icons/Star.vue'
95+
import StarOutlineIcon from 'vue-material-design-icons/StarOutline.vue'
6396
import RouterMixin from '../../mixins/RouterMixin.js'
6497
6598
export default {
@@ -69,6 +102,9 @@ export default {
69102
ListItem,
70103
NcAvatar,
71104
CheckIcon,
105+
NcActionButton,
106+
StarIcon,
107+
StarOutlineIcon,
72108
},
73109
74110
mixins: [
@@ -117,6 +153,10 @@ export default {
117153
},
118154
119155
computed: {
156+
isFavorite() {
157+
return this.source.favorite
158+
},
159+
120160
// contact is not draggable when it has not been saved on server as it can't be added to groups/circles before
121161
isDraggable() {
122162
return !!this.source.dav && this.source.addressbook.id !== 'z-server-generated--system' && !this.isStatic
@@ -151,6 +191,26 @@ export default {
151191
},
152192
153193
methods: {
194+
async toggleFavorite() {
195+
const contact = this.$store.getters.getContact(this.source.key)
196+
if (!contact) {
197+
this.logger.error('Could not find contact in store', this.source.key)
198+
showError(t('contacts', 'Could not update favorite status'))
199+
return
200+
}
201+
if (!contact.dav) {
202+
this.logger.error('Missing DAV object for contact', { contactKey: contact.key })
203+
showError(t('contacts', 'Could not update favorite status'))
204+
return
205+
}
206+
try {
207+
await this.$store.dispatch('toggleFavorite', contact)
208+
} catch (error) {
209+
logger.error('Could not toggle favorite state', error)
210+
showError(t('contacts', 'Could not update favorite status'))
211+
}
212+
},
213+
154214
startDrag(evt, item) {
155215
evt.dataTransfer.dropEffect = 'move'
156216
evt.dataTransfer.effectAllowed = 'move'
@@ -286,6 +346,7 @@ export default {
286346
287347
.contacts-list__item-icon {
288348
cursor: pointer !important;
349+
position: relative;
289350
}
290351
291352
.contacts-list__item-avatar {
@@ -309,4 +370,25 @@ export default {
309370
background-color: var(--color-primary-light-hover);
310371
}
311372
}
373+
374+
.favorite-star {
375+
position: absolute;
376+
top: calc(var(--default-grid-baseline) * -1);
377+
inset-inline-end: calc(var(--default-grid-baseline) * -1);
378+
opacity: 0;
379+
cursor: pointer;
380+
}
381+
382+
.contacts-list__item-icon:hover .favorite-star {
383+
opacity: 1;
384+
}
385+
386+
.favorite-star.favorite {
387+
opacity: 1;
388+
color: var(--color-favorite)
389+
}
390+
391+
.favorite-icon {
392+
color: var(--color-favorite);
393+
}
312394
</style>

src/models/contact.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ export default class Contact {
8484
this._vCard = shallowRef(value)
8585
}
8686

87+
get favorite() {
88+
if (this.dav) {
89+
return this.dav.favorite || false
90+
}
91+
return false
92+
}
93+
94+
set favorite(value) {
95+
if (this.dav) {
96+
this.dav.favorite = value
97+
}
98+
}
99+
87100
/**
88101
* Update internal data of this contact
89102
*

src/store/contacts.js

Lines changed: 128 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
4563
const state = {
4664
// Using objects for performance
4765
// https://codepen.io/skjnldsv/pen/ZmKvQo
@@ -51,7 +69,6 @@ const state = {
5169
}
5270

5371
const 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

275346
const 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

Comments
 (0)