refactor: add utils folder

This commit is contained in:
sevichecc 2023-04-16 22:25:34 +08:00
parent d5860b63a0
commit 54a6188bc5
Signed by untrusted user who does not match committer: SevicheCC
GPG key ID: C577000000000000
9 changed files with 164 additions and 71 deletions

View file

@ -37,6 +37,22 @@
"placeholder": "default value : 20" "placeholder": "default value : 20"
} }
] ]
},
{
"name": "my-status",
"title": "View My Status",
"description": "View your statuses",
"mode": "view",
"preferences": [
{
"name": "statusLimit",
"type": "textfield",
"required": false,
"title": "Maximum number of statuses",
"description": "Maximum number of statuses to be show",
"placeholder": "default value : 20"
}
]
} }
], ],
"preferences": [ "preferences": [

View file

@ -1,22 +1,16 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Action, ActionPanel, List, Toast, showToast, Cache } from "@raycast/api"; import { Action, ActionPanel, List, Toast, showToast, Cache } from "@raycast/api";
import { BookmarkedStatus, AkkomaError } from "./utils/types";
import { NodeHtmlMarkdown } from "node-html-markdown"; import { Status, AkkomaError } from "./utils/types";
import apiServer from "./utils/api"; import apiServer from "./utils/api";
import { authorize } from "./utils/oauth"; import { authorize } from "./utils/oauth";
import { statusParser } from "./utils/util";
const cache = new Cache(); const cache = new Cache();
const nhm = new NodeHtmlMarkdown();
const dateTimeFormatter = new Intl.DateTimeFormat("default", {
hour: "numeric",
minute: "numeric",
day: "numeric",
month: "long",
});
export default function BookmarkCommand() { export default function BookmarkCommand() {
const cached = cache.get("latest_bookmarks"); const cached = cache.get("latest_bookmarks");
const [bookmarks, setBookmarks] = useState<BookmarkedStatus[]>(cached ? JSON.parse(cached) : []); const [bookmarks, setBookmarks] = useState<Status[]>(cached ? JSON.parse(cached) : []);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
@ -25,7 +19,7 @@ export default function BookmarkCommand() {
await authorize(); await authorize();
showToast(Toast.Style.Animated, "Loading bookmarks..."); showToast(Toast.Style.Animated, "Loading bookmarks...");
const newBookmarks = await apiServer.fetchBookmarks(); const newBookmarks = await apiServer.fetchBookmarks();
setBookmarks(newBookmarks); setBookmarks((prevBookmarks) => [...prevBookmarks, ...newBookmarks]);
showToast(Toast.Style.Success, "Bookmarked has been loaded"); showToast(Toast.Style.Success, "Bookmarked has been loaded");
cache.set("latest_bookmarks", JSON.stringify(newBookmarks)); cache.set("latest_bookmarks", JSON.stringify(newBookmarks));
} catch (error) { } catch (error) {
@ -38,23 +32,13 @@ export default function BookmarkCommand() {
getBookmark(); getBookmark();
}, []); }, []);
const parseStatus = ({ content, media_attachments, account, created_at }: BookmarkedStatus) => {
const images = media_attachments.filter((attachment) => attachment.type === "image");
const parsedImages = images.reduce((link, image) => link + `![${image.description}](${image.remote_url})`, "");
const date = new Date(created_at);
const parsedTime = dateTimeFormatter.format(date);
return ` _@${account.acct} (${parsedTime})_ ` + nhm.translate("<br>" + content) + parsedImages;
};
return ( return (
<List isShowingDetail isLoading={isLoading} searchBarPlaceholder="Search bookmarks"> <List isShowingDetail isLoading={isLoading} searchBarPlaceholder="Search bookmarks">
{bookmarks?.map((bookmark) => ( {bookmarks?.map((bookmark) => (
<List.Item <List.Item
title={bookmark.pleroma.content["text/plain"]} title={bookmark.pleroma.content["text/plain"]}
key={bookmark.id} key={bookmark.id}
detail={<List.Item.Detail markdown={parseStatus(bookmark)} />} detail={<List.Item.Detail markdown={statusParser(bookmark)} />}
actions={ actions={
<ActionPanel> <ActionPanel>
<Action.OpenInBrowser title="Open Original Status" url={bookmark.url} /> <Action.OpenInBrowser title="Open Original Status" url={bookmark.url} />

View file

@ -1,19 +1,18 @@
import { getPreferenceValues, Color, Icon, Form } from "@raycast/api"; import { getPreferenceValues, Color, Icon, Form } from "@raycast/api";
import { Preference, VisibilityOption } from "../types"; import { Preference, VisibilityOption } from "../utils/types";
const VisibilityDropdown = () => { const visibilityOptions: VisibilityOption[] = [
const { defaultVisibility }: Preference = getPreferenceValues();
const visibilityOptions: VisibilityOption[] = [
{ value: "public", title: "Public", icon: Icon.Livestream }, { value: "public", title: "Public", icon: Icon.Livestream },
{ value: "unlisted", title: "Unlisted", icon: Icon.LivestreamDisabled }, { value: "unlisted", title: "Unlisted", icon: Icon.LivestreamDisabled },
{ value: "private", title: "Followers-only", icon: Icon.TwoPeople }, { value: "private", title: "Followers-only", icon: Icon.TwoPeople },
{ value: "direct", title: "Direct", icon: Icon.Envelope }, { value: "direct", title: "Direct", icon: Icon.Envelope },
{ value: "local", title: "Local-only", icon: Icon.Pin }, { value: "local", title: "Local-only", icon: Icon.Pin },
]; ];
const VisibilityDropdown = () => {
const { defaultVisibility }: Preference = getPreferenceValues();
return ( return (
<>
<Form.Dropdown id="visibility" title="Visibility" storeValue={true} defaultValue={defaultVisibility}> <Form.Dropdown id="visibility" title="Visibility" storeValue={true} defaultValue={defaultVisibility}>
{visibilityOptions.map(({ value, title, icon }) => ( {visibilityOptions.map(({ value, title, icon }) => (
<Form.Dropdown.Item <Form.Dropdown.Item
@ -24,7 +23,6 @@ const VisibilityDropdown = () => {
/> />
))} ))}
</Form.Dropdown> </Form.Dropdown>
</>
); );
}; };

51
src/my-status.tsx Normal file
View file

@ -0,0 +1,51 @@
import { useEffect, useState } from "react";
import { Action, ActionPanel, List, Toast, showToast, Cache } from "@raycast/api";
import { Status, AkkomaError } from "./utils/types";
import { authorize } from "./utils/oauth";
import apiServer from "./utils/api";
import { statusParser } from "./utils/util";
const cache = new Cache();
export default function ViewStatusCommand() {
const cached = cache.get("latest_statuses");
const [status, setStatus] = useState<Status[]>(cached ? JSON.parse(cached) : []);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const getBookmark = async () => {
try {
await authorize();
showToast(Toast.Style.Animated, "Loading Status...");
const status = await apiServer.fetchUserStatus();
setStatus(status);
showToast(Toast.Style.Success, "Statuses has been loaded");
cache.set("latest_statuses", JSON.stringify(status));
} catch (error) {
const requestErr = error as AkkomaError;
showToast(Toast.Style.Failure, "Error", requestErr.message);
} finally {
setIsLoading(false);
}
};
getBookmark();
}, []);
return (
<List isShowingDetail isLoading={isLoading} searchBarPlaceholder="Search bookmarks">
{status?.map((statu) => (
<List.Item
title={statu.pleroma.content["text/plain"]}
key={statu.id}
detail={<List.Item.Detail markdown={statusParser(statu)} />}
actions={
<ActionPanel>
<Action.OpenInBrowser title="Open Original Status" url={statu.url} />
</ActionPanel>
}
/>
))}
</List>
);
}

View file

@ -13,35 +13,25 @@ import {
LocalStorage, LocalStorage,
} from "@raycast/api"; } from "@raycast/api";
import apiServer from "./utils/api"; import apiServer from "./utils/api";
import { AkkomaError, StatusResponse, Preference, Status } from "./utils/types"; import { AkkomaError, StatusResponse, Preference, StatusRequest } from "./utils/types";
import { authorize } from "./utils/oauth"; import { authorize } from "./utils/oauth";
import { dateTimeFormatter } from "./utils/util";
import VisibilityDropdown from "./components/VisibilityDropdown"; import VisibilityDropdown from "./components/VisibilityDropdown";
const cache = new Cache(); const cache = new Cache();
const { instance } = getPreferenceValues<Preference>();
interface CommandProps extends LaunchProps<{ draftValues: Partial<Status> }> { interface CommandProps extends LaunchProps<{ draftValues: Partial<StatusRequest> }> {
children?: React.ReactNode; children?: React.ReactNode;
} }
interface StatusForm extends Status { interface StatusForm extends StatusRequest {
files: string[]; files: string[];
description?: string; description?: string;
} }
const labelText = (time: Date) => {
return new Intl.DateTimeFormat("default", {
hour: "numeric",
minute: "numeric",
day: "numeric",
month: "long",
weekday: "long",
dayPeriod: "narrow",
}).format(time);
};
export default function SimpleCommand(props: CommandProps) { export default function SimpleCommand(props: CommandProps) {
const { instance } = getPreferenceValues<Preference>();
const { draftValues } = props; const { draftValues } = props;
const [state, setState] = useState({ const [state, setState] = useState({
@ -86,7 +76,7 @@ export default function SimpleCommand(props: CommandProps) {
}) })
); );
const newStatus: Partial<Status> = { const newStatus: Partial<StatusRequest> = {
...value, ...value,
content_type: state.isMarkdown ? "text/markdown" : "text/plain", content_type: state.isMarkdown ? "text/markdown" : "text/plain",
media_ids: mediaIds, media_ids: mediaIds,
@ -96,7 +86,7 @@ export default function SimpleCommand(props: CommandProps) {
const response = await apiServer.postNewStatus(newStatus); const response = await apiServer.postNewStatus(newStatus);
value.scheduled_at value.scheduled_at
? showToast(Toast.Style.Success, "Scheduled", labelText(value.scheduled_at)) ? showToast(Toast.Style.Success, "Scheduled", dateTimeFormatter(value.scheduled_at, "long"))
: showToast(Toast.Style.Success, "Status has been published (≧∇≦)/ ! "); : showToast(Toast.Style.Success, "Status has been published (≧∇≦)/ ! ");
setStatusInfo(response); setStatusInfo(response);

View file

@ -1,16 +1,16 @@
import { Form, LaunchProps } from "@raycast/api"; import { Form, LaunchProps } from "@raycast/api";
import { Status } from "./utils/types"; import { StatusRequest } from "./utils/types";
import { useState } from "react"; import { useState } from "react";
import VisibilityDropdown from "./components/VisibilityDropdown"; import VisibilityDropdown from "./components/VisibilityDropdown";
import SimpleCommand from "./simple-status"; import SimpleCommand from "./simple-status";
export default function DetailCommand(props: LaunchProps<{ draftValues: Partial<Status> }>) { export default function DetailCommand(props: LaunchProps<{ draftValues: Partial<StatusRequest> }>) {
const [files, setFiles] = useState<string[]>([]); const [files, setFiles] = useState<string[]>([]);
return ( return (
<SimpleCommand {...props}> <SimpleCommand {...props}>
<Form.FilePicker id="files" value={files} onChange={setFiles} title="Attechments"/> <Form.FilePicker id="files" value={files} onChange={setFiles} title="Attechments" />
{files.length === 1 && <Form.TextArea id="description" title="Alt text" />} {files.length === 1 && <Form.TextArea id="description" title="Alt text" />}
<Form.DatePicker id="scheduled_at" title="Scheduled Time" /> <Form.DatePicker id="scheduled_at" title="Scheduled Time" />
<VisibilityDropdown /> <VisibilityDropdown />

View file

@ -5,12 +5,12 @@ import { OAuth, getPreferenceValues } from "@raycast/api";
import { import {
Credentials, Credentials,
Preference, Preference,
Status, StatusRequest,
StatusResponse, StatusResponse,
Account, Account,
StatusAttachment, StatusAttachment,
UploadAttachResponse, UploadAttachResponse,
BookmarkedStatus, Status,
} from "./types"; } from "./types";
import { client } from "./oauth"; import { client } from "./oauth";
import { RequestInit, Response } from "node-fetch"; import { RequestInit, Response } from "node-fetch";
@ -21,6 +21,7 @@ const CONFIG = {
tokenUrl: "/oauth/token", tokenUrl: "/oauth/token",
appUrl: "/api/v1/apps", appUrl: "/api/v1/apps",
statusesUrl: "/api/v1/statuses", statusesUrl: "/api/v1/statuses",
accountsUrl: "/api/v1/accounts",
verifyCredentialsUrl: "/api/v1/accounts/verify_credentials", verifyCredentialsUrl: "/api/v1/accounts/verify_credentials",
mediaUrl: "/api/v1/media/", mediaUrl: "/api/v1/media/",
bookmarkUrl: "/api/v1/bookmarks", bookmarkUrl: "/api/v1/bookmarks",
@ -64,7 +65,7 @@ const createApp = async (): Promise<Credentials> => {
return (await response.json()) as Credentials; return (await response.json()) as Credentials;
}; };
const postNewStatus = async (statusOptions: Partial<Status>): Promise<StatusResponse> => { const postNewStatus = async (statusOptions: Partial<StatusRequest>): Promise<StatusResponse> => {
const response = await fetchWithAuth(apiUrl(instance, CONFIG.statusesUrl), { const response = await fetchWithAuth(apiUrl(instance, CONFIG.statusesUrl), {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -101,14 +102,32 @@ const uploadAttachment = async ({ file, description }: StatusAttachment): Promis
return (await response.json()) as UploadAttachResponse; return (await response.json()) as UploadAttachResponse;
}; };
const fetchBookmarks = async (): Promise<BookmarkedStatus[]> => { const fetchBookmarks = async (): Promise<Status[]> => {
const { bookmarkLimit } = getPreferenceValues<Preference>(); const { bookmarkLimit } = getPreferenceValues<Preference>();
const url = bookmarkLimit ? CONFIG.bookmarkUrl + `?&limit=${bookmarkLimit}` : CONFIG.bookmarkUrl; const url = bookmarkLimit ? CONFIG.bookmarkUrl + `?&limit=${bookmarkLimit}` : CONFIG.bookmarkUrl;
const response = await fetchWithAuth(apiUrl(instance, url)); const response = await fetchWithAuth(apiUrl(instance, url));
if (!response.ok) throw new Error("Could not fetch bookmarks"); if (!response.ok) throw new Error("Could not fetch bookmarks");
return (await response.json()) as BookmarkedStatus[]; return (await response.json()) as Status[];
}; };
export default { fetchToken, createApp, postNewStatus, fetchAccountInfo, uploadAttachment, fetchBookmarks }; const fetchUserStatus = async (): Promise<Status[]> => {
const { id } = await fetchAccountInfo();
const url = CONFIG.accountsUrl + id + "/statuses?exclude_replies=false&with_muted=true";
const response = await fetchWithAuth(apiUrl(instance, url));
if (!response.ok) throw new Error("Could not fetch user's status");
return (await response.json()) as Status[];
};
export default {
fetchToken,
createApp,
postNewStatus,
fetchAccountInfo,
uploadAttachment,
fetchBookmarks,
fetchUserStatus,
};

View file

@ -44,7 +44,7 @@ interface Poll {
options: string[]; options: string[];
} }
export interface Status { export interface StatusRequest {
spoiler_text?: string; spoiler_text?: string;
status: string; status: string;
content_type: string; content_type: string;
@ -61,7 +61,7 @@ export interface Status {
to?: string[]; to?: string[];
} }
export interface BookmarkedStatus { export interface Status {
created_at: Date; created_at: Date;
media_attachments: UploadAttachResponse[]; media_attachments: UploadAttachResponse[];
account: { account: {
@ -98,6 +98,7 @@ export interface Account {
display_name: string; display_name: string;
fqn: string; fqn: string;
avatar_static: string; avatar_static: string;
id: string;
} }
// Attachments // Attachments

34
src/utils/util.ts Normal file
View file

@ -0,0 +1,34 @@
import { Status } from "./types";
import { NodeHtmlMarkdown } from "node-html-markdown";
const nhm = new NodeHtmlMarkdown();
export const dateTimeFormatter = (time: Date, type: "short" | "long") => {
const options: Intl.DateTimeFormatOptions = {
hour: "numeric",
minute: "numeric",
day: "numeric",
month: "long",
};
return type === "short"
? new Intl.DateTimeFormat("default", {
...options,
}).format(time)
: new Intl.DateTimeFormat("default", {
...options,
weekday: "long",
dayPeriod: "narrow",
}).format(time);
};
export const statusParser = ({ content, media_attachments, account, created_at }: Status) => {
const images = media_attachments.filter((attachment) => attachment.type === "image");
const parsedImages = images.reduce((link, image) => link + `![${image.description}](${image.remote_url})`, "");
const date = new Date(created_at);
const parsedTime = dateTimeFormatter(date, "short");
if (account) return ` _@${account.acct} (${parsedTime})_ ` + nhm.translate("<br>" + content) + parsedImages;
return parsedTime + nhm.translate("<br>" + content) + parsedImages;
};