+
+ {canScrollLeft && (
+ <>
+
+
+ >
+ )}
+ {canScrollRight && (
+ <>
+
+
+ >
+ )}
);
}
diff --git a/components/brain/notifications/generic/NotificationGeneric.tsx b/components/brain/notifications/generic/NotificationGeneric.tsx
new file mode 100644
index 0000000000..b07f84170c
--- /dev/null
+++ b/components/brain/notifications/generic/NotificationGeneric.tsx
@@ -0,0 +1,102 @@
+import type { INotificationGeneric } from "@/types/feed.types";
+import NotificationHeader from "../subcomponents/NotificationHeader";
+import NotificationTimestamp from "../subcomponents/NotificationTimestamp";
+
+function formatCause(cause: string): string {
+ return cause
+ .replaceAll("_", " ")
+ .toLowerCase()
+ .replaceAll(/\b\w/g, (c) => c.toUpperCase());
+}
+
+function formatContextValue(value: unknown): string | null {
+ if (value === null || value === undefined) return null;
+ if (typeof value === "string") return value;
+ if (typeof value === "number" || typeof value === "boolean") {
+ return String(value);
+ }
+ return null;
+}
+
+function ContextDetails({
+ context,
+}: {
+ readonly context: Record
| undefined;
+}) {
+ if (!context || Object.keys(context).length === 0) return null;
+
+ const displayableEntries = Object.entries(context)
+ .map(([key, value]) => [key, formatContextValue(value)] as const)
+ .filter((entry): entry is [string, string] => entry[1] !== null);
+
+ if (displayableEntries.length === 0) return null;
+
+ return (
+ <>
+
+ •
+
+
+ {displayableEntries.map(([key, value]) => (
+
+ {key}: {value}
+
+ ))}
+
+ >
+ );
+}
+
+function NotificationContent({
+ causeLabel,
+ createdAt,
+ context,
+}: {
+ readonly causeLabel: string;
+ readonly createdAt: number;
+ readonly context: Record | undefined;
+}) {
+ return (
+ <>
+
+ {causeLabel}
+
+
+
+ >
+ );
+}
+
+export default function NotificationGeneric({
+ notification,
+}: {
+ readonly notification: INotificationGeneric;
+}) {
+ const causeLabel = formatCause(notification.cause);
+
+ if (notification.related_identity) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {causeLabel}
+
+
+
+
+
+ );
+}
diff --git a/components/brain/notifications/hooks/useNotificationsController.ts b/components/brain/notifications/hooks/useNotificationsController.ts
index 4d7965da0b..42763fa485 100644
--- a/components/brain/notifications/hooks/useNotificationsController.ts
+++ b/components/brain/notifications/hooks/useNotificationsController.ts
@@ -153,7 +153,7 @@ export const useNotificationsController =
activeProfileProxy: !!activeProfileProxy,
limit: "30",
reverse: true,
- cause: activeFilter?.cause,
+ cause: activeFilter?.cause?.length ? activeFilter.cause : null,
});
useEffect(() => {
diff --git a/components/brain/notifications/identity-rating/NotificationIdentityRating.tsx b/components/brain/notifications/identity-rating/NotificationIdentityRating.tsx
new file mode 100644
index 0000000000..28d1f8d51c
--- /dev/null
+++ b/components/brain/notifications/identity-rating/NotificationIdentityRating.tsx
@@ -0,0 +1,113 @@
+import { AuthContext } from "@/components/auth/Auth";
+import { UserFollowBtnSize } from "@/components/user/utils/UserFollowBtn";
+import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
+import { formatNumberWithCommas } from "@/helpers/Helpers";
+import type {
+ INotificationIdentityNic,
+ INotificationIdentityRep,
+} from "@/types/feed.types";
+import Link from "next/link";
+import { useContext } from "react";
+import NotificationsFollowBtn from "../NotificationsFollowBtn";
+import NotificationHeader from "../subcomponents/NotificationHeader";
+import NotificationTimestamp from "../subcomponents/NotificationTimestamp";
+
+interface NotificationIdentityRatingProps {
+ readonly notification: INotificationIdentityRep | INotificationIdentityNic;
+}
+
+function getRatingColor(rating: number): string {
+ if (rating > 0) return "tw-text-green";
+ if (rating < 0) return "tw-text-red";
+ return "tw-text-iron-400";
+}
+
+function formatRating(rating: number): string {
+ const prefix = rating > 0 ? "+" : "";
+ return `${prefix}${formatNumberWithCommas(rating)}`;
+}
+
+export default function NotificationIdentityRating({
+ notification,
+}: NotificationIdentityRatingProps) {
+ const { connectedProfile } = useContext(AuthContext);
+ const isRep = notification.cause === ApiNotificationCause.IdentityRep;
+ const { amount, total } = notification.additional_context;
+ const category =
+ "category" in notification.additional_context
+ ? notification.additional_context.category
+ : null;
+
+ const myHandle = connectedProfile?.handle;
+ const getProfileLink = (): string | null => {
+ if (!myHandle) return null;
+ return isRep ? `/${myHandle}/rep` : `/${myHandle}/identity`;
+ };
+ const linkHref = getProfileLink();
+
+ const ratingLabel = isRep ? "REP" : "NIC";
+
+ return (
+
+
+ }
+ >
+
+ updated your{" "}
+
+ {linkHref ? (
+
+
+ {ratingLabel}
+
+ {category && (
+
+ {" "}
+ for category '{category}'
+
+ )}
+ by
+
+ {formatRating(amount)}
+
+
+ ) : (
+
+
+ {ratingLabel}
+
+ {category && (
+
+ {" "}
+ for category '{category}'
+
+ )}
+ by
+
+ {formatRating(amount)}
+
+
+ )}
+
+
+ •
+
+ New Total:{" "}
+
+ {formatNumberWithCommas(total)}
+
+
+
+
+
+ );
+}
diff --git a/components/brain/notifications/index.tsx b/components/brain/notifications/index.tsx
index 9ea109703d..8752ded81c 100644
--- a/components/brain/notifications/index.tsx
+++ b/components/brain/notifications/index.tsx
@@ -16,14 +16,16 @@ interface NotificationsProps {
const NOTIFICATION_CAUSE_PRIORITY: Record = {
[ApiNotificationCause.IdentitySubscribed]: 0,
[ApiNotificationCause.IdentityMentioned]: 1,
- [ApiNotificationCause.DropQuoted]: 2,
- [ApiNotificationCause.DropReplied]: 3,
- [ApiNotificationCause.DropVoted]: 4,
- [ApiNotificationCause.DropBoosted]: 5,
- [ApiNotificationCause.DropReacted]: 6,
- [ApiNotificationCause.WaveCreated]: 7,
- [ApiNotificationCause.AllDrops]: 8,
- [ApiNotificationCause.PriorityAlert]: 9,
+ [ApiNotificationCause.IdentityRep]: 2,
+ [ApiNotificationCause.IdentityNic]: 3,
+ [ApiNotificationCause.DropQuoted]: 4,
+ [ApiNotificationCause.DropReplied]: 5,
+ [ApiNotificationCause.DropVoted]: 6,
+ [ApiNotificationCause.DropBoosted]: 7,
+ [ApiNotificationCause.DropReacted]: 8,
+ [ApiNotificationCause.WaveCreated]: 9,
+ [ApiNotificationCause.AllDrops]: 10,
+ [ApiNotificationCause.PriorityAlert]: 11,
};
const compareNotificationCause = (
@@ -51,7 +53,7 @@ export default function Notifications({
const activeFilterKey = useMemo(
() =>
- activeFilter?.cause
+ activeFilter?.cause?.length
? [...activeFilter.cause].sort(compareNotificationCause).join("|")
: "notifications-filter-all",
[activeFilter]
diff --git a/components/header/AppUserConnect.tsx b/components/header/AppUserConnect.tsx
index 262c5cae5f..d8407c8cdc 100644
--- a/components/header/AppUserConnect.tsx
+++ b/components/header/AppUserConnect.tsx
@@ -1,8 +1,11 @@
import {
- ArrowRightEndOnRectangleIcon,
- ArrowsRightLeftIcon,
+ ArrowRightEndOnRectangleIcon,
+ ArrowsRightLeftIcon,
+ Cog6ToothIcon,
} from "@heroicons/react/24/outline";
+import { useState } from "react";
import { useSeizeConnectContext } from "../auth/SeizeConnectContext";
+import PushNotificationSettings from "./PushNotificationSettings";
import HeaderQRScanner from "./share/HeaderQRScanner";
export default function AppUserConnect({
@@ -12,6 +15,7 @@ export default function AppUserConnect({
}) {
const { address, seizeConnect, seizeDisconnectAndLogout } =
useSeizeConnectContext();
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const qrScanner = ;
@@ -22,21 +26,31 @@ export default function AppUserConnect({
onNavigate();
}}
type="button"
- className="tw-whitespace-nowrap tw-flex tw-w-full tw-items-center tw-justify-center tw-cursor-pointer tw-bg-primary-500 tw-px-4 tw-py-2.5 tw-text-sm tw-leading-6 tw-rounded-lg tw-font-semibold tw-text-white tw-border-0 tw-ring-1 tw-ring-inset tw-ring-primary-500 hover:tw-ring-primary-600 placeholder:tw-text-iron-300 focus:tw-outline-none focus:tw-ring-1 focus:tw-ring-inset tw-shadow-sm hover:tw-bg-primary-600 tw-transition tw-duration-300 tw-ease-out">
+ className="tw-flex tw-w-full tw-cursor-pointer tw-items-center tw-justify-center tw-whitespace-nowrap tw-rounded-lg tw-border-0 tw-bg-primary-500 tw-px-4 tw-py-2.5 tw-text-sm tw-font-semibold tw-leading-6 tw-text-white tw-shadow-sm tw-ring-1 tw-ring-inset tw-ring-primary-500 tw-transition tw-duration-300 tw-ease-out placeholder:tw-text-iron-300 hover:tw-bg-primary-600 hover:tw-ring-primary-600 focus:tw-outline-none focus:tw-ring-1 focus:tw-ring-inset"
+ >
Connect
);
const connectedButtons = (
<>
+
+ setIsSettingsOpen(false)}
+ />
>
);
diff --git a/components/header/PushNotificationSettings.tsx b/components/header/PushNotificationSettings.tsx
new file mode 100644
index 0000000000..21035d60de
--- /dev/null
+++ b/components/header/PushNotificationSettings.tsx
@@ -0,0 +1,263 @@
+"use client";
+
+import { useAuth } from "@/components/auth/Auth";
+import MobileWrapperDialog from "@/components/mobile-wrapper-dialog/MobileWrapperDialog";
+import { getStableDeviceId } from "@/components/notifications/stable-device-id";
+import type { ApiPushNotificationSettings } from "@/generated/models/ApiPushNotificationSettings";
+import { commonApiFetch, commonApiPut } from "@/services/api/common-api";
+import { useMutation } from "@tanstack/react-query";
+import { useCallback, useEffect, useRef, useState } from "react";
+import Toggle from "react-toggle";
+
+interface PushNotificationSettingsProps {
+ readonly isOpen: boolean;
+ readonly onClose: () => void;
+}
+
+const SETTINGS_LABELS: Record = {
+ identity_rep: "Identity - REP",
+ identity_nic: "Identity - NIC",
+ identity_subscribed: "New Follows",
+ identity_mentioned: "Mentions",
+ drop_quoted: "Drops - Quoted",
+ drop_replied: "Drops - Replied",
+ drop_voted: "Drops - Voted",
+ drop_reacted: "Drops - Reacted",
+ drop_boosted: "Drops - Boosted",
+ wave_created: "Wave Invites",
+};
+
+const DEFAULT_SETTINGS: ApiPushNotificationSettings = {
+ identity_subscribed: true,
+ identity_mentioned: true,
+ identity_rep: true,
+ identity_nic: true,
+ drop_quoted: true,
+ drop_replied: true,
+ drop_voted: true,
+ drop_reacted: true,
+ drop_boosted: true,
+ wave_created: true,
+};
+
+export default function PushNotificationSettings({
+ isOpen,
+ onClose,
+}: PushNotificationSettingsProps) {
+ const { setToast } = useAuth();
+ const [deviceId, setDeviceId] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [originalSettings, setOriginalSettings] =
+ useState(null);
+ const [currentSettings, setCurrentSettings] =
+ useState(null);
+ const scrollRef = useRef(null);
+ const [canScrollUp, setCanScrollUp] = useState(false);
+ const [canScrollDown, setCanScrollDown] = useState(false);
+
+ useEffect(() => {
+ const el = scrollRef.current;
+ if (!el) return;
+
+ const updateScrollState = () => {
+ setCanScrollUp(el.scrollTop > 0);
+ setCanScrollDown(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
+ };
+
+ updateScrollState();
+ el.addEventListener("scroll", updateScrollState);
+ const resizeObserver = new ResizeObserver(updateScrollState);
+ resizeObserver.observe(el);
+
+ return () => {
+ el.removeEventListener("scroll", updateScrollState);
+ resizeObserver.disconnect();
+ };
+ }, [currentSettings]);
+
+ // Get device ID and fetch settings when opened
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const loadSettings = async () => {
+ setIsLoading(true);
+ try {
+ const id = await getStableDeviceId();
+ setDeviceId(id);
+
+ try {
+ const settings = await commonApiFetch({
+ endpoint: `push-notifications/settings/${id}`,
+ });
+ setOriginalSettings(settings);
+ setCurrentSettings(settings);
+ } catch (error: unknown) {
+ const status =
+ (error as { status?: number })?.status ??
+ (error as { response?: { status?: number } })?.response?.status;
+ if (status === 404) {
+ setOriginalSettings(DEFAULT_SETTINGS);
+ setCurrentSettings(DEFAULT_SETTINGS);
+ } else {
+ console.error("Error fetching push notification settings:", error);
+ setToast({
+ message: "Failed to load notification settings",
+ type: "error",
+ });
+ }
+ }
+ } catch (error) {
+ console.error("Error loading push notification settings:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadSettings();
+ }, [isOpen]);
+
+ const hasChanges =
+ currentSettings &&
+ originalSettings &&
+ Object.keys(originalSettings).some(
+ (key) =>
+ originalSettings[key as keyof ApiPushNotificationSettings] !==
+ currentSettings[key as keyof ApiPushNotificationSettings]
+ );
+
+ const updateSetting = useCallback(
+ (key: keyof ApiPushNotificationSettings, value: boolean) => {
+ setCurrentSettings((prev) => (prev ? { ...prev, [key]: value } : prev));
+ },
+ []
+ );
+
+ const { mutateAsync: saveSettings, isPending: isSaving } = useMutation({
+ mutationFn: async () => {
+ if (!deviceId || !currentSettings) return;
+ return await commonApiPut<
+ ApiPushNotificationSettings,
+ ApiPushNotificationSettings
+ >({
+ endpoint: `push-notifications/settings/${deviceId}`,
+ body: currentSettings,
+ });
+ },
+ onSuccess: () => {
+ setOriginalSettings(currentSettings);
+ setToast({
+ message: "Notification settings updated",
+ type: "success",
+ });
+ onClose();
+ },
+ onError: (error: unknown) => {
+ console.error("Error saving push notification settings:", error);
+ setToast({
+ message: "Failed to save notification settings",
+ type: "error",
+ });
+ },
+ });
+
+ const handleSave = useCallback(async () => {
+ await saveSettings();
+ }, [saveSettings]);
+
+ const settingKeys = Object.keys(SETTINGS_LABELS) as Array<
+ keyof ApiPushNotificationSettings
+ >;
+
+ return (
+
+
+ {isLoading && (
+
+ )}
+ {!isLoading && !currentSettings && (
+
+
+ Unable to load notification settings.
+
+
+ )}
+ {!isLoading && currentSettings && (
+ <>
+
+ Choose which notifications you want to receive on this device.
+
+
+
+ {canScrollUp && (
+
+ )}
+
+
+ {settingKeys.map((key) => (
+
+
+ updateSetting(key, e.target.checked)}
+ />
+
+ ))}
+
+
+ {canScrollDown && (
+
+ )}
+
+
+
+ {hasChanges && (
+
+ Tap below to save your changes
+
+ )}
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/components/nft-image/renderers/NFTHTMLRenderer.tsx b/components/nft-image/renderers/NFTHTMLRenderer.tsx
index b14c6a9052..da9ecf58e1 100644
--- a/components/nft-image/renderers/NFTHTMLRenderer.tsx
+++ b/components/nft-image/renderers/NFTHTMLRenderer.tsx
@@ -23,7 +23,8 @@ export default function NFTHTMLRenderer(props: Readonly) {
return (
+ className={`${styles["nftAnimation"]} ${props.heightStyle} ${props.imageStyle} ${props.bgStyle} d-flex justify-content-center align-items-center`}
+ >
{props.showBalance && (
`/${path}`,
- profile: ({ handle }: { handle: string }) => `/${handle}`,
+ profile: ({ handle, subroute }: { handle: string; subroute?: string }) => {
+ if (!subroute) return `/${handle}`;
+ const validTab = getUserPageTabByRoute(subroute);
+ if (!validTab) return `/${handle}`;
+ return `/${handle}/${validTab.route}`;
+ },
"the-memes": ({ id }: { id: string }) => `/the-memes/${id}`,
"6529-gradient": ({ id }: { id: string }) => `/6529-gradient/${id}`,
"meme-lab": ({ id }: { id: string }) => `/meme-lab/${id}`,
@@ -94,6 +100,7 @@ interface NotificationData {
profile_id?: string | undefined;
path?: string | undefined;
handle?: string | undefined;
+ subroute?: string | undefined;
id?: string | undefined;
wave_id?: string | undefined;
drop_id?: string | undefined;
diff --git a/components/providers/CapacitorSetup.tsx b/components/providers/CapacitorSetup.tsx
index c89c02e5be..c8139f1a77 100644
--- a/components/providers/CapacitorSetup.tsx
+++ b/components/providers/CapacitorSetup.tsx
@@ -1,7 +1,7 @@
"use client";
-import { useEffect, useRef } from "react";
import useCapacitor from "@/hooks/useCapacitor";
+import { useEffect, useRef } from "react";
export default function CapacitorSetup() {
const { isCapacitor } = useCapacitor();
@@ -44,5 +44,6 @@ export default function CapacitorSetup() {
}
};
}, [isCapacitor]);
+
return null;
}
diff --git a/components/providers/LayoutWrapper.tsx b/components/providers/LayoutWrapper.tsx
index a92624e20f..80a3be8d2f 100644
--- a/components/providers/LayoutWrapper.tsx
+++ b/components/providers/LayoutWrapper.tsx
@@ -9,8 +9,10 @@ import { SIDEBAR_MOBILE_BREAKPOINT } from "@/constants/sidebar";
import useIsMobileScreen from "@/hooks/isMobileScreen";
import useDeviceInfo from "@/hooks/useDeviceInfo";
import { usePathname } from "next/navigation";
+import { useGlobalRefresh } from "@/contexts/RefreshContext";
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
import { ErrorBoundary } from "react-error-boundary";
+import PullToRefresh from "./PullToRefresh";
export default function LayoutWrapper({
children,
@@ -18,6 +20,7 @@ export default function LayoutWrapper({
readonly children: ReactNode;
}) {
const { isApp, hasTouchScreen } = useDeviceInfo();
+ const { refreshKey } = useGlobalRefresh();
const isSmallScreen = useIsMobileScreen();
const [isTouchTabletViewport, setIsTouchTabletViewport] = useState(() => {
if (globalThis.window === undefined) {
@@ -85,9 +88,11 @@ export default function LayoutWrapper({
return (
+ {isApp && }
{children}
diff --git a/components/providers/Providers.tsx b/components/providers/Providers.tsx
index 45c450b610..43c754f224 100644
--- a/components/providers/Providers.tsx
+++ b/components/providers/Providers.tsx
@@ -10,6 +10,7 @@ import NewVersionToast from "@/components/utils/NewVersionToast";
import { EmojiProvider } from "@/contexts/EmojiContext";
import { HeaderProvider } from "@/contexts/HeaderContext";
import { NavigationHistoryProvider } from "@/contexts/NavigationHistoryContext";
+import { RefreshProvider } from "@/contexts/RefreshContext";
import { ScrollPositionProvider } from "@/contexts/ScrollPositionContext";
import { SeizeSettingsProvider } from "@/contexts/SeizeSettingsContext";
import { TitleProvider } from "@/contexts/TitleContext";
@@ -35,42 +36,44 @@ export default function Providers({
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/providers/PullToRefresh.tsx b/components/providers/PullToRefresh.tsx
new file mode 100644
index 0000000000..481f31a1de
--- /dev/null
+++ b/components/providers/PullToRefresh.tsx
@@ -0,0 +1,268 @@
+"use client";
+
+import { useGlobalRefresh } from "@/contexts/RefreshContext";
+import useCapacitor from "@/hooks/useCapacitor";
+import { useCallback, useContext, useEffect, useRef, useState } from "react";
+import { ReactQueryWrapperContext } from "../react-query-wrapper/ReactQueryWrapper";
+
+const PULL_THRESHOLD = 80;
+const PULL_MAX = 140;
+const INDICATOR_SIZE = 36;
+
+export default function PullToRefresh() {
+ const { isCapacitor } = useCapacitor();
+ const { invalidateAll } = useContext(ReactQueryWrapperContext);
+ const { globalRefresh } = useGlobalRefresh();
+ const [pullDistance, setPullDistance] = useState(0);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const touchStartY = useRef(0);
+ const isPulling = useRef(false);
+ const contentRef = useRef(null);
+ const refreshTimeoutRef = useRef | null>(null);
+ const pullDistanceRef = useRef(0);
+ const isRefreshingRef = useRef(false);
+
+ const isAtTop = useCallback(() => {
+ return window.scrollY <= 0;
+ }, []);
+
+ const getScrollableParent = useCallback(
+ (element: HTMLElement | null): HTMLElement | null => {
+ if (!element) return null;
+ const style = globalThis.getComputedStyle(element);
+ const overflowY = style.overflowY;
+ if (overflowY === "scroll" || overflowY === "auto") {
+ if (element.scrollHeight > element.clientHeight) {
+ return element;
+ }
+ }
+ return getScrollableParent(element.parentElement);
+ },
+ []
+ );
+
+ const resetContentStyles = useCallback(() => {
+ if (!contentRef.current) return;
+ contentRef.current.style.transform = "";
+ contentRef.current.style.transition = "";
+ }, []);
+
+ const handleTouchStart = useCallback(
+ (e: TouchEvent) => {
+ if (isRefreshingRef.current) return;
+
+ const target = e.target as HTMLElement;
+ const scrollableParent = getScrollableParent(target);
+
+ if (scrollableParent && scrollableParent.scrollTop > 0) {
+ return;
+ }
+
+ if (!isAtTop()) return;
+ const touch = e.touches[0];
+ if (!touch) return;
+ touchStartY.current = touch.clientY;
+
+ isPulling.current = true;
+ contentRef.current = document.body;
+ },
+ [isAtTop, getScrollableParent]
+ );
+
+ const handleTouchMove = useCallback(
+ (e: TouchEvent) => {
+ if (!isPulling.current || isRefreshingRef.current) return;
+
+ const target = e.target as HTMLElement;
+ const scrollableParent = getScrollableParent(target);
+ const isScrolledInParent =
+ scrollableParent && scrollableParent.scrollTop > 0;
+
+ if (isScrolledInParent || !isAtTop()) {
+ pullDistanceRef.current = 0;
+ setPullDistance(0);
+ resetContentStyles();
+ return;
+ }
+
+ const touch = e.touches[0];
+ if (!touch) return;
+
+ const diff = touch.clientY - touchStartY.current;
+
+ if (diff <= 0) {
+ pullDistanceRef.current = 0;
+ setPullDistance(0);
+ resetContentStyles();
+ return;
+ }
+
+ const resistance = 0.5;
+ const distance = Math.min(diff * resistance, PULL_MAX);
+ pullDistanceRef.current = distance;
+ setPullDistance(distance);
+
+ if (contentRef.current) {
+ contentRef.current.style.transform = `translateY(${distance}px)`;
+ contentRef.current.style.transition = "none";
+ }
+
+ if (distance > 10) {
+ e.preventDefault();
+ }
+ },
+ [isAtTop, getScrollableParent, resetContentStyles]
+ );
+
+ const handleTouchEnd = useCallback(() => {
+ if (!isPulling.current) return;
+ isPulling.current = false;
+
+ if (pullDistanceRef.current >= PULL_THRESHOLD && !isRefreshingRef.current) {
+ isRefreshingRef.current = true;
+ setIsRefreshing(true);
+
+ if (contentRef.current) {
+ contentRef.current.style.transform = `translateY(${INDICATOR_SIZE + 20}px)`;
+ contentRef.current.style.transition = "transform 0.3s ease-out";
+ }
+ pullDistanceRef.current = INDICATOR_SIZE + 20;
+ setPullDistance(INDICATOR_SIZE + 20);
+
+ invalidateAll();
+ globalRefresh();
+
+ refreshTimeoutRef.current = setTimeout(() => {
+ isRefreshingRef.current = false;
+ setIsRefreshing(false);
+ pullDistanceRef.current = 0;
+ setPullDistance(0);
+ if (contentRef.current) {
+ contentRef.current.style.transform = "";
+ contentRef.current.style.transition = "transform 0.3s ease-out";
+ }
+ refreshTimeoutRef.current = null;
+ }, 1000);
+ } else {
+ pullDistanceRef.current = 0;
+ setPullDistance(0);
+ if (contentRef.current) {
+ contentRef.current.style.transform = "";
+ contentRef.current.style.transition = "transform 0.3s ease-out";
+ }
+ }
+ }, [invalidateAll, globalRefresh]);
+
+ const handleTouchCancel = useCallback(() => {
+ isPulling.current = false;
+
+ if (refreshTimeoutRef.current) {
+ clearTimeout(refreshTimeoutRef.current);
+ refreshTimeoutRef.current = null;
+ }
+
+ isRefreshingRef.current = false;
+ setIsRefreshing(false);
+ pullDistanceRef.current = 0;
+ setPullDistance(0);
+
+ if (contentRef.current) {
+ contentRef.current.style.transform = "";
+ contentRef.current.style.transition = "";
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!isCapacitor) return;
+
+ document.addEventListener("touchstart", handleTouchStart, {
+ passive: true,
+ });
+ document.addEventListener("touchmove", handleTouchMove, { passive: false });
+ document.addEventListener("touchend", handleTouchEnd, { passive: true });
+ document.addEventListener("touchcancel", handleTouchCancel, {
+ passive: true,
+ });
+
+ return () => {
+ document.removeEventListener("touchstart", handleTouchStart);
+ document.removeEventListener("touchmove", handleTouchMove);
+ document.removeEventListener("touchend", handleTouchEnd);
+ document.removeEventListener("touchcancel", handleTouchCancel);
+ if (refreshTimeoutRef.current) {
+ clearTimeout(refreshTimeoutRef.current);
+ refreshTimeoutRef.current = null;
+ }
+ if (contentRef.current) {
+ contentRef.current.style.transform = "";
+ contentRef.current.style.transition = "";
+ }
+ };
+ }, [
+ isCapacitor,
+ handleTouchStart,
+ handleTouchMove,
+ handleTouchEnd,
+ handleTouchCancel,
+ ]);
+
+ if (!isCapacitor || pullDistance === 0) return null;
+
+ const progress = Math.min(pullDistance / PULL_THRESHOLD, 1);
+ const shouldTrigger = pullDistance >= PULL_THRESHOLD;
+ const rotation = progress * 180;
+
+ return (
+
+
+ {isRefreshing ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/components/user/user-page-header/UserPageHeaderClient.tsx b/components/user/user-page-header/UserPageHeaderClient.tsx
index 61ca453026..5cda0f356c 100644
--- a/components/user/user-page-header/UserPageHeaderClient.tsx
+++ b/components/user/user-page-header/UserPageHeaderClient.tsx
@@ -50,7 +50,8 @@ export default function UserPageHeaderClient({
const params = useParams();
const router = useRouter();
const { isApp } = useDeviceInfo();
- const routeHandleOrWallet = params?.["user"]?.toString().toLowerCase() ?? null;
+ const routeHandleOrWallet =
+ params?.["user"]?.toString().toLowerCase() ?? null;
const normalizedHandleOrWallet =
routeHandleOrWallet ?? handleOrWallet.toLowerCase();
@@ -162,10 +163,10 @@ export default function UserPageHeaderClient({
defaultBanner2={defaultBanner2}
canEdit={canEdit}
/>
-
+
-
+
void;
+ refreshKey: number;
+};
+
+const Ctx = createContext(null);
+
+export function RefreshProvider({
+ children,
+}: {
+ readonly children: React.ReactNode;
+}) {
+ const [refreshKey, setRefreshKey] = useState(0);
+
+ const globalRefresh = useCallback(() => {
+ setRefreshKey((prev) => prev + 1);
+ }, []);
+
+ const value = useMemo(
+ () => ({ globalRefresh, refreshKey }),
+ [globalRefresh, refreshKey]
+ );
+
+ return {children};
+}
+
+export function useGlobalRefresh() {
+ const ctx = useContext(Ctx);
+ if (!ctx) {
+ throw new Error("useGlobalRefresh must be used under ");
+ }
+ return ctx;
+}
diff --git a/generated/models/ApiMintMetrics.ts b/generated/models/ApiMintMetrics.ts
index 7f501c207f..a00de66710 100644
--- a/generated/models/ApiMintMetrics.ts
+++ b/generated/models/ApiMintMetrics.ts
@@ -18,6 +18,8 @@ export class ApiMintMetrics {
'mint_time': number;
'subscriptions': number;
'mints': number;
+ 'edition_size': number | null;
+ 'unminted': number;
static readonly discriminator: string | undefined = undefined;
@@ -47,6 +49,18 @@ export class ApiMintMetrics {
"baseName": "mints",
"type": "number",
"format": "int64"
+ },
+ {
+ "name": "edition_size",
+ "baseName": "edition_size",
+ "type": "number",
+ "format": "int64"
+ },
+ {
+ "name": "unminted",
+ "baseName": "unminted",
+ "type": "number",
+ "format": "int64"
} ];
static getAttributeTypeMap() {
diff --git a/generated/models/ApiNotificationCause.ts b/generated/models/ApiNotificationCause.ts
index c587915cf2..7eeca9fc74 100644
--- a/generated/models/ApiNotificationCause.ts
+++ b/generated/models/ApiNotificationCause.ts
@@ -16,6 +16,8 @@ import { HttpFile } from '../http/http';
export enum ApiNotificationCause {
IdentitySubscribed = 'IDENTITY_SUBSCRIBED',
IdentityMentioned = 'IDENTITY_MENTIONED',
+ IdentityRep = 'IDENTITY_REP',
+ IdentityNic = 'IDENTITY_NIC',
DropQuoted = 'DROP_QUOTED',
DropReplied = 'DROP_REPLIED',
DropVoted = 'DROP_VOTED',
diff --git a/generated/models/ApiPushNotificationDevice.ts b/generated/models/ApiPushNotificationDevice.ts
new file mode 100644
index 0000000000..46e8bc1a33
--- /dev/null
+++ b/generated/models/ApiPushNotificationDevice.ts
@@ -0,0 +1,58 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiPushNotificationDevice {
+ 'device_id': string;
+ 'platform'?: string | null;
+ 'created_at': Date;
+ 'updated_at': Date;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "device_id",
+ "baseName": "device_id",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "platform",
+ "baseName": "platform",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "created_at",
+ "baseName": "created_at",
+ "type": "Date",
+ "format": "date-time"
+ },
+ {
+ "name": "updated_at",
+ "baseName": "updated_at",
+ "type": "Date",
+ "format": "date-time"
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiPushNotificationDevice.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ApiPushNotificationSettings.ts b/generated/models/ApiPushNotificationSettings.ts
new file mode 100644
index 0000000000..37139eb986
--- /dev/null
+++ b/generated/models/ApiPushNotificationSettings.ts
@@ -0,0 +1,130 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiPushNotificationSettings {
+ /**
+ * Notify when someone follows you
+ */
+ 'identity_subscribed': boolean;
+ /**
+ * Notify when someone mentions you
+ */
+ 'identity_mentioned': boolean;
+ /**
+ * Notify when someone gives you REP
+ */
+ 'identity_rep': boolean;
+ /**
+ * Notify when someone gives you NIC
+ */
+ 'identity_nic': boolean;
+ /**
+ * Notify when someone quotes your drop
+ */
+ 'drop_quoted': boolean;
+ /**
+ * Notify when someone replies to your drop
+ */
+ 'drop_replied': boolean;
+ /**
+ * Notify when someone votes on your drop
+ */
+ 'drop_voted': boolean;
+ /**
+ * Notify when someone reacts to your drop
+ */
+ 'drop_reacted': boolean;
+ /**
+ * Notify when someone boosts your drop
+ */
+ 'drop_boosted': boolean;
+ /**
+ * Notify when you are invited to a wave
+ */
+ 'wave_created': boolean;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "identity_subscribed",
+ "baseName": "identity_subscribed",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "identity_mentioned",
+ "baseName": "identity_mentioned",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "identity_rep",
+ "baseName": "identity_rep",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "identity_nic",
+ "baseName": "identity_nic",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_quoted",
+ "baseName": "drop_quoted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_replied",
+ "baseName": "drop_replied",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_voted",
+ "baseName": "drop_voted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_reacted",
+ "baseName": "drop_reacted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_boosted",
+ "baseName": "drop_boosted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "wave_created",
+ "baseName": "wave_created",
+ "type": "boolean",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiPushNotificationSettings.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ApiPushNotificationSettingsUpdate.ts b/generated/models/ApiPushNotificationSettingsUpdate.ts
new file mode 100644
index 0000000000..fd9f66ef37
--- /dev/null
+++ b/generated/models/ApiPushNotificationSettingsUpdate.ts
@@ -0,0 +1,100 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiPushNotificationSettingsUpdate {
+ 'identity_subscribed'?: boolean;
+ 'identity_mentioned'?: boolean;
+ 'identity_rep'?: boolean;
+ 'identity_nic'?: boolean;
+ 'drop_quoted'?: boolean;
+ 'drop_replied'?: boolean;
+ 'drop_voted'?: boolean;
+ 'drop_reacted'?: boolean;
+ 'drop_boosted'?: boolean;
+ 'wave_created'?: boolean;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "identity_subscribed",
+ "baseName": "identity_subscribed",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "identity_mentioned",
+ "baseName": "identity_mentioned",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "identity_rep",
+ "baseName": "identity_rep",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "identity_nic",
+ "baseName": "identity_nic",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_quoted",
+ "baseName": "drop_quoted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_replied",
+ "baseName": "drop_replied",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_voted",
+ "baseName": "drop_voted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_reacted",
+ "baseName": "drop_reacted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "drop_boosted",
+ "baseName": "drop_boosted",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "wave_created",
+ "baseName": "wave_created",
+ "type": "boolean",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiPushNotificationSettingsUpdate.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ApiWave.ts b/generated/models/ApiWave.ts
index b8f9b04585..e634808e19 100644
--- a/generated/models/ApiWave.ts
+++ b/generated/models/ApiWave.ts
@@ -18,7 +18,6 @@ import { ApiWaveConfig } from '../models/ApiWaveConfig';
import { ApiWaveContributorOverview } from '../models/ApiWaveContributorOverview';
import { ApiWaveDecisionPause } from '../models/ApiWaveDecisionPause';
import { ApiWaveMetrics } from '../models/ApiWaveMetrics';
-import { ApiWaveOutcomeOld } from '../models/ApiWaveOutcomeOld';
import { ApiWaveParticipationConfig } from '../models/ApiWaveParticipationConfig';
import { ApiWaveSubscriptionTargetAction } from '../models/ApiWaveSubscriptionTargetAction';
import { ApiWaveVisibilityConfig } from '../models/ApiWaveVisibilityConfig';
@@ -50,7 +49,6 @@ export class ApiWave {
'participation': ApiWaveParticipationConfig;
'chat': ApiWaveChatConfig;
'wave': ApiWaveConfig;
- 'outcomes': Array;
'contributors_overview': Array;
'subscribed_actions': Array;
'metrics': ApiWaveMetrics;
@@ -134,12 +132,6 @@ export class ApiWave {
"type": "ApiWaveConfig",
"format": ""
},
- {
- "name": "outcomes",
- "baseName": "outcomes",
- "type": "Array",
- "format": ""
- },
{
"name": "contributors_overview",
"baseName": "contributors_overview",
diff --git a/generated/models/ApiWaveOutcomeOld.ts b/generated/models/ApiWaveOutcomeOld.ts
deleted file mode 100644
index 6bdd9f4c97..0000000000
--- a/generated/models/ApiWaveOutcomeOld.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-// @ts-nocheck
-/**
- * 6529.io API
- * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
- *
- * OpenAPI spec version: 1.0.0
- *
- *
- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
- * https://openapi-generator.tech
- * Do not edit the class manually.
- */
-
-import { ApiWaveOutcomeCredit } from '../models/ApiWaveOutcomeCredit';
-import { ApiWaveOutcomeDistributionItem } from '../models/ApiWaveOutcomeDistributionItem';
-import { ApiWaveOutcomeSubType } from '../models/ApiWaveOutcomeSubType';
-import { ApiWaveOutcomeType } from '../models/ApiWaveOutcomeType';
-import { HttpFile } from '../http/http';
-
-export class ApiWaveOutcomeOld {
- 'type': ApiWaveOutcomeType;
- 'subtype'?: ApiWaveOutcomeSubType;
- 'description': string;
- 'credit'?: ApiWaveOutcomeCredit;
- 'rep_category'?: string;
- 'amount'?: number;
- 'distribution'?: Array;
- 'index': number;
-
- static readonly discriminator: string | undefined = undefined;
-
- static readonly mapping: {[index: string]: string} | undefined = undefined;
-
- static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
- {
- "name": "type",
- "baseName": "type",
- "type": "ApiWaveOutcomeType",
- "format": ""
- },
- {
- "name": "subtype",
- "baseName": "subtype",
- "type": "ApiWaveOutcomeSubType",
- "format": ""
- },
- {
- "name": "description",
- "baseName": "description",
- "type": "string",
- "format": ""
- },
- {
- "name": "credit",
- "baseName": "credit",
- "type": "ApiWaveOutcomeCredit",
- "format": ""
- },
- {
- "name": "rep_category",
- "baseName": "rep_category",
- "type": "string",
- "format": ""
- },
- {
- "name": "amount",
- "baseName": "amount",
- "type": "number",
- "format": "int64"
- },
- {
- "name": "distribution",
- "baseName": "distribution",
- "type": "Array",
- "format": ""
- },
- {
- "name": "index",
- "baseName": "index",
- "type": "number",
- "format": "int64"
- } ];
-
- static getAttributeTypeMap() {
- return ApiWaveOutcomeOld.attributeTypeMap;
- }
-
- public constructor() {
- }
-}
-
-
diff --git a/generated/models/ObjectSerializer.ts b/generated/models/ObjectSerializer.ts
index 58e33c9797..1415ba9d91 100644
--- a/generated/models/ObjectSerializer.ts
+++ b/generated/models/ObjectSerializer.ts
@@ -130,6 +130,9 @@ export * from '../models/ApiProfileMin';
export * from '../models/ApiProfileProxy';
export * from '../models/ApiProfileProxyAction';
export * from '../models/ApiProfileProxyActionType';
+export * from '../models/ApiPushNotificationDevice';
+export * from '../models/ApiPushNotificationSettings';
+export * from '../models/ApiPushNotificationSettingsUpdate';
export * from '../models/ApiQuotedDrop';
export * from '../models/ApiQuotedDropResponse';
export * from '../models/ApiRateMatter';
@@ -180,7 +183,6 @@ export * from '../models/ApiWaveOutcome';
export * from '../models/ApiWaveOutcomeCredit';
export * from '../models/ApiWaveOutcomeDistributionItem';
export * from '../models/ApiWaveOutcomeDistributionItemsPage';
-export * from '../models/ApiWaveOutcomeOld';
export * from '../models/ApiWaveOutcomeSubType';
export * from '../models/ApiWaveOutcomeType';
export * from '../models/ApiWaveOutcomesPage';
@@ -380,6 +382,9 @@ import { ApiProfileMin } from '../models/ApiProfileMin';
import { ApiProfileProxy } from '../models/ApiProfileProxy';
import { ApiProfileProxyAction } from '../models/ApiProfileProxyAction';
import { ApiProfileProxyActionType } from '../models/ApiProfileProxyActionType';
+import { ApiPushNotificationDevice } from '../models/ApiPushNotificationDevice';
+import { ApiPushNotificationSettings } from '../models/ApiPushNotificationSettings';
+import { ApiPushNotificationSettingsUpdate } from '../models/ApiPushNotificationSettingsUpdate';
import { ApiQuotedDrop } from '../models/ApiQuotedDrop';
import { ApiQuotedDropResponse } from '../models/ApiQuotedDropResponse';
import { ApiRateMatter } from '../models/ApiRateMatter';
@@ -430,7 +435,6 @@ import { ApiWaveOutcome } from '../models/ApiWaveOutcome';
import { ApiWaveOutcomeCredit } from '../models/ApiWaveOutcomeCredit';
import { ApiWaveOutcomeDistributionItem } from '../models/ApiWaveOutcomeDistributionItem';
import { ApiWaveOutcomeDistributionItemsPage } from '../models/ApiWaveOutcomeDistributionItemsPage';
-import { ApiWaveOutcomeOld } from '../models/ApiWaveOutcomeOld';
import { ApiWaveOutcomeSubType } from '../models/ApiWaveOutcomeSubType';
import { ApiWaveOutcomeType } from '../models/ApiWaveOutcomeType';
import { ApiWaveOutcomesPage } from '../models/ApiWaveOutcomesPage';
@@ -663,6 +667,9 @@ let typeMap: {[index: string]: any} = {
"ApiProfileMin": ApiProfileMin,
"ApiProfileProxy": ApiProfileProxy,
"ApiProfileProxyAction": ApiProfileProxyAction,
+ "ApiPushNotificationDevice": ApiPushNotificationDevice,
+ "ApiPushNotificationSettings": ApiPushNotificationSettings,
+ "ApiPushNotificationSettingsUpdate": ApiPushNotificationSettingsUpdate,
"ApiQuotedDrop": ApiQuotedDrop,
"ApiQuotedDropResponse": ApiQuotedDropResponse,
"ApiRatingWithProfileInfoAndLevel": ApiRatingWithProfileInfoAndLevel,
@@ -708,7 +715,6 @@ let typeMap: {[index: string]: any} = {
"ApiWaveOutcome": ApiWaveOutcome,
"ApiWaveOutcomeDistributionItem": ApiWaveOutcomeDistributionItem,
"ApiWaveOutcomeDistributionItemsPage": ApiWaveOutcomeDistributionItemsPage,
- "ApiWaveOutcomeOld": ApiWaveOutcomeOld,
"ApiWaveOutcomesPage": ApiWaveOutcomesPage,
"ApiWaveParticipationConfig": ApiWaveParticipationConfig,
"ApiWaveRequiredMetadata": ApiWaveRequiredMetadata,
diff --git a/hooks/useNotificationsQuery.tsx b/hooks/useNotificationsQuery.tsx
index e3756463a1..443ffb9f7d 100644
--- a/hooks/useNotificationsQuery.tsx
+++ b/hooks/useNotificationsQuery.tsx
@@ -1,13 +1,11 @@
"use client";
-import { useCallback, useEffect, useMemo } from "react";
-import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
-import { commonApiFetch } from "@/services/api/common-api";
-import type {
- TypedNotificationsResponse,
-} from "@/types/feed.types";
-import type { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import type { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
+import { commonApiFetch } from "@/services/api/common-api";
+import type { TypedNotificationsResponse } from "@/types/feed.types";
+import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
+import { useCallback, useEffect, useMemo } from "react";
interface UseNotificationsQueryProps {
/**
@@ -33,7 +31,7 @@ interface UseNotificationsQueryProps {
readonly limit?: string | undefined;
/**
- * The cause of the notifications to fetch.
+ * The cause of the notifications to fetch (include filter).
*/
readonly cause?: ApiNotificationCause[] | null | undefined;
}
@@ -49,7 +47,17 @@ const getIdentityNotificationsQueryKey = (
identity: string | null | undefined,
limit: string,
cause: ApiNotificationCause[] | null
-) => [QueryKey.IDENTITY_NOTIFICATIONS, { identity, limit, cause }] as const;
+) =>
+ [
+ QueryKey.IDENTITY_NOTIFICATIONS,
+ {
+ identity,
+ limit,
+ cause: cause?.length
+ ? [...cause].sort((a, b) => a.localeCompare(b)).join(",")
+ : null,
+ },
+ ] as const;
const fetchNotifications = async ({
limit,
@@ -117,7 +125,8 @@ export function useNotificationsQuery({
(error as any)?.response?.status ??
(error as any)?.cause?.status;
if (status === 401) return false;
- if (typeof error === "string" && /unauthorized/i.test(error)) return false;
+ if (typeof error === "string" && /unauthorized/i.test(error))
+ return false;
if (error instanceof Error && /unauthorized/i.test(error.message)) {
return false;
}
@@ -130,9 +139,9 @@ export function useNotificationsQuery({
return [];
}
- const data = (
- query.data.pages as TypedNotificationsResponse[]
- ).flatMap((page) => page.notifications);
+ const data = (query.data.pages as TypedNotificationsResponse[]).flatMap(
+ (page) => page.notifications
+ );
return reverse ? [...data].reverse() : data;
}, [query.data, reverse]);
@@ -166,7 +175,11 @@ export function usePrefetchNotifications() {
return;
}
queryClient.prefetchInfiniteQuery({
- queryKey: getIdentityNotificationsQueryKey(identity, limit, cause),
+ queryKey: getIdentityNotificationsQueryKey(
+ identity,
+ limit,
+ cause?.length ? cause : null
+ ),
queryFn: ({
pageParam,
signal,
diff --git a/openapi.yaml b/openapi.yaml
index 8af135b1f6..d3c51d5451 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -814,6 +814,12 @@ paths:
required: false
schema:
$ref: "#/components/schemas/ApiDropType"
+ - name: ids
+ in: query
+ description: Comma-separated list of drop IDs to fetch
+ required: false
+ schema:
+ type: string
responses:
"200":
description: successful operation
@@ -1908,6 +1914,24 @@ paths:
schema:
type: number
format: int64
+ - name: cause
+ in: query
+ description: Comma-separated list of notification causes to include
+ required: false
+ schema:
+ type: string
+ - name: cause_exclude
+ in: query
+ description: Comma-separated list of notification causes to exclude
+ required: false
+ schema:
+ type: string
+ - name: unread_only
+ in: query
+ description: Only return unread notifications
+ required: false
+ schema:
+ type: boolean
responses:
"200":
description: successful operation
@@ -2738,6 +2762,100 @@ paths:
description: Invalid request
"401":
description: Unauthorized
+ /push-notifications/devices:
+ get:
+ tags:
+ - Push Notifications
+ summary: Get all registered devices for the authenticated user
+ operationId: getDevices
+ responses:
+ "200":
+ description: List of registered devices
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiPushNotificationDevice"
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden - profile required
+ /push-notifications/devices/{device_id}:
+ delete:
+ tags:
+ - Push Notifications
+ summary: Delete a registered device
+ operationId: deleteDevice
+ parameters:
+ - name: device_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The device ID to delete
+ responses:
+ "204":
+ description: Device deleted successfully
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden - profile required
+ /push-notifications/settings/{device_id}:
+ get:
+ tags:
+ - Push Notifications
+ summary: Get push notification settings for a device
+ operationId: getPushNotificationSettings
+ parameters:
+ - name: device_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The device ID to get settings for
+ responses:
+ "200":
+ description: Push notification settings
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiPushNotificationSettings"
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden - profile required
+ put:
+ tags:
+ - Push Notifications
+ summary: Update push notification settings for a device
+ operationId: updatePushNotificationSettings
+ parameters:
+ - name: device_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The device ID to update settings for
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiPushNotificationSettingsUpdate"
+ responses:
+ "200":
+ description: Updated push notification settings
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiPushNotificationSettings"
+ "400":
+ description: Invalid request
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden - profile required
/bulk-rep:
post:
tags:
@@ -7429,6 +7547,8 @@ components:
- mint_time
- subscriptions
- mints
+ - edition_size
+ - unminted
properties:
card:
type: number
@@ -7442,6 +7562,13 @@ components:
mints:
type: number
format: int64
+ edition_size:
+ type: number
+ format: int64
+ nullable: true
+ unminted:
+ type: number
+ format: int64
ApiMintMetricsPage:
type: object
required:
@@ -7702,6 +7829,8 @@ components:
enum:
- IDENTITY_SUBSCRIBED
- IDENTITY_MENTIONED
+ - IDENTITY_REP
+ - IDENTITY_NIC
- DROP_QUOTED
- DROP_REPLIED
- DROP_VOTED
@@ -8147,6 +8276,91 @@ components:
- READ_WAVE
- CREATE_DROP_TO_WAVE
- RATE_WAVE_DROP
+ ApiPushNotificationDevice:
+ type: object
+ required:
+ - device_id
+ - created_at
+ - updated_at
+ properties:
+ device_id:
+ type: string
+ platform:
+ type: string
+ nullable: true
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+ ApiPushNotificationSettings:
+ type: object
+ required:
+ - identity_subscribed
+ - identity_mentioned
+ - identity_rep
+ - identity_nic
+ - drop_quoted
+ - drop_replied
+ - drop_voted
+ - drop_reacted
+ - drop_boosted
+ - wave_created
+ properties:
+ identity_subscribed:
+ type: boolean
+ description: Notify when someone follows you
+ identity_mentioned:
+ type: boolean
+ description: Notify when someone mentions you
+ identity_rep:
+ type: boolean
+ description: Notify when someone gives you REP
+ identity_nic:
+ type: boolean
+ description: Notify when someone gives you NIC
+ drop_quoted:
+ type: boolean
+ description: Notify when someone quotes your drop
+ drop_replied:
+ type: boolean
+ description: Notify when someone replies to your drop
+ drop_voted:
+ type: boolean
+ description: Notify when someone votes on your drop
+ drop_reacted:
+ type: boolean
+ description: Notify when someone reacts to your drop
+ drop_boosted:
+ type: boolean
+ description: Notify when someone boosts your drop
+ wave_created:
+ type: boolean
+ description: Notify when you are invited to a wave
+ ApiPushNotificationSettingsUpdate:
+ type: object
+ properties:
+ identity_subscribed:
+ type: boolean
+ identity_mentioned:
+ type: boolean
+ identity_rep:
+ type: boolean
+ identity_nic:
+ type: boolean
+ drop_quoted:
+ type: boolean
+ drop_replied:
+ type: boolean
+ drop_voted:
+ type: boolean
+ drop_reacted:
+ type: boolean
+ drop_boosted:
+ type: boolean
+ wave_created:
+ type: boolean
ApiQuotedDrop:
type: object
required:
@@ -8655,7 +8869,6 @@ components:
- participation
- chat
- wave
- - outcomes
- created_at
- contributors_overview
- subscribed_actions
@@ -8695,10 +8908,6 @@ components:
$ref: "#/components/schemas/ApiWaveChatConfig"
wave:
$ref: "#/components/schemas/ApiWaveConfig"
- outcomes:
- type: array
- items:
- $ref: "#/components/schemas/ApiWaveOutcomeOld"
contributors_overview:
type: array
items:
@@ -9133,33 +9342,6 @@ components:
type: array
items:
$ref: "#/components/schemas/ApiWaveOutcomeDistributionItem"
- ApiWaveOutcomeOld:
- type: object
- required:
- - type
- - description
- - index
- properties:
- type:
- $ref: "#/components/schemas/ApiWaveOutcomeType"
- subtype:
- $ref: "#/components/schemas/ApiWaveOutcomeSubType"
- description:
- type: string
- credit:
- $ref: "#/components/schemas/ApiWaveOutcomeCredit"
- rep_category:
- type: string
- amount:
- type: number
- format: int64
- distribution:
- type: array
- items:
- $ref: "#/components/schemas/ApiWaveOutcomeDistributionItem"
- index:
- type: number
- format: int64
ApiWaveOutcomesPage:
type: object
required:
diff --git a/scripts/refresh-api.sh b/scripts/refresh-api.sh
index 3bc5db733b..1afc702156 100644
--- a/scripts/refresh-api.sh
+++ b/scripts/refresh-api.sh
@@ -4,4 +4,4 @@ if [ $# -eq 0 ]
then
BRANCH='main'
fi
-curl https://raw.githubusercontent.com/6529-Collections/6529seize-backend/${BRANCH}/src/api-serverless/openapi.yaml > openapi.yaml
\ No newline at end of file
+curl -H 'Accept: application/vnd.github.v3.raw' "https://api.github.com/repos/6529-Collections/6529seize-backend/contents/src/api-serverless/openapi.yaml?ref=${BRANCH}" > openapi.yaml
\ No newline at end of file
diff --git a/types/feed.types.ts b/types/feed.types.ts
index 9de734de31..be8aa1fd02 100644
--- a/types/feed.types.ts
+++ b/types/feed.types.ts
@@ -33,122 +33,115 @@ export type TypedFeedItem =
| IFeedItemDropCreated
| IFeedItemDropReplied;
-export type INotificationIdentitySubscribed = {
+/**
+ * Base notification fields shared by all notification types.
+ */
+type NotificationBase = {
readonly id: number;
- readonly cause: ApiNotificationCause.IdentitySubscribed;
readonly created_at: number;
readonly read_at: number | null;
readonly related_identity: ApiProfileMin;
};
-export type INotificationIdentityMentioned = {
- readonly id: number;
- readonly cause: ApiNotificationCause.IdentityMentioned;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
+type WithDrops = {
readonly related_drops: Array;
};
-export type INotificationDropVoted = {
- readonly id: number;
- readonly cause: ApiNotificationCause.DropVoted;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
- readonly related_drops: Array;
+export type INotificationIdentitySubscribed = NotificationBase & {
+ readonly cause: ApiNotificationCause.IdentitySubscribed;
+};
+
+export type INotificationIdentityRep = NotificationBase & {
+ readonly cause: ApiNotificationCause.IdentityRep;
readonly additional_context: {
- readonly vote: number;
+ readonly amount: number;
+ readonly total: number;
+ readonly category: string;
};
};
-export type INotificationDropReacted = {
- readonly id: number;
- readonly cause: ApiNotificationCause.DropReacted;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
- readonly related_drops: Array;
+export type INotificationIdentityNic = NotificationBase & {
+ readonly cause: ApiNotificationCause.IdentityNic;
readonly additional_context: {
- readonly reaction: string;
+ readonly amount: number;
+ readonly total: number;
};
};
-export type INotificationDropBoosted = {
- readonly id: number;
- readonly cause: ApiNotificationCause.DropBoosted;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
- readonly related_drops: Array;
- readonly additional_context: Record;
-};
+export type INotificationIdentityMentioned = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.IdentityMentioned;
+ };
-export type INotificationDropQuoted = {
- readonly id: number;
- readonly cause: ApiNotificationCause.DropQuoted;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
- readonly related_drops: Array;
- readonly additional_context: {
- readonly quote_drop_id: string;
- readonly quote_drop_part: string;
- readonly quoted_drop_id: string;
- readonly quoted_drop_part: string;
+export type INotificationDropVoted = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.DropVoted;
+ readonly additional_context: {
+ readonly vote: number;
+ };
};
-};
-export type INotificationDropReplied = {
- readonly id: number;
- readonly cause: ApiNotificationCause.DropReplied;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
- readonly related_drops: Array;
- readonly additional_context: {
- readonly reply_drop_id: string;
- readonly replied_drop_id: string;
- readonly replied_drop_part: string;
+export type INotificationDropReacted = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.DropReacted;
+ readonly additional_context: {
+ readonly reaction: string;
+ };
};
-};
-export type INotificationWaveCreated = {
- readonly id: number;
+export type INotificationDropBoosted = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.DropBoosted;
+ readonly additional_context: Record;
+ };
+
+export type INotificationDropQuoted = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.DropQuoted;
+ readonly additional_context: {
+ readonly quote_drop_id: string;
+ readonly quote_drop_part: string;
+ readonly quoted_drop_id: string;
+ readonly quoted_drop_part: string;
+ };
+ };
+
+export type INotificationDropReplied = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.DropReplied;
+ readonly additional_context: {
+ readonly reply_drop_id: string;
+ readonly replied_drop_id: string;
+ readonly replied_drop_part: string;
+ };
+ };
+
+export type INotificationWaveCreated = NotificationBase & {
readonly cause: ApiNotificationCause.WaveCreated;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
readonly additional_context: {
readonly wave_id: string;
};
};
-export type INotificationAllDrops = {
- readonly id: number;
- readonly cause: ApiNotificationCause.AllDrops;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
- readonly related_drops: Array;
- readonly additional_context: {
- readonly vote: number;
+export type INotificationAllDrops = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.AllDrops;
+ readonly additional_context: {
+ readonly vote: number;
+ };
};
-};
-export type INotificationPriorityAlert = {
- readonly id: number;
- readonly cause: ApiNotificationCause.PriorityAlert;
- readonly created_at: number;
- readonly read_at: number | null;
- readonly related_identity: ApiProfileMin;
- readonly related_drops: Array;
- readonly additional_context: any;
-};
+export type INotificationPriorityAlert = NotificationBase &
+ WithDrops & {
+ readonly cause: ApiNotificationCause.PriorityAlert;
+ readonly additional_context: Record;
+ };
export type TypedNotification =
| INotificationIdentitySubscribed
| INotificationIdentityMentioned
+ | INotificationIdentityRep
+ | INotificationIdentityNic
| INotificationDropVoted
| INotificationDropReacted
| INotificationDropBoosted
@@ -158,6 +151,20 @@ export type TypedNotification =
| INotificationAllDrops
| INotificationPriorityAlert;
+/**
+ * Fallback type for unknown/unsupported notification causes.
+ * Used to render generic notifications that don't match known causes.
+ */
+export type INotificationGeneric = {
+ readonly id: number;
+ readonly cause: string;
+ readonly created_at: number;
+ readonly read_at: number | null;
+ readonly related_identity?: ApiProfileMin;
+ readonly related_drops?: Array;
+ readonly additional_context?: Record;
+};
+
export interface TypedNotificationsResponse extends Omit<
ApiNotificationsResponse,
"notifications"