🚸(backend) sort user search results by proximity with the active user (#1802)

## Purpose
Allows a user to find more easily the other users they search, with the
following order of priority:
- users they already share documents with (more recent first)
- users that share the same full email domain
- ~~users that share the same partial email domain (last two parts)~~
- ~~other users~~

Edit: We need to ilter out other users in order to not reveal email
addresses from members of other organisations. It's still possible to
invite them by email.

Solves #1521

## Proposal
- [x] Add a new function in `core/utils.py`:
`users_sharing_documents_with()`
- [x] Use it as a key to sort the results of a basic user search
- [x] Filter user results to avoid reveal of users (and email addresses)
of other orgs or that have not been interacted with.
- [x] User research through "full" email address (contains the '@') is
left unaffected.

---------

Co-authored-by: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr>
This commit is contained in:
Sylvain Boissel
2026-02-11 18:51:45 +01:00
committed by GitHub
parent 9af540de35
commit 685464f2d7
14 changed files with 416 additions and 35 deletions

View File

@@ -8,6 +8,7 @@ export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
export const CONFIG = {
AI_FEATURE_ENABLED: true,
API_USERS_SEARCH_QUERY_MIN_LENGTH: 3,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,

View File

@@ -16,6 +16,7 @@ interface ThemeCustomization {
export interface ConfigResponse {
AI_FEATURE_ENABLED?: boolean;
API_USERS_SEARCH_QUERY_MIN_LENGTH?: number;
COLLABORATION_WS_URL?: string;
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;
CONVERSION_FILE_EXTENSIONS_ALLOWED: string[];

View File

@@ -11,6 +11,7 @@ import {
QuickSearchData,
QuickSearchGroup,
} from '@/components/quick-search/';
import { useConfig } from '@/core';
import { Doc } from '@/docs/doc-management';
import { User } from '@/features/auth';
import { useResponsiveStore } from '@/stores';
@@ -57,6 +58,9 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const { t } = useTranslation();
const selectedUsersRef = useRef<HTMLDivElement>(null);
const queryClient = useQueryClient();
const { data: config } = useConfig();
const API_USERS_SEARCH_QUERY_MIN_LENGTH =
config?.API_USERS_SEARCH_QUERY_MIN_LENGTH || 5;
const { isDesktop } = useResponsiveStore();
@@ -83,7 +87,6 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const canViewAccesses = doc.abilities.accesses_view;
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
const showFooter = selectedUsers.length === 0 && !inputValue;
const MIN_CHARACTERS_FOR_SEARCH = 4;
const onSelect = (user: User) => {
setSelectedUsers((prev) => [...prev, user]);
@@ -111,7 +114,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const searchUsersQuery = useUsers(
{ query: userQuery, docId: doc.id },
{
enabled: userQuery?.length > MIN_CHARACTERS_FOR_SEARCH,
enabled: userQuery?.length >= API_USERS_SEARCH_QUERY_MIN_LENGTH,
queryKey: [KEY_LIST_USER, { query: userQuery }],
},
);