Skip to content
22 changes: 21 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,23 @@ export default async function RootLayout({
? `${navFontFamily}, var(--font-sans, Inter, system-ui, -apple-system, sans-serif)`
: "var(--font-sans, Inter, system-ui, -apple-system, sans-serif)";
const navFontColor = systemConfig.ui?.fontColor || "#0f172a";
const castButtonFontColor = systemConfig.ui?.castButtonFontColor || "#ffffff";
const castButtonFontColor =
systemConfig.ui?.castButton?.fontColor ||
systemConfig.ui?.castButtonFontColor ||
"#ffffff";
const navBackgroundColor = systemConfig.ui?.backgroundColor || "#ffffff";
const castButtonBackgroundColor =
systemConfig.ui?.castButton?.backgroundColor ||
systemConfig.ui?.primaryColor ||
"rgb(37, 99, 235)";
const castButtonHoverColor =
systemConfig.ui?.castButton?.hoverColor ||
systemConfig.ui?.primaryHoverColor ||
"rgb(29, 78, 216)";
const castButtonActiveColor =
systemConfig.ui?.castButton?.activeColor ||
systemConfig.ui?.primaryActiveColor ||
"rgb(30, 64, 175)";

return (
<html lang="en" suppressHydrationWarning>
Expand All @@ -134,6 +150,10 @@ export default async function RootLayout({
["--ns-nav-font" as string]: navFontStack,
["--ns-nav-font-color" as string]: navFontColor,
["--ns-cast-button-font-color" as string]: castButtonFontColor,
["--ns-background-color" as string]: navBackgroundColor,
["--ns-cast-button-background-color" as string]: castButtonBackgroundColor,
["--ns-cast-button-hover-color" as string]: castButtonHoverColor,
["--ns-cast-button-active-color" as string]: castButtonActiveColor,
} as React.CSSProperties
}
>
Expand Down
165 changes: 139 additions & 26 deletions src/app/notifications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useNotifications } from "@/common/lib/hooks/useNotifications";
import { useCurrentFid } from "@/common/lib/hooks/useCurrentFid";
import { FaCircleExclamation } from "react-icons/fa6";
import { FaReply } from "react-icons/fa6";
import {
import {
HeartIcon,
UserPlusIcon,
ArrowPathRoundedSquareIcon,
Expand All @@ -25,6 +25,7 @@ import {
import moment from "moment";
import useDelayedValueChange from "@/common/lib/hooks/useDelayedValueChange";
import { useRouter } from "next/navigation";
import { useUIColors } from "@/common/lib/hooks/useUIColors";

const TAB_OPTIONS = {
ALL: "all",
Expand All @@ -39,6 +40,11 @@ export type NotificationRowProps = React.FC<{
notification: Notification;
onSelect: (castHash: string, username: string) => void;
isUnseen?: boolean;
fontColor: string;
secondaryTextColor: string;
borderColor: string;
fontFamily: string;
castButtonBackgroundColor: string;
}>;

// Type guard to safely extract error message
Expand All @@ -64,13 +70,17 @@ const ErrorPanel = ({ message }: { message: string }) => {
);
};

const FormattedUsersText = ({ users }: { users: User[] }) => {
const FormattedUsersText = ({ users, fontColor }: { users: User[]; fontColor: string }) => {
if (users.length === 0) {
return "Nobody";
}

const firstUserLink = (
<PriorityLink href={`/s/${users[0].username}`} className="hover:underline text-foreground font-semibold">
<PriorityLink
href={`/s/${users[0].username}`}
className="hover:underline font-semibold"
style={{ color: fontColor }}
>
{users[0].display_name}
</PriorityLink>
);
Expand Down Expand Up @@ -125,7 +135,16 @@ const getNotificationIcon = (type: NotificationTypeEnum): React.ReactNode => {
}
};

const NotificationRow: NotificationRowProps = ({ notification, onSelect, isUnseen = false }) => {
const NotificationRow: NotificationRowProps = ({
notification,
onSelect,
isUnseen = false,
fontColor,
secondaryTextColor,
borderColor,
fontFamily,
castButtonBackgroundColor,
}) => {
const handleClick = useCallback(() => {
if (notification.cast?.hash && notification.cast?.author?.username) {
onSelect(notification.cast.hash, notification.cast.author.username);
Expand Down Expand Up @@ -160,11 +179,19 @@ const NotificationRow: NotificationRowProps = ({ notification, onSelect, isUnsee
return (
<div
className={`
px-4 py-3 border-b border-border/20 cursor-pointer transition-all duration-200
hover:bg-accent/50
${isUnseen ? "bg-blue-50/30 border-l-4 border-l-blue-500" : ""}
px-4 py-3 border-b cursor-pointer transition-all duration-200 hover:bg-[rgba(128,128,128,0.08)]
${isUnseen ? "border-l-4" : ""}
`}
onClick={handleClick}
style={{
borderTopColor: borderColor,
borderRightColor: borderColor,
borderBottomColor: borderColor,
borderLeftColor: isUnseen ? castButtonBackgroundColor : borderColor,
backgroundColor: isUnseen ? "rgba(128, 128, 128, 0.2)" : undefined,
color: fontColor,
fontFamily,
}}
>
{/* Avatars row (for all notification types) */}
{showAvatars && relatedUsers.length > 0 && (
Expand Down Expand Up @@ -213,7 +240,10 @@ const NotificationRow: NotificationRowProps = ({ notification, onSelect, isUnsee
))}
{/* Show overflow indicator */}
{relatedUsers.length > maxAvatarsToShow && (
<div className="w-8 h-8 bg-muted border-2 border-background rounded-full flex items-center justify-center text-xs font-medium text-muted-foreground">
<div
className="w-8 h-8 bg-muted border-2 border-background rounded-full flex items-center justify-center text-xs font-medium"
style={{ color: secondaryTextColor, borderColor }}
>
+{relatedUsers.length - maxAvatarsToShow}
</div>
)}
Expand All @@ -228,25 +258,30 @@ const NotificationRow: NotificationRowProps = ({ notification, onSelect, isUnsee
{/* Header with users and action */}
<div className="flex items-center gap-2 mb-1 flex-wrap">
<div className="flex items-center gap-1.5">
<FormattedUsersText users={relatedUsers} />
<FormattedUsersText users={relatedUsers} fontColor={fontColor} />
</div>
<span className="text-muted-foreground text-sm">{getNotificationActionText(notification.type)}</span>
<span className="text-muted-foreground text-xs">•</span>
<span className="text-muted-foreground text-xs">
<span className="text-sm opacity-80" style={{ color: secondaryTextColor }}>
{getNotificationActionText(notification.type)}
</span>
<span className="text-xs opacity-80" style={{ color: secondaryTextColor }}>
</span>
<span className="text-xs opacity-80" style={{ color: secondaryTextColor }}>
{moment(notification.most_recent_timestamp).fromNow()}
</span>
</div>

{/* Cast content if present */}
{notification.cast && (
<div className="mt-2 text-sm text-muted-foreground">
<div className="mt-2 text-sm opacity-90" style={{ color: secondaryTextColor }}>
<CastBody
cast={notification.cast}
castTextStyle={{
fontSize: "14px",
lineHeight: "1.4",
color: "rgb(100 116 139)",
color: fontColor,
fontWeight: "400",
fontFamily,
}}
/>
</div>
Expand Down Expand Up @@ -294,6 +329,17 @@ function NotificationsPageContent() {
const { mutate: updateLastSeenCursor } = useMutateNotificationsLastSeenCursor(fid, identityPublicKey);

const router = useRouter();
const uiColors = useUIColors();
const castButtonColors = useMemo(
() => ({
backgroundColor: uiColors.castButton.backgroundColor,
fontColor: uiColors.castButtonFontColor,
}),
[uiColors.castButton.backgroundColor, uiColors.castButtonFontColor]
);
const tabBarBackground = "rgba(128, 128, 128, 0.2)";
const borderColor = "rgba(128, 128, 128, 0.2)";
const secondaryTextColor = uiColors.fontColor;
Comment on lines +332 to +342
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded colors break theming consistency; secondary text color lacks visual hierarchy.

Two issues here:

  1. Lines 337-338: tabBarBackground and borderColor are hardcoded as rgba(128, 128, 128, 0.2), which bypasses the theming system. These should be derived from uiColors (e.g., semi-transparent variants of backgroundColor or provided by the hook).

  2. Line 339: Setting secondaryTextColor = uiColors.fontColor defeats the purpose of having a secondary text color. Secondary text typically requires reduced opacity or a lighter shade to establish visual hierarchy.

🔎 Suggested approach

Consider deriving these values from the theming system:

  const uiColors = useUIColors();
  const castButtonColors = useMemo(
    () => ({
      backgroundColor: uiColors.castButton.backgroundColor,
      fontColor: uiColors.castButtonFontColor,
    }),
    [uiColors.castButton.backgroundColor, uiColors.castButtonFontColor]
  );
- const tabBarBackground = "rgba(128, 128, 128, 0.2)";
- const borderColor = "rgba(128, 128, 128, 0.2)";
- const secondaryTextColor = uiColors.fontColor;
+ // Derive from theme or add to useUIColors hook
+ const tabBarBackground = `${uiColors.backgroundColor}cc`; // or from hook
+ const borderColor = `${uiColors.fontColor}33`; // 20% opacity
+ const secondaryTextColor = `${uiColors.fontColor}cc`; // 80% opacity

Alternatively, extend the useUIColors hook to return these derived values for consistency across components.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const uiColors = useUIColors();
const castButtonColors = useMemo(
() => ({
backgroundColor: uiColors.castButton.backgroundColor,
fontColor: uiColors.castButtonFontColor,
}),
[uiColors.castButton.backgroundColor, uiColors.castButtonFontColor]
);
const tabBarBackground = "rgba(128, 128, 128, 0.2)";
const borderColor = "rgba(128, 128, 128, 0.2)";
const secondaryTextColor = uiColors.fontColor;
const uiColors = useUIColors();
const castButtonColors = useMemo(
() => ({
backgroundColor: uiColors.castButton.backgroundColor,
fontColor: uiColors.castButtonFontColor,
}),
[uiColors.castButton.backgroundColor, uiColors.castButtonFontColor]
);
// Derive from theme or add to useUIColors hook
const tabBarBackground = `${uiColors.backgroundColor}cc`; // or from hook
const borderColor = `${uiColors.fontColor}33`; // 20% opacity
const secondaryTextColor = `${uiColors.fontColor}cc`; // 80% opacity
🤖 Prompt for AI Agents
In src/app/notifications/page.tsx around lines 329 to 339, tabBarBackground and
borderColor are hardcoded and secondaryTextColor incorrectly mirrors the primary
font color; update these to come from the theming system by either deriving
semi-transparent variants from uiColors (e.g., use uiColors.backgroundColor or
uiColors.surface with rgba/alpha for tabBarBackground and borderColor) or by
extending useUIColors to expose tabBarBackground, borderColor, and
secondaryFontColor; set secondaryTextColor to a proper secondary/color variant
(e.g., uiColors.secondaryFontColor or uiColors.fontColor with reduced opacity)
so visual hierarchy and theming consistency are preserved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while it's true that the tabBarBackground and borderColor are hardcoded, they are semi-transparent, meaning they will always match the background. in the future we may add some vars to ui_config for these greys, but for now hardcoding is sufficient

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@willyogo, that makes sense—semi-transparent greys will indeed blend with the background regardless of the theme. I appreciate the context that ui_config vars for these values may be added later. The current approach is reasonable for now. 👍


const onTabChange = useCallback((value: string) => {
setTab(value);
Expand Down Expand Up @@ -385,47 +431,105 @@ function NotificationsPageContent() {
);

return (
<div className="w-full min-h-screen bg-background">
<div
className="w-full min-h-screen"
style={{
backgroundColor: uiColors.backgroundColor,
color: uiColors.fontColor,
fontFamily: uiColors.fontFamily,
}}
>
<Tabs value={tab} onValueChange={onTabChange} className="min-h-full">
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="px-4 py-4 border-b border-border/40 bg-background sticky top-0 z-10">
<h1 className="text-xl font-semibold text-foreground mb-4">Notifications</h1>
<div
className="px-4 py-4 border-b sticky top-0 z-10"
style={{ borderColor, backgroundColor: uiColors.backgroundColor }}
>
<h1
className="text-xl font-semibold mb-4"
style={{ color: uiColors.fontColor, fontFamily: uiColors.fontFamily }}
>
Notifications
</h1>
<div className="overflow-x-auto pb-2 -mx-4 px-4 md:overflow-visible md:pb-0 md:mx-0 md:px-0">
<TabsList className="grid min-w-[600px] md:min-w-fit w-full grid-cols-6 max-w-2xl bg-muted">
<TabsList
className="grid min-w-[600px] md:min-w-fit w-full grid-cols-6 max-w-2xl"
style={{
backgroundColor: tabBarBackground,
fontFamily: uiColors.fontFamily,
borderColor,
}}
>
<TabsTrigger
value={TAB_OPTIONS.ALL}
className="data-[state=active]:bg-background data-[state=active]:text-foreground"
className="data-[state=active]:bg-[var(--tab-active-bg)] data-[state=active]:text-[var(--tab-active-color)]"
style={{
color: tab === TAB_OPTIONS.ALL ? castButtonColors.fontColor : uiColors.fontColor,
fontFamily: uiColors.fontFamily,
["--tab-active-bg" as string]: castButtonColors.backgroundColor,
["--tab-active-color" as string]: castButtonColors.fontColor,
}}
>
All
</TabsTrigger>
<TabsTrigger
value={TAB_OPTIONS.MENTIONS}
className="data-[state=active]:bg-background data-[state=active]:text-foreground"
className="data-[state=active]:bg-[var(--tab-active-bg)] data-[state=active]:text-[var(--tab-active-color)]"
style={{
color: tab === TAB_OPTIONS.MENTIONS ? castButtonColors.fontColor : uiColors.fontColor,
fontFamily: uiColors.fontFamily,
["--tab-active-bg" as string]: castButtonColors.backgroundColor,
["--tab-active-color" as string]: castButtonColors.fontColor,
}}
>
Mentions
</TabsTrigger>
<TabsTrigger
value={TAB_OPTIONS.FOLLOWS}
className="data-[state=active]:bg-background data-[state=active]:text-foreground"
className="data-[state=active]:bg-[var(--tab-active-bg)] data-[state=active]:text-[var(--tab-active-color)]"
style={{
color: tab === TAB_OPTIONS.FOLLOWS ? castButtonColors.fontColor : uiColors.fontColor,
fontFamily: uiColors.fontFamily,
["--tab-active-bg" as string]: castButtonColors.backgroundColor,
["--tab-active-color" as string]: castButtonColors.fontColor,
}}
>
Follows
</TabsTrigger>
<TabsTrigger
value={TAB_OPTIONS.RECASTS}
className="data-[state=active]:bg-background data-[state=active]:text-foreground"
className="data-[state=active]:bg-[var(--tab-active-bg)] data-[state=active]:text-[var(--tab-active-color)]"
style={{
color: tab === TAB_OPTIONS.RECASTS ? castButtonColors.fontColor : uiColors.fontColor,
fontFamily: uiColors.fontFamily,
["--tab-active-bg" as string]: castButtonColors.backgroundColor,
["--tab-active-color" as string]: castButtonColors.fontColor,
}}
>
Recasts
</TabsTrigger>
<TabsTrigger
value={TAB_OPTIONS.REPLIES}
className="data-[state=active]:bg-background data-[state=active]:text-foreground"
className="data-[state=active]:bg-[var(--tab-active-bg)] data-[state=active]:text-[var(--tab-active-color)]"
style={{
color: tab === TAB_OPTIONS.REPLIES ? castButtonColors.fontColor : uiColors.fontColor,
fontFamily: uiColors.fontFamily,
["--tab-active-bg" as string]: castButtonColors.backgroundColor,
["--tab-active-color" as string]: castButtonColors.fontColor,
}}
>
Replies
</TabsTrigger>
<TabsTrigger
value={TAB_OPTIONS.LIKES}
className="data-[state=active]:bg-background data-[state=active]:text-foreground"
className="data-[state=active]:bg-[var(--tab-active-bg)] data-[state=active]:text-[var(--tab-active-color)]"
style={{
color: tab === TAB_OPTIONS.LIKES ? castButtonColors.fontColor : uiColors.fontColor,
fontFamily: uiColors.fontFamily,
["--tab-active-bg" as string]: castButtonColors.backgroundColor,
["--tab-active-color" as string]: castButtonColors.fontColor,
}}
>
Likes
</TabsTrigger>
Expand All @@ -436,7 +540,7 @@ function NotificationsPageContent() {
<TabsContent value={tab} className="mt-0">
{/* Notifications List */}
<div className="relative">
<Suspense fallback={<div className="p-4 text-center text-muted-foreground">Loading...</div>}>
<Suspense fallback={<div className="p-4 text-center" style={{ color: secondaryTextColor }}>Loading...</div>}>
{data?.pages?.map((page, pageIndex) => (
<React.Fragment key={pageIndex}>
{filterByType(page?.notifications ?? []).map((notification, pageItemIndex) => {
Expand All @@ -451,6 +555,11 @@ function NotificationsPageContent() {
notification={notification}
onSelect={onSelectNotification}
isUnseen={isUnseen}
fontColor={uiColors.fontColor}
secondaryTextColor={secondaryTextColor}
borderColor={borderColor}
fontFamily={uiColors.fontFamily}
castButtonBackgroundColor={castButtonColors.backgroundColor}
key={notificationKey}
/>
);
Expand All @@ -473,7 +582,11 @@ function NotificationsPageContent() {
<Loading />
</div>
)}
{!isFetching && !hasNextPage && <p className="text-muted-foreground text-sm">No more notifications</p>}
{!isFetching && !hasNextPage && (
<p className="text-sm" style={{ color: secondaryTextColor }}>
No more notifications
</p>
)}
</div>
)}
</TabsContent>
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/atoms/icons/ExploreIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

const ExploreIcon = () => (
<svg
className="w-6 h-6 text-gray-800 dark:text-white"
className="w-6 h-6 text-current"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/atoms/icons/HomeIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

const HomeIcon = () => (
<svg
className="w-6 h-6 text-gray-800 dark:text-white"
className="w-6 h-6 text-current"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/atoms/icons/LoginIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

const LoginIcon = () => (
<svg
className="w-[24px] h-[24px] text-gray-800 dark:text-white"
className="w-[24px] h-[24px] text-current"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/atoms/icons/LogoutIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

const LogoutIcon = () => (
<svg
className="w-[24px] h-[24px] text-gray-800 dark:text-white scale-x-[-1] translate-x-[2px]"
className="w-[24px] h-[24px] text-current scale-x-[-1] translate-x-[2px]"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/atoms/icons/NotificationsIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

const NotificationsIcon = () => (
<svg
className="w-6 h-6 text-gray-800 dark:text-white"
className="w-6 h-6 text-current"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
Expand Down
3 changes: 1 addition & 2 deletions src/common/components/atoms/icons/RobotIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { RiRobot2Line } from "react-icons/ri";

const RobotIcon = () => (
<RiRobot2Line
className="w-6 h-6 text-gray-800 dark:text-white"
className="w-6 h-6 text-current"
aria-hidden="true"
/>
);

export default RobotIcon;

Loading