fix: DM close visibility flag, wire Remove Friend button, harden deploy script
- Redesign DM close as a visibility flag (closed column) instead of row deletion, so message broadcasts still reach users who closed a DM conversation - Add broadcastDmMessage() helper that auto-resurfaces closed DMs when new messages arrive - Wire Remove Friend button in DM welcome header with onClick handler and friend check - Fix deploy.sh to cd to its own directory so rsync always runs from project root
This commit is contained in:
@@ -27,6 +27,65 @@ function sanitizeUser(row: typeof schema.users.$inferSelect): User {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a DM message to all members of a DM channel.
|
||||
* For members who have closed the channel (closed=1), also sends a
|
||||
* dm_channel_created event to resurface the channel in their sidebar,
|
||||
* and flips their closed flag back to 0.
|
||||
*/
|
||||
export function broadcastDmMessage(dmChannelId: string, message: DmMessageWithUser): void {
|
||||
const db = getDb();
|
||||
const dmMembers = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.dmChannelId, dmChannelId))
|
||||
.all();
|
||||
|
||||
for (const member of dmMembers) {
|
||||
// If this member had closed the DM, resurface it first
|
||||
if (member.closed === 1) {
|
||||
db.update(schema.dmMembers)
|
||||
.set({ closed: 0 })
|
||||
.where(and(
|
||||
eq(schema.dmMembers.dmChannelId, dmChannelId),
|
||||
eq(schema.dmMembers.userId, member.userId),
|
||||
))
|
||||
.run();
|
||||
|
||||
// Build and send dm_channel_created so their sidebar picks it up
|
||||
const allMemberRows = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.dmChannelId, dmChannelId))
|
||||
.all();
|
||||
const memberUserIds = allMemberRows.map(m => m.userId);
|
||||
const users = memberUserIds.length > 0
|
||||
? db.select().from(schema.users).where(inArray(schema.users.id, memberUserIds)).all()
|
||||
: [];
|
||||
|
||||
const dmChannel = db.select()
|
||||
.from(schema.dmChannels)
|
||||
.where(eq(schema.dmChannels.id, dmChannelId))
|
||||
.get();
|
||||
|
||||
if (dmChannel) {
|
||||
connectionManager.sendToUser(member.userId, {
|
||||
type: 'dm_channel_created',
|
||||
dmChannel: {
|
||||
id: dmChannel.id,
|
||||
createdAt: dmChannel.createdAt,
|
||||
members: users.map(sanitizeUser),
|
||||
lastMessage: message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connectionManager.sendToUser(member.userId, {
|
||||
type: 'dm_message_created',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
// GET /api/dm - List user's DM channels
|
||||
app.get('/api/dm', {
|
||||
@@ -36,7 +95,10 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
const memberships = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.userId, request.userId))
|
||||
.where(and(
|
||||
eq(schema.dmMembers.userId, request.userId),
|
||||
eq(schema.dmMembers.closed, 0),
|
||||
))
|
||||
.all();
|
||||
|
||||
const dmChannels: DmChannel[] = [];
|
||||
@@ -115,7 +177,8 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(404).send({ error: 'User not found', statusCode: 404 });
|
||||
}
|
||||
|
||||
// Check if DM channel already exists between these two users (both are members)
|
||||
// Check if DM channel already exists between these two users
|
||||
// (both have membership rows, regardless of closed state)
|
||||
const myDms = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.userId, request.userId))
|
||||
@@ -131,7 +194,7 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
.get();
|
||||
|
||||
if (otherMember) {
|
||||
// DM channel already exists, both users are members
|
||||
// DM channel already exists between these users
|
||||
const dmChannel = db.select()
|
||||
.from(schema.dmChannels)
|
||||
.where(eq(schema.dmChannels.id, myDm.dmChannelId))
|
||||
@@ -139,6 +202,17 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
if (!dmChannel) continue;
|
||||
|
||||
// Reopen if the requesting user had closed it
|
||||
if (myDm.closed === 1) {
|
||||
db.update(schema.dmMembers)
|
||||
.set({ closed: 0 })
|
||||
.where(and(
|
||||
eq(schema.dmMembers.dmChannelId, myDm.dmChannelId),
|
||||
eq(schema.dmMembers.userId, request.userId),
|
||||
))
|
||||
.run();
|
||||
}
|
||||
|
||||
const dmMemberRows = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.dmChannelId, myDm.dmChannelId))
|
||||
@@ -173,79 +247,6 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the other user has a DM channel with us that we left (closed)
|
||||
// If so, re-add ourselves to it instead of creating a new one
|
||||
const theirDms = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.userId, userId))
|
||||
.all();
|
||||
|
||||
for (const theirDm of theirDms) {
|
||||
// Check if any messages exist between us on this channel (indicates a previous DM)
|
||||
const dmChannel = db.select()
|
||||
.from(schema.dmChannels)
|
||||
.where(eq(schema.dmChannels.id, theirDm.dmChannelId))
|
||||
.get();
|
||||
|
||||
if (!dmChannel) continue;
|
||||
|
||||
// Check total members — a DM channel should only have the other user if we left
|
||||
const allMembers = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.dmChannelId, theirDm.dmChannelId))
|
||||
.all();
|
||||
|
||||
// If it's a 1-member channel (just them) or we see past messages from us, re-join
|
||||
const onlyOtherUser = allMembers.length === 1 && allMembers[0]!.userId === userId;
|
||||
if (onlyOtherUser) {
|
||||
// Check if there are any messages from us in this channel (confirms it was our DM)
|
||||
const ourOldMessages = db.select()
|
||||
.from(schema.dmMessages)
|
||||
.where(and(
|
||||
eq(schema.dmMessages.dmChannelId, theirDm.dmChannelId),
|
||||
eq(schema.dmMessages.userId, request.userId),
|
||||
))
|
||||
.limit(1)
|
||||
.all();
|
||||
|
||||
if (ourOldMessages.length > 0) {
|
||||
// Re-add ourselves to this existing DM channel
|
||||
db.insert(schema.dmMembers).values({
|
||||
dmChannelId: theirDm.dmChannelId,
|
||||
userId: request.userId,
|
||||
}).run();
|
||||
|
||||
const currentUserRow = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
||||
const members = [currentUserRow, targetUser]
|
||||
.filter((u): u is NonNullable<typeof u> => u !== undefined)
|
||||
.map(sanitizeUser);
|
||||
|
||||
const lastMsgRows = db.select()
|
||||
.from(schema.dmMessages)
|
||||
.where(eq(schema.dmMessages.dmChannelId, theirDm.dmChannelId))
|
||||
.orderBy(desc(schema.dmMessages.createdAt))
|
||||
.limit(1)
|
||||
.all();
|
||||
const lastMsg = lastMsgRows[0] ?? null;
|
||||
|
||||
const result: DmChannel = {
|
||||
id: dmChannel.id,
|
||||
createdAt: dmChannel.createdAt,
|
||||
members,
|
||||
lastMessage: lastMsg ? {
|
||||
id: lastMsg.id,
|
||||
dmChannelId: lastMsg.dmChannelId,
|
||||
userId: lastMsg.userId,
|
||||
content: lastMsg.content,
|
||||
createdAt: lastMsg.createdAt,
|
||||
} : null,
|
||||
};
|
||||
|
||||
return reply.code(200).send(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new DM channel
|
||||
const dmChannelId = generateSnowflake();
|
||||
const now = Date.now();
|
||||
@@ -306,8 +307,9 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(403).send({ error: 'You are not a member of this DM channel', statusCode: 403 });
|
||||
}
|
||||
|
||||
// Soft close: remove the user's membership row (Discord-style hide)
|
||||
db.delete(schema.dmMembers)
|
||||
// Soft close: set closed flag (preserves membership for future message delivery)
|
||||
db.update(schema.dmMembers)
|
||||
.set({ closed: 1 })
|
||||
.where(and(
|
||||
eq(schema.dmMembers.dmChannelId, id),
|
||||
eq(schema.dmMembers.userId, request.userId),
|
||||
@@ -428,18 +430,8 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
user: sanitizeUser(user),
|
||||
};
|
||||
|
||||
// Broadcast via WebSocket to all DM members
|
||||
const dmMembers = db.select()
|
||||
.from(schema.dmMembers)
|
||||
.where(eq(schema.dmMembers.dmChannelId, id))
|
||||
.all();
|
||||
|
||||
for (const member of dmMembers) {
|
||||
connectionManager.sendToUser(member.userId, {
|
||||
type: 'dm_message_created',
|
||||
message,
|
||||
});
|
||||
}
|
||||
// Broadcast to all DM members (including those who closed the channel)
|
||||
broadcastDmMessage(id, message);
|
||||
|
||||
return reply.code(201).send(message);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user