mirror of
https://github.com/Sevichecc/raycast-akkoma-extension.git
synced 2025-04-30 06:39:30 +08:00
refactor: add utils folder
This commit is contained in:
parent
d5860b63a0
commit
54a6188bc5
9 changed files with 164 additions and 71 deletions
16
package.json
16
package.json
|
@ -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": [
|
||||
|
|
|
@ -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 + ``, "");
|
||||
|
||||
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} />
|
||||
|
|
|
@ -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
51
src/my-status.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
34
src/utils/util.ts
Normal 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 + ``, "");
|
||||
|
||||
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;
|
||||
};
|
Loading…
Reference in a new issue