fix: federation reaction identity + remote server reload race condition
- Include user object in reaction_added WS broadcasts for isSelf() resolution - Use isSelf() instead of userId comparison for reaction ownership checks - Load remote server detail after ready event to prevent empty channel list
This commit is contained in:
@@ -709,19 +709,24 @@ function handleReactionAdd(event: Record<string, unknown>, userId: string): void
|
|||||||
if (!serverId || !isMember(serverId, userId)) return;
|
if (!serverId || !isMember(serverId, userId)) return;
|
||||||
|
|
||||||
const reactionId = generateSnowflake();
|
const reactionId = generateSnowflake();
|
||||||
|
const now = Date.now();
|
||||||
try {
|
try {
|
||||||
db.insert(schema.reactions).values({
|
db.insert(schema.reactions).values({
|
||||||
id: reactionId,
|
id: reactionId,
|
||||||
messageId,
|
messageId,
|
||||||
userId,
|
userId,
|
||||||
emoji,
|
emoji,
|
||||||
createdAt: Date.now(),
|
createdAt: now,
|
||||||
}).run();
|
}).run();
|
||||||
|
|
||||||
|
// Include user object so remote clients can use isSelf() for identity resolution
|
||||||
|
const reactionUser = db.select().from(schema.users).where(eq(schema.users.id, userId)).get();
|
||||||
|
const userObj = reactionUser ? sanitizeUser(reactionUser) : undefined;
|
||||||
|
|
||||||
connectionManager.sendToChannel(serverId, message.channelId, {
|
connectionManager.sendToChannel(serverId, message.channelId, {
|
||||||
type: 'reaction_added',
|
type: 'reaction_added',
|
||||||
messageId,
|
messageId,
|
||||||
reaction: { id: reactionId, messageId, userId, emoji, createdAt: Date.now() },
|
reaction: { id: reactionId, messageId, userId, emoji, createdAt: now, user: userObj },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Unique constraint violation (already reacted)
|
// Unique constraint violation (already reacted)
|
||||||
@@ -734,19 +739,24 @@ function handleReactionAdd(event: Record<string, unknown>, userId: string): void
|
|||||||
if (!dmMsg || !isDmMember(dmMsg.dmChannelId, userId)) return;
|
if (!dmMsg || !isDmMember(dmMsg.dmChannelId, userId)) return;
|
||||||
|
|
||||||
const reactionId = generateSnowflake();
|
const reactionId = generateSnowflake();
|
||||||
|
const now = Date.now();
|
||||||
try {
|
try {
|
||||||
db.insert(schema.dmReactions).values({
|
db.insert(schema.dmReactions).values({
|
||||||
id: reactionId,
|
id: reactionId,
|
||||||
dmMessageId: messageId,
|
dmMessageId: messageId,
|
||||||
userId,
|
userId,
|
||||||
emoji,
|
emoji,
|
||||||
createdAt: Date.now(),
|
createdAt: now,
|
||||||
}).run();
|
}).run();
|
||||||
|
|
||||||
|
// Include user object so remote clients can use isSelf() for identity resolution
|
||||||
|
const reactionUser = db.select().from(schema.users).where(eq(schema.users.id, userId)).get();
|
||||||
|
const userObj = reactionUser ? sanitizeUser(reactionUser) : undefined;
|
||||||
|
|
||||||
connectionManager.sendToDmMembers(dmMsg.dmChannelId, {
|
connectionManager.sendToDmMembers(dmMsg.dmChannelId, {
|
||||||
type: 'reaction_added',
|
type: 'reaction_added',
|
||||||
messageId,
|
messageId,
|
||||||
reaction: { id: reactionId, messageId, userId, emoji, createdAt: Date.now() },
|
reaction: { id: reactionId, messageId, userId, emoji, createdAt: now, user: userObj },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Unique constraint violation (already reacted)
|
// Unique constraint violation (already reacted)
|
||||||
|
|||||||
@@ -59,8 +59,11 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
|
|||||||
const removeReaction = useChatStore((s) => s.removeReaction);
|
const removeReaction = useChatStore((s) => s.removeReaction);
|
||||||
const setReplyTo = useChatStore((s) => s.setReplyTo);
|
const setReplyTo = useChatStore((s) => s.setReplyTo);
|
||||||
|
|
||||||
|
const isOwnReaction = (r: { userId: string; user?: { id: string; username: string; homeInstance?: string | null } | null }) =>
|
||||||
|
r.user ? isSelf(r.user, currentUser) : r.userId === currentUser?.id;
|
||||||
|
|
||||||
const toggleReaction = (emoji: string) => {
|
const toggleReaction = (emoji: string) => {
|
||||||
const hasReacted = message.reactions?.some(r => r.userId === currentUser?.id && r.emoji === emoji);
|
const hasReacted = message.reactions?.some(r => isOwnReaction(r) && r.emoji === emoji);
|
||||||
if (hasReacted) {
|
if (hasReacted) {
|
||||||
removeReaction(message.id, emoji);
|
removeReaction(message.id, emoji);
|
||||||
} else {
|
} else {
|
||||||
@@ -71,7 +74,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
|
|||||||
const reactionGroups = (message.reactions || []).reduce((acc, r) => {
|
const reactionGroups = (message.reactions || []).reduce((acc, r) => {
|
||||||
const group = acc[r.emoji] || { count: 0, me: false };
|
const group = acc[r.emoji] || { count: 0, me: false };
|
||||||
group.count++;
|
group.count++;
|
||||||
if (r.userId === currentUser?.id) {
|
if (isOwnReaction(r)) {
|
||||||
group.me = true;
|
group.me = true;
|
||||||
}
|
}
|
||||||
acc[r.emoji] = group;
|
acc[r.emoji] = group;
|
||||||
|
|||||||
@@ -118,6 +118,19 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
loadServerDetail(currentServerId);
|
loadServerDetail(currentServerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For remote instances: if user was viewing one of these servers, load its details
|
||||||
|
// (fixes race condition on page reload — route params effect fires before remote WS connects)
|
||||||
|
if (!isHome) {
|
||||||
|
const { currentServerId: curServerId, loadServerDetail: loadDetail } = useServerStore.getState();
|
||||||
|
if (curServerId && event.servers.some((s: any) => s.id === curServerId)) {
|
||||||
|
loadDetail(curServerId);
|
||||||
|
const { currentChannelId, loadMessages } = useChatStore.getState();
|
||||||
|
if (currentChannelId) {
|
||||||
|
loadMessages(currentChannelId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only force-reload the current channel on reconnect; other channels keep their cache
|
// Only force-reload the current channel on reconnect; other channels keep their cache
|
||||||
if (isHome) {
|
if (isHome) {
|
||||||
const { loadMessages: reloadMessages, currentChannelId, setReadStates } = useChatStore.getState();
|
const { loadMessages: reloadMessages, currentChannelId, setReadStates } = useChatStore.getState();
|
||||||
|
|||||||
Reference in New Issue
Block a user