2025-12-01 15:27:27 +02:00

146 lines
4.4 KiB
TypeScript

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<HttpResponse<LoginResponse>>;
http: <T = any>(url: string, method?: "GET" | "POST", body?: unknown) => Promise<HttpResponse<T>>;
getAvatarUrl: (id?: string, hash?: string | null) => ImageSourcePropType;
populateMessages: (channelId: string, opts?: { before?: string; limit?: number }) => Promise<void>;
sendMessage: (channelId: string, content: string) => Promise<HttpResponse<Message>>
}
const ApiContext = createContext<ApiContextType | null>(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 <T = any>(
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<LoginResponse>(
`${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<Message[]>(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<Message>(
`/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 (
<ApiContext.Provider
value={{
http,
login,
getAvatarUrl,
populateMessages,
sendMessage,
}}
>
{children}
</ApiContext.Provider>
);
};
export const useApi = () => {
const ctx = useContext(ApiContext);
if (!ctx) throw new Error("useApi must be used inside ApiProvider");
return ctx;
};