@communecter/cocolight-api-client 1.0.140 → 1.0.142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cocolight-api-client.browser.js +1 -1
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js.map +1 -1
- package/package.json +1 -1
- package/src/ApiClient.ts +5 -1
- package/src/api/BaseEntity.ts +113 -0
- package/src/api/EndpointApi.ts +16 -1
- package/src/api/EndpointApi.types.ts +103 -0
- package/src/api/Notifications.ts +196 -0
- package/src/api/User.ts +72 -1
- package/src/api/serverDataType/Notification.ts +81 -0
- package/src/endpoints.module.ts +1 -1
- package/src/index.ts +9 -0
- package/types/api/BaseEntity.d.ts +68 -1
- package/types/api/EndpointApi.d.ts +10 -1
- package/types/api/EndpointApi.types.d.ts +96 -0
- package/types/api/Notifications.d.ts +79 -0
- package/types/api/User.d.ts +30 -0
- package/types/api/serverDataType/Notification.d.ts +73 -0
- package/types/endpoints.module.d.ts +338 -0
- package/types/index.d.ts +4 -1
package/package.json
CHANGED
package/src/ApiClient.ts
CHANGED
|
@@ -1648,7 +1648,11 @@ export default class ApiClient extends EventEmitter {
|
|
|
1648
1648
|
"imageThumb",
|
|
1649
1649
|
"imageMediumPath",
|
|
1650
1650
|
"logo",
|
|
1651
|
-
"logoMin"
|
|
1651
|
+
"logoMin",
|
|
1652
|
+
// `image` : chemin relatif renvoyé par Document::getDocumentPath (ex: valeurs
|
|
1653
|
+
// thématiques de COSTUM_FILTER_COFORM_BY_PATH). _normalizeImage ne touche que les
|
|
1654
|
+
// strings non vides → "" et non-strings restent inchangés (pas d'effet de bord).
|
|
1655
|
+
"image"
|
|
1652
1656
|
];
|
|
1653
1657
|
|
|
1654
1658
|
/**
|
package/src/api/BaseEntity.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
SearchMemberAutocompleteData,
|
|
47
47
|
GetEventsData,
|
|
48
48
|
CostumFilterCoformData,
|
|
49
|
+
CostumFilterCoformByPathData,
|
|
49
50
|
GetCountriesData,
|
|
50
51
|
SearchZonesData,
|
|
51
52
|
CoformAnswersByFormsData,
|
|
@@ -252,6 +253,31 @@ export type CostumContextFields = {
|
|
|
252
253
|
|
|
253
254
|
export type WithCostumContext<T> = T & CostumContextFields;
|
|
254
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Une valeur distincte d'une thématique CoForm (retournée par `coformFilterByPath`).
|
|
258
|
+
*/
|
|
259
|
+
export interface CoformThematicValue {
|
|
260
|
+
/** Nom de la valeur de thématique (ex: "Cafés cantines solidaires"). */
|
|
261
|
+
name: string;
|
|
262
|
+
/** URL de l'image associée (chaîne vide si aucune). */
|
|
263
|
+
image: string;
|
|
264
|
+
/** Identifiants des éléments liés via le finder (orgas/projets/poi selon finderPath). */
|
|
265
|
+
orgaNameArray: string[];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Résultat normalisé de `coformFilterByPath` (endpoint COSTUM_FILTER_COFORM_BY_PATH).
|
|
270
|
+
* Reconstruit proprement la réponse backend que le normalizer générique aplatit.
|
|
271
|
+
*/
|
|
272
|
+
export interface CoformFilterByPathResult {
|
|
273
|
+
/** _id des answers concernées par la thématique (dédupliqués). */
|
|
274
|
+
distinctElements: string[];
|
|
275
|
+
/** Valeurs distinctes de la thématique, chacune avec image/name/éléments liés. */
|
|
276
|
+
values: CoformThematicValue[];
|
|
277
|
+
/** Compteurs renvoyés par le backend (ex: `{ answers: 14 }`). */
|
|
278
|
+
count: Record<string, number>;
|
|
279
|
+
}
|
|
280
|
+
|
|
255
281
|
// Type pour le curseur de pagination utilisé dans _createPaginatorEngine
|
|
256
282
|
type PaginationCursor = {
|
|
257
283
|
searchType?: string[];
|
|
@@ -5369,6 +5395,93 @@ export class BaseEntity<TServerData = any> {
|
|
|
5369
5395
|
return wrappedFinalizer(data);
|
|
5370
5396
|
}
|
|
5371
5397
|
|
|
5398
|
+
/**
|
|
5399
|
+
* Récupère les valeurs distinctes d'**une seule** thématique CoForm, query pilotée
|
|
5400
|
+
* côté client via `params.thematicPath` (+ `params.finderPath` optionnel).
|
|
5401
|
+
*
|
|
5402
|
+
* Distinct de {@link coformFiltersSearch} (qui passe `searchedData` et fait le batch
|
|
5403
|
+
* multi-filtres piloté serveur) : ici on cible UN filtre précis et on construit
|
|
5404
|
+
* soi-même la projection (`fields`) et les `filters`. Côté backend, c'est la branche
|
|
5405
|
+
* `isset($_POST["params"])` du controller AutoGlobalThematicNtwrkAction.
|
|
5406
|
+
*
|
|
5407
|
+
* Utile pour un **filtre par réseau régional/thématique** : on combine `params` avec
|
|
5408
|
+
* `notSourceKey: true` (cherche dans tout le réseau) ou `sourceKey: ["<réseau>"]`
|
|
5409
|
+
* (scope un réseau précis — cf. `_withCostumContext`).
|
|
5410
|
+
*
|
|
5411
|
+
* @param data - `params.thematicPath` est requis ; le reste (fields/filters/searchType/
|
|
5412
|
+
* sortBy/indexMin/indexStep/notSourceKey/locality) est optionnel.
|
|
5413
|
+
* @returns {@link CoformFilterByPathResult} : `{ distinctElements, values, count }`.
|
|
5414
|
+
* Reconstruit proprement la map backend que le normalizer générique aplatit
|
|
5415
|
+
* (la clé `results` est traitée comme wrapper de liste par `_transformData`).
|
|
5416
|
+
* @throws {ApiError} 400 si `params.thematicPath` est absent.
|
|
5417
|
+
*
|
|
5418
|
+
* @example
|
|
5419
|
+
* const res = await project.coformFilterByPath({
|
|
5420
|
+
* params: {
|
|
5421
|
+
* thematicPath: "lesCommunsDesTierslieux17102023_108_0.multiCheckboxPlus...",
|
|
5422
|
+
* finderPath: "answers.lesCommunsDesTierslieux17102023_108_0.finder...",
|
|
5423
|
+
* },
|
|
5424
|
+
* fields: [
|
|
5425
|
+
* "answers.lesCommunsDesTierslieux17102023_108_0.multiCheckboxPlus...",
|
|
5426
|
+
* "answers.lesCommunsDesTierslieux17102023_108_0.finder...",
|
|
5427
|
+
* ],
|
|
5428
|
+
* filters: {
|
|
5429
|
+
* "answers.lesCommunsDesTierslieux17102023_108_0.multiCheckboxPlus...": { $exists: true },
|
|
5430
|
+
* },
|
|
5431
|
+
* searchType: ["answers"],
|
|
5432
|
+
* notSourceKey: true,
|
|
5433
|
+
* });
|
|
5434
|
+
* res.count.answers; // 14
|
|
5435
|
+
* res.values[0].name; // "Cafés cantines solidaires"
|
|
5436
|
+
* res.values[0].orgaNameArray; // ["5b740..."]
|
|
5437
|
+
* res.distinctElements; // string[] des _id concernés
|
|
5438
|
+
*/
|
|
5439
|
+
async coformFilterByPath(
|
|
5440
|
+
data: Partial<CostumFilterCoformByPathData> & { params: CostumFilterCoformByPathData["params"] },
|
|
5441
|
+
): Promise<CoformFilterByPathResult> {
|
|
5442
|
+
if (!data?.params || typeof data.params.thematicPath !== "string" || data.params.thematicPath.trim() === "") {
|
|
5443
|
+
throw new ApiError("coformFilterByPath : params.thematicPath est requis (string non vide).", 400);
|
|
5444
|
+
}
|
|
5445
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
5446
|
+
(finalData: CostumFilterCoformByPathData) => this.endpointApi.costumFilterCoformByPath(finalData)
|
|
5447
|
+
);
|
|
5448
|
+
const raw = await wrappedFinalizer(data) as Record<string, unknown>;
|
|
5449
|
+
|
|
5450
|
+
// Le normalizer (_transformData) convertit la map `results` en array où chaque
|
|
5451
|
+
// entrée reçoit `id: <cléOriginale>` :
|
|
5452
|
+
// - distinctElements (array) → { id: "distinctElements", 0: "<_id>", 1: "<_id>", ... }
|
|
5453
|
+
// - chaque valeur thématique → { id: "<nom>", name, image, orgaNameArray }
|
|
5454
|
+
// On reconstruit la structure propre à partir de ça.
|
|
5455
|
+
const rawResults = Array.isArray(raw?.results) ? (raw.results as Array<Record<string, unknown>>) : [];
|
|
5456
|
+
|
|
5457
|
+
let distinctElements: string[] = [];
|
|
5458
|
+
const values: CoformThematicValue[] = [];
|
|
5459
|
+
|
|
5460
|
+
for (const entry of rawResults) {
|
|
5461
|
+
if (!entry || typeof entry !== "object") continue;
|
|
5462
|
+
if (entry.id === "distinctElements") {
|
|
5463
|
+
distinctElements = Object.entries(entry)
|
|
5464
|
+
.filter(([k]) => /^\d+$/.test(k))
|
|
5465
|
+
.sort(([a], [b]) => Number(a) - Number(b))
|
|
5466
|
+
.map(([, v]) => String(v));
|
|
5467
|
+
continue;
|
|
5468
|
+
}
|
|
5469
|
+
values.push({
|
|
5470
|
+
name: typeof entry.name === "string" ? entry.name : String(entry.id ?? ""),
|
|
5471
|
+
image: typeof entry.image === "string" ? entry.image : "",
|
|
5472
|
+
orgaNameArray: Array.isArray(entry.orgaNameArray)
|
|
5473
|
+
? (entry.orgaNameArray as unknown[]).map((x) => String(x))
|
|
5474
|
+
: [],
|
|
5475
|
+
});
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
const count = (raw?.count && typeof raw.count === "object")
|
|
5479
|
+
? (raw.count as Record<string, number>)
|
|
5480
|
+
: { answers: values.length };
|
|
5481
|
+
|
|
5482
|
+
return { distinctElements, values, count };
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5372
5485
|
|
|
5373
5486
|
/**
|
|
5374
5487
|
* Recherche des zones géographiques selon un pays et un niveau administratif.
|
package/src/api/EndpointApi.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ApiAuthenticationError } from "../error.js";
|
|
3
3
|
|
|
4
4
|
import type ApiClient from "../ApiClient.js";
|
|
5
|
-
import type { PersonRegisterData, AuthenticateUrlData, RefreshTokenUrlData, PasswordRecoveryData, ServerExchangeTokenData, ChangePasswordData, DeleteAccountData, UpdateSettingsData, UpdateBlockDescriptionData, UpdateBlockInfoData, UpdateBlockSocialData, UpdateBlockLocalityData, UpdateBlockSlugData, CheckData, ProfilImageData, ProfilBannerData, GetElementsAboutData, MulticonnectData, GetNewsData, GetNewsByIdData, AddNewsData, AddImageNewsData, AddFileNewsData, DeleteNewsData, UpdateNewsData, ShareNewsData, GetCommentsData, AddCommentsData, DeleteCommentsData, UpdateCommentsData, SearchTagsData, ShowVoteData, GlobalAutocompleteData, CityAutocompleteData, CityAutocompleteByCountryData, SuggestionInputData, GetProjectsNoAdminData, GetProjectsAdminData, GetPoisNoAdminData, GetPoisAdminData, GetOrganizationsNoAdminData, GetOrganizationsAdminData, GetMembersNoAdminData, GetMembersAdminData, GetFriendsAdminData, GetSubscriptionsData, GetSubscriptionsAdminData, GetSubscribersData, GetSubscribersAdminData, GetContributorsNoAdminData, GetContributorsAdminData, GetBadgesData, GetBadgesFiltersData, ConnectData, DisconnectData, GetElementsKeyData, GetFavorisData, DeleteFavorisData, AddFavorisData, AddOrganizationData, AddProjectData, AddPoiData, AddEventData, DeletePoiData, DeleteEventData, DeleteElementData, AddImageElementData, LinkValidateData, SearchMemberAutocompleteData, GetNotificationsData, GetNotificationsCountData, NotificationUpdateData, MarkNotificationAsReadData, ActivitypubSearchData, ActivitypubLinkData, ActivitypubGetCommunityData, GetBadgeData, AddBadgesData, AssignBadgesData, GetEventsData, ShareEventsData, InviteEventData, FollowData, GetCostumJsonData, GlobalAutocompleteCostumData, NavigatorGettlData, CostumEventRequestActorsData, CostumEventRequestSubeventsData, CostumEventRequestElementEventData, CostumEventRequestCategoriesData, CostumEventRequestDatesData, CostumEventRequestEventData, CostumEventRequestLinkTlToEventData, CostumEventRequestLoadContextTagData, GetGalleryData, GetAttendeesNoAdminData, GetAttendeesAdminData, CoformAnswersSearchData, CoformAnswersByIdData, GetCoformByIdData, CoformUploadAnswerFileData, CoformGetAnswerFilesData, SaveCoformAnswerData, AddVoteData, AddReportAbuseData, UpdatePathValueData, DeleteDocumentByContextData, DeleteDocumentByIdData, DemoteAdminData, CostumFilterCoformData, GetCountriesData, SearchZonesData, CoformAnswersByFormsData, GenerateAnswerFromFormData, FundingEnvelopeData, CoremuOperationData, CostumProjectActionRequestNewData, CostumProjectActionRequestSetStatusData, CostumProjectActionRequestSetDateData, CostumProjectActionRequestSetContributorsData, CostumProjectActionRequestCancelData, CostumProjectActionRequestArchiveData, LinkDiscourseAccountData, UnlinkDiscourseAccountData, DiscourseProfileData, DiscourseCheckEmailData, DiscourseDismissLinkData, LinkMediawikiAccountData, UnlinkMediawikiAccountData, GetMediawikiContributionsData, AddClassifiedData } from "./EndpointApi.types.js";
|
|
5
|
+
import type { PersonRegisterData, AuthenticateUrlData, RefreshTokenUrlData, PasswordRecoveryData, ServerExchangeTokenData, ChangePasswordData, DeleteAccountData, UpdateSettingsData, UpdateBlockDescriptionData, UpdateBlockInfoData, UpdateBlockSocialData, UpdateBlockLocalityData, UpdateBlockSlugData, CheckData, ProfilImageData, ProfilBannerData, GetElementsAboutData, MulticonnectData, GetNewsData, GetNewsByIdData, AddNewsData, AddImageNewsData, AddFileNewsData, DeleteNewsData, UpdateNewsData, ShareNewsData, GetCommentsData, AddCommentsData, DeleteCommentsData, UpdateCommentsData, SearchTagsData, ShowVoteData, GlobalAutocompleteData, CityAutocompleteData, CityAutocompleteByCountryData, SuggestionInputData, GetProjectsNoAdminData, GetProjectsAdminData, GetPoisNoAdminData, GetPoisAdminData, GetOrganizationsNoAdminData, GetOrganizationsAdminData, GetMembersNoAdminData, GetMembersAdminData, GetFriendsAdminData, GetSubscriptionsData, GetSubscriptionsAdminData, GetSubscribersData, GetSubscribersAdminData, GetContributorsNoAdminData, GetContributorsAdminData, GetBadgesData, GetBadgesFiltersData, ConnectData, DisconnectData, GetElementsKeyData, GetFavorisData, DeleteFavorisData, AddFavorisData, AddOrganizationData, AddProjectData, AddPoiData, AddEventData, DeletePoiData, DeleteEventData, DeleteElementData, AddImageElementData, LinkValidateData, SearchMemberAutocompleteData, GetNotificationsData, GetNotificationsCountData, NotificationUpdateData, MarkNotificationAsReadData, ActivitypubSearchData, ActivitypubLinkData, ActivitypubGetCommunityData, GetBadgeData, AddBadgesData, AssignBadgesData, GetEventsData, ShareEventsData, InviteEventData, FollowData, GetCostumJsonData, GlobalAutocompleteCostumData, NavigatorGettlData, CostumEventRequestActorsData, CostumEventRequestSubeventsData, CostumEventRequestElementEventData, CostumEventRequestCategoriesData, CostumEventRequestDatesData, CostumEventRequestEventData, CostumEventRequestLinkTlToEventData, CostumEventRequestLoadContextTagData, GetGalleryData, GetAttendeesNoAdminData, GetAttendeesAdminData, CoformAnswersSearchData, CoformAnswersByIdData, GetCoformByIdData, CoformUploadAnswerFileData, CoformGetAnswerFilesData, SaveCoformAnswerData, AddVoteData, AddReportAbuseData, UpdatePathValueData, DeleteDocumentByContextData, DeleteDocumentByIdData, DemoteAdminData, CostumFilterCoformData, CostumFilterCoformByPathData, GetCountriesData, SearchZonesData, CoformAnswersByFormsData, GenerateAnswerFromFormData, FundingEnvelopeData, CoremuOperationData, CostumProjectActionRequestNewData, CostumProjectActionRequestSetStatusData, CostumProjectActionRequestSetDateData, CostumProjectActionRequestSetContributorsData, CostumProjectActionRequestCancelData, CostumProjectActionRequestArchiveData, LinkDiscourseAccountData, UnlinkDiscourseAccountData, DiscourseProfileData, DiscourseCheckEmailData, DiscourseDismissLinkData, LinkMediawikiAccountData, UnlinkMediawikiAccountData, GetMediawikiContributionsData, AddClassifiedData } from "./EndpointApi.types.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Classe EndpointApi générée automatiquement depuis endpoints-copie.json
|
|
@@ -1829,6 +1829,21 @@ export class EndpointApi {
|
|
|
1829
1829
|
return this.call("COSTUM_FILTER_COFORM", data);
|
|
1830
1830
|
}
|
|
1831
1831
|
|
|
1832
|
+
/**
|
|
1833
|
+
* Filtre CoForm par path (thématique unique) : Récupère les valeurs distinctes d'UNE thématique CoForm (branche `isset($_POST['params'])` du controller AutoGlobalThematicNtwrkAction). La query est construite côté client via `params.thematicPath` (+ `params.finderPath` optionnel) ; le serveur fait un seul globalAutoComplete + tri. Distinct de COSTUM_FILTER_COFORM (branche `searchedData`, multi-filtres pilotés serveur).
|
|
1834
|
+
* Constant : COSTUM_FILTER_COFORM_BY_PATH
|
|
1835
|
+
* @param data - Données envoyées à l'API
|
|
1836
|
+
* @returns Les données de réponse.
|
|
1837
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
1838
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
1839
|
+
*/
|
|
1840
|
+
async costumFilterCoformByPath(data: CostumFilterCoformByPathData): Promise<any> {
|
|
1841
|
+
if (!data || typeof data !== "object") {
|
|
1842
|
+
throw new TypeError("Le paramètre data doit être un objet.");
|
|
1843
|
+
}
|
|
1844
|
+
return this.call("COSTUM_FILTER_COFORM_BY_PATH", data);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1832
1847
|
/**
|
|
1833
1848
|
* Récupérer la liste des pays : Récupérer la liste de tous les pays
|
|
1834
1849
|
* Constant : GET_COUNTRIES
|
|
@@ -5733,6 +5733,109 @@ export interface CostumFilterCoformData {
|
|
|
5733
5733
|
}
|
|
5734
5734
|
|
|
5735
5735
|
|
|
5736
|
+
export interface CostumFilterCoformByPathData {
|
|
5737
|
+
/**
|
|
5738
|
+
* Discriminant de la branche B : chemins de la thématique et du finder. Sa présence aiguille le controller vers le mode single-filtre client-driven.
|
|
5739
|
+
*/
|
|
5740
|
+
params: {
|
|
5741
|
+
/**
|
|
5742
|
+
* Chemin de la thématique dans answers (ex: `formKey.multiCheckboxPlus...`). Devient `path` côté backend.
|
|
5743
|
+
*/
|
|
5744
|
+
thematicPath: string;
|
|
5745
|
+
/**
|
|
5746
|
+
* Chemin du finder (ex: `answers.formKey.finder...`). Optionnel : défaut backend `links.organizations`.
|
|
5747
|
+
*/
|
|
5748
|
+
finderPath?: string;
|
|
5749
|
+
};
|
|
5750
|
+
/**
|
|
5751
|
+
* Types d'entités à inclure (typiquement `["answers"]`).
|
|
5752
|
+
*/
|
|
5753
|
+
searchType?: string[];
|
|
5754
|
+
/**
|
|
5755
|
+
* Projection : champs à retourner (incluant thematicPath et finderPath).
|
|
5756
|
+
*/
|
|
5757
|
+
fields?: string[];
|
|
5758
|
+
/**
|
|
5759
|
+
* Filtres MongoDB additionnels (objet) ou chaîne vide.
|
|
5760
|
+
*/
|
|
5761
|
+
filters?:
|
|
5762
|
+
| {
|
|
5763
|
+
[k: string]: unknown;
|
|
5764
|
+
}
|
|
5765
|
+
| "";
|
|
5766
|
+
/**
|
|
5767
|
+
* Champ de tri (clé = champ, valeur = 1 ou -1).
|
|
5768
|
+
*/
|
|
5769
|
+
sortBy?: {
|
|
5770
|
+
[k: string]: 1 | -1;
|
|
5771
|
+
};
|
|
5772
|
+
/**
|
|
5773
|
+
* Index de départ pour la pagination.
|
|
5774
|
+
*/
|
|
5775
|
+
indexMin?: number;
|
|
5776
|
+
/**
|
|
5777
|
+
* Nombre d'éléments à récupérer (haut par défaut : agrégation des valeurs distinctes).
|
|
5778
|
+
*/
|
|
5779
|
+
indexStep?: number;
|
|
5780
|
+
/**
|
|
5781
|
+
* Comptage des types dans la réponse.
|
|
5782
|
+
*/
|
|
5783
|
+
count?: boolean;
|
|
5784
|
+
/**
|
|
5785
|
+
* Extension Fediverse (toujours désactivée).
|
|
5786
|
+
*/
|
|
5787
|
+
fediverse?: boolean;
|
|
5788
|
+
/**
|
|
5789
|
+
* Si true, ignore le filtre sourceKey côté backend (recherche dans tout le réseau, pas seulement le costum courant).
|
|
5790
|
+
*/
|
|
5791
|
+
notSourceKey?: boolean;
|
|
5792
|
+
/**
|
|
5793
|
+
* Type initial de la recherche (vide par défaut).
|
|
5794
|
+
*/
|
|
5795
|
+
initType?: string;
|
|
5796
|
+
/**
|
|
5797
|
+
* Localités ciblées (object keyé, array d'objets, ou chaîne vide).
|
|
5798
|
+
*/
|
|
5799
|
+
locality?:
|
|
5800
|
+
| {
|
|
5801
|
+
[k: string]: unknown;
|
|
5802
|
+
}
|
|
5803
|
+
| {
|
|
5804
|
+
[k: string]: unknown;
|
|
5805
|
+
}[]
|
|
5806
|
+
| "";
|
|
5807
|
+
/**
|
|
5808
|
+
* ID du contexte costum (auto-injecté par _withCostumContext).
|
|
5809
|
+
*/
|
|
5810
|
+
contextId?: string;
|
|
5811
|
+
/**
|
|
5812
|
+
* Type du contexte costum (auto-injecté).
|
|
5813
|
+
*/
|
|
5814
|
+
contextType?: "projects" | "organizations";
|
|
5815
|
+
/**
|
|
5816
|
+
* Slug du costum (auto-injecté par _withCostumContext).
|
|
5817
|
+
*/
|
|
5818
|
+
costumSlug?: string;
|
|
5819
|
+
/**
|
|
5820
|
+
* Alias historique de contextId (auto-injecté).
|
|
5821
|
+
*/
|
|
5822
|
+
costumId?: string;
|
|
5823
|
+
/**
|
|
5824
|
+
* Alias historique de contextType (auto-injecté).
|
|
5825
|
+
*/
|
|
5826
|
+
costumType?: "projects" | "organizations";
|
|
5827
|
+
/**
|
|
5828
|
+
* Mode édition costum (toujours désactivé).
|
|
5829
|
+
*/
|
|
5830
|
+
costumEditMode?: boolean;
|
|
5831
|
+
/**
|
|
5832
|
+
* Clés de source pour filtrer (ignoré si notSourceKey=true).
|
|
5833
|
+
*/
|
|
5834
|
+
sourceKey?: string[];
|
|
5835
|
+
[k: string]: unknown;
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5838
|
+
|
|
5736
5839
|
export interface GetCountriesData {
|
|
5737
5840
|
/**
|
|
5738
5841
|
* Slug personnalisé
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { batch, reactive } from "../utils/reactive.js";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
NotificationAuthor,
|
|
5
|
+
NotificationItemData
|
|
6
|
+
} from "./serverDataType/Notification.js";
|
|
7
|
+
|
|
8
|
+
type User = import("./User.js").User; // backref ; User porte __entityTag donc non proxifié par reactive()
|
|
9
|
+
|
|
10
|
+
/** Taille de page côté serveur (ActivityStream::getNotificationsByStep indexStep=15). */
|
|
11
|
+
const DEFAULT_PAGE_SIZE = 15;
|
|
12
|
+
|
|
13
|
+
/** isUnread/isUnseen de CE destinataire (le serveur unset la clé quand lu/vu -> absence = faux). */
|
|
14
|
+
function stateFor(data: NotificationItemData, userId: string | null, key: "isUnread" | "isUnseen"): boolean {
|
|
15
|
+
if (!userId) return false;
|
|
16
|
+
return data?.notify?.id?.[userId]?.[key] === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Item de notification fin. N'étend PAS BaseEntity : il garde la donnée brute, un backref
|
|
21
|
+
* User (pour router les appels via callIsMe + endpointApi) et un backref manager (pour le recount).
|
|
22
|
+
* Les drapeaux lu/vu sont réactifs (objet reactive({...}) — surface scalaire fiable).
|
|
23
|
+
*/
|
|
24
|
+
export class Notification {
|
|
25
|
+
private readonly _data: NotificationItemData;
|
|
26
|
+
private readonly _owner: User;
|
|
27
|
+
private _manager: Notifications | null;
|
|
28
|
+
private readonly _state: { isUnread: boolean; isUnseen: boolean };
|
|
29
|
+
|
|
30
|
+
constructor(data: NotificationItemData, owner: User, manager: Notifications | null = null) {
|
|
31
|
+
this._data = data;
|
|
32
|
+
this._owner = owner;
|
|
33
|
+
this._manager = manager;
|
|
34
|
+
const uid = owner.userId;
|
|
35
|
+
this._state = reactive({
|
|
36
|
+
isUnread: stateFor(data, uid, "isUnread"),
|
|
37
|
+
isUnseen: stateFor(data, uid, "isUnseen")
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static fromData(data: NotificationItemData, owner: User, manager: Notifications | null = null): Notification {
|
|
42
|
+
return new Notification(data, owner, manager);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Lie l'item à son manager (appelé par le manager au wrap). */
|
|
46
|
+
_attach(manager: Notifications): this { this._manager = manager; return this; }
|
|
47
|
+
|
|
48
|
+
get id(): string { return this._data.id; } // injecté par ApiClient._transformData (clé de la map = hex)
|
|
49
|
+
get isUnread(): boolean { return this._state.isUnread; } // lecture réactive
|
|
50
|
+
get isUnseen(): boolean { return this._state.isUnseen; }
|
|
51
|
+
get data(): NotificationItemData { return this._data; }
|
|
52
|
+
get author(): NotificationAuthor { return this._data.author; }
|
|
53
|
+
get createdAt(): Date {
|
|
54
|
+
// ApiClient._transformData rend `created` en Date. La string ISO survient après un
|
|
55
|
+
// round-trip toJSON()->JSON.parse->restore(). On fait confiance à la normalisation amont.
|
|
56
|
+
const c = this._data.created;
|
|
57
|
+
return c instanceof Date ? c : new Date(c as string | number);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// setters locaux (utilisés par l'optimiste, sans appel réseau)
|
|
61
|
+
_setReadLocal(): void { this._state.isUnread = false; }
|
|
62
|
+
_setSeenLocal(): void { this._state.isUnseen = false; }
|
|
63
|
+
_restoreLocal(unread: boolean, unseen: boolean): void { this._state.isUnread = unread; this._state.isUnseen = unseen; }
|
|
64
|
+
|
|
65
|
+
/** Marque CET item comme lu : optimiste + rollback + recount manager. */
|
|
66
|
+
async markRead(): Promise<void> {
|
|
67
|
+
if (!this._state.isUnread) return;
|
|
68
|
+
const pu = this._state.isUnread, ps = this._state.isUnseen;
|
|
69
|
+
this._setReadLocal();
|
|
70
|
+
this._manager?._recount();
|
|
71
|
+
try {
|
|
72
|
+
await this._owner.markNotificationAsRead(this.id);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this._restoreLocal(pu, ps);
|
|
75
|
+
this._manager?._recount();
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Pas d'endpoint "seen" par item : flip local uniquement. */
|
|
81
|
+
markSeen(): void { this._setSeenLocal(); this._manager?._recount(); }
|
|
82
|
+
|
|
83
|
+
toJSON(): NotificationItemData { return this._data; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Gestionnaire de notifications de l'utilisateur connecté. Accroché à User via `me.notifications`.
|
|
88
|
+
* N'étend PAS BaseEntity. Items dans un tableau SIMPLE (jamais proxifié) ; seuls les scalaires
|
|
89
|
+
* vivent dans un reactive({...}) (loadedUnreadCount, unseenTotal, cursor, hasMore, version).
|
|
90
|
+
* Aucun effect/computed créé ici (pas d'API de dispose -> pas de fuite).
|
|
91
|
+
*/
|
|
92
|
+
export class Notifications {
|
|
93
|
+
private readonly _owner: User;
|
|
94
|
+
private readonly _pageSize: number;
|
|
95
|
+
private _items: Notification[] = [];
|
|
96
|
+
private readonly _state: {
|
|
97
|
+
loadedUnreadCount: number; // non-lus PARMI les items chargés (dérivé)
|
|
98
|
+
unseenTotal: number; // total NON-VUS côté serveur (countNotif) — badge cloche
|
|
99
|
+
cursor: number; // offset indexMin (nb d'items déjà chargés)
|
|
100
|
+
hasMore: boolean;
|
|
101
|
+
version: number; // bump à chaque mutation de liste -> réactivité de `items`
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
constructor(owner: User, pageSize: number = DEFAULT_PAGE_SIZE) {
|
|
105
|
+
this._owner = owner;
|
|
106
|
+
this._pageSize = pageSize;
|
|
107
|
+
this._state = reactive({ loadedUnreadCount: 0, unseenTotal: 0, cursor: 0, hasMore: true, version: 0 });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// surface réactive consommateur
|
|
111
|
+
get items(): Notification[] { void this._state.version; return this._items; }
|
|
112
|
+
get loadedUnreadCount(): number { return this._state.loadedUnreadCount; }
|
|
113
|
+
get unseenTotal(): number { return this._state.unseenTotal; } // badge "non vus"
|
|
114
|
+
get hasMore(): boolean { return this._state.hasMore; }
|
|
115
|
+
|
|
116
|
+
_recount(): void { this._state.loadedUnreadCount = this._items.filter((n) => n.isUnread).length; }
|
|
117
|
+
private _wrap(raw: NotificationItemData[]): Notification[] { return raw.map((d) => Notification.fromData(d, this._owner, this)); }
|
|
118
|
+
private _commit(items: Notification[]): void { this._items = items; this._state.version++; this._recount(); }
|
|
119
|
+
|
|
120
|
+
/** Première page (reset le curseur). Tri serveur : updated desc, 15/page. */
|
|
121
|
+
async list({ indexMin = 0 }: { indexMin?: number } = {}): Promise<Notification[]> {
|
|
122
|
+
const next = this._wrap(await this._owner.fetchNotifications({ indexMin }));
|
|
123
|
+
this._commit(next);
|
|
124
|
+
this._state.cursor = next.length;
|
|
125
|
+
this._state.hasMore = next.length >= this._pageSize;
|
|
126
|
+
return next;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Page suivante (curseur indexMin = nb d'items déjà chargés). Concatène par réassignation. */
|
|
130
|
+
async loadMore(): Promise<Notification[]> {
|
|
131
|
+
if (!this._state.hasMore) return [];
|
|
132
|
+
const page = this._wrap(await this._owner.fetchNotifications({ indexMin: this._state.cursor }));
|
|
133
|
+
this._commit([...this._items, ...page]);
|
|
134
|
+
this._state.cursor += page.length;
|
|
135
|
+
this._state.hasMore = page.length >= this._pageSize;
|
|
136
|
+
return page;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Recharge depuis le début. */
|
|
140
|
+
async refresh(): Promise<Notification[]> { this._state.cursor = 0; this._state.hasMore = true; return this.list(); }
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Badge : total des NON-VUS via GET_NOTIFICATIONS_COUNT (refreshTimestamp = maintenant ->
|
|
144
|
+
* liste vide + countNotif). Met à jour unseenTotal et le retourne.
|
|
145
|
+
*/
|
|
146
|
+
async count(): Promise<number> {
|
|
147
|
+
this._state.unseenTotal = await this._owner.fetchNotificationsCount();
|
|
148
|
+
return this._state.unseenTotal;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* PUR : convertit des `NotificationItemData[]` plats (ex. cache React Query après
|
|
153
|
+
* hydratation SSR) en `Notification[]` liées à l'utilisateur, SANS modifier l'état du
|
|
154
|
+
* manager. À utiliser dans un `select` React Query (cache plat -> instances vivantes).
|
|
155
|
+
*/
|
|
156
|
+
toItems(data: NotificationItemData[]): Notification[] {
|
|
157
|
+
return this._wrap(data);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async markAllRead(): Promise<void> { await this._bulk("read"); }
|
|
161
|
+
async markAllSeen(): Promise<void> { await this._bulk("seen"); }
|
|
162
|
+
|
|
163
|
+
private async _bulk(action: "seen" | "read"): Promise<void> {
|
|
164
|
+
const snap = this._items.map((n) => ({ n, u: n.isUnread, s: n.isUnseen }));
|
|
165
|
+
batch(() => { for (const { n } of snap) { if (action === "read") n._setReadLocal(); else n._setSeenLocal(); } });
|
|
166
|
+
if (action === "seen") this._state.unseenTotal = 0;
|
|
167
|
+
this._recount();
|
|
168
|
+
try {
|
|
169
|
+
await this._owner.markAllNotifications(action);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
batch(() => { for (const { n, u, s } of snap) n._restoreLocal(u, s); });
|
|
172
|
+
this._recount();
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Supprime toutes les notifications de l'utilisateur + vide l'état local. */
|
|
178
|
+
async clear(): Promise<void> {
|
|
179
|
+
await this._owner.removeAllNotifications();
|
|
180
|
+
this._commit([]);
|
|
181
|
+
this._state.cursor = 0;
|
|
182
|
+
this._state.hasMore = false;
|
|
183
|
+
this._state.unseenTotal = 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// sérialisation (donnée brute ; le backref owner + l'état réactif sont re-construits au restore)
|
|
187
|
+
toJSON(): NotificationItemData[] { return this._items.map((n) => n.toJSON()); }
|
|
188
|
+
restore(rawItems: NotificationItemData[]): this {
|
|
189
|
+
this._commit(this._wrap(rawItems));
|
|
190
|
+
this._state.cursor = rawItems.length;
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
static restore(rawItems: NotificationItemData[], owner: User): Notifications {
|
|
194
|
+
return new Notifications(owner).restore(rawItems);
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/api/User.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiError } from "../error.js";
|
|
2
2
|
import { BaseEntity } from "./BaseEntity.js";
|
|
3
|
+
import { Notifications } from "./Notifications.js";
|
|
3
4
|
import { UserMixin } from "../mixin/UserMixin.js";
|
|
4
5
|
import { createSocialTransform } from "../types/transforms.js";
|
|
5
6
|
|
|
@@ -17,9 +18,14 @@ import type {
|
|
|
17
18
|
GetOrganizationsNoAdminData,
|
|
18
19
|
GetOrganizationsAdminData,
|
|
19
20
|
GetFriendsAdminData,
|
|
20
|
-
DemoteAdminData
|
|
21
|
+
DemoteAdminData,
|
|
22
|
+
GetNotificationsData,
|
|
23
|
+
GetNotificationsCountData,
|
|
24
|
+
NotificationUpdateData,
|
|
25
|
+
MarkNotificationAsReadData
|
|
21
26
|
} from "./EndpointApi.types.js";
|
|
22
27
|
import type { Organization } from "./Organization.js";
|
|
28
|
+
import type { NotificationItemData } from "./serverDataType/Notification.js";
|
|
23
29
|
import type { EntityTypes } from "@/types/entities.js";
|
|
24
30
|
|
|
25
31
|
type ApiClient = import("../ApiClient.js").default;
|
|
@@ -1717,6 +1723,71 @@ export class User extends BaseEntity<UserItemNormalized> {
|
|
|
1717
1723
|
return retour;
|
|
1718
1724
|
}
|
|
1719
1725
|
|
|
1726
|
+
// ===========================================================================
|
|
1727
|
+
// Notifications (sous-ressource de l'utilisateur connecté)
|
|
1728
|
+
// ===========================================================================
|
|
1729
|
+
|
|
1730
|
+
/** Gestionnaire de notifications, créé paresseusement et mis en cache. */
|
|
1731
|
+
private _notifications?: Notifications;
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Point d'entrée `me.notifications` (composition, PAS une entité BaseEntity).
|
|
1735
|
+
* Sûr : endpointApi/apiClient sont initialisés dans le constructeur avant tout accès getter.
|
|
1736
|
+
*/
|
|
1737
|
+
get notifications(): Notifications {
|
|
1738
|
+
return (this._notifications ??= new Notifications(this));
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Récupère les notifications (mode liste). STATELESS + plat (`NotificationItemData[]`),
|
|
1743
|
+
* idéal comme `queryFn` React Query / prefetch SSR — ne touche PAS le manager réactif
|
|
1744
|
+
* (`me.notifications`). `ApiClient._transformData` garantit `notif` = tableau (map -> array
|
|
1745
|
+
* + `id` injecté), donc on fait confiance à la normalisation amont.
|
|
1746
|
+
* pathParams.id est OBLIGATOIRE : le défaut "@userId" ne matche pas le pattern de l'endpoint.
|
|
1747
|
+
*/
|
|
1748
|
+
async fetchNotifications({ indexMin }: { indexMin?: number } = {}): Promise<NotificationItemData[]> {
|
|
1749
|
+
const res = (await this.callIsMe(() => {
|
|
1750
|
+
const data: GetNotificationsData = {
|
|
1751
|
+
pathParams: { type: "citoyens", id: this.userId! },
|
|
1752
|
+
...(indexMin != null ? { indexMin } : {})
|
|
1753
|
+
};
|
|
1754
|
+
return this.endpointApi.getNotifications(data);
|
|
1755
|
+
})) as { notif?: NotificationItemData[] };
|
|
1756
|
+
return Array.isArray(res?.notif) ? res.notif : [];
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Compte les notifications NON VUES (badge). STATELESS. refreshTimestamp = maintenant ->
|
|
1761
|
+
* le serveur renvoie `countNotif` (total non-vus) + une liste vide.
|
|
1762
|
+
*/
|
|
1763
|
+
async fetchNotificationsCount(): Promise<number> {
|
|
1764
|
+
const res = (await this.callIsMe(() => {
|
|
1765
|
+
const data: GetNotificationsCountData = {
|
|
1766
|
+
pathParams: { type: "citoyens", id: this.userId! },
|
|
1767
|
+
refreshTimestamp: Math.floor(Date.now() / 1000)
|
|
1768
|
+
};
|
|
1769
|
+
return this.endpointApi.getNotificationsCount(data);
|
|
1770
|
+
})) as { countNotif?: number };
|
|
1771
|
+
return typeof res?.countNotif === "number" ? res.countNotif : 0;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/** Marque une notification comme lue (MARK_NOTIFICATION_AS_READ). */
|
|
1775
|
+
async markNotificationAsRead(id: string): Promise<unknown> {
|
|
1776
|
+
const data: MarkNotificationAsReadData = { id };
|
|
1777
|
+
return this.callIsMe(() => this.endpointApi.markNotificationAsRead(data));
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/** Marque toutes les notifications comme vues/lues (NOTIFICATION_UPDATE). */
|
|
1781
|
+
async markAllNotifications(action: "seen" | "read"): Promise<unknown> {
|
|
1782
|
+
const data: NotificationUpdateData = { action, all: true };
|
|
1783
|
+
return this.callIsMe(() => this.endpointApi.notificationUpdate(data));
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/** Supprime toutes les notifications de l'utilisateur (REMOVE_ALL_NOTIFICATIONS). */
|
|
1787
|
+
async removeAllNotifications(): Promise<unknown> {
|
|
1788
|
+
return this.callIsMe(() => this.endpointApi.removeAllNotifications());
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1720
1791
|
}
|
|
1721
1792
|
|
|
1722
1793
|
// Incorporation des mixins dans User
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DateValue, IdObject } from "./common.js";
|
|
2
|
+
import type EJSONType from "../../EJSONType.js";
|
|
3
|
+
|
|
4
|
+
// L'_id peut arriver sous 3 formes selon le chemin d'appel :
|
|
5
|
+
// - instance ObjectID (transform + _fromJSONValue actif — cas observé sur User.getNotifications)
|
|
6
|
+
// - { $id } (forme normalisée non-revivée)
|
|
7
|
+
// - string hex (24)
|
|
8
|
+
type ObjectIDInstance = InstanceType<typeof EJSONType["ObjectID"]>;
|
|
9
|
+
export type NotificationId = ObjectIDInstance | IdObject | string;
|
|
10
|
+
|
|
11
|
+
/** Auteur APRÈS aplatissement par _transformData (la map oid->objet devient un objet). */
|
|
12
|
+
export interface NotificationAuthor {
|
|
13
|
+
id?: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
name: string;
|
|
16
|
+
profilThumbImageUrl?: string; // complété en URL absolue par la normalisation image
|
|
17
|
+
[k: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NotificationTarget {
|
|
21
|
+
type: string;
|
|
22
|
+
id: string;
|
|
23
|
+
parent?: { id: string; type: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** État lu/vu, par destinataire. Un champ ABSENT (clé unset côté serveur) = état "faux". */
|
|
27
|
+
export interface NotificationReadState {
|
|
28
|
+
isUnread?: boolean;
|
|
29
|
+
isUnseen?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NotificationNotify {
|
|
33
|
+
objectType?: string;
|
|
34
|
+
/** keyé par oid de destinataire -> état lu/vu de CE destinataire. */
|
|
35
|
+
id: Record<string, NotificationReadState>;
|
|
36
|
+
displayName?: string;
|
|
37
|
+
icon?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
labelArray?: Record<string, string[]>;
|
|
41
|
+
labelAuthorObject?: string;
|
|
42
|
+
repeat?: boolean;
|
|
43
|
+
[k: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Un élément du tableau `notif` (forme transformée). created/updated : Date instance OU {sec,usec}. */
|
|
47
|
+
export interface NotificationItemData {
|
|
48
|
+
id: string; // injecté depuis la clé de la map notif (= oid hex)
|
|
49
|
+
_id: NotificationId;
|
|
50
|
+
type: string;
|
|
51
|
+
verb: string;
|
|
52
|
+
author: NotificationAuthor;
|
|
53
|
+
created: Date | DateValue;
|
|
54
|
+
updated: Date | DateValue;
|
|
55
|
+
target: NotificationTarget;
|
|
56
|
+
notify: NotificationNotify;
|
|
57
|
+
timestamp?: number; // ajouté serveur (= updated.sec)
|
|
58
|
+
timeAgo?: string; // présent en brut, retiré au transform
|
|
59
|
+
[k: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Réponse de GET_NOTIFICATIONS (mode liste, body indexMin) APRÈS transform :
|
|
64
|
+
* `notif` est un tableau (la map oid->item est convertie). Vide => [].
|
|
65
|
+
*/
|
|
66
|
+
export interface GetNotificationsResult {
|
|
67
|
+
notif: NotificationItemData[];
|
|
68
|
+
[k: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Réponse de GET_NOTIFICATIONS_COUNT (mode badge, body refreshTimestamp non-nul) :
|
|
73
|
+
* `notif` = notifications mises à jour APRÈS le timestamp (souvent [] si timestamp = maintenant),
|
|
74
|
+
* `countNotif` = nombre TOTAL de notifications NON VUES (isUnseen) — indépendant du timestamp.
|
|
75
|
+
* countNotif n'est présent QUE si refreshTimestamp est fourni et non-nul (sinon mode liste).
|
|
76
|
+
*/
|
|
77
|
+
export interface GetNotificationsCountResult {
|
|
78
|
+
notif: NotificationItemData[];
|
|
79
|
+
countNotif?: number;
|
|
80
|
+
[k: string]: unknown;
|
|
81
|
+
}
|