146 lines
4.4 KiB
TypeScript
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;
|
|
}; |