Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -560,9 +560,9 @@ const TimelineSection = ({
: await services.api.bookmark(account, status.id);
timeline.updateItem(updated);
if (isBookmarking) {
showToast("북마크했습니다.", { tone: "success" });
showToast("북마크했습니다.");
} else {
showToast("북마크를 취소했습니다.", { tone: "success" });
showToast("북마크를 취소했습니다.");
}
} catch (err) {
onError(err instanceof Error ? err.message : "북마크 처리에 실패했습니다.");
Expand Down
27 changes: 4 additions & 23 deletions src/infra/MastodonHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,12 @@ export class MastodonHttpClient implements MastodonApi {
}
}

async favourite(_account: Account, _statusId: string): Promise<Status> {
throw new Error("즐겨찾기는 미스키 계정에서만 사용할 수 있습니다.");
async favourite(account: Account, statusId: string): Promise<Status> {
return this.postAction(account, statusId, "favourite");
}

async unfavourite(_account: Account, _statusId: string): Promise<Status> {
throw new Error("즐겨찾기는 미스키 계정에서만 사용할 수 있습니다.");
async unfavourite(account: Account, statusId: string): Promise<Status> {
return this.postAction(account, statusId, "unfavourite");
}

async bookmark(account: Account, statusId: string): Promise<Status> {
Expand All @@ -422,25 +422,6 @@ export class MastodonHttpClient implements MastodonApi {
throw new Error("리액션은 미스키 계정에서만 사용할 수 있습니다.");
}

async fetchNoteState(account: Account, noteId: string): Promise<{ isFavourited: boolean; isReblogged: boolean; bookmarked: boolean }> {
// 마스토돈은 이미 Status 객체에 즐겨찾기, 리블로그, 북마크 상태가 포함되어 있음
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
50 changes: 13 additions & 37 deletions src/infra/MisskeyHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,23 @@ export class MisskeyHttpClient implements MastodonApi {
}

async favourite(account: Account, statusId: string): Promise<Status> {
await this.postSimple(account, "/api/notes/favorites/create", { noteId: statusId });
try {
await this.postSimple(account, "/api/notes/reactions/create", {
noteId: statusId,
reaction: DEFAULT_REACTION
});
} catch {
await this.postSimple(account, "/api/notes/favorites/create", { noteId: statusId });
}
return this.fetchNote(account, statusId);
}

async unfavourite(account: Account, statusId: string): Promise<Status> {
await this.postSimple(account, "/api/notes/favorites/delete", { noteId: statusId });
try {
await this.postSimple(account, "/api/notes/reactions/delete", { noteId: statusId });
} catch {
await this.postSimple(account, "/api/notes/favorites/delete", { noteId: statusId });
}
return this.fetchNote(account, statusId);
}

Expand Down Expand Up @@ -515,41 +526,6 @@ export class MisskeyHttpClient implements MastodonApi {
return data.map((item) => mapMisskeyStatusWithInstance(item, account.instanceUrl));
}

async fetchNoteState(account: Account, noteId: string): Promise<{ isFavourited: boolean; isReblogged: boolean; bookmarked: boolean }> {
const response = await fetch(`${normalizeInstanceUrl(account.instanceUrl)}/api/notes/state`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(buildBody(account, { noteId }))
});
if (!response.ok) {
throw new Error("게시물 상태를 불러오지 못했습니다.");
}
const data = (await response.json()) as Record<string, unknown>;
return {
isFavourited: Boolean(data.isFavorited ?? data.isFavourited ?? false),
isReblogged: Boolean(data.isRenoted ?? false),
bookmarked: Boolean(data.bookmarked ?? false)
};
}

async bookmark(_account: Account, _statusId: string): Promise<Status> {
throw new Error("북마크는 마스토돈 계정에서만 사용할 수 있습니다.");
}

async unbookmark(_account: Account, _statusId: string): Promise<Status> {
throw new Error("북마크는 마스토돈 계정에서만 사용할 수 있습니다.");
}

async fetchBookmarks(_account: Account, _limit?: number, _maxId?: string): Promise<Status[]> {
throw new Error("북마크는 마스토돈 계정에서만 사용할 수 있습니다.");
}

async fetchThreadContext(account: Account, statusId: string): Promise<ThreadContext> {
return this.fetchConversation(account, statusId);
}

private async fetchNotifications(
account: Account,
limit: number,
Expand Down
4 changes: 0 additions & 4 deletions src/infra/UnifiedApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@ export class UnifiedApiClient implements MastodonApi {
return this.getClient(account).unreblog(account, statusId);
}

fetchNoteState(account: Account, noteId: string) {
return this.getClient(account).fetchNoteState(account, noteId);
}

async fetchThreadContext(account: Account, statusId: string): Promise<ThreadContext> {
if (account.platform === "misskey") {
// MisskeyHttpClient에는 fetchConversation 메서드가 있음
Expand Down
1 change: 0 additions & 1 deletion src/services/MastodonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,4 @@ 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 }>;
}
2 changes: 1 addition & 1 deletion src/ui/components/ProfileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export const ProfileModal = ({
? await api.unbookmark(account, target.id)
: await api.bookmark(account, target.id);
updateItem(updated);
showToast(isBookmarking ? "북마크했습니다." : "북마크를 취소했습니다.", { tone: "success" });
showToast(isBookmarking ? "북마크했습니다." : "북마크를 취소했습니다.");
} catch (error) {
setItemsError(error instanceof Error ? error.message : "북마크 처리에 실패했습니다.");
}
Expand Down
87 changes: 10 additions & 77 deletions src/ui/components/TimelineItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { ReactionPicker } from "./ReactionPicker";
import { useClickOutside } from "../hooks/useClickOutside";
import { useImageZoom } from "../hooks/useImageZoom";
import { AccountLabel } from "./AccountLabel";
import { useToast } from "../state/ToastContext";

const normalizeMentionHandle = (handle: string): string =>
handle.replace(/^@/, "").trim().toLowerCase();
Expand Down Expand Up @@ -72,31 +71,10 @@ export const TimelineItem = ({
const [activeImageIndex, setActiveImageIndex] = useState<number | null>(null);
const [showContent, setShowContent] = useState(() => displayStatus.spoilerText.length === 0);
const [menuOpen, setMenuOpen] = useState(false);
const [favouriteState, setFavouriteState] = useState<boolean | null>(false);
const imageContainerRef = useRef<HTMLDivElement | null>(null);
const imageRef = useRef<HTMLImageElement | null>(null);
const menuRef = useRef<HTMLDivElement | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const { showToast } = useToast();

// 메뉴 열 때 즐겨찾기 상태 확인 (미스키만)
const handleMenuToggle = useCallback(async () => {
const willOpen = !menuOpen;
setMenuOpen(willOpen);

if (willOpen && account && api && account.platform === "misskey") {
// 초기 상태를 null로 설정하여 비활성화 상태로 표시
setFavouriteState(null);

try {
const state = await api.fetchNoteState(account, displayStatus.id);
setFavouriteState(state.isFavourited);
} catch (error) {
console.error("즐겨찾기 상태 확인 실패:", error);
setFavouriteState(false); // 실패 시 기본값은 false로 설정
}
}
}, [menuOpen, account, api, displayStatus.id]);

// useImageZoom 사용
const {
Expand Down Expand Up @@ -874,7 +852,7 @@ export const TimelineItem = ({
className="icon-button"
aria-label="게시글 메뉴 열기" aria-haspopup="menu"
aria-expanded={menuOpen}
onClick={handleMenuToggle}
onClick={() => setMenuOpen((current) => !current)}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="5" r="1.7" />
Expand All @@ -886,60 +864,15 @@ export const TimelineItem = ({
<>
<div className="overlay-backdrop" aria-hidden="true" />
<div ref={menuRef} className="section-menu-panel status-menu-panel" role="menu">
{/* 미스키: 즐겨찾기만 사용 */}
{account?.platform === "misskey" && (
<button
type="button"
disabled={favouriteState === null}
onClick={() => {
// 메뉴 즉시 닫기
setMenuOpen(false);

// 즐겨찾기 처리 비동기 실행
(async () => {
try {
// 현재 상태에 따라 적절한 API 호출
let updatedStatus: Status;
if (favouriteState) {
updatedStatus = await api.unfavourite(account!, displayStatus.id);
} else {
updatedStatus = await api.favourite(account!, displayStatus.id);
}

// API 호출 후 최신 상태 다시 확인
const state = await api.fetchNoteState(account!, displayStatus.id);
const newFavouriteState = state.isFavourited;
setFavouriteState(newFavouriteState);

// 토스트 메시지 표시
showToast(
newFavouriteState ? "즐겨찾기에 추가했습니다." : "즐겨찾기에서 해제했습니다.",
{ tone: "success" }
);
} catch (error) {
console.error("즐겨찾기 처리 실패:", error);
showToast("즐겨찾기 처리에 실패했습니다.", { tone: "error" });
}
})();
}}
>
{favouriteState === null ? "로딩..." : (favouriteState ? "즐겨찾기 취소" : "즐겨찾기")}
</button>
)}

{/* 마스토돈: 북마크만 사용 */}
{account?.platform === "mastodon" && (
<button
type="button"
onClick={() => {
onToggleBookmark(displayStatus);
setMenuOpen(false);
}}
>
{displayStatus.bookmarked ? "북마크 취소" : "북마크"}
</button>
)}

<button
type="button"
onClick={() => {
onToggleBookmark(displayStatus);
setMenuOpen(false);
}}
>
{displayStatus.bookmarked ? "북마크 취소" : "북마크"}
</button>
<button type="button" onClick={handleOpenOrigin} disabled={!originUrl}>
원본 서버에서 보기
</button>
Expand Down
Loading