Skip to content

Commit d9bcc9b

Browse files
Francisca105luckspt
authored andcommitted
refactor: extended notifications api backend and improved frontend
1 parent ce87496 commit d9bcc9b

File tree

3 files changed

+128
-84
lines changed

3 files changed

+128
-84
lines changed

backend/src/mongodb/notification.go

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type NotificationsType struct {
2020

2121
var tagRegexCompiler, _ = regexp.Compile(`@[a-zA-Z0-9\.]+`)
2222

23-
//Notify creates a notification and adds it to every subscriber
23+
// Notify creates a notification and adds it to every subscriber
2424
func (n *NotificationsType) Notify(author primitive.ObjectID, data CreateNotificationData) {
2525

2626
event, err := Events.GetCurrentEvent()
@@ -137,7 +137,7 @@ func (n *NotificationsType) Notify(author primitive.ObjectID, data CreateNotific
137137
}
138138
}
139139

140-
//CreateNotificationData holds data needed to create a notification
140+
// CreateNotificationData holds data needed to create a notification
141141
type CreateNotificationData struct {
142142
Kind models.NotificationKind
143143
Post *primitive.ObjectID
@@ -148,7 +148,7 @@ type CreateNotificationData struct {
148148
Session *primitive.ObjectID
149149
}
150150

151-
//NotifyMember adds a notification to a member
151+
// NotifyMember adds a notification to a member
152152
func (n *NotificationsType) NotifyMember(memberID primitive.ObjectID, data CreateNotificationData) {
153153
ctx = context.Background()
154154

@@ -214,11 +214,11 @@ func (n *NotificationsType) GetNotification(id primitive.ObjectID) (*models.Noti
214214
return &notification, nil
215215
}
216216

217-
//GetMemberNotifications gets all notifications for a member
218-
func (n *NotificationsType) GetMemberNotifications(memberID primitive.ObjectID) ([]*models.Notification, error) {
217+
// GetMemberNotifications gets all notifications for a member
218+
func (n *NotificationsType) GetMemberNotifications(memberID primitive.ObjectID) ([]map[string]interface{}, error) {
219219
ctx = context.Background()
220220

221-
var notifications = make([]*models.Notification, 0)
221+
var notifications = make([]map[string]interface{}, 0)
222222

223223
filter := bson.M{
224224
"member": memberID,
@@ -231,15 +231,57 @@ func (n *NotificationsType) GetMemberNotifications(memberID primitive.ObjectID)
231231

232232
for cur.Next(ctx) {
233233

234-
// create a value into which the single document can be decoded
234+
// decode into models.Notification first
235235
var notification models.Notification
236236

237237
err := cur.Decode(&notification)
238238
if err != nil {
239239
return nil, err
240240
}
241241

242-
notifications = append(notifications, &notification)
242+
// build a JSON-friendly map for the response
243+
notifMap := map[string]interface{}{
244+
"id": notification.ID.Hex(),
245+
"kind": notification.Kind,
246+
"signature": notification.Signature,
247+
"member": notification.Member.Hex(),
248+
"date": notification.Date.Format(time.RFC3339),
249+
}
250+
251+
if notification.Post != nil {
252+
notifMap["post"] = notification.Post.Hex()
253+
}
254+
if notification.Thread != nil {
255+
notifMap["thread"] = notification.Thread.Hex()
256+
}
257+
if notification.Meeting != nil {
258+
notifMap["meeting"] = notification.Meeting.Hex()
259+
}
260+
if notification.Session != nil {
261+
notifMap["session"] = notification.Session.Hex()
262+
}
263+
264+
// Try to embed the speaker object if present
265+
if notification.Speaker != nil {
266+
if sp, err := Speakers.GetSpeaker(*notification.Speaker); err == nil {
267+
// embed full speaker object
268+
notifMap["speaker"] = sp
269+
} else {
270+
// fallback to id
271+
notifMap["speaker"] = notification.Speaker.Hex()
272+
}
273+
}
274+
275+
// Try to embed the company object if present
276+
if notification.Company != nil {
277+
if co, err := Companies.GetCompany(*notification.Company); err == nil {
278+
notifMap["company"] = co
279+
} else {
280+
notifMap["company"] = notification.Company.Hex()
281+
}
282+
}
283+
284+
notifications = append(notifications, notifMap)
243285
}
244286

245287
if err := cur.Err(); err != nil {

frontend/src/components/navbar/Notification.vue

Lines changed: 75 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { computed } from "vue";
33
import { useRouter } from "vue-router";
44
import { useQuery } from "@pinia/colada";
55
import { getMyNotifications } from "@/api/notifications";
6-
import { getSpeakerById } from "@/api/speakers";
7-
import { getCompanyById } from "@/api/companies";
86
import {
97
useDeleteNotificationMutation,
108
useDeleteAllNotificationsMutation,
119
} from "@/mutations/notifications";
12-
import type { EnrichedActor, EnrichedNotification } from "@/dto/notifications";
10+
import type { Notification } from "@/dto/notifications";
11+
import type { Speaker } from "@/dto/speakers";
12+
import type { Company } from "@/dto/companies";
1313
import { Bell, Trash } from "lucide-vue-next";
1414
import {
1515
Popover,
@@ -19,8 +19,36 @@ import {
1919
import Badge from "../ui/badge/Badge.vue";
2020
import Image from "../Image.vue";
2121
22-
const makeMessage = (kind: string, actor: EnrichedActor) => {
23-
const actorName = actor.name;
22+
const getActor = (notification: Notification) => {
23+
if (notification.speaker && typeof notification.speaker === "object") {
24+
return {
25+
id: (notification.speaker as Speaker).id,
26+
name: (notification.speaker as Speaker).name,
27+
avatar:
28+
(notification.speaker as Speaker).imgs.internal ||
29+
(notification.speaker as Speaker).imgs.speaker,
30+
};
31+
}
32+
if (notification.company && typeof notification.company === "object") {
33+
return {
34+
id: (notification.company as Company).id,
35+
name: (notification.company as Company).name,
36+
avatar:
37+
(notification.company as Company).imgs?.internal ||
38+
(notification.company as Company).imgs?.public,
39+
};
40+
}
41+
return null;
42+
};
43+
44+
const makeMessage = (notification: Notification) => {
45+
const thread = notification.thread;
46+
const kind = notification.kind;
47+
// Actor is the entity (company/speaker) related to the notification
48+
const actor = getActor(notification);
49+
const actorName = actor ? actor.name : "";
50+
const isActorPresent = actorName.length > 0;
51+
2452
switch (kind) {
2553
case "UPDATED_PARTICIPATION":
2654
return "Participation updated";
@@ -37,70 +65,45 @@ const makeMessage = (kind: string, actor: EnrichedActor) => {
3765
case "UPDATED_PRIVATE_IMAGE":
3866
return "Private image updated";
3967
case "UPDATED":
68+
if (thread) {
69+
if (isActorPresent) {
70+
return "Communication updated";
71+
}
72+
return "Thread updated";
73+
}
74+
4075
return "Updated details";
4176
case "CREATED":
77+
if (thread) {
78+
if (isActorPresent) {
79+
return "New communication created";
80+
}
81+
return "Thread created";
82+
}
83+
4284
return "Created";
4385
case "DELETED":
44-
return actorName ? `${actorName} deleted` : "Deleted";
86+
if (thread) {
87+
if (isActorPresent) {
88+
return "Communication deleted";
89+
}
90+
return "Thread deleted";
91+
}
92+
return "Deleted";
4593
case "TAGGED":
4694
return "You were tagged in a post";
4795
default:
48-
return actorName ? `${kind} - ${actorName}` : kind;
96+
return kind;
4997
}
5098
};
5199
52-
const fetchEnrichedNotifications = async () => {
53-
const res = await getMyNotifications();
54-
const data = await Promise.all(
55-
(res.data || []).map(async (n) => {
56-
const enriched: EnrichedNotification = { ...n };
57-
try {
58-
const speakerId = n.speaker;
59-
const companyId = n.company;
60-
61-
if (speakerId) {
62-
const spRes = await getSpeakerById(speakerId);
63-
const sp = spRes?.data || spRes;
64-
if (sp) {
65-
enriched.actor = {
66-
type: "speaker",
67-
id: sp.id,
68-
name: sp.name,
69-
avatar: sp.imgs?.speaker || sp.imgs?.internal || undefined,
70-
};
71-
enriched.message = makeMessage(n.kind, enriched.actor);
72-
}
73-
} else if (companyId) {
74-
const coRes = await getCompanyById(companyId);
75-
const co = coRes?.data || coRes;
76-
if (co) {
77-
enriched.actor = {
78-
type: "company",
79-
id: co.id,
80-
name: co.name,
81-
avatar: co.imgs?.public || co.imgs?.internal || undefined,
82-
};
83-
enriched.message = makeMessage(n.kind, enriched.actor);
84-
}
85-
}
86-
} catch {
87-
// ignore enrichment errors
88-
}
89-
if (!enriched.message && enriched.actor)
90-
enriched.message = makeMessage(n.kind, enriched.actor);
91-
return enriched;
92-
}),
93-
);
94-
return { ...res, data };
95-
};
96-
97100
const { data: notifications } = useQuery({
98101
key: ["notifications"],
99-
query: fetchEnrichedNotifications,
102+
query: getMyNotifications,
100103
});
101104
102105
const notificationItems = computed(() => {
103-
const items = (notifications.value?.data as EnrichedNotification[]) || [];
106+
const items = (notifications.value?.data as Notification[]) || [];
104107
// sort newest first — try common timestamp fields (date, createdAt, created_at)
105108
return items.slice().sort((a, b) => b.date?.localeCompare(a.date ?? "") || 0);
106109
});
@@ -119,19 +122,26 @@ const removeAllNotifications = async () => {
119122
};
120123
121124
const router = useRouter();
122-
const navigateNotification = (n: EnrichedNotification) => {
123-
const actor = n.actor;
124-
if (actor?.type === "speaker" && actor?.id) {
125-
router.push({ name: "speaker", params: { speakerId: actor.id } });
125+
const navigateNotification = (n: Notification) => {
126+
// ensure speaker is an object (not a string) before accessing .id
127+
if (n.speaker && typeof n.speaker === "object" && (n.speaker as Speaker).id) {
128+
router.push({
129+
name: "speaker",
130+
params: { speakerId: (n.speaker as Speaker).id },
131+
});
126132
return;
127133
}
128-
if (actor?.type === "company" && actor?.id) {
129-
router.push({ name: "company", params: { companyId: actor.id } });
134+
// ensure company is an object (not a string) before accessing .id
135+
if (n.company && typeof n.company === "object" && (n.company as Company).id) {
136+
router.push({
137+
name: "company",
138+
params: { companyId: (n.company as Company).id },
139+
});
130140
return;
131141
}
132142
};
133143
134-
const onNotificationClick = async (n: EnrichedNotification) => {
144+
const onNotificationClick = async (n: Notification) => {
135145
if (!n || !n.id) return;
136146
try {
137147
await removeNotification(n.id);
@@ -186,15 +196,15 @@ const onNotificationClick = async (n: EnrichedNotification) => {
186196
>
187197
<div class="flex items-center gap-3">
188198
<Image
189-
v-if="n.actor && n.actor.avatar"
190-
:src="n.actor.avatar"
199+
v-if="getActor(n)?.avatar"
200+
:src="getActor(n)?.avatar"
191201
alt="actor"
192202
class="h-8 w-8 rounded-full object-cover"
193203
/>
194204
<div class="text-sm">
195-
<div class="font-medium">{{ n.message || n.kind }}</div>
205+
<div class="font-medium">{{ makeMessage(n) }}</div>
196206
<div class="text-xs text-gray-500">
197-
{{ n.actor?.name || n.date }}
207+
{{ getActor(n)?.name || n.date }}
198208
</div>
199209
</div>
200210
</div>

frontend/src/dto/notifications.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,10 @@ export interface Notification {
2929
date?: string; // ISO date string
3030
// optional references to entities
3131
event?: number;
32-
company?: string;
33-
speaker?: string;
32+
// company/speaker can be either the id (string) or an embedded object
33+
company?: string | Company;
34+
speaker?: string | Speaker;
3435
meeting?: string;
3536
thread?: string;
3637
post?: string;
3738
}
38-
39-
export type EnrichedActor = Pick<Company | Speaker, "id" | "name"> & {
40-
type: "company" | "speaker";
41-
avatar?: string;
42-
};
43-
export interface EnrichedNotification extends Notification {
44-
actor?: EnrichedActor; // populated user who triggered the notification
45-
message?: string; // short human-readable message
46-
}

0 commit comments

Comments
 (0)