fix: Bug - Users appear as both online and offline
Align chat message sender ids with per-server presence identities so profile cards opened from message authors resolve the same live user state as the members panel. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import type { Message } from '../../../../shared-kernel';
|
||||
import { resolveIncomingChatMessageSenderId, resolveRoomMessageSenderId } from './message-sender-identity.rules';
|
||||
|
||||
function createMessage(overrides: Partial<Message> = {}): Message {
|
||||
return {
|
||||
id: 'message-1',
|
||||
roomId: 'room-1',
|
||||
senderId: 'home-user-1',
|
||||
senderName: 'Alice',
|
||||
content: 'hello',
|
||||
timestamp: 1,
|
||||
reactions: [],
|
||||
isDeleted: false,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('message-sender-identity.rules', () => {
|
||||
it('resolveRoomMessageSenderId uses the per-server actor id', () => {
|
||||
const senderId = resolveRoomMessageSenderId(
|
||||
{ id: 'home-user-1', oderId: 'home-oder-1' },
|
||||
'https://signal.example.com',
|
||||
(_serverUrl, fallback) => fallback === 'home-oder-1' ? 'server-user-1' : fallback
|
||||
);
|
||||
|
||||
expect(senderId).toBe('server-user-1');
|
||||
});
|
||||
|
||||
it('resolveIncomingChatMessageSenderId prefers relay sender identity over message senderId', () => {
|
||||
const senderId = resolveIncomingChatMessageSenderId(
|
||||
createMessage({ senderId: 'home-user-1' }),
|
||||
{
|
||||
senderId: 'server-user-1',
|
||||
fromPeerId: 'peer-1'
|
||||
}
|
||||
);
|
||||
|
||||
expect(senderId).toBe('server-user-1');
|
||||
});
|
||||
|
||||
it('resolveIncomingChatMessageSenderId falls back to fromPeerId for P2P chat', () => {
|
||||
const senderId = resolveIncomingChatMessageSenderId(
|
||||
createMessage({ senderId: 'home-user-1' }),
|
||||
{ fromPeerId: 'server-user-1' }
|
||||
);
|
||||
|
||||
expect(senderId).toBe('server-user-1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Message } from '../../../../shared-kernel';
|
||||
|
||||
/** Resolve the sender id that should be stored for a room chat message. */
|
||||
export function resolveRoomMessageSenderId(
|
||||
currentUser: Pick<{ id: string; oderId: string }, 'id' | 'oderId'>,
|
||||
roomSourceUrl: string | undefined,
|
||||
resolveActorUserId: (serverUrl: string | undefined, fallbackUserId: string) => string
|
||||
): string {
|
||||
const homeUserKey = currentUser.oderId || currentUser.id;
|
||||
|
||||
return resolveActorUserId(roomSourceUrl, homeUserKey);
|
||||
}
|
||||
|
||||
interface IncomingChatMessageEnvelope {
|
||||
senderId?: string;
|
||||
fromPeerId?: string;
|
||||
fromUserId?: string;
|
||||
}
|
||||
|
||||
/** Normalize incoming chat sender ids to the per-server identity used by presence. */
|
||||
export function resolveIncomingChatMessageSenderId(
|
||||
message: Pick<Message, 'senderId'>,
|
||||
envelope: IncomingChatMessageEnvelope
|
||||
): string {
|
||||
const relayIdentity = [envelope.senderId, envelope.fromUserId]
|
||||
.find((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
||||
|
||||
if (relayIdentity) {
|
||||
return relayIdentity.trim();
|
||||
}
|
||||
|
||||
if (envelope.fromPeerId?.trim()) {
|
||||
return envelope.fromPeerId.trim();
|
||||
}
|
||||
|
||||
return message.senderId;
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import { hasDedicatedChatEmbed } from '../../../../domain/rules/link-embed.rules
|
||||
import { shouldShowMessageEditedLabel } from '../../../../domain/rules/message.rules';
|
||||
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../../../core/i18n';
|
||||
import { Message, User } from '../../../../../../shared-kernel';
|
||||
import { resolveUserByIdentity } from '../../../../../../store/users/user-identity-lookup.rules';
|
||||
import { ThemeNodeDirective } from '../../../../../theme';
|
||||
import { PluginRenderHostComponent } from '../../../../../plugins/feature/plugin-render-host/plugin-render-host.component';
|
||||
import { PluginRequirementStateService, PluginUiRegistryService } from '../../../../../plugins';
|
||||
@@ -221,7 +222,7 @@ export class ChatMessageItemComponent implements OnDestroy {
|
||||
readonly showEmojiPicker = signal(false);
|
||||
readonly senderUser = computed<User>(() => {
|
||||
const msg = this.message();
|
||||
const found = this.userLookup().get(msg.senderId);
|
||||
const found = resolveUserByIdentity(this.userLookup(), msg.senderId);
|
||||
|
||||
return (
|
||||
found ?? {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
ChatMessageReplyEvent
|
||||
} from '../../models/chat-messages.model';
|
||||
import { selectAllUsers } from '../../../../../../store/users/users.selectors';
|
||||
import { buildUserIdentityLookup } from '../../../../../../store/users/user-identity-lookup.rules';
|
||||
import { APP_TRANSLATE_IMPORTS } from '../../../../../../core/i18n';
|
||||
import { ThemeNodeDirective } from '../../../../../theme';
|
||||
import { ChatMessageItemComponent } from '../message-item/chat-message-item.component';
|
||||
@@ -146,21 +147,11 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
|
||||
});
|
||||
|
||||
readonly userLookup = computed<ReadonlyMap<string, User>>(() => {
|
||||
const lookup = new Map<string, User>();
|
||||
|
||||
for (const user of this.allUsers()) {
|
||||
lookup.set(user.id, user);
|
||||
|
||||
if (user.oderId && user.oderId !== user.id) {
|
||||
lookup.set(user.oderId, user);
|
||||
}
|
||||
}
|
||||
const lookup = new Map(buildUserIdentityLookup(this.allUsers()));
|
||||
|
||||
for (const user of this.userLookupOverrides()) {
|
||||
lookup.set(user.id, user);
|
||||
|
||||
if (user.oderId && user.oderId !== user.id) {
|
||||
lookup.set(user.oderId, user);
|
||||
for (const [key, value] of buildUserIdentityLookup([user])) {
|
||||
lookup.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user