};
diff --git a/plugins/githubindiscord/Settings.tsx b/plugins/githubindiscord/Settings.tsx
new file mode 100644
index 0000000..1279e50
--- /dev/null
+++ b/plugins/githubindiscord/Settings.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useState } from "react";
+import { components } from "replugged";
+import { pluginSettings } from "./utils";
+import { default as customTheme } from "./theme";
+import { SelectMenu } from "./components";
+
+const { FormText, TextInput } = components;
+
+export default () => {
+ const [key, setKey] = useState(pluginSettings.get("key", ""));
+ const [darkTheme, setDarkTheme] = useState(pluginSettings.get("darkTheme", "dark_discord"));
+ const [lightTheme, setLightTheme] = useState(pluginSettings.get("lightTheme", "light_discord"));
+
+ useEffect(() => {
+ pluginSettings.set("key", key);
+ pluginSettings.set("darkTheme", darkTheme);
+ pluginSettings.set("lightTheme", lightTheme);
+ }, [key, darkTheme, lightTheme]);
+
+ const darkThemes = Object.keys(customTheme.colorSchemes).filter((t) => t.includes("dark"));
+ const lightThemes = Object.keys(customTheme.colorSchemes).filter((t) => t.includes("light"));
+
+ return (
+
+
+ Github Token (reload for the token to take effect)
+
+
+ Dark Theme
+ {SelectMenu && (
+ ({ label: t, value: t }))}
+ onChange={setDarkTheme}
+ />
+ )}
+ Light Theme
+ {SelectMenu && (
+ ({ label: t, value: t }))}
+ onChange={setLightTheme}
+ />
+ )}
+
+ );
+};
diff --git a/plugins/githubindiscord/index.tsx b/plugins/githubindiscord/index.tsx
index 0be234d..47ea218 100644
--- a/plugins/githubindiscord/index.tsx
+++ b/plugins/githubindiscord/index.tsx
@@ -3,9 +3,10 @@ import { Injector, components, webpack } from "replugged";
import { openGithubModal } from "./Modal";
import { MarkGithubIcon } from "@primer/styled-octicons";
import { Box } from "@primer/react";
-import { ModuleExports, ModuleExportsWithProps } from "replugged/dist/types";
+import type { ModuleExports, ModuleExportsWithProps } from "replugged/dist/types";
const { MenuItem, MenuGroup } = components.ContextMenu;
const { Tooltip } = components;
+export { default as Settings } from "./Settings";
const injector = new Injector();
@@ -24,18 +25,20 @@ export function getExportsForProto<
const ghRegex =
/https?:\/\/(?:www.)?github.com\/([\w-]+\/[\w-]+)(?:\/((?:tree|blob)\/([\w-]+)\/([\w/.?]+)|((issues|pulls)(?:\/([0-9]+))?)))?/g;
-export function start() {
- const e = webpack.getModule<{ ZP: Function }>((m) =>
+export async function start() {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const module = await webpack.waitForModule((m) =>
Boolean(getExportsForProto(m.exports, ["renderTitle"])),
);
- if (!e) return;
- injector.after(e.ZP?.prototype, "renderProvider", (args, res) => {
+
+ if (!module) return;
+
+ injector.after(module.ZP?.prototype, "renderProvider", (_, res) => {
if (!res) return res;
- const link = res._owner?.stateNode?.props?.embed?.url;
+ const link = res._owner?.stateNode?.props?.embed?.url as string;
if (!link) return res;
- const msg = checkMessage(link);
+ const msg = checkMessage(link)?.[0];
if (!msg) return res;
-
return (
{res.props.children.props.children}
@@ -55,26 +58,40 @@ export function stop(): void {
injector.uninjectAll();
}
+const tabs = {
+ issues: "Issues",
+ pulls: "Pull Request",
+};
+
function checkMessage(content: string) {
- const match = [...content.matchAll(ghRegex)]?.[0];
+ const match = [...content.matchAll(ghRegex)];
if (!match) return null;
- const tab = match[2] === "issues" ? "Issues" : match[2] === "pulls" ? "Pull Requests" : "";
- return {
- url: match[1],
- tab,
- };
+
+ return match.map((d) => ({
+ url: d[1],
+ tab: d[2] && tabs[d[2] as keyof typeof tabs],
+ }));
}
export function menu(content: string, href?: string) {
const msg = checkMessage(href || content);
- if (!msg) return null;
+ if (!msg?.length) return null;
+
return (
);
}
diff --git a/plugins/githubindiscord/paginate.ts b/plugins/githubindiscord/paginate.ts
index dd63587..b47dcc4 100644
--- a/plugins/githubindiscord/paginate.ts
+++ b/plugins/githubindiscord/paginate.ts
@@ -1,6 +1,7 @@
-import { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
+import * as OctokitTypes from "@octokit/types";
+import { GetResponseDataTypeFromEndpointMethod } from "@octokit/types";
import { useEffect, useState } from "react";
-import { Issue } from "./utils";
+import { Issue, getCommit, octokit, sortCommits } from "./utils";
const regex = {
prev: /page=([0-9]+)>;\s*rel="prev"/g,
@@ -8,213 +9,344 @@ const regex = {
last: /page=([0-9]+)>;\s*rel="last"/g,
};
-export interface Paginate {
- page: {
- open: Issue[][];
- closed: Issue[][];
- all: Issue[][];
- totalOpen: number;
- totalClosed: number;
- };
- info: {
+export interface Paginate {
+ pageInfo: {
currentPage: number;
- pages: {
- open?: Page;
- closed?: Page;
- all?: Page;
- };
+ lastPage?: number;
+ nextPage?: number;
+ prevPage?: number;
};
- state: "open" | "closed";
- nextPage: () => void;
- previousPage: () => void;
- viewClosed: () => void;
- viewOpen: () => void;
+ pages: T[];
}
-interface Page {
- prev?: number;
- next?: number;
- last: number;
-}
-
-const cache = new Map();
+const cache = new Map();
-export function usePaginate(
- Octokit: Octokit,
- params: RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"],
- { force, onError }: { force?: boolean; onError: (err: string) => void },
+export function usePaginate(
+ octokit: T,
+ params: Parameters[0],
) {
- const [page, setPage] = useState({
- open: [],
- closed: [],
- all: [],
- totalClosed: 0,
- totalOpen: 0,
+ const [data, setData] = useState> | null>(null);
+
+ const fetch = (force?: boolean, savePage?: boolean, currParams?: typeof params) => {
+ if (currParams) params = currParams;
+
+ return new Promise((resolve: (value: NonNullable) => void) =>
+ setData((prevData) => {
+ // if data already exist set current page to 1 if "savePage" isn't set
+ if (!force && prevData) {
+ const info: Paginate["pageInfo"] = {
+ currentPage: savePage ? prevData.pageInfo.currentPage : 1,
+ lastPage: prevData.pageInfo.lastPage,
+ nextPage: prevData.pageInfo.nextPage && 2,
+ };
+ resolve({ ...prevData, pageInfo: info });
+ return { ...prevData, pageInfo: info };
+ // get from cache
+ } else if (
+ !force &&
+ cache.has(
+ JSON.stringify({
+ ...(params as unknown as object),
+ url: octokit.endpoint.DEFAULTS.url,
+ }),
+ )
+ ) {
+ resolve(
+ cache.get(
+ JSON.stringify({
+ ...(params as unknown as object),
+ url: octokit.endpoint.DEFAULTS.url,
+ }),
+ ) as NonNullable,
+ );
+ return cache.get(
+ JSON.stringify({
+ ...(params as unknown as object),
+ url: octokit.endpoint.DEFAULTS.url,
+ }),
+ ) as NonNullable;
+ }
+
+ // get data for the first
+ (async () => {
+ const res = await octokit(params);
+ const page = res.headers.link ? lastAndNextPage(res.headers.link) : undefined;
+ const epicData: typeof data = {
+ pageInfo: {
+ currentPage: 1,
+ lastPage: page?.last,
+ prevPage: page?.prev,
+ nextPage: page?.next,
+ },
+ pages: [res.data],
+ };
+
+ resolve(epicData);
+ setData(epicData);
+ })();
+
+ return prevData;
+ }),
+ );
+ };
+
+ const nextPage = () => {
+ return new Promise((resolve: (value: NonNullable) => void) =>
+ setData((prevData) => {
+ if (!prevData?.pageInfo.nextPage) {
+ resolve(prevData!);
+ return prevData;
+ }
+
+ const info = {
+ currentPage: prevData.pageInfo.currentPage + 1,
+ nextPage:
+ prevData.pageInfo.nextPage < prevData.pageInfo.lastPage!
+ ? prevData.pageInfo.nextPage + 1
+ : undefined,
+ prevPage: prevData.pageInfo.currentPage,
+ lastPage: prevData.pageInfo.lastPage,
+ };
+ // if the next page has already fetch return that
+ if (prevData.pages[prevData.pageInfo.nextPage - 1]) {
+ resolve({
+ ...prevData,
+ pageInfo: info,
+ });
+ return {
+ ...prevData,
+ pageInfo: info,
+ };
+ }
+
+ // fetch the next page for the first time
+ (async () => {
+ // @ts-expect-error die
+ const res = await octokit({ ...params, page: prevData.pageInfo.nextPage });
+ prevData.pages.push(res.data);
+ resolve({ ...prevData, pageInfo: info });
+ setData({ ...prevData, pageInfo: info });
+ })();
+
+ return prevData;
+ }),
+ );
+ };
+
+ const previousPage = () => {
+ return new Promise((resolve: (value: NonNullable) => void) =>
+ setData((prevData) => {
+ if (!prevData?.pageInfo.prevPage) {
+ resolve(prevData!);
+ return prevData;
+ }
+ const info = {
+ currentPage: prevData.pageInfo.currentPage - 1,
+ nextPage: prevData.pageInfo.currentPage - 1 === 1 ? 2 : prevData.pageInfo.nextPage! - 1,
+ prevPage: prevData.pageInfo.prevPage === 1 ? undefined : prevData.pageInfo.prevPage - 1,
+ lastPage: prevData.pageInfo.lastPage,
+ };
+ resolve({ ...prevData, pageInfo: info });
+ return { ...prevData, pageInfo: info };
+ }),
+ );
+ };
+
+ useEffect(() => {
+ if (!data) return;
+ // save to cache when ever "data" changes
+ // console.log(params);
+ cache.set(
+ JSON.stringify({ ...(params as unknown as object), url: octokit.endpoint.DEFAULTS.url }),
+ data,
+ );
+ }, [data]);
+
+ return { data, fetch, nextPage, previousPage };
+}
+
+export function useIssues(repo: string, type: "issue" | "pr") {
+ const openedPaginate = usePaginate(octokit.search.issuesAndPullRequests, {
+ q: `repo:${repo} is:${type} is:open`,
});
- const [pageInfo, setPageInfo] = useState({
- currentPage: 1,
- pages: {},
+ const closedPaginate = usePaginate(octokit.search.issuesAndPullRequests, {
+ q: `repo:${repo} is:${type} is:closed`,
});
- const [state, setState] = useState<"open" | "closed">("open");
- useEffect(() => {
- try {
- (async () => {
- if (!force && cache.has(params.q)) {
- setPage(cache.get(params.q)!.page);
- setPageInfo({ currentPage: 1, pages: cache.get(params.q)!.info });
- return;
- } else cache.forEach((_, k) => k === params.q && cache.delete(k));
- const openedIssues = await Octokit.search.issuesAndPullRequests({
- ...params,
- q: `${params.q} state:open`,
- });
- setPage({
- all: [],
- closed: [],
- open: [[...openedIssues.data.items]],
- totalOpen: openedIssues.data.total_count,
- totalClosed: 0,
- });
-
- const info = openedIssues.headers.link
- ? lastAndNextPage(openedIssues.headers.link)
- : undefined;
- setPageInfo({ currentPage: 1, pages: { open: info } });
- setState("open");
- cache.set(params.q, {
- info: {
- open: info,
- },
- page: {
- all: [],
- closed: [],
- open: [[...openedIssues.data.items]],
- totalClosed: 0,
- totalOpen: openedIssues.data.total_count,
- },
- });
- })();
- } catch (err) {
- // @ts-expect-error stfu
- onError(err.message as string);
- }
- }, [force]);
+ const [data, setData] = useState<{
+ open: Issue[][];
+ closed: Issue[][];
+ all: Issue[][];
+ totalOpen: number;
+ totalClosed: number;
+ state: "open" | "closed";
+ } | null>(null);
- const nextPage = () => {
- try {
- // "page" won't be update to date so use this
- setPage((prevPageState) => {
- setPageInfo((prevInfoState) => {
- const info = prevInfoState.pages[state];
- if (!info?.next) return prevInfoState;
- // setting the next page
- if (prevPageState[state].length >= info.next) {
- info.prev = prevInfoState.currentPage;
- if (info.last <= info.next) info.next = undefined;
- else ++info.next;
- ++prevInfoState.currentPage;
- return { ...prevInfoState };
- }
- (async () => {
- if (!info?.next) return;
- const request = await Octokit.search.issuesAndPullRequests({
- ...params,
- q: `${params.q} state:${state}`,
- page: info.next,
+ const fetch = (force?: boolean) => {
+ return new Promise((res) =>
+ setData((prevData) => {
+ (async () => {
+ const openedRes = await openedPaginate.fetch(force);
+ const closedRes = await closedPaginate.fetch(force);
+
+ if (!prevData || force) {
+ res(null);
+ setData({
+ all: [],
+ closed: closedRes.pages.map((p) => p.items),
+ open: openedRes.pages.map((p) => p.items),
+ totalOpen: openedRes.pages[0].total_count,
+ totalClosed: closedRes.pages[0].total_count,
+ state: "open",
});
- prevPageState[state].push([...request.data.items]);
- setPage(prevPageState);
- setPageInfo((p) => ({
- currentPage: ++prevInfoState.currentPage,
- pages: {
- ...p.pages,
- [state]: { ...lastAndNextPage(request.headers.link!), last: p.pages[state]!.last },
- },
- }));
- })();
- return prevInfoState;
- });
- // return we only need the current state
- return prevPageState;
- });
- } catch (err) {
- // @ts-expect-error stfu
- onError(err.message);
- }
+ } else res(null);
+ })();
+
+ return prevData;
+ }),
+ );
};
- const previousPage = () => {
- setPage((prevPageState) => {
- setPageInfo((prevInfoState) => {
- const info = prevInfoState.pages[state];
- if (!info?.prev) return prevInfoState;
- if (prevInfoState.currentPage === 2) info.prev = undefined;
- else --info.prev;
- --prevInfoState.currentPage;
- const next = info.next! || info.last;
- info.next = next - 1;
- return { ...prevInfoState };
- });
- return prevPageState;
+ const nextPage = () => {
+ setData((prevData) => {
+ (async () => {
+ if (!prevData) return;
+ const res =
+ prevData.state === "open"
+ ? await openedPaginate.nextPage()
+ : await closedPaginate.nextPage();
+ prevData[prevData.state] = res.pages.map((p) => p.items);
+ setData({ ...prevData });
+ })();
+ return prevData;
});
};
- const viewClosed = () => {
- setPage((prevPageState) => {
- setPageInfo((prevInfoState) => {
- if (prevInfoState.pages[state]) {
- prevInfoState.pages[state]!.next = 2;
- prevInfoState.pages[state]!.prev = undefined;
- }
- return { ...prevInfoState, currentPage: 1 };
- });
- if (prevPageState.closed.length) {
- setState("closed");
- return { ...prevPageState };
- } else {
- setPageInfo((prevInfoState) => {
- (async () => {
- const closedIssues = await Octokit.search.issuesAndPullRequests({
- ...params,
- q: `${params.q} state:closed`,
- });
- setPage((prev) => ({
- ...prev,
- closed: [[...closedIssues.data.items]],
- totalClosed: closedIssues.data.total_count,
- }));
-
- const info = closedIssues.headers.link
- ? lastAndNextPage(closedIssues.headers.link)
- : undefined;
-
- setPageInfo((prev) => ({ ...prev, pages: { ...prev.pages, closed: info } }));
- setState("closed");
- })();
- return prevInfoState;
- });
- }
- return prevPageState;
+ const previousPage = () => {
+ setData((prevData) => {
+ if (!prevData) return prevData;
+ if (prevData.state === "open") void openedPaginate.previousPage();
+ else void closedPaginate.previousPage();
+ return prevData;
});
};
const viewOpen = () => {
- setPageInfo((prevInfoState) => {
- if (prevInfoState.pages[state]) {
- prevInfoState.pages[state]!.next = 2;
- prevInfoState.pages[state]!.prev = undefined;
+ setData((prev) => prev && { ...prev, state: "open" });
+ };
+
+ const viewClosed = () => {
+ setData((prevData) => {
+ if (prevData?.closed.length) {
+ void closedPaginate.fetch(); // set page to 1
+ return { ...prevData, state: "closed" };
}
- return { ...prevInfoState, currentPage: 1 };
+ (async () => {
+ if (!prevData) return;
+ const res = await closedPaginate.fetch();
+ prevData.closed = res.pages.map((p) => p.items);
+ // prevData.currentIdx = 1;
+ prevData.state = "closed";
+ prevData.totalClosed = res.pages[0].total_count;
+ setData({ ...prevData });
+ })();
+ return prevData;
});
- setState("open");
};
+ return {
+ data,
+ fetch,
+ nextPage,
+ previousPage,
+ viewClosed,
+ viewOpen,
+ info: { open: openedPaginate, closed: closedPaginate },
+ };
+}
+
+export function useTimeline(url: string, issue: number) {
+ const paginate = usePaginate(octokit.issues.listEventsForTimeline, {
+ owner: url.split("/")[0],
+ repo: url.split("/")[1],
+ issue_number: issue,
+ });
+ const [data, setData] = useState["pages"][0] | null>(null);
+
useEffect(() => {
- cache.set(params.q, { info: pageInfo.pages, page });
- }, [pageInfo, page]);
+ void paginate.fetch(false, true);
+ }, []);
+
+ useEffect(() => {
+ if (!paginate.data?.pages) return;
+ setData((prevData) => {
+ (async () => {
+ const data = await Promise.all(
+ paginate.data!.pages.map(async (t) => {
+ return await Promise.all(
+ t.map(async (t) => {
+ if (t.event === "committed") {
+ const commit = cache.get(`${url}${t.sha}`) || (await getCommit(url, t.sha!));
+ cache.set(`${url}${t.sha}`, commit);
+ // @ts-expect-error now it does
+ t.commit = commit;
+ }
+ return t;
+ }),
+ );
+ }),
+ );
+ setData([...data.flat()]);
+ })();
+ return prevData;
+ });
+ }, [paginate.data?.pages.length]);
+
+ return { ...paginate, data: { page: data, info: paginate.data?.pageInfo } };
+}
+
+export function useCommits(url: string, { pr, branch }: { pr?: number; branch?: string }) {
+ const paginate = usePaginate(pr ? octokit.pulls.listCommits : octokit.repos.listCommits, {
+ owner: url.split("/")[0],
+ repo: url.split("/")[1],
+ pull_number: pr,
+ sha: pr ? undefined : branch,
+ });
+ const [data, setData] = useState["pages"]> | null>(
+ null,
+ );
+
+ const fetch = async (force?: boolean, currBranch?: string) => {
+ branch = currBranch;
+ const page = await paginate.fetch(force, false, {
+ owner: url.split("/")[0],
+ repo: url.split("/")[1],
+ pull_number: pr,
+ sha: pr ? undefined : branch,
+ });
+ const pages = page.pages.map((p) => {
+ return p.map((c) => {
+ if (cache.has(`${url}${c.sha}`)) return cache.get(`${url}${c.sha}`) as typeof c;
+ return c;
+ });
+ });
+ const data = pages.map((p) => sortCommits(p));
+ setData(data);
+ return data;
+ };
+
+ const nextPage = async () => {
+ const page = await paginate.nextPage();
+ const data = page.pages.map((p) => {
+ return p.map((c) => {
+ if (cache.has(`${url}${c.sha}`)) return cache.get(`${url}${c.sha}`) as typeof c;
+ return c;
+ });
+ });
+ setData(data.map((p) => sortCommits(p)));
+ };
- return { info: pageInfo, page, nextPage, previousPage, state, viewClosed, viewOpen };
+ return { ...paginate, nextPage, fetch, data: { page: data, info: paginate.data?.pageInfo } };
}
function lastAndNextPage(link: string) {
diff --git a/plugins/githubindiscord/parser.tsx b/plugins/githubindiscord/parser.tsx
new file mode 100644
index 0000000..2c4d2c7
--- /dev/null
+++ b/plugins/githubindiscord/parser.tsx
@@ -0,0 +1,198 @@
+import { useTheme } from "@primer/react";
+import { ReactNode } from "react";
+import { common, webpack } from "replugged";
+import { Parser } from "replugged/dist/renderer/modules/webpack/common";
+import { AnyFunction } from "replugged/dist/types";
+import { textClasses } from "./components";
+import { classes } from "./utils";
+
+const defaultParser = webpack.getByProps(
+ "sanitizeText",
+ "markdownToReact",
+ "defaultRules",
+ "sanitizeUrl",
+);
+const defaultRules = defaultParser?.defaultRules as typeof common.parser.defaultRules;
+
+// const regex = /^( *)(?:[*+-]) [\s\S]+?(?:\n{1,}(?! )(?!\1(?:[*+-]|\d+\.) )\n*|\s*\n*$)/
+const headings = {
+ h1: textClasses?.["heading-xxl/bold"],
+ h2: textClasses?.["heading-xl/bold"],
+ h3: textClasses?.["heading-lg/bold"],
+ h4: textClasses?.["heading-md/bold"],
+ h5: textClasses?.["heading-sm/bold"],
+ h6: textClasses?.["heading-sm/bold"],
+};
+
+const rules: Parser["defaultRules"] = {
+ ...defaultRules,
+ heading: {
+ ...defaultRules.heading,
+ // order: 30,
+ match: (source) => /^ *(#{1,6}) (.+)(?:\n)?/.exec(source),
+ // @ts-expect-error okay
+ react(props: { content: string; level: number }, t: AnyFunction, n: { key: string }) {
+ const { theme } = useTheme();
+ const Element = `h${props.level}` as keyof JSX.IntrinsicElements;
+ return (
+
+ {t(props.content, n) as ReactNode}
+
+ );
+ },
+ },
+ list: {
+ ...defaultRules.list,
+ match(source, state) {
+ const prev = /(?:^|\n)( *)$/.exec(state.prevCapture ? state.prevCapture[0] : "");
+
+ if (prev) {
+ source = prev[1] + source;
+ return /^( *)((?:[*+-]|\d+\.)) [\s\S]+?(?:\n{1,}(?! )(?!\1(?:[*+-]|\d+\.) )\n*|\s*\n*$)/.exec(
+ source,
+ );
+ }
+ return null;
+ },
+ // @ts-expect-error okay
+ react(
+ props: { ordered: boolean; start: string; items: string[] },
+ t: AnyFunction,
+ n: { key: string },
+ ) {
+ const Element = props.ordered ? "ol" : ("ul" as keyof JSX.IntrinsicElements);
+ return (
+
+ {props.items.map((l, i) => (
+ {t(l, n) as ReactNode}
+ ))}
+
+ );
+ },
+ },
+ blockQuote: {
+ ...defaultRules.blockQuote,
+ match: (source) => /^( *>[^\n]+(\n[^\n]+)*\n*)+/.exec(source),
+ // @ts-expect-error okay
+ react(props: { content: string }, t: AnyFunction, n: { key: string }) {
+ const { theme } = useTheme();
+ return (
+
+ {t(props.content, n) as ReactNode}
+
+ );
+ },
+ },
+ codeBlock: common.parser.defaultRules.codeBlock,
+ image: {
+ ...defaultRules.image,
+ match: (source) =>
+ /^|^!\[((?:\[[^\]]*\]|[^[\]]|\](?=[^[]*\]))*)\]\(\s*((?:\([^)]*\)|[^\s\\]|\\.)*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*\)/.exec(
+ source,
+ ),
+ parse(match) {
+ return {
+ alt: /alt="(.*?)"/.exec(match[0])?.[1] || match[1],
+ width: /width="(.*?)"/.exec(match[0])?.[1],
+ height: /height="(.*?)"/.exec(match[0])?.[1],
+ src: /src="(.*?)"/.exec(match[0])?.[1] || match[2],
+ title: match[3],
+ };
+ },
+ // @ts-expect-error okay
+ react(
+ props: { alt: string; width?: string; height?: string; src: string; title?: string },
+ _t: AnyFunction,
+ n: { key: string },
+ ) {
+ return (
+
+ );
+ },
+ },
+ url: {
+ ...defaultRules.url,
+ match: (source) => /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/.exec(source),
+ },
+ link: {
+ ...defaultRules.link,
+ // @ts-expect-error okay
+ react(props, t: AnyFunction, n: { key: string }) {
+ return (
+ string)(props.target)}
+ title={props.title}
+ target="_blank">
+ {t(props.content, n) as ReactNode}
+
+ );
+ },
+ },
+ details: {
+ order: 5,
+ match: (source) => /^(.+)<\/summary>(.+)<\/details>/s.exec(source),
+ // @ts-expect-error okay
+ parse(match: string[], t: AnyFunction, n: { key: string }) {
+ return {
+ summary: match[1].trim(),
+ content: t(match[2].trim(), n),
+ };
+ },
+ // @ts-expect-error okay
+ react(props: { content: unknown[]; summary: string }, t: AnyFunction, n: { key: string }) {
+ return (
+
+ {props.summary}
+ {t(props.content, n) as ReactNode}
+
+ );
+ },
+ },
+ italic: {
+ order: 20,
+ match: (source) => /^\*((?:\\[\s\S]|[^\\])+?)\*/.exec(source),
+ // @ts-expect-error okay
+ parse(match: RegExpExecArray, t: AnyFunction, n: { key: string }) {
+ return {
+ content: t(match[1], n),
+ };
+ },
+ // @ts-expect-error okay
+ react(props: { content: string[] }, t: AnyFunction, n: { key: string }) {
+ return {t(props.content, n) as ReactNode};
+ },
+ },
+};
+
+delete rules.Array;
+delete rules.paragraph;
+
+const parse = (
+ defaultParser?.parserFor as (rules: typeof defaultRules) => (source: string) => unknown
+)(rules);
+const reactOutput = (
+ defaultParser?.outputFor as (rules: typeof defaultRules, outputFor: "react") => AnyFunction
+)(rules, "react");
+console.log(rules);
+export function parseMarkdown(markdown: string): ReactNode {
+ return reactOutput(parse(markdown)) as ReactNode;
+}
diff --git a/plugins/githubindiscord/style.scss b/plugins/githubindiscord/style.scss
index 357c87f..fc2ae70 100644
--- a/plugins/githubindiscord/style.scss
+++ b/plugins/githubindiscord/style.scss
@@ -1,3 +1,15 @@
+.errorModal {
+ background-color: var(--modal-background) !important;
+}
+
+.gid-heading {
+ padding-bottom: 0.3em;
+ border-style: solid;
+ border-bottom-width: 1px;
+ margin-top: 24px;
+ margin-bottom: 16px;
+}
+
.githubModel {
// color: var(--text-normal);
width: fit-content;
diff --git a/plugins/githubindiscord/utils.ts b/plugins/githubindiscord/utils.ts
index 8602da8..d0bcd62 100644
--- a/plugins/githubindiscord/utils.ts
+++ b/plugins/githubindiscord/utils.ts
@@ -2,7 +2,7 @@ import { settings } from "replugged";
import { Octokit } from "@octokit/rest";
import { components, operations } from "@octokit/openapi-types";
import { useEffect, useState } from "react";
-import { usePaginate } from "./paginate";
+import { useCommits, useIssues } from "./paginate";
export type Branch = components["schemas"]["short-branch"] & {
commit: components["schemas"]["commit"];
@@ -31,9 +31,18 @@ export type Issue = components["schemas"]["issue-search-result-item"] & {
marked?: boolean;
};
-export const pluginSettings = await settings.init("dev.eboi.githubindiscord");
-
-const octokit = new Octokit({ auth: pluginSettings.get("key") });
+export const pluginSettings = await settings.init<{
+ key: string;
+ darkTheme: string;
+ lightTheme: string;
+ view: "standard" | "treeview";
+}>("dev.eboi.githubindiscord", {
+ darkTheme: "dark_discord",
+ lightTheme: "light_discord",
+ view: "standard",
+});
+
+export const octokit = new Octokit({ auth: pluginSettings.get("key") });
const cache = new Map<
string,
{
@@ -81,48 +90,35 @@ export function useRepo({ url, query }: { url: string; query: RepoQuery }) {
tags: Array;
tree: TreeWithContent[];
branches: Branch[];
+ currentBranch: Branch;
+ url: string;
}>();
- const [status, setStatus] = useState<"loading" | "err" | "complete">("loading");
- const [error, setError] = useState();
+ const [status, setStatus] = useState<"loading" | "complete">("loading");
const [iQuery, setQuery] = useState(query);
const [force, setForce] = useState(false);
- const issues = usePaginate(
- octokit,
- { q: `repo:${url} is:issue` },
- {
- force,
- onError: (e) => {
- setStatus("err");
- setError(e);
- },
- },
- );
- const prs = usePaginate(
- octokit,
- { q: `repo:${url} is:pr` },
- {
- force,
- onError: (e) => {
- setStatus("err");
- setError(e);
- },
- },
- );
+ const issues = useIssues(url, "issue");
+ const prs = useIssues(url, "pr");
+ const commits = useCommits(url, { branch: repo?.currentBranch.name });
useEffect(() => {
setStatus("loading");
(async () => {
- try {
- const r = await getAll(url, iQuery, force);
- setRepo(r);
- setForce(false);
- setStatus("complete");
- } catch (err) {
- // @ts-expect-error stfu
- setError(err.message as string);
- setStatus("err");
- console.error(err);
- }
+ const r = await getAll(url, iQuery, force);
+ await issues.fetch(force);
+ await prs.fetch(force);
+
+ const currentBranch = iQuery.branch
+ ? r.branches.find((b) => b.name === iQuery.branch)!
+ : r.branches[0];
+ await commits.fetch(force, currentBranch.name);
+
+ setRepo({
+ ...r,
+ url,
+ currentBranch,
+ });
+ setForce(false);
+ setStatus("complete");
})();
}, [JSON.stringify(iQuery), url, force]);
@@ -132,7 +128,17 @@ export function useRepo({ url, query }: { url: string; query: RepoQuery }) {
setQuery(q);
};
- return { data: (repo && { ...repo, issues, prs }) || null, status, error, refetch };
+ const switchBranch = (branch: string) => {
+ if (branch === repo?.currentBranch.name) return;
+ refetch({ ...query, branch });
+ };
+
+ return {
+ data: (repo && { ...repo, issues, prs, commits }) || null,
+ status,
+ refetch,
+ switchBranch,
+ };
}
export async function getBranches(
@@ -142,6 +148,7 @@ export async function getBranches(
const branches = await octokit.repos.listBranches({
owner: url.split("/")[0]!,
repo: url.split("/")[1],
+ per_page: 100,
...query,
});
return await Promise.all(
@@ -255,8 +262,8 @@ export async function getTimeline(
});
return await Promise.all(
timeline.data.map(async (t) => {
- if (t.event === "commented" || t.event === "reviewed")
- t.body = await getMarkdown(t.body ?? "*No description provided.*");
+ // if (t.event === "commented" || t.event === "reviewed")
+ // t.body = await getMarkdown(t.body ?? "*No description provided.*");
// @ts-expect-error now it does
if (t.event === "committed") t.commit = await getCommit(url, t.sha!);
return t;
@@ -333,6 +340,29 @@ function sortTree(tree: components["schemas"]["git-tree"]["tree"]) {
return arr;
}
+export function sortCommits(commits: Array>) {
+ const arr: Array = [[commits[0]]];
+ let currentIdx = 0;
+
+ commits.sort((a, b) => {
+ const aDate = new Date(a.commit.author!.date!),
+ bDate = new Date(b.commit.author!.date!);
+
+ if (aDate.getMonth() === bDate.getMonth() && aDate.getDate() === bDate.getDate())
+ arr[currentIdx].push(a);
+ else {
+ ++currentIdx;
+ arr.push([a]);
+ }
+
+ return 1;
+ });
+
+ return arr;
+}
+
+export const classes = (...classes: unknown[]) => classes.filter(Boolean).join(" ");
+
export function abbreviateNumber(value: number): string {
// eslint-disable-next-line new-cap
return Intl.NumberFormat(undefined, { notation: "compact" }).format(value);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f908601..340d644 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,7 @@ specifiers:
'@electron/asar': ^3.2.1
'@octokit/openapi-types': ^14.0.0
'@octokit/rest': ^19.0.5
+ '@octokit/types': ^8.1.1
'@primer/react': ^35.16.0
'@primer/styled-octicons': ^17.10.0
'@types/node': ^18.11.2
@@ -20,7 +21,7 @@ specifiers:
prettier: ^2.8.1
react: ^18.2.0
react-dom: ^18.2.0
- replugged: 4.0.0-beta0.21
+ replugged: 4.0.0-beta0.22
styled-components: ^5.3.6
tsx: ^3.10.3
typescript: ^4.8.4
@@ -37,6 +38,7 @@ dependencies:
devDependencies:
'@electron/asar': 3.2.2
'@octokit/openapi-types': 14.0.0
+ '@octokit/types': 8.1.1
'@types/node': 18.11.18
'@types/react': 18.0.26
'@typescript-eslint/eslint-plugin': 5.47.1_txmweb6yn7coi7nfrp22gpyqmy
@@ -48,7 +50,7 @@ devDependencies:
eslint-plugin-node: 11.1.0_eslint@8.30.0
eslint-plugin-react: 7.31.11_eslint@8.30.0
prettier: 2.8.1
- replugged: 4.0.0-beta0.21
+ replugged: 4.0.0-beta0.22
tsx: 3.12.1
typescript: 4.9.4
@@ -2368,8 +2370,8 @@ packages:
engines: {node: '>=8'}
dev: true
- /replugged/4.0.0-beta0.21:
- resolution: {integrity: sha512-ImyGRnovKTVGNbz4x0nug1xh0kOHnwFSNZI55Lr0X5klzeDHDgsa55HXnL4mbkNQl10fBN3wiUgfN3MOOEuDMw==}
+ /replugged/4.0.0-beta0.22:
+ resolution: {integrity: sha512-zR61aYEQmOEBcSxJEzx0sfd0EsN348uVE06kzWXNtBpQeWzlp0c4sZCz4DNZ5hvs/aavRFvUNpoaMvtM9QJrbw==}
engines: {node: '>=14.0.0'}
dependencies:
'@octokit/rest': 19.0.5
diff --git a/scripts/build.ts b/scripts/build.ts
index 33cb5f9..f41d088 100644
--- a/scripts/build.ts
+++ b/scripts/build.ts
@@ -6,7 +6,7 @@ import { cp, mkdir, readdir, rm, writeFile } from "fs/promises";
import { PluginManifest } from "replugged/dist/types/addon";
import { pathToFileURL } from "url";
-const NODE_VERSION = "14";
+// const NODE_VERSION = "14";
const CHROME_VERSION = "91";
const globalModules: esbuild.Plugin = {
@@ -95,12 +95,14 @@ const watch = process.argv.includes("--watch");
const common: esbuild.BuildOptions = {
absWorkingDir: join(__dirname, ".."),
bundle: true,
- minify: true,
- sourcemap: false,
- format: "cjs" as esbuild.Format,
+ minify: !watch,
+ sourcemap: !watch,
+ format: "esm" as esbuild.Format,
logLevel: "info",
watch,
plugins: [install, globalModules, sassPlugin()],
+ platform: "browser",
+ target: `chrome${CHROME_VERSION}`,
};
async function buildPlugin(path: string): Promise {
@@ -115,55 +117,19 @@ async function buildPlugin(path: string): Promise {
esbuild.build({
...common,
entryPoints: [join(path, manifest.renderer)],
- platform: "browser",
- target: `chrome${CHROME_VERSION}`,
outfile: `dist/${manifest.id}/renderer.js`,
- format: "esm" as esbuild.Format,
}),
);
manifest.renderer = "renderer.js";
}
- if ("preload" in manifest) {
- targets.push(
- esbuild.build({
- ...common,
- entryPoints: [join(path, manifest.preload)],
- platform: "node",
- target: [`node${NODE_VERSION}`, `chrome${CHROME_VERSION}`],
- outfile: `dist/${manifest.id}/preload.js`,
- external: ["electron"],
- }),
- );
-
- manifest.preload = "preload.js";
- }
-
- if ("main" in manifest) {
- targets.push(
- esbuild.build({
- ...common,
- entryPoints: [join(path, manifest.main)],
- platform: "node",
- target: `node${NODE_VERSION}`,
- outfile: `dist/${manifest.id}/main.js`,
- external: ["electron"],
- }),
- );
-
- manifest.main = "main.js";
- }
-
if ("plaintextPatches" in manifest) {
targets.push(
esbuild.build({
...common,
entryPoints: [join(path, manifest.plaintextPatches)],
- platform: "browser",
- target: `chrome${CHROME_VERSION}`,
outfile: `dist/${manifest.id}/plaintextPatches.js`,
- format: "esm" as esbuild.Format,
}),
);
diff --git a/tsconfig.json b/tsconfig.json
index 8e58829..10ca0c3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -99,5 +99,6 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
- }
+ },
+ "exclude": ["node_modules"]
}