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"
}
]
},
{
"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": [

View file

@ -1,22 +1,16 @@
import { useEffect, useState } from "react";
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 { authorize } from "./utils/oauth";
import { statusParser } from "./utils/util";
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() {
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);
useEffect(() => {
@ -25,7 +19,7 @@ export default function BookmarkCommand() {
await authorize();
showToast(Toast.Style.Animated, "Loading bookmarks...");
const newBookmarks = await apiServer.fetchBookmarks();
setBookmarks(newBookmarks);
setBookmarks((prevBookmarks) => [...prevBookmarks, ...newBookmarks]);
showToast(Toast.Style.Success, "Bookmarked has been loaded");
cache.set("latest_bookmarks", JSON.stringify(newBookmarks));
} catch (error) {
@ -38,23 +32,13 @@ export default function BookmarkCommand() {
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 (
<List isShowingDetail isLoading={isLoading} searchBarPlaceholder="Search bookmarks">
{bookmarks?.map((bookmark) => (
<List.Item
title={bookmark.pleroma.content["text/plain"]}
key={bookmark.id}
detail={<List.Item.Detail markdown={parseStatus(bookmark)} />}
detail={<List.Item.Detail markdown={statusParser(bookmark)} />}
actions={
<ActionPanel>
<Action.OpenInBrowser title="Open Original Status" url={bookmark.url} />

View file

@ -1,30 +1,28 @@
import { getPreferenceValues, Color, Icon, Form } from "@raycast/api";
import { Preference, VisibilityOption } from "../types";
import { Preference, VisibilityOption } from "../utils/types";
const visibilityOptions: VisibilityOption[] = [
{ value: "public", title: "Public", icon: Icon.Livestream },
{ value: "unlisted", title: "Unlisted", icon: Icon.LivestreamDisabled },
{ value: "private", title: "Followers-only", icon: Icon.TwoPeople },
{ value: "direct", title: "Direct", icon: Icon.Envelope },
{ value: "local", title: "Local-only", icon: Icon.Pin },
];
const VisibilityDropdown = () => {
const { defaultVisibility }: Preference = getPreferenceValues();
const visibilityOptions: VisibilityOption[] = [
{ value: "public", title: "Public", icon: Icon.Livestream },
{ value: "unlisted", title: "Unlisted", icon: Icon.LivestreamDisabled },
{ value: "private", title: "Followers-only", icon: Icon.TwoPeople },
{ value: "direct", title: "Direct", icon: Icon.Envelope },
{ value: "local", title: "Local-only", icon: Icon.Pin },
];
return (
<>
<Form.Dropdown id="visibility" title="Visibility" storeValue={true} defaultValue={defaultVisibility}>
{visibilityOptions.map(({ value, title, icon }) => (
<Form.Dropdown.Item
key={value}
value={value}
title={title}
icon={{ source: icon, tintColor: Color.SecondaryText }}
/>
))}
</Form.Dropdown>
</>
<Form.Dropdown id="visibility" title="Visibility" storeValue={true} defaultValue={defaultVisibility}>
{visibilityOptions.map(({ value, title, icon }) => (
<Form.Dropdown.Item
key={value}
value={value}
title={title}
icon={{ source: icon, tintColor: Color.SecondaryText }}
/>
))}
</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,
} from "@raycast/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 { dateTimeFormatter } from "./utils/util";
import VisibilityDropdown from "./components/VisibilityDropdown";
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;
}
interface StatusForm extends Status {
interface StatusForm extends StatusRequest {
files: 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) {
const { instance } = getPreferenceValues<Preference>();
const { draftValues } = props;
const [state, setState] = useState({
@ -86,7 +76,7 @@ export default function SimpleCommand(props: CommandProps) {
})
);
const newStatus: Partial<Status> = {
const newStatus: Partial<StatusRequest> = {
...value,
content_type: state.isMarkdown ? "text/markdown" : "text/plain",
media_ids: mediaIds,
@ -96,7 +86,7 @@ export default function SimpleCommand(props: CommandProps) {
const response = await apiServer.postNewStatus(newStatus);
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 (≧∇≦)/ ! ");
setStatusInfo(response);

View file

@ -1,16 +1,16 @@
import { Form, LaunchProps } from "@raycast/api";
import { Status } from "./utils/types";
import { StatusRequest } from "./utils/types";
import { useState } from "react";
import VisibilityDropdown from "./components/VisibilityDropdown";
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[]>([]);
return (
<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" />}
<Form.DatePicker id="scheduled_at" title="Scheduled Time" />
<VisibilityDropdown />

View file

@ -5,12 +5,12 @@ import { OAuth, getPreferenceValues } from "@raycast/api";
import {
Credentials,
Preference,
Status,
StatusRequest,
StatusResponse,
Account,
StatusAttachment,
UploadAttachResponse,
BookmarkedStatus,
Status,
} from "./types";
import { client } from "./oauth";
import { RequestInit, Response } from "node-fetch";
@ -21,6 +21,7 @@ const CONFIG = {
tokenUrl: "/oauth/token",
appUrl: "/api/v1/apps",
statusesUrl: "/api/v1/statuses",
accountsUrl: "/api/v1/accounts",
verifyCredentialsUrl: "/api/v1/accounts/verify_credentials",
mediaUrl: "/api/v1/media/",
bookmarkUrl: "/api/v1/bookmarks",
@ -64,7 +65,7 @@ const createApp = async (): Promise<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), {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -101,14 +102,32 @@ const uploadAttachment = async ({ file, description }: StatusAttachment): Promis
return (await response.json()) as UploadAttachResponse;
};
const fetchBookmarks = async (): Promise<BookmarkedStatus[]> => {
const fetchBookmarks = async (): Promise<Status[]> => {
const { bookmarkLimit } = getPreferenceValues<Preference>();
const url = bookmarkLimit ? CONFIG.bookmarkUrl + `?&limit=${bookmarkLimit}` : CONFIG.bookmarkUrl;
const response = await fetchWithAuth(apiUrl(instance, url));
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[];
}
export interface Status {
export interface StatusRequest {
spoiler_text?: string;
status: string;
content_type: string;
@ -61,7 +61,7 @@ export interface Status {
to?: string[];
}
export interface BookmarkedStatus {
export interface Status {
created_at: Date;
media_attachments: UploadAttachResponse[];
account: {
@ -98,6 +98,7 @@ export interface Account {
display_name: string;
fqn: string;
avatar_static: string;
id: string;
}
// 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;
};