Skip to content

Commit eacf57c

Browse files
authored
Merge pull request #198 from deholic/release/v0.13.0
release: v0.13.0
2 parents f2922c1 + 2267f9d commit eacf57c

22 files changed

Lines changed: 3262 additions & 1326 deletions

src/App.tsx

Lines changed: 402 additions & 1286 deletions
Large diffs are not rendered by default.

src/infra/MastodonHttpClient.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,27 @@ export class MastodonHttpClient implements MastodonApi {
422422
throw new Error("리액션은 미스키 계정에서만 사용할 수 있습니다.");
423423
}
424424

425+
async fetchNoteState(
426+
account: Account,
427+
noteId: string
428+
): Promise<{ isFavourited: boolean; isReblogged: boolean; bookmarked: boolean }> {
429+
const response = await fetch(`${account.instanceUrl}/api/v1/statuses/${noteId}`, {
430+
headers: {
431+
"Authorization": `Bearer ${account.accessToken}`
432+
}
433+
});
434+
if (!response.ok) {
435+
throw new Error("게시물 상태를 불러오지 못했습니다.");
436+
}
437+
const data = (await response.json()) as unknown;
438+
const status = mapStatus(data);
439+
return {
440+
isFavourited: status.favourited,
441+
isReblogged: status.reblogged,
442+
bookmarked: status.bookmarked
443+
};
444+
}
445+
425446
async reblog(account: Account, statusId: string): Promise<Status> {
426447
return this.postAction(account, statusId, "reblog");
427448
}

src/services/MastodonApi.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,8 @@ export interface MastodonApi {
4040
unblockAccount(account: Account, accountId: string): Promise<AccountRelationship>;
4141
fetchAccountStatuses(account: Account, accountId: string, limit: number, maxId?: string): Promise<Status[]>;
4242
fetchThreadContext(account: Account, statusId: string): Promise<ThreadContext>;
43+
fetchNoteState(
44+
account: Account,
45+
noteId: string
46+
): Promise<{ isFavourited: boolean; isReblogged: boolean; bookmarked: boolean }>;
4347
}

src/ui/components/AccountSelector.tsx

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React, { useMemo, useRef, useState } from "react";
1+
import React, { useEffect, useMemo, useRef, useState } from "react";
22
import type { Account } from "../../domain/types";
3+
import type { Ref } from "react";
34
import { formatHandle } from "../utils/account";
45
import { useClickOutside } from "../hooks/useClickOutside";
56
import { AccountLabel } from "./AccountLabel";
@@ -8,15 +9,24 @@ export const AccountSelector = ({
89
accounts,
910
activeAccountId,
1011
setActiveAccount,
12+
onSelectionDone,
13+
summaryRef,
14+
summaryTitle,
1115
variant = "panel"
1216
}: {
1317
accounts: Account[];
1418
activeAccountId: string | null;
1519
setActiveAccount: (id: string) => void;
20+
onSelectionDone?: () => void;
21+
summaryRef?: Ref<HTMLElement>;
22+
summaryTitle?: string;
1623
variant?: "panel" | "inline";
1724
}) => {
1825
const [dropdownOpen, setDropdownOpen] = useState(false);
26+
const [highlightedAccountId, setHighlightedAccountId] = useState<string | null>(null);
27+
const detailsRef = useRef<HTMLDetailsElement | null>(null);
1928
const dropdownRef = useRef<HTMLDivElement | null>(null);
29+
const selectionChangeRef = useRef(false);
2030

2131
useClickOutside(dropdownRef, dropdownOpen, () => setDropdownOpen(false));
2232

@@ -25,6 +35,75 @@ export const AccountSelector = ({
2535
[accounts, activeAccountId]
2636
);
2737

38+
useEffect(() => {
39+
if (!dropdownOpen) {
40+
setHighlightedAccountId(null);
41+
return;
42+
}
43+
setHighlightedAccountId(activeAccountId ?? accounts[0]?.id ?? null);
44+
}, [activeAccountId, accounts, dropdownOpen]);
45+
46+
useEffect(() => {
47+
if (!dropdownOpen && selectionChangeRef.current) {
48+
selectionChangeRef.current = false;
49+
onSelectionDone?.();
50+
}
51+
}, [dropdownOpen, onSelectionDone]);
52+
53+
useEffect(() => {
54+
if (!dropdownOpen) {
55+
return;
56+
}
57+
58+
const handleKeyDown = (event: KeyboardEvent) => {
59+
if (!dropdownOpen) {
60+
return;
61+
}
62+
if (!detailsRef.current?.contains(document.activeElement)) {
63+
return;
64+
}
65+
if (accounts.length === 0) {
66+
return;
67+
}
68+
69+
const currentIndex = Math.max(
70+
0,
71+
accounts.findIndex((account) => account.id === (highlightedAccountId ?? activeAccountId))
72+
);
73+
74+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
75+
event.preventDefault();
76+
const offset = event.key === "ArrowDown" ? 1 : -1;
77+
const nextIndex = (currentIndex + offset + accounts.length) % accounts.length;
78+
const nextAccount = accounts[nextIndex];
79+
if (nextAccount) {
80+
setHighlightedAccountId(nextAccount.id);
81+
selectionChangeRef.current = true;
82+
setActiveAccount(nextAccount.id);
83+
}
84+
return;
85+
}
86+
87+
if (event.key === "Enter") {
88+
event.preventDefault();
89+
if (highlightedAccountId) {
90+
selectionChangeRef.current = true;
91+
setActiveAccount(highlightedAccountId);
92+
}
93+
setDropdownOpen(false);
94+
return;
95+
}
96+
97+
if (event.key === "Escape") {
98+
event.preventDefault();
99+
setDropdownOpen(false);
100+
}
101+
};
102+
103+
window.addEventListener("keydown", handleKeyDown);
104+
return () => window.removeEventListener("keydown", handleKeyDown);
105+
}, [accounts, activeAccountId, dropdownOpen, highlightedAccountId, setActiveAccount]);
106+
28107
const wrapperClassName =
29108
variant === "panel" ? "panel account-selector-panel" : "account-selector-inline";
30109
const Wrapper = variant === "panel" ? "section" : "div";
@@ -33,11 +112,16 @@ export const AccountSelector = ({
33112
<Wrapper className={wrapperClassName}>
34113
<div className="account-selector-header">
35114
<details
115+
ref={detailsRef}
36116
className="account-selector"
37117
open={dropdownOpen}
38118
onToggle={(event) => setDropdownOpen(event.currentTarget.open)}
39119
>
40-
<summary className="account-selector-summary">
120+
<summary
121+
ref={summaryRef}
122+
className="account-selector-summary"
123+
title={summaryTitle ?? "계정 선택 (Ctrl+Shift+A)"}
124+
>
41125
{activeAccount ? (
42126
<AccountLabel
43127
avatarUrl={activeAccount.avatarUrl}
@@ -59,11 +143,22 @@ export const AccountSelector = ({
59143
<ul className="account-list">
60144
{accounts.map((account) => {
61145
const isActiveAccount = account.id === activeAccountId;
146+
const classNames = [] as string[];
147+
if (account.id === highlightedAccountId) {
148+
classNames.push("is-highlighted");
149+
}
150+
if (isActiveAccount) {
151+
classNames.push("active");
152+
}
62153
return (
63-
<li key={account.id} className={isActiveAccount ? "active" : ""}>
154+
<li
155+
key={account.id}
156+
className={classNames.join(" ")}
157+
>
64158
<button
65159
type="button"
66160
onClick={() => {
161+
selectionChangeRef.current = true;
67162
setActiveAccount(account.id);
68163
setDropdownOpen(false);
69164
}}

0 commit comments

Comments
 (0)