fix(notifications): notifications sent from the services name their actor by name

The routes on dev now name the actor of a notification by username instead
of the address. The chat, note, invite and packing notifications the
services send for both surfaces read the name too, so they no longer go out
with an empty actor.
This commit is contained in:
Maurice
2026-10-22 21:51:44 +00:00
parent f5197159f7
commit ab02794ff2
14 changed files with 41 additions and 41 deletions
+3 -3
View File
@@ -254,8 +254,8 @@ export class CollabService {
}
/** The writer as a notification names them: by id, and by the address the route read off its session. */
private async actorOf(writer: TripWriter): Promise<Pick<User, 'id' | 'email'>> {
return { id: writer.userId, email: await this.notifications.actorEmail(writer.userId) };
private async actorOf(writer: TripWriter): Promise<Pick<User, 'id' | 'username'>> {
return { id: writer.userId, username: await this.notifications.actorName(writer.userId) };
}
/* ------------------------------------------------------------------ */
@@ -907,7 +907,7 @@ export class CollabService {
}
/** Fire-and-forget collab notification (mirrors the legacy route's dynamic import). */
async notifyCollab(tripId: string, actor: Pick<User, 'id' | 'email'>, preview?: string): Promise<void> {
async notifyCollab(tripId: string, actor: Pick<User, 'id' | 'username'>, preview?: string): Promise<void> {
// Injected, not a lazy import of the old notifications bridge. The laziness bought
// nothing the module graph does not already give — NotificationsModule
// reaches nothing in this direction — and it hid the edge while handing the
@@ -662,9 +662,9 @@ export class NotificationsService {
return true;
}
/** The address a notification names its actor by, as the routes read it off their session user. */
async actorEmail(userId: number): Promise<string> {
return (await this.mailer.getUserEmail(userId)) ?? '';
/** The name a notification gives its actor, as the routes read it off their session user. */
async actorName(userId: number): Promise<string> {
return (await this.notificationsRepo.findUserBasic(userId))?.username ?? '';
}
async deleteOne(notificationId: number, userId: number): Promise<boolean> {
@@ -2,7 +2,7 @@ import type { User } from '../../types';
import { isUpdateConflict } from '../common/conflictResult';
import { DomainError } from '../common/domain-error';
import { requireTripWrite, type TripWriter } from '../common/trip-writer';
import { MailerService } from '../notifications/mailer/mailer.service';
import { NotificationsService } from '../notifications/notifications.service';
import { PermissionsService } from '../permissions/permissions.service';
import { PackingService, isInvalidBagRef, type PackingVisibility } from './packing.service';
import { Injectable } from '@nestjs/common';
@@ -56,7 +56,7 @@ export class PackingWritesService {
constructor(
private readonly packing: PackingService,
private readonly permissions: PermissionsService,
private readonly mailer: MailerService,
private readonly notifications: NotificationsService,
) {}
/**
@@ -209,10 +209,10 @@ export class PackingWritesService {
const assignees = await this.packing.updateCategoryAssignees(tripId, category, userIds);
writer.events.emit('packing:assignees', { category, assignees });
if (userIds.length > 0) {
// The notification names its actor by email. A writer carries only the id,
// so the address is read here; a guest never signs in, so never writes.
const email = await this.mailer.getUserEmail(writer.userId);
if (email) await this.packing.notifyTagged(String(tripId), { id: writer.userId, email }, category, userIds);
// The notification names its actor by name. A writer carries only the id,
// so the name is read here; an account that is gone notifies nobody.
const username = await this.notifications.actorName(writer.userId);
if (username) await this.packing.notifyTagged(String(tripId), { id: writer.userId, username }, category, userIds);
}
return assignees;
}
+1 -1
View File
@@ -891,7 +891,7 @@ export class PackingService {
}
/** Fire-and-forget tag notification, mirroring the legacy dynamic import. */
async notifyTagged(tripId: string, actor: Pick<User, 'id' | 'email'>, category: string, userIds: unknown): Promise<void> {
async notifyTagged(tripId: string, actor: Pick<User, 'id' | 'username'>, category: string, userIds: unknown): Promise<void> {
if (!Array.isArray(userIds) || userIds.length === 0) return;
// Injected, not a lazy import of the old notifications bridge. The laziness bought
// nothing the module graph does not already give — NotificationsModule
@@ -38,8 +38,8 @@ export class TripMemberWritesService {
async addMember(tripId: string | number, identifier: string, tripOwnerId: number, writer: TripWriter) {
const result = await this.roster.addMember(tripId, identifier, tripOwnerId, writer.userId);
writer.events.emit('member:added', { member: result.member });
// The invitation names who sent it by address, as the route always did with its session user.
const actor = { id: writer.userId, email: (await this.users.getEmail(writer.userId)) ?? '' };
// The invitation names who sent it by name, as the route does with its session user.
const actor = { id: writer.userId, username: (await this.users.findById(writer.userId))?.username ?? '' };
this.roster.notifyInvite(String(tripId), actor, result.targetUserId, result.tripTitle, result.member.email);
return result;
}
@@ -120,7 +120,7 @@ export class TripMembersService {
/** Fire-and-forget trip-invite notification (mirrors the route's dynamic import). */
notifyInvite(
tripId: string,
actor: Pick<User, 'id' | 'email'>,
actor: Pick<User, 'id' | 'username'>,
targetUserId: number,
tripTitle: string,
inviteeEmail: string,