Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
98732cf
Merge pull request #180 from deholic/feature/misskey-favorite-toast
deholic Jan 18, 2026
b43be73
Fix: 마스토돈 좋아요 기능 오류 수정
deholic Jan 19, 2026
992a0ad
Merge pull request #181 from deholic/feature/mastodon-favourite-fix
deholic Jan 19, 2026
0b7862d
Feat: 뽀모도로 타이머 진행 상태 점 표시 기능 추가
deholic Jan 20, 2026
f4691bb
Fix: 미완료 점 표시를 하얀 선에서 회색 점으로 변경
deholic Jan 20, 2026
aab47d0
Fix: 뽀모도로 진행 상태 점 크기를 10px에서 8px로 축소
deholic Jan 20, 2026
71bd862
Fix: 뽀모도로 진행 상태 점이 모두 채워지도록 세션 관리 로직 수정
deholic Jan 20, 2026
5b49376
Feat: 뽀모도로 사이클 완료 시 진행 상태 점 리셋 기능
deholic Jan 20, 2026
c8debd8
Feat: 뽀모도로 타이머 페이지 리로드 시 세션 상태 유지
deholic Jan 20, 2026
b694b26
Fix: 뽀모도로 진행 상태 점을 타이머 바로 아래로 이동
deholic Jan 20, 2026
ae6a9c3
Fix: 뽀모도로 타이머와 진행 상태 점을 시각적으로 묶기
deholic Jan 20, 2026
82d2a59
Fix: 뽀모도로 타이머와 진행 상태 점을 세로로 정렬
deholic Jan 20, 2026
ce890a9
Fix: 뽀모도로 레이아웃을 좌우 한 줄로 재배치
deholic Jan 20, 2026
88c6340
Fix: 뽀모도로 타이머와 점 사이 간격 조절
deholic Jan 20, 2026
4908bb8
Refactor: 뽀모도로 목표 사이클 수 설정 제거 및 4사이클로 고정
deholic Jan 20, 2026
bec0647
Fix: 뽀모도로 집중 세션 시 타임라인 가리기 기능 복구
deholic Jan 20, 2026
dd4f61a
Merge pull request #182 from deholic/feature/pomodoro-progress-dots
deholic Jan 20, 2026
f6016fd
feat: 설정 팝업 스크롤 기능 구현
deholic Jan 20, 2026
a1454a4
Merge pull request #183 from deholic/feature/settings-modal-scroll
deholic Jan 20, 2026
5cc2d73
feat: 모바일 접속 차단 안내 추가
deholic Jan 20, 2026
af326ae
Merge pull request #184 from deholic/feature/mobile-blocker
deholic Jan 20, 2026
1199be9
feat: 뽀모도로 투두 리스트 추가
deholic Jan 21, 2026
5ae7304
Merge pull request #185 from deholic/feature/pomodoro-todo
deholic Jan 21, 2026
5916bb2
feat: add compose box shortcut hints
deholic Jan 22, 2026
f122102
feat: support keyboard selection in account dropdown
deholic Jan 22, 2026
c0298e1
feat: focus compose after account selection
deholic Jan 22, 2026
e0fc85c
Merge pull request #187 from deholic/feature/editor-shortcuts
deholic Jan 22, 2026
39d6d07
feat: add keyboard timeline selection
deholic Jan 22, 2026
7a50d12
feat: align cross-column keyboard selection
deholic Jan 22, 2026
8b8b387
chore: remove timeline drag handling
deholic Jan 22, 2026
f2c4dc1
chore: disable dragging in status cards
deholic Jan 22, 2026
40b8809
Merge pull request #189 from deholic/feature/keyboard-navigation
deholic Jan 22, 2026
313a291
feat: extend timeline keyboard shortcuts
Jan 22, 2026
0fc915d
Merge pull request #191 from deholic/feature/timeline-shortcuts
deholic Jan 22, 2026
6d779e9
fix: block timeline selection when overlays open
deholic Jan 22, 2026
0bf9249
Merge pull request #192 from deholic/feature/timeline-dropdown-keyboa…
deholic Jan 22, 2026
f4bab3a
feat: add shortcuts info modal
deholic Jan 23, 2026
7402e9e
feat: add timeline post shortcuts
deholic Jan 23, 2026
4abcc46
fix: focus compose box on reply
deholic Jan 23, 2026
e7f867f
Merge pull request #193 from deholic/feature/shortcuts-popup
deholic Jan 24, 2026
a9d223f
fix: keep selection on overlay esc
Jan 24, 2026
cb5357d
fix: tune shortcut text colors
Jan 24, 2026
0277ed1
merge: sync develop
Jan 24, 2026
802b8fb
fix: avoid shortcut overlap
Jan 24, 2026
5c54a87
fix: reassign notification shortcut
Jan 24, 2026
e52654f
Merge pull request #195 from deholic/feature/post-shortcuts
deholic Jan 24, 2026
8b14010
refactor: split App into modules
Jan 24, 2026
a31b768
Merge pull request #196 from deholic/feature/app-refactor
deholic Jan 24, 2026
2267f9d
merge: resolve v0.13.0 conflicts
Jan 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,688 changes: 402 additions & 1,286 deletions src/App.tsx

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/infra/MastodonHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,27 @@ export class MastodonHttpClient implements MastodonApi {
throw new Error("리액션은 미스키 계정에서만 사용할 수 있습니다.");
}

async fetchNoteState(
account: Account,
noteId: string
): Promise<{ isFavourited: boolean; isReblogged: boolean; bookmarked: boolean }> {
const response = await fetch(`${account.instanceUrl}/api/v1/statuses/${noteId}`, {
headers: {
"Authorization": `Bearer ${account.accessToken}`
}
});
if (!response.ok) {
throw new Error("게시물 상태를 불러오지 못했습니다.");
}
const data = (await response.json()) as unknown;
const status = mapStatus(data);
return {
isFavourited: status.favourited,
isReblogged: status.reblogged,
bookmarked: status.bookmarked
};
}

async reblog(account: Account, statusId: string): Promise<Status> {
return this.postAction(account, statusId, "reblog");
}
Expand Down
4 changes: 4 additions & 0 deletions src/services/MastodonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ export interface MastodonApi {
unblockAccount(account: Account, accountId: string): Promise<AccountRelationship>;
fetchAccountStatuses(account: Account, accountId: string, limit: number, maxId?: string): Promise<Status[]>;
fetchThreadContext(account: Account, statusId: string): Promise<ThreadContext>;
fetchNoteState(
account: Account,
noteId: string
): Promise<{ isFavourited: boolean; isReblogged: boolean; bookmarked: boolean }>;
}
101 changes: 98 additions & 3 deletions src/ui/components/AccountSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useMemo, useRef, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import type { Account } from "../../domain/types";
import type { Ref } from "react";
import { formatHandle } from "../utils/account";
import { useClickOutside } from "../hooks/useClickOutside";
import { AccountLabel } from "./AccountLabel";
Expand All @@ -8,15 +9,24 @@ export const AccountSelector = ({
accounts,
activeAccountId,
setActiveAccount,
onSelectionDone,
summaryRef,
summaryTitle,
variant = "panel"
}: {
accounts: Account[];
activeAccountId: string | null;
setActiveAccount: (id: string) => void;
onSelectionDone?: () => void;
summaryRef?: Ref<HTMLElement>;
summaryTitle?: string;
variant?: "panel" | "inline";
}) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [highlightedAccountId, setHighlightedAccountId] = useState<string | null>(null);
const detailsRef = useRef<HTMLDetailsElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);
const selectionChangeRef = useRef(false);

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

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

useEffect(() => {
if (!dropdownOpen) {
setHighlightedAccountId(null);
return;
}
setHighlightedAccountId(activeAccountId ?? accounts[0]?.id ?? null);
}, [activeAccountId, accounts, dropdownOpen]);

useEffect(() => {
if (!dropdownOpen && selectionChangeRef.current) {
selectionChangeRef.current = false;
onSelectionDone?.();
}
}, [dropdownOpen, onSelectionDone]);

useEffect(() => {
if (!dropdownOpen) {
return;
}

const handleKeyDown = (event: KeyboardEvent) => {
if (!dropdownOpen) {
return;
}
if (!detailsRef.current?.contains(document.activeElement)) {
return;
}
if (accounts.length === 0) {
return;
}

const currentIndex = Math.max(
0,
accounts.findIndex((account) => account.id === (highlightedAccountId ?? activeAccountId))
);

if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
const offset = event.key === "ArrowDown" ? 1 : -1;
const nextIndex = (currentIndex + offset + accounts.length) % accounts.length;
const nextAccount = accounts[nextIndex];
if (nextAccount) {
setHighlightedAccountId(nextAccount.id);
selectionChangeRef.current = true;
setActiveAccount(nextAccount.id);
}
return;
}

if (event.key === "Enter") {
event.preventDefault();
if (highlightedAccountId) {
selectionChangeRef.current = true;
setActiveAccount(highlightedAccountId);
}
setDropdownOpen(false);
return;
}

if (event.key === "Escape") {
event.preventDefault();
setDropdownOpen(false);
}
};

window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [accounts, activeAccountId, dropdownOpen, highlightedAccountId, setActiveAccount]);

const wrapperClassName =
variant === "panel" ? "panel account-selector-panel" : "account-selector-inline";
const Wrapper = variant === "panel" ? "section" : "div";
Expand All @@ -33,11 +112,16 @@ export const AccountSelector = ({
<Wrapper className={wrapperClassName}>
<div className="account-selector-header">
<details
ref={detailsRef}
className="account-selector"
open={dropdownOpen}
onToggle={(event) => setDropdownOpen(event.currentTarget.open)}
>
<summary className="account-selector-summary">
<summary
ref={summaryRef}
className="account-selector-summary"
title={summaryTitle ?? "계정 선택 (Ctrl+Shift+A)"}
>
{activeAccount ? (
<AccountLabel
avatarUrl={activeAccount.avatarUrl}
Expand All @@ -59,11 +143,22 @@ export const AccountSelector = ({
<ul className="account-list">
{accounts.map((account) => {
const isActiveAccount = account.id === activeAccountId;
const classNames = [] as string[];
if (account.id === highlightedAccountId) {
classNames.push("is-highlighted");
}
if (isActiveAccount) {
classNames.push("active");
}
return (
<li key={account.id} className={isActiveAccount ? "active" : ""}>
<li
key={account.id}
className={classNames.join(" ")}
>
<button
type="button"
onClick={() => {
selectionChangeRef.current = true;
setActiveAccount(account.id);
setDropdownOpen(false);
}}
Expand Down
Loading
Loading