diff --git a/package.json b/package.json index 60390ff..59d4987 100644 --- a/package.json +++ b/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": [ diff --git a/src/bookmark.tsx b/src/bookmark.tsx index fce65f9..d5d7c0d 100644 --- a/src/bookmark.tsx +++ b/src/bookmark.tsx @@ -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(cached ? JSON.parse(cached) : []); + const [bookmarks, setBookmarks] = useState(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("
" + content) + parsedImages; - }; - return ( {bookmarks?.map((bookmark) => ( } + detail={} actions={ diff --git a/src/components/VisibilityDropdown.tsx b/src/components/VisibilityDropdown.tsx index e814908..745001e 100644 --- a/src/components/VisibilityDropdown.tsx +++ b/src/components/VisibilityDropdown.tsx @@ -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 ( - <> - - {visibilityOptions.map(({ value, title, icon }) => ( - - ))} - - + + {visibilityOptions.map(({ value, title, icon }) => ( + + ))} + ); }; diff --git a/src/my-status.tsx b/src/my-status.tsx new file mode 100644 index 0000000..0bac8da --- /dev/null +++ b/src/my-status.tsx @@ -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(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 ( + + {status?.map((statu) => ( + } + actions={ + + + + } + /> + ))} + + ); +} diff --git a/src/simple-status.tsx b/src/simple-status.tsx index 59a4666..fd33169 100644 --- a/src/simple-status.tsx +++ b/src/simple-status.tsx @@ -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(); -interface CommandProps extends LaunchProps<{ draftValues: Partial }> { +interface CommandProps extends LaunchProps<{ draftValues: Partial }> { 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(); const { draftValues } = props; const [state, setState] = useState({ @@ -86,7 +76,7 @@ export default function SimpleCommand(props: CommandProps) { }) ); - const newStatus: Partial = { + const newStatus: Partial = { ...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); diff --git a/src/status.tsx b/src/status.tsx index 7242b53..89883c8 100644 --- a/src/status.tsx +++ b/src/status.tsx @@ -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 }>) { +export default function DetailCommand(props: LaunchProps<{ draftValues: Partial }>) { const [files, setFiles] = useState([]); return ( - + {files.length === 1 && } diff --git a/src/utils/api.ts b/src/utils/api.ts index 1f1d5a5..a14fde8 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -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 => { return (await response.json()) as Credentials; }; -const postNewStatus = async (statusOptions: Partial): Promise => { +const postNewStatus = async (statusOptions: Partial): Promise => { 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 => { +const fetchBookmarks = async (): Promise => { const { bookmarkLimit } = getPreferenceValues(); 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 => { + 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, +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index 17f9748..eaf30ef 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -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 diff --git a/src/utils/util.ts b/src/utils/util.ts new file mode 100644 index 0000000..f1cf01c --- /dev/null +++ b/src/utils/util.ts @@ -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("
" + content) + parsedImages; + + return parsedTime + nhm.translate("
" + content) + parsedImages; +};