Skip to content

feat: show recent people on contact icon - #62401

Closed
kristian-zendato wants to merge 3 commits into
masterfrom
feat/people-menu
Closed

feat: show recent people on contact icon#62401
kristian-zendato wants to merge 3 commits into
masterfrom
feat/people-menu

Conversation

@kristian-zendato

@kristian-zendato kristian-zendato commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the People (contacts) header menu more inviting by showing a stack of up to 3 overlapping avatars of recent contacts on the menu trigger, instead of only the contacts icon.

  • Adds GET /contactsmenu/preview-avatars which returns the first 3 entries from the same unfiltered contacts list as the menu (Manager::getEntries with an empty filter), with optional team filtering matching the existing index endpoint.
  • Frontend loads those contacts on mount and shows an NcAvatar stack when at least 2 are available; otherwise keeps the existing contacts icon.
  • Widens the header trigger (width: fit-content) so the stack does not overlap notifications/profile and remains fully clickable.
  • Adds PHP and Vue unit tests for the new endpoint and trigger behavior.

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@kristian-zendato
kristian-zendato requested review from a team as code owners July 22, 2026 10:51
@kristian-zendato
kristian-zendato requested review from icewind1991, nfebe, salmart-dev, skjnldsv and sorbaugh and removed request for a team July 22, 2026 10:51
Comment thread core/src/views/ContactsMenu.vue Outdated
}
}
teams.value = [...userTeams]
await loadPreviewAvatars()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fires twice: setting selectedTeam above already triggers the new watcher. Dropping this call should be enough I think?

const view = render(ContactsMenu)
await view.findByRole('button')

expect(view.container.querySelector('.contactsmenu__trigger-avatars')).toBeNull()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if this runs before loadPreviewAvatars resolves, so it could pass even with the threshold set to >= 1. Perhaps we can add a similar vi.waitFor from the test below?

Comment thread core/src/views/ContactsMenu.vue Outdated
margin-inline-end: calc(2 * var(--default-grid-baseline));

:deep(.header-menu__trigger) {
opacity: 1 !important;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this important working around? To remove the hover/focus feedback?

@kristian-zendato

Copy link
Copy Markdown
Contributor Author

@pringelmann Thank you for your comments.
I updated the code for your comments.

Signed-off-by: Kristian Zendato <kristian.zendato@nextcloud.com>
return [];
}

$entries = $this->manager->getEntries($user, '');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that we are now calling this on every single page load? I wonder if retrieving all contacts without a filter like this will impact performance. Would you mind briefly investigating?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a quick look myself: searching with an empty filter takes the recent-statuses path, which does one address book search per recent status, up to 25, then runs every action provider over all 25 results. We keep 3. Those searches can't use an index, so they get slower as the directory grows.

Nothing waits on the response, so it's not a latency problem, but it is recurring database work on every page load.

Limiting the query to 3 instead of 25 and skipping the action providers would cover most of it. Caching per user for a few minutes on top would make repeat loads free, and slightly stale recent contacts are fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I think even 5 minutes caching would be fine here

Comment thread core/src/views/ContactsMenu.vue Outdated
})

// immediate: load header avatars on mount and whenever the team filter changes
watch(selectedTeam, loadPreviewAvatars, { immediate: true })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kristian-zendato I didn't mean to add a second watcher here. This one has the same problem: it fires immediately, then always fires a second time we set selectedTeam above. Also since we can't predict in which order the responses arrive, this can lead to race conditions.

storage.getItem is synchronous, so perhaps we can seed the ref instead of assigning in onMounted:

const storedTeam = storage.getItem('core:contacts:team')
const selectedTeam = ref<string>(storedTeam ? JSON.parse(storedTeam) : '$_all_$')

Then drop the if (team) block. One request, correct team, all cases.

Seed the team filter from storage to avoid a double-fetch race, and load
header avatars via a limited query that skips action providers and caches
per user for 5 minutes.

Assisted-by: Cursor:Composer
Co-authored-by: Cursor <cursoragent@cursor.com>
Address remaining review feedback: document why the avatar trigger
clears NcHeaderMenu's icon mask, and wait for the icon assertion in the
preview-avatars unit test. Rebuild assets after merging master.

Assisted-by: Cursor:Composer
Co-authored-by: Cursor <cursoragent@cursor.com>

@SebastianKrupinski SebastianKrupinski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please submit this PR without the compiled JS files

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make People menu more inviting

5 participants