fix: DM reactions + eradicate broken CSS variable opacity modifiers

Server-side: handleReactionAdd/Remove now check dm_messages table when
message isn't found in server messages, enabling DM emoji reactions
via the same unified client event path.

Client-side: replaced all remaining broken opacity modifiers on CSS
variables (border-border-soft/50, bg-accent-rose/10, bg-surface-*/N,
etc.) with explicit rgba values. Tailwind can't decompose var() for
opacity, causing fallback to currentColor. Fixes 16 files across
error boxes, table borders, surface tints, and input separators.
This commit is contained in:
Jannis Braun
2026-03-02 17:36:33 +01:00
parent 2c98704198
commit 0e235fdf1c
17 changed files with 90 additions and 46 deletions
+71 -27
View File
@@ -713,32 +713,52 @@ function handleReactionAdd(event: Record<string, unknown>, userId: string): void
if (!messageId || !emoji) return;
const db = getDb();
// Try server message first
const message = db.select().from(schema.messages).where(eq(schema.messages.id, messageId)).get();
if (!message) return;
if (message) {
const serverId = getChannelServerId(message.channelId);
if (!serverId || !isMember(serverId, userId)) return;
const serverId = getChannelServerId(message.channelId);
if (!serverId || !isMember(serverId, userId)) return;
const reactionId = generateSnowflake();
try {
db.insert(schema.reactions).values({
id: reactionId,
messageId,
userId,
emoji,
createdAt: Date.now(),
}).run();
connectionManager.sendToChannel(serverId, message.channelId, {
type: 'reaction_added',
messageId,
reaction: {
const reactionId = generateSnowflake();
try {
db.insert(schema.reactions).values({
id: reactionId,
messageId,
userId,
emoji,
createdAt: Date.now(),
},
}).run();
connectionManager.sendToChannel(serverId, message.channelId, {
type: 'reaction_added',
messageId,
reaction: { id: reactionId, messageId, userId, emoji, createdAt: Date.now() },
});
} catch (err) {
// Unique constraint violation (already reacted)
}
return;
}
// Fall through to DM message
const dmMsg = db.select().from(schema.dmMessages).where(eq(schema.dmMessages.id, messageId)).get();
if (!dmMsg || !isDmMember(dmMsg.dmChannelId, userId)) return;
const reactionId = generateSnowflake();
try {
db.insert(schema.dmReactions).values({
id: reactionId,
dmMessageId: messageId,
userId,
emoji,
createdAt: Date.now(),
}).run();
connectionManager.sendToDmMembers(dmMsg.dmChannelId, {
type: 'reaction_added',
messageId,
reaction: { id: reactionId, messageId, userId, emoji, createdAt: Date.now() },
});
} catch (err) {
// Unique constraint violation (already reacted)
@@ -752,22 +772,46 @@ function handleReactionRemove(event: Record<string, unknown>, userId: string): v
if (!messageId || !emoji) return;
const db = getDb();
// Try server message first
const message = db.select().from(schema.messages).where(eq(schema.messages.id, messageId)).get();
if (!message) return;
if (message) {
const serverId = getChannelServerId(message.channelId);
if (!serverId || !isMember(serverId, userId)) return;
const serverId = getChannelServerId(message.channelId);
if (!serverId || !isMember(serverId, userId)) return;
const result = db.delete(schema.reactions)
.where(and(
eq(schema.reactions.messageId, messageId),
eq(schema.reactions.userId, userId),
eq(schema.reactions.emoji, emoji)
))
.run();
const result = db.delete(schema.reactions)
if (result.changes > 0) {
connectionManager.sendToChannel(serverId, message.channelId, {
type: 'reaction_removed',
messageId,
userId,
emoji,
});
}
return;
}
// Fall through to DM message
const dmMsg = db.select().from(schema.dmMessages).where(eq(schema.dmMessages.id, messageId)).get();
if (!dmMsg || !isDmMember(dmMsg.dmChannelId, userId)) return;
const result = db.delete(schema.dmReactions)
.where(and(
eq(schema.reactions.messageId, messageId),
eq(schema.reactions.userId, userId),
eq(schema.reactions.emoji, emoji)
eq(schema.dmReactions.dmMessageId, messageId),
eq(schema.dmReactions.userId, userId),
eq(schema.dmReactions.emoji, emoji)
))
.run();
if (result.changes > 0) {
connectionManager.sendToChannel(serverId, message.channelId, {
connectionManager.sendToDmMembers(dmMsg.dmChannelId, {
type: 'reaction_removed',
messageId,
userId,
@@ -42,7 +42,7 @@ export function LoginPage() {
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-4 p-3 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
<div className="mb-4 p-3 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -54,7 +54,7 @@ export function RegisterPage() {
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-4 p-3 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
<div className="mb-4 p-3 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -161,7 +161,7 @@ export function FriendsPage() {
</button>
</form>
{addStatus && (
<div className={`text-sm p-3 rounded-lg border ${addStatus.type === 'success' ? 'text-txt-positive border-status-online/20 bg-status-online/5' : 'text-txt-danger border-accent-rose/20 bg-accent-rose/5'}`}>
<div className={`text-sm p-3 rounded-lg border ${addStatus.type === 'success' ? 'text-txt-positive border-[rgba(134,239,172,0.20)] bg-[rgba(134,239,172,0.05)]' : 'text-txt-danger border-[rgba(253,164,175,0.20)] bg-[rgba(253,164,175,0.05)]'}`}>
{addStatus.message}
</div>
)}
@@ -203,7 +203,7 @@ function buildComponents(): Components {
),
thead: ({ children }) => <thead className="border-b border-border-soft">{children}</thead>,
tbody: ({ children }) => <tbody>{children}</tbody>,
tr: ({ children }) => <tr className="border-b border-border-soft/50">{children}</tr>,
tr: ({ children }) => <tr className="border-b border-white/[0.06]">{children}</tr>,
th: ({ children }) => <th className="px-3 py-1.5 text-left text-txt-primary font-semibold">{children}</th>,
td: ({ children }) => <td className="px-3 py-1.5">{children}</td>,
};
+1 -1
View File
@@ -286,7 +286,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
key={att.id}
href={`/api/uploads/${att.filename}`}
download={att.originalName}
className="flex items-center gap-3 p-4 bg-surface-channel/50 rounded-lg border border-border-hard hover:bg-interactive-hover transition-all max-w-[400px] mt-1 group/att"
className="flex items-center gap-3 p-4 bg-[rgba(26,26,35,0.50)] rounded-lg border border-border-hard hover:bg-interactive-hover transition-all max-w-[400px] mt-1 group/att"
>
<div className="p-2 bg-surface-base rounded text-txt-tertiary group-hover/att:text-txt-primary transition-colors">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -230,7 +230,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
return (
<div data-pip-obstacle="bottom" className="px-3 pb-3 flex-shrink-0 md:absolute md:bottom-3 md:left-3 md:right-3 md:z-[110] md:px-0 md:pb-0 md:glass-bubble md:rounded-[14px]">
{replyTo && (
<div className="bg-interactive-hover rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-border-hard/50">
<div className="bg-interactive-hover rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-white/[0.06]">
<div className="flex items-center gap-1 text-[14px] text-txt-message truncate">
<span className="opacity-60">Replying to</span>
<span className="font-bold">{replyTo.user.displayName ?? replyTo.user.username}</span>
@@ -261,7 +261,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
{/* File previews */}
{files.length > 0 && (
<div className="p-4 flex flex-wrap gap-4 bg-surface-channel/30">
<div className="p-4 flex flex-wrap gap-4 bg-[rgba(26,26,35,0.30)]">
{files.map((file, i) => (
<div key={i} className="relative group bg-surface-channel rounded-lg p-2 max-w-[200px] shadow-elevation-low border border-border-hard">
{file.type.startsWith('image/') ? (
@@ -32,7 +32,7 @@ export function TypingIndicator({ channelId }: TypingIndicatorProps) {
return (
<div className="h-[24px] px-4 flex items-center text-[12px] text-txt-primary font-medium select-none pointer-events-none">
<div className="flex items-center gap-2">
<div className="flex gap-[2px] bg-surface-elevated/20 rounded-full px-2 py-1">
<div className="flex gap-[2px] bg-[rgba(37,37,48,0.20)] rounded-full px-2 py-1">
<div className="w-[5px] h-[5px] bg-txt-message rounded-full animate-bounce" style={{ animationDelay: '0ms', animationDuration: '0.8s' }} />
<div className="w-[5px] h-[5px] bg-txt-message rounded-full animate-bounce" style={{ animationDelay: '150ms', animationDuration: '0.8s' }} />
<div className="w-[5px] h-[5px] bg-txt-message rounded-full animate-bounce" style={{ animationDelay: '300ms', animationDuration: '0.8s' }} />
@@ -121,7 +121,7 @@ export function ChannelSidebar() {
<>
<div className="w-60 md:w-full bg-surface-channel flex flex-col flex-shrink-0 select-none md:pl-[72px] border-r border-border-hard">
<div className="h-12 px-[10px] flex items-center border-b border-border-hard z-10">
<button className="flex-1 bg-surface-base text-txt-tertiary text-[13px] font-medium py-[5px] px-2 rounded-[4px] text-left hover:bg-surface-base/80 transition-colors">
<button className="flex-1 bg-surface-base text-txt-tertiary text-[13px] font-medium py-[5px] px-2 rounded-[4px] text-left hover:bg-surface-base transition-colors">
Find or start a conversation
</button>
</div>
@@ -104,7 +104,7 @@ export function ChannelSettingsModal() {
</div>
{error && (
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
<div className="p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -139,7 +139,7 @@ export function ChannelSettingsModal() {
</div>
{isPrivate && !isFetching && (
<div className="flex items-start gap-2 p-2 bg-surface-input/50 rounded text-xs text-txt-tertiary">
<div className="flex items-start gap-2 p-2 bg-[rgba(17,17,24,0.50)] rounded text-xs text-txt-tertiary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className="flex-shrink-0 mt-0.5 text-txt-secondary">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" />
</svg>
@@ -48,7 +48,7 @@ export function CreateChannelModal() {
<Modal isOpen={isOpen} onClose={closeModal} title="Create Channel">
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -41,7 +41,7 @@ export function CreateServerModal() {
<Modal isOpen={isOpen} onClose={closeModal} title="Create a Server">
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -55,7 +55,7 @@ export function InviteModal() {
Share this invite link with friends to let them join your server.
</p>
{error && (
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -52,7 +52,7 @@ export function JoinServerModal() {
Enter an invite code to join an existing server.
</p>
{error && (
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -189,7 +189,7 @@ function StreamingLimitsPanel() {
{/* Save / Reset */}
{saveError && (
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
<div className="p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">{saveError}</div>
)}
{saveSuccess && (
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
@@ -369,7 +369,7 @@ export function ServerSettingsModal() {
{/* Content */}
<div className="flex-1 min-w-0">
{error && (
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div>
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">{error}</div>
)}
{tab === 'overview' && (
@@ -76,7 +76,7 @@ export function UserSettingsModal() {
</div>
{error && (
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div>
<div className="p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">{error}</div>
)}
{success && (
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">{success}</div>
@@ -134,7 +134,7 @@ export function VoiceGrid({ participants }: VoiceGridProps) {
{/* Bottom strip of other tiles */}
{!stripHidden && otherTiles.length > 0 && (
<div className="h-[120px] flex-shrink-0 flex items-center justify-center gap-2 p-2 bg-surface-base/50 overflow-x-auto no-scrollbar">
<div className="h-[120px] flex-shrink-0 flex items-center justify-center gap-2 p-2 bg-[rgba(11,11,16,0.50)] overflow-x-auto no-scrollbar">
{otherTiles.map((t) => (
<div
key={t.key}