fix(federation): auto-create and hydrate user profiles during DM relay
Federated 1-on-1 DMs showed the raw snowflake ID as the display name and no avatar when the remote user had no pre-existing local record. processCreateEvent used resolveLocalUser (find-only) instead of resolveOrCreateReplicatedUser, and relay events carried no profile data for participants. - Add profile snapshot (displayName, avatar, avatarColor) to FederationRelayParticipant and populate it in getDmParticipants - Change processCreateEvent to auto-create replicated user stubs and hydrate them with profile data from the relay event - Fix hydrateReplicatedUserProfile URL resolution for homeInstance values without protocol prefix - Fix WelcomeHeader: return null while DM data is loading (eliminates "unknown" flash on reload), use displayName for @mention text
This commit is contained in:
@@ -1119,17 +1119,21 @@ function processCreateEvent(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve ALL participants to local users
|
// Resolve ALL participants to local users, auto-creating replicated stubs
|
||||||
|
// for remote users that don't have a local record yet. This ensures 1-on-1
|
||||||
|
// federated DMs work even when the remote user hasn't connected or friended.
|
||||||
const resolvedParticipants: Array<{
|
const resolvedParticipants: Array<{
|
||||||
localUser: typeof schema.users.$inferSelect;
|
localUser: typeof schema.users.$inferSelect;
|
||||||
homeUserId: string;
|
homeUserId: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const p of event.participants) {
|
for (const p of event.participants) {
|
||||||
const localUser = resolveLocalUser(p.homeUserId, db);
|
let localUser = resolveOrCreateReplicatedUser(p.homeUserId, p.homeInstance, db);
|
||||||
if (localUser) {
|
// Hydrate with profile data from the relay event (displayName, avatar, etc.)
|
||||||
resolvedParticipants.push({ localUser, homeUserId: p.homeUserId });
|
if (p.profile) {
|
||||||
|
localUser = hydrateReplicatedUserProfile(localUser, p.profile, db);
|
||||||
}
|
}
|
||||||
|
resolvedParticipants.push({ localUser, homeUserId: p.homeUserId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedParticipants.length < 2) {
|
if (resolvedParticipants.length < 2) {
|
||||||
@@ -1875,10 +1879,11 @@ function hydrateReplicatedUserProfile(
|
|||||||
// Resolve bare filenames to absolute URLs pointing to the home instance.
|
// Resolve bare filenames to absolute URLs pointing to the home instance.
|
||||||
// The home WS doesn't run normalizeUserAssets on replicated users' avatars,
|
// The home WS doesn't run normalizeUserAssets on replicated users' avatars,
|
||||||
// so they must be stored as absolute URLs to render correctly.
|
// so they must be stored as absolute URLs to render correctly.
|
||||||
|
const baseUrl = user.homeInstance!.startsWith('http') ? user.homeInstance! : `https://${user.homeInstance}`;
|
||||||
const resolveUrl = (filename: string | null | undefined): string | null => {
|
const resolveUrl = (filename: string | null | undefined): string | null => {
|
||||||
if (!filename) return null;
|
if (!filename) return null;
|
||||||
if (filename.startsWith('http')) return filename;
|
if (filename.startsWith('http')) return filename;
|
||||||
return `${user.homeInstance}/api/uploads/${filename}`;
|
return `${baseUrl}/api/uploads/${filename}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updates: Record<string, string | null> = {};
|
const updates: Record<string, string | null> = {};
|
||||||
|
|||||||
@@ -245,6 +245,9 @@ export function getDmParticipants(dmChannelId: string): FederationRelayParticipa
|
|||||||
homeUserId: schema.users.homeUserId,
|
homeUserId: schema.users.homeUserId,
|
||||||
homeInstance: schema.users.homeInstance,
|
homeInstance: schema.users.homeInstance,
|
||||||
id: schema.users.id,
|
id: schema.users.id,
|
||||||
|
displayName: schema.users.displayName,
|
||||||
|
avatar: schema.users.avatar,
|
||||||
|
avatarColor: schema.users.avatarColor,
|
||||||
})
|
})
|
||||||
.from(schema.dmMembers)
|
.from(schema.dmMembers)
|
||||||
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
|
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
|
||||||
@@ -256,6 +259,11 @@ export function getDmParticipants(dmChannelId: string): FederationRelayParticipa
|
|||||||
return members.map(m => ({
|
return members.map(m => ({
|
||||||
homeUserId: m.homeUserId || m.id,
|
homeUserId: m.homeUserId || m.id,
|
||||||
homeInstance: m.homeInstance || domainOrigin,
|
homeInstance: m.homeInstance || domainOrigin,
|
||||||
|
profile: {
|
||||||
|
displayName: m.displayName ?? null,
|
||||||
|
avatar: m.avatar ?? null,
|
||||||
|
avatarColor: m.avatarColor ?? null,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -734,6 +734,7 @@ export interface AdminResetPasswordResponse {
|
|||||||
export interface FederationRelayParticipant {
|
export interface FederationRelayParticipant {
|
||||||
homeUserId: string;
|
homeUserId: string;
|
||||||
homeInstance: string;
|
homeInstance: string;
|
||||||
|
profile?: FederationRelayProfileSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FederationRelayEvent {
|
export interface FederationRelayEvent {
|
||||||
|
|||||||
@@ -373,10 +373,11 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
|
|||||||
|
|
||||||
if (isDm) {
|
if (isDm) {
|
||||||
const dm = dmChannels.find(d => d.id === channelId);
|
const dm = dmChannels.find(d => d.id === channelId);
|
||||||
const otherUser = dm?.members.find(m => !isSelf(m, authUser));
|
if (!dm) return null; // DM data not yet loaded (WebSocket ready pending)
|
||||||
const { baseName } = parseFederatedUsername(otherUser?.username ?? 'unknown');
|
const otherUser = dm.members.find(m => !isSelf(m, authUser));
|
||||||
const displayName = otherUser?.displayName ?? baseName;
|
const { baseName } = parseFederatedUsername(otherUser?.username ?? '');
|
||||||
const username = baseName;
|
const displayName = otherUser?.displayName ?? (baseName || 'Direct Message');
|
||||||
|
const mentionName = otherUser?.displayName ?? baseName;
|
||||||
const isFriend = otherUser ? friends.some(f => f.id === otherUser.id) : false;
|
const isFriend = otherUser ? friends.some(f => f.id === otherUser.id) : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -386,7 +387,7 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-[32px] leading-10 font-bold text-txt-primary">{displayName}</h3>
|
<h3 className="text-[32px] leading-10 font-bold text-txt-primary">{displayName}</h3>
|
||||||
<p className="text-txt-secondary text-[14px] mt-1">
|
<p className="text-txt-secondary text-[14px] mt-1">
|
||||||
This is the beginning of your direct message history with <strong>@{username}</strong>.
|
This is the beginning of your direct message history with <strong>@{mentionName}</strong>.
|
||||||
</p>
|
</p>
|
||||||
{otherUser?.homeInstance && (
|
{otherUser?.homeInstance && (
|
||||||
<p className="text-xs text-txt-tertiary mt-1">
|
<p className="text-xs text-txt-tertiary mt-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user