import React, { createContext, useContext } from "react"; import { useBaseStore } from "../stores/base"; import { useChatStore } from "../stores/chats"; import { Message } from "../types/discord"; import { HttpResponse, LoginResponse } from "../types/types"; import { URLSearchParams } from "react-native-url-polyfill"; import { ImageSourcePropType } from "react-native"; interface ApiContextType { login: (email: string, password: string, instanceUrl: string) => Promise>; http: (url: string, method?: "GET" | "POST", body?: unknown) => Promise>; getAvatarUrl: (id?: string, hash?: string | null) => ImageSourcePropType; populateMessages: (channelId: string, opts?: { before?: string; limit?: number }) => Promise; sendMessage: (channelId: string, content: string) => Promise> } const ApiContext = createContext(null); export const ApiProvider = ({ children }: { children: React.ReactNode }) => { const setToken = useBaseStore((s) => s.setToken); const setBaseUrl = useBaseStore((s) => s.setBaseUrl); const token = useBaseStore((s) => s.token); const baseUrl = useBaseStore((s) => s.baseUrl); const http = async ( url: string, method: "GET" | "POST" = "GET", body?: unknown ) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const isAbsolute = /^https?:\/\//i.test(url); const newUrl = isAbsolute ? url : `${baseUrl}${url}`; try { const res = await fetch(newUrl, { method, headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); const contentType = res.headers.get("content-type") || ""; const data: T | null = contentType.includes("application/json") ? await res.json() : null; if (res.ok) return { ok: true, status: res.status, data }; return { ok: false, status: res.status, data, error: (data as any)?.message || `HTTP ${res.status}` }; } catch (err: any) { const error = err.name === "AbortError" ? "timeout" : err?.message ?? String(err); return { ok: false, status: 0, data: null, error }; } finally { clearTimeout(timeout); } }; const login = async ( email: string, password: string, instanceUrl: string ) => { const res = await http( `${instanceUrl}/api/v9/auth/login`, "POST", { login: email, password } ); if (res.ok && res.data?.token) { setToken(res.data.token); setBaseUrl(instanceUrl); } return res; }; const populateMessages = async ( channelId: string, opts?: { before?: string; limit?: number } ) => { const { before, limit = 50 } = opts ?? {}; const params = new URLSearchParams(); if (before) params.set("before", before); if (limit) params.set("limit", String(limit)); const query = params.toString(); const endpoint = `/api/v9/channels/${channelId}/messages${query ? `?${query}` : ""}`; const res = await http(endpoint, "GET"); if (!res.ok || !res.data) throw new Error(res.error ?? `Failed to fetch messages for channel ${channelId}`); const fetched: Message[] = res.data ?? []; useChatStore.getState().appendOlder(channelId, fetched); }; const sendMessage = async ( channelId: string, content: string ) => { const nonce = Math.floor(Math.random() * 10 ** 8) + ""; const res = await http( `/api/v9/channels/${channelId}/messages`, "POST", { content, nonce, tts: false } ); return res; } const getAvatarUrl = (id?: string, hash?: string | null): ImageSourcePropType => { if (id && hash) { return { uri: `${baseUrl}/avatars/${id}/${hash}` }; } else { return require("../../../assets/default.png"); } }; return ( {children} ); }; export const useApi = () => { const ctx = useContext(ApiContext); if (!ctx) throw new Error("useApi must be used inside ApiProvider"); return ctx; };