MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 4 / 0 / 4
Functions: 100.0% 4 / 0 / 4
Branches: 100.0% 2 / 0 / 2

presentation/QuickContactActions.h
Line Branch Exec Source
1 #pragma once
2
3 #include <QHash>
4 #include <QObject>
5 #include <QPair>
6 #include <QSet>
7 #include <QVariantList>
8 #include <QUrl>
9 #include <QVariantMap>
10 #include <functional>
11
12 #include "data/ContactModels.h"
13
14 class ContactSyncCoordinator;
15 class QuickContactModel;
16
17 // Contact write entry points for QML (Sprint 97 S97.1.4/S97.1.7/S97.1.8,
18 // D-8, D-14; keeps QuickPimBackend small). Every call takes card keys
19 // ("<bookId>/<uid>"), never row indices. Writes go through the session's
20 // ContactOutbox (local first); the model updates the affected rows.
21 //
22 // Delete, move and edit leave an undo step (D-8: no confirmation for one
23 // contact; Ctrl+Z / the toast's Undo within the session).
24 class QuickContactActions final : public QObject {
25 2335 Q_OBJECT
26 // Text of the step Undo would revert ("" = nothing to undo).
27 Q_PROPERTY(QString undoText READ undoText NOTIFY undoChanged)
28 // D-5: contacts that exist only on this computer and wait for a choice.
29 Q_PROPERTY(int localOnlyCount READ localOnlyCount NOTIFY booksChanged)
30 // D-14: the book a new contact goes to when no book is being viewed.
31 Q_PROPERTY(qint64 lastUsedBookId READ lastUsedBookId NOTIFY booksChanged)
32 Q_PROPERTY(bool collectAddresses READ collectAddresses WRITE setCollectAddresses NOTIFY settingsChanged)
33 // Sprint 103 D-6: the card that is the user ("" when none is set or it is
34 // gone). Per profile (QSettings contacts/meCard), never synced.
35 Q_PROPERTY(QString meKey READ meKey NOTIFY meChanged)
36 public:
37 using CoordinatorProvider = std::function<ContactSyncCoordinator *()>;
38 QuickContactActions(CoordinatorProvider coordinator, QuickContactModel &model, QObject *parent = nullptr);
39
40 // {ok, key} or {ok: false, error, errors: {field: message}, duplicates:
41 // [{key, name, book}]} — S97.1.8: an address another card of the same
42 // book already uses is reported (never blocking: pass force: true).
43 Q_INVOKABLE QVariantMap save(const QString &key, const QVariantMap &fields, bool force = false);
44 Q_INVOKABLE QVariantMap create(qint64 bookId, const QVariantMap &fields, bool force = false);
45 Q_INVOKABLE bool remove(const QString &key);
46 Q_INVOKABLE QVariantMap move(const QString &key, qint64 bookId);
47 Q_INVOKABLE bool undo();
48 Q_INVOKABLE bool resolveConflict(const QString &key, const QString &choice);
49 Q_INVOKABLE void retryNow();
50
51 // Books for pickers: {bookId, name, account, readOnly, local, showInAll,
52 // useForCompletion, error, lastSync}.
53 Q_INVOKABLE QVariantList books(bool writableOnly = false) const;
54 // D-14: viewed book, else the last used one, else the first writable
55 // CardDAV book, else "On this computer".
56 Q_INVOKABLE qint64 defaultBookId(qint64 viewedBookId = 0) const;
57 Q_INVOKABLE bool setBookFlags(qint64 bookId, bool showInAll, bool useForCompletion);
58
59 Q_INVOKABLE bool forgetCollected();
60 Q_INVOKABLE bool forgetAddress(const QString &address);
61 // Card for an address (collected or from a mail): prefilled, D-14 book.
62 Q_INVOKABLE QVariantMap draftFor(const QString &name, const QString &address) const;
63 // "c" / "Write mail" (D-10): the preferred address of every card as one
64 // To line ("Name <address>, …"); cards without an address are skipped.
65 Q_INVOKABLE QString recipientsFor(const QStringList &keys) const;
66 // Bulk and menu actions on card keys with one undo step (S97.4.4):
67 // "delete", "move" {bookId}, "addGroup"/"removeGroup" {group}; Sprint 103
68 // S103.3.8: "relate" {otherKey, type, label}, "setMet" {met}.
69 // {ok, done: n, keys: [keys after the action], error}.
70 Q_INVOKABLE QVariantMap apply(const QStringList &keys, const QString &action, const QVariantMap &fields = {});
71 // S97.5.5 groups (D-7): rename or delete a group of a book as one undo
72 // step. A CATEGORIES group changes its members' cards; a group card
73 // (KIND:group, Apple group) is renamed or deleted itself. Deleting a group
74 // never deletes its members.
75 Q_INVOKABLE QVariantMap renameGroup(qint64 bookId, const QString &group, const QString &name);
76 Q_INVOKABLE QVariantMap deleteGroup(qint64 bookId, const QString &group);
77 // S97.5.9: other cards that are probably the same person
78 // [{key, displayName, bookName, sameBook}].
79 Q_INVOKABLE QVariantList possibleDuplicates(const QString &key) const;
80 // Merges `otherKey` into `keepKey` (same book): every address, phone,
81 // postal address, website and group of both, empty fields filled, notes
82 // joined; the other card is deleted. One undo step. {ok, key, error}.
83 Q_INVOKABLE QVariantMap merge(const QString &keepKey, const QString &otherKey);
84 // F100-15: the proposal for merging `keys` (≥ 2, one book, writable):
85 // {ok, error, keep (the most complete card), keys, names [{key, name}],
86 // conflicts [{field, label, options [{text, key}], chosen (key)}],
87 // emails, phones, addresses, groups (counts/names after the merge)}.
88 // With `keepKey`, `chosen` is that card's value (the Keep choice).
89 Q_INVOKABLE QVariantMap mergePlan(const QStringList &keys, const QString &keepKey = QString()) const;
90 // Merges every card of `keys` into `keepKey`; `choices` maps a conflict
91 // field to the key whose value wins. The others are deleted; one undo
92 // step. {ok, key, error}.
93 // `excluded`: value ids of the plan's `sets` the user unchecked (Sprint 107 D-16).
94 Q_INVOKABLE QVariantMap mergeContacts(const QString &keepKey, const QStringList &keys, const QVariantMap &choices,
95 const QStringList &excluded = {});
96 // "Name <address>, …" of the group's members for Write mail (To or Bcc).
97 Q_INVOKABLE QString groupRecipients(qint64 bookId, const QString &group) const;
98 // Card values (S97.4.1): copy to the clipboard; open only http(s) web
99 // addresses, tel: and geo: (S97.1.9 scheme allowlist), never on hover.
100 Q_INVOKABLE bool copyText(const QString &text) const;
101 Q_INVOKABLE bool openLink(const QString &kind, const QString &value) const;
102 // The editor's date field (S97.3.6): {text} for an ISO date; {ok, iso,
103 // text} for what the user typed ("12.04." → "--04-12").
104 Q_INVOKABLE QString dateText(const QString &iso) const;
105 Q_INVOKABLE QVariantMap parseDate(const QString &text) const;
106 // Sprint 103 I-6: the current time in an IANA zone ("02:14"); "" for an unknown zone.
107 Q_INVOKABLE QString localTimeText(const QString &zone) const;
108 // Sprint 103 S103.3.1/S103.3.8 (D-5): one RELATED line on the card `key`
109 // saying what `otherKey` is to it ("parent": the other is this person's
110 // parent). `otherKey` is a card key or, for a person without a card, a
111 // name (text form, D-3). `label` names a "custom" type. One undo step.
112 // {ok, key, error}.
113 // A family is one vCard (X-MAILJD-KIND:family) with its members on it
114 // (QuickContactActionsFamilies.cpp).
115 Q_INVOKABLE bool isFamily(const QString &key) const;
116 // Converts older family data once (idempotent, quiet): member-of lines on
117 // people move onto the family's entry, created for a family by name;
118 // entries listing members get the marker. Returns the cards changed.
119 Q_INVOKABLE int migrateFamilies();
120 Q_INVOKABLE QVariantMap addRelation(const QString &key, const QString &otherKey, const QString &type,
121 const QString &label = {});
122 // Removes the line `sourceIndex` (RelationIndex row) from the card that
123 // stores it; the inverse row of the other card goes with it.
124 Q_INVOKABLE QVariantMap removeRelation(const QString &storedOnKey, int sourceIndex);
125 // Sprint 103 S103.3.2 editor helpers (interaction.md §3).
126 // The relationship types for the type combobox [{id, label, category,
127 // categoryLabel}], D-4 order; "custom" is the editor's own last entry.
128 // `id` is always the stored type (what the other person is to the
129 // holder). With `holderSide` the labels name the holder's side instead,
130 // as the card reads ("Parent of" for the stored type child), in the order
131 // of those roles — the editor row "Rui · [Parent of ▾] · Ana" can no
132 // longer be read the wrong way round. `holderGender` (the card's gender,
133 // "F"/"M"/…) picks the gendered holder labels ("Mother of", "Daughter of").
134 Q_INVOKABLE QVariantList relationTypes(bool holderSide = false, const QString &holderGender = {}) const;
135 // The live guard sentence under a relationship row (G2): "Marta Silva is
136 // Ana's sibling"; "" until person and type are set.
137 Q_INVOKABLE QString relationSentence(const QString &targetName, const QString &type, const QString &holderName,
138 const QString &targetGender = {}, const QString &custom = {}) const;
139 // The same relationship with the holder first ("Rui Costa is Ana's
140 // parent" for the stored type child); the target-first sentence for types
141 // without an inverse (custom labels).
142 Q_INVOKABLE QString relationHolderSentence(const QString &holderName, const QString &type,
143 const QString &targetName, const QString &custom = {}) const;
144 // *When* of a meeting: {ok, iso, text} ("Juni 2019" → "2019-06").
145 Q_INVOKABLE QVariantMap parseLooseDate(const QString &text) const;
146 Q_INVOKABLE QString looseDateText(const QString &iso) const;
147 // Paste recognition (I-10): {recognised, service, kind, label, handle,
148 // value, glyph}; never fetches anything (D-8).
149 Q_INVOKABLE QVariantMap recognizeSocial(const QString &text, const QString &kind) const;
150 // The services for *Change ▾* [{id, label}].
151 Q_INVOKABLE QVariantList socialServices(const QString &kind) const;
152 // Languages for the chips' completion [{code, name}] (≤ 8) and the name of a code.
153 Q_INVOKABLE QVariantList languageOptions(const QString &fragment) const;
154 Q_INVOKABLE QString languageName(const QString &code) const;
155 // S103.3.3 people picker: cards whose name or address starts a word with
156 // `fragment`, most used first, ≤ 8 [{key, uid, name, address, book,
157 // initials, avatarColor, hasPhoto, gender}]; groups and `exceptKey` left out.
158 // With a me card, "me"/"ich" or the user's name puts it first ({isMe:
159 // true}); without one, "me"/"ich" offers the cards with an account
160 // address ({suggestMe: true}, "Is this you?").
161 Q_INVOKABLE QVariantList searchPeople(const QString &fragment, const QString &exceptKey = {}) const;
162 // F103-03: the relationship picker's list — the user's own card always
163 // first (without one, the cards with an account address as "Is this
164 // you?"), then the matches of `fragment`, or the most contacted people
165 // when it is empty (the list opens on focus).
166 Q_INVOKABLE QVariantList pickPeople(const QString &fragment, const QString &exceptKey = {}) const;
167 // F103-21: the *Family* picker's list — family entries (cards with members)
168 // and families by name only, each with "N members" as `detail`; the names
169 // containing `fragment` (all when empty), at most 8, by name. A family by
170 // name is {name, textOnly: true}; one named like a family entry is that entry.
171 Q_INVOKABLE QVariantList familyNames(const QString &fragment, const QString &exceptKey = {}) const;
172 // S103.4.5 pin mode (D-10): the GEO parameter of the address at
173 // `addressIndex` (its place in the card's address list), or its removal
174 // ("Use computed position"); one undo step each. {ok, key, error}.
175 Q_INVOKABLE QVariantMap pinAddress(const QString &key, int addressIndex, double latitude, double longitude);
176 Q_INVOKABLE QVariantMap clearPin(const QString &key, int addressIndex);
177 // S103.3.3: *Create contact "Rui Costa"* in the picker — a card with only
178 // the name in the D-14 book, one undo step; returns the picker entry or {}.
179 Q_INVOKABLE QVariantMap createPerson(const QString &name);
180 // S103.3.5 "Met someone…" (interaction.md §5): the prefill {date, dateText,
181 // eventUid, eventTitle, place, people (offered introducers)} from the
182 // event running now or ended in the last 6 hours (or `eventUid`).
183 Q_INVOKABLE QVariantMap metContext(const QString &eventUid = {}) const;
184 // The duplicate guard: the card with this address or exactly this name ({} when none).
185 Q_INVOKABLE QVariantMap existingPerson(const QString &name, const QString &email) const;
186 // {name, email, date, place, eventUid, via, bookId} → a new card with its
187 // meeting (one undo step), or with `existingKey` only the meeting on that
188 // card. {ok, key, error}.
189 Q_INVOKABLE QVariantMap createMet(const QVariantMap &fields);
190 // S103.6.1: the picker entry of the card with this UID, or of the card
191 // that owns this address ({} when none) — event people and attendees.
192 Q_INVOKABLE QVariantMap personForUid(const QString &uid) const;
193 Q_INVOKABLE QVariantMap personForAddress(const QString &address) const;
194 // S103.6.5 (I-5): *Add to contacts* on someone in a mail whose sender is a
195 // contact suggests "met by mail on <day>, through <sender>": the editor's
196 // met map with suggestedBy, or {} (sender unknown, the person is the
197 // sender, no date).
198 Q_INVOKABLE QVariantMap introductionFor(const QString &senderHeader, const QString &address,
199 const QString &dateIso) const;
200 // S103.6.3 (D-15): "this mail is about the person" — link or unlink the
201 // mail (Message-ID, subject, day) on the card; one undo step each.
202 Q_INVOKABLE QVariantMap linkMail(const QString &key, const QString &messageId, const QString &subject,
203 const QString &dateIso);
204 Q_INVOKABLE QVariantMap unlinkMail(const QString &key, const QString &messageId);
205 // The cards that link this mail (the reader's *About:* line) — picker entries.
206 Q_INVOKABLE QVariantList cardsAboutMail(const QString &messageId) const;
207 // S103.6.6: the @-mention at the cursor of the compose body ({start,
208 // fragment}, {} when "@" should stay a plain character — MentionRule).
209 Q_INVOKABLE QVariantMap mentionAt(const QString &text, int cursor) const;
210 // D-6 / S103.1.6: mark a card as the user, or clear the mark.
211 Q_INVOKABLE bool setMe(const QString &key);
212 Q_INVOKABLE void clearMe();
213 // Cards whose address is one of the accounts' [{key, name, address}].
214 Q_INVOKABLE QVariantList meSuggestions() const;
215 // S97.5.7: a picture file for the editor → {ok, data (base64 JPEG), error}.
216 Q_INVOKABLE QVariantMap preparePhoto(const QUrl &file) const;
217 // Sprint 103 S103.3.6 (interaction.md §3.8/§3.9): an image from a file
218 // (chosen or dropped) or the clipboard becomes the photo draft —
219 // {ok, preview (base64 JPEG ≤ 1024 px), width, height} or {ok: false,
220 // error} ("This image cannot be used." above 20 MiB or undecodable);
221 // framePhoto() turns the framed square (fractions of the preview) into
222 // the card photo {ok, data (base64 JPEG)}.
223 Q_INVOKABLE QVariantMap loadPhoto(const QUrl &file);
224 Q_INVOKABLE QVariantMap loadPhotoFromClipboard();
225 Q_INVOKABLE bool clipboardHasImage() const;
226 Q_INVOKABLE QVariantMap framePhoto(qreal x, qreal y, qreal width, qreal height);
227 // The stored vCard as text ("Copy as vCard text", export).
228 Q_INVOKABLE QString vcardText(const QString &key) const;
229 Q_INVOKABLE bool uploadLocalOnly(qint64 bookId);
230 Q_INVOKABLE void keepLocalOnly();
231
232
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 34 times.
39 QString undoText() const { return m_undo.isEmpty() ? QString() : m_undo.last().text; }
233 int localOnlyCount() const;
234 qint64 lastUsedBookId() const;
235 bool collectAddresses() const;
236 void setCollectAddresses(bool enabled);
237 QString meKey() const;
238 // The account addresses for meSuggestions() (set by QuickPimBackend).
239 797 void setOwnAddresses(std::function<QStringList()> provider) { m_ownAddresses = std::move(provider); }
240 // The calendar database for metContext() (set by QuickPimBackend).
241 797 void setCalendarPath(const QString &path) { m_calendarPath = path; }
242
243 signals:
244 void undoChanged();
245 void booksChanged();
246 void settingsChanged();
247 void meChanged();
248 // A mail was linked or unlinked (the reader's *About:* line follows).
249 void mailLinksChanged();
250 // A write finished locally; the toast shows message with Undo when undoable.
251 void done(const QString &message, bool undoable);
252
253 private:
254 QString mergeCards(const QStringList &keys, QList<QVariantMap> *cards) const;
255 // After a merge: cards naming a removed card (relationship, introducer,
256 // group member) name the kept card instead (ContactPeopleMerge::retarget).
257 void retargetReferences(const QSet<QString> &goneUids, const QString &keptKey);
258 struct UndoPart {
259 QString kind; // "delete", "move", "edit", "created"
260 StoredCard card; // delete: the removed card; edit: the card before
261 QString key; // move: the moved card's key now
262 qint64 bookId = 0; // move: the book it came from
263 };
264 // One user action; a bulk action (S97.4.4) is one step with many parts.
265 struct UndoStep {
266 QString text;
267 QList<UndoPart> parts;
268 };
269 // Parses the stored card, applies `change` (returns "" or why it refused)
270 // and saves it through the outbox with an "edit" undo step.
271 QVariantMap editPeople(const QString &key, const std::function<QString(ContactCard *)> &change,
272 const QString &message);
273 QVariantMap duplicates(qint64 bookId, const ContactCard &card, const QString &exceptKey) const;
274 // F103-22: a relationship saved on the other card, changed or removed in this card's editor.
275 struct MirrorEdit {
276 QString storedOnKey;
277 int sourceIndex = -1;
278 QString otherKey; // the person chosen now ("" when removed)
279 QString type; // the type seen from the edited card ("" when removed)
280 QString label;
281 };
282 // Takes the mirrored rows out of `fields`: unchanged ones stay on the other
283 // card, changed ones are saved here (or, on the me card, there), removed ones go.
284 QVariantMap splitMirrors(const QString &key, const QVariantMap &fields, QList<MirrorEdit> *edits) const;
285 // The number of other cards that could not be updated (S106-255).
286 int applyMirrorEdits(const QString &key, const QList<MirrorEdit> &edits);
287 // *Belongs to family* entries of an editor that are not yet on a family's
288 // entry: taken out of the person's relations, written as member lines on
289 // the family (found by key or name, created when missing) after the save.
290 struct FamilyJoin {
291 QString familyKey; // a chosen family entry
292 QString name; // else the family's name
293 };
294 QVariantMap takeFamilyJoins(const QVariantMap &fields, QList<FamilyJoin> *joins) const;
295 QString ensureFamily(const QString &name, qint64 bookId, QString *error);
296 QVariantMap addMember(const QString &familyKey, const QString &personKey);
297 // Returns how many memberships could not be written.
298 int applyFamilyJoins(const QString &personKey, const QList<FamilyJoin> &joins);
299 void pushUndo(const QString &text, const UndoPart &part);
300 bool undoPart(ContactSyncCoordinator *sync, const UndoPart &part);
301 bool changeGroup(const QString &key, const QString &group, bool add);
302 void rememberBook(qint64 bookId);
303 // Single actions report their own result; inside apply() they are quiet.
304 void report(const QString &message, bool undoable);
305
306 QVariantMap personEntry(const QString &key) const;
307 // S106-298: a UID can live in several books (a copied card keeps it): the
308 // copy in `preferredBook` wins, then one in a writable book, then the oldest.
309 QString cardKeyForUid(const QString &uid, qint64 preferredBook = 0) const;
310 QVariantMap holdPhoto(const QByteArray &bytes);
311 CoordinatorProvider m_coordinator;
312 std::function<QStringList()> m_ownAddresses;
313 QString m_calendarPath;
314 QByteArray m_photoDraft; // the image waiting in the framing step
315 QuickContactModel &m_model;
316 QList<UndoStep> m_undo;
317 // Set while apply() collects the parts of one bulk step.
318 QList<UndoPart> *m_collect = nullptr;
319 bool m_migratingFamilies = false;
320 // S106-297: during migrateFamilies() the writable family entries by
321 // case-folded name (book id, key), read once per run instead of one
322 // full scan per join.
323 using FamilyIndex = QHash<QString, QList<QPair<qint64, QString>>>;
324 FamilyIndex *m_familyIndex = nullptr;
325 // People whose joins failed, with the raw card they failed with: not
326 // tried again in this session until the card changes (S106-297).
327 QHash<QString, size_t> m_familyJoinsFailed;
328 };
329