mobile download

This commit is contained in:
Macbook
2026-05-28 14:44:19 +09:00
parent e2816b037f
commit 3503ef33f5
152 changed files with 11021 additions and 687 deletions
+37
View File
@@ -0,0 +1,37 @@
import React, { createContext, useContext, useMemo, useState, useCallback } from 'react';
import type { TabId } from '../components/TabBar';
interface NavContextValue {
tab: TabId;
setTab: (t: TabId) => void;
focusMarket: string | null;
openVirtualFocus: (market: string) => void;
clearVirtualFocus: () => void;
}
const NavContext = createContext<NavContextValue | null>(null);
export function NavigationProvider({ children }: { children: React.ReactNode }) {
const [tab, setTab] = useState<TabId>('virtual');
const [focusMarket, setFocusMarket] = useState<string | null>(null);
const openVirtualFocus = useCallback((market: string) => {
setFocusMarket(market);
setTab('virtual');
}, []);
const clearVirtualFocus = useCallback(() => setFocusMarket(null), []);
const value = useMemo(
() => ({ tab, setTab, focusMarket, openVirtualFocus, clearVirtualFocus }),
[tab, focusMarket, openVirtualFocus, clearVirtualFocus],
);
return <NavContext.Provider value={value}>{children}</NavContext.Provider>;
}
export function useNavigation() {
const ctx = useContext(NavContext);
if (!ctx) throw new Error('useNavigation requires NavigationProvider');
return ctx;
}
@@ -0,0 +1,216 @@
import {
deleteAllTradeSignals,
deleteTradeSignal,
deleteTradeSignalsBatch,
loadTradeSignals,
type TradeSignalDto,
} from '../lib/shared';
import { getUiPreferences, patchUiPreferences } from '@frontend/utils/uiPreferencesDb';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
export interface TradeSignalInfo {
market: string;
signalType: string;
price: number;
candleTime: number;
strategyName?: string;
strategyId?: number;
executionType?: string;
candleType?: string;
}
export interface TradeNotificationItem extends TradeSignalInfo {
id: string;
dbId?: number;
isRead: boolean;
receivedAt: number;
}
interface TradeNotificationContextValue {
toastNotifications: TradeNotificationItem[];
allNotifications: TradeNotificationItem[];
unreadCount: number;
addNotification: (signal: TradeSignalInfo & { dbId?: number }) => void;
dismissToast: (id: string) => void;
dismissAllToasts: () => void;
markAsRead: (id: string) => void;
markAllAsRead: () => void;
deleteNotification: (id: string) => Promise<void>;
deleteAllNotifications: () => Promise<void>;
refreshHistory: () => Promise<void>;
}
const TradeNotificationContext = createContext<TradeNotificationContextValue | null>(null);
function makeId(signal: Pick<TradeSignalInfo, 'market' | 'candleTime' | 'signalType'>): string {
return `${signal.market}:${signal.candleTime}:${signal.signalType}`;
}
function loadReadIds(): Set<string> {
return new Set(getUiPreferences().tradeNotifications?.readIds ?? []);
}
function saveReadIds(ids: Set<string>) {
patchUiPreferences({ tradeNotifications: { readIds: [...ids] } });
}
function loadHiddenIds(): Set<string> {
return new Set(getUiPreferences().tradeNotifications?.hiddenIds ?? []);
}
function saveHiddenIds(ids: Set<string>) {
patchUiPreferences({ tradeNotifications: { hiddenIds: [...ids] } });
}
function dtoToItem(dto: TradeSignalDto, readIds: Set<string>): TradeNotificationItem {
const id = `${dto.market}:${dto.candleTime}:${dto.signalType}`;
return {
id,
dbId: dto.id,
market: dto.market,
signalType: dto.signalType,
price: dto.price,
candleTime: dto.candleTime,
strategyName: dto.strategyName,
strategyId: dto.strategyId,
executionType: dto.executionType,
candleType: dto.candleType,
isRead: readIds.has(id),
receivedAt: new Date(dto.createdAt).getTime() || dto.candleTime * 1000,
};
}
export function useTradeNotification() {
const ctx = useContext(TradeNotificationContext);
if (!ctx) throw new Error('useTradeNotification requires provider');
return ctx;
}
export function TradeNotificationProvider({
children,
soundEnabled = true,
}: {
children: React.ReactNode;
soundEnabled?: boolean;
}) {
const [toasts, setToasts] = useState<TradeNotificationItem[]>([]);
const [all, setAll] = useState<TradeNotificationItem[]>([]);
const readRef = useRef(loadReadIds());
const hiddenRef = useRef(loadHiddenIds());
const refreshHistory = useCallback(async () => {
readRef.current = loadReadIds();
hiddenRef.current = loadHiddenIds();
const dtos = await loadTradeSignals();
const items = (dtos ?? [])
.map(d => dtoToItem(d, readRef.current))
.filter(i => !hiddenRef.current.has(i.id))
.sort((a, b) => b.receivedAt - a.receivedAt);
setAll(items);
}, []);
useEffect(() => {
void refreshHistory();
const t = setInterval(() => void refreshHistory(), 20000);
return () => clearInterval(t);
}, [refreshHistory]);
const addNotification = useCallback((signal: TradeSignalInfo & { dbId?: number }) => {
const id = makeId(signal);
if (hiddenRef.current.has(id)) return;
const item: TradeNotificationItem = {
...signal,
id,
isRead: readRef.current.has(id),
receivedAt: Date.now(),
};
setAll(prev => {
const filtered = prev.filter(p => p.id !== id);
return [item, ...filtered].slice(0, 300);
});
if (!item.isRead) {
setToasts(prev => [item, ...prev.filter(p => p.id !== id)].slice(0, 50));
if (soundEnabled && typeof navigator !== 'undefined' && 'vibrate' in navigator) {
navigator.vibrate(80);
}
}
}, [soundEnabled]);
const dismissToast = useCallback((id: string) => {
setToasts(prev => prev.filter(t => t.id !== id));
readRef.current.add(id);
saveReadIds(readRef.current);
setAll(prev => prev.map(n => (n.id === id ? { ...n, isRead: true } : n)));
}, []);
const dismissAllToasts = useCallback(() => {
setToasts(prev => {
prev.forEach(t => readRef.current.add(t.id));
saveReadIds(readRef.current);
return [];
});
setAll(prev => prev.map(n => ({ ...n, isRead: true })));
}, []);
const markAsRead = useCallback((id: string) => {
readRef.current.add(id);
saveReadIds(readRef.current);
setAll(prev => prev.map(n => (n.id === id ? { ...n, isRead: true } : n)));
setToasts(prev => prev.filter(t => t.id !== id));
}, []);
const markAllAsRead = useCallback(() => {
all.forEach(n => readRef.current.add(n.id));
saveReadIds(readRef.current);
setAll(prev => prev.map(n => ({ ...n, isRead: true })));
setToasts([]);
}, [all]);
const deleteNotification = useCallback(async (id: string) => {
const item = all.find(n => n.id === id);
hiddenRef.current.add(id);
saveHiddenIds(hiddenRef.current);
if (item?.dbId) await deleteTradeSignal(item.dbId);
setAll(prev => prev.filter(n => n.id !== id));
setToasts(prev => prev.filter(t => t.id !== id));
}, [all]);
const deleteAllNotifications = useCallback(async () => {
await deleteAllTradeSignals();
all.forEach(n => hiddenRef.current.add(n.id));
saveHiddenIds(hiddenRef.current);
setAll([]);
setToasts([]);
}, [all]);
const unreadCount = useMemo(() => all.filter(n => !n.isRead).length, [all]);
const value = useMemo(
() => ({
toastNotifications: toasts,
allNotifications: all,
unreadCount,
addNotification,
dismissToast,
dismissAllToasts,
markAsRead,
markAllAsRead,
deleteNotification,
deleteAllNotifications,
refreshHistory,
}),
[toasts, all, unreadCount, addNotification, dismissToast, dismissAllToasts, markAsRead, markAllAsRead, deleteNotification, deleteAllNotifications, refreshHistory],
);
return (
<TradeNotificationContext.Provider value={value}>{children}</TradeNotificationContext.Provider>
);
}