Skip to content
Open
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
6 changes: 5 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"$schema": "https://biomejs.dev/schemas/1.9.1/schema.json",
"files": {
"ignore": ["./src/routeTree.gen.ts"],
"ignoreUnknown": true
Expand All @@ -11,6 +11,10 @@
},
"linter": {
"rules": {
"correctness": {
"noUnusedImports": "warn",
"noUnusedVariables": "warn"
},
"style": {
"noNonNullAssertion": "warn"
}
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@phosphor-icons/react": "^2.1.7",
"@tanstack/react-router": "^1.56.5",
"@tanstack/react-router": "^1.57.17",
"@yamada-ui/carousel": "^1.0.37",
"@yamada-ui/react": "^1.5.0",
"@yamada-ui/react": "^1.5.1",
"misskey-js": "^2024.8.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -32,18 +32,18 @@
"zustand": "^4.5.5"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@tanstack/router-cli": "^1.56.4",
"@tanstack/router-devtools": "^1.56.5",
"@tanstack/router-plugin": "^1.56.4",
"@biomejs/biome": "1.9.1",
"@tanstack/router-cli": "^1.57.15",
"@tanstack/router-devtools": "^1.57.17",
"@tanstack/router-plugin": "^1.57.15",
"@types/node": "^20.16.5",
"@types/react": "^18.3.5",
"@types/react": "^18.3.7",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"@yamada-ui/cli": "^1.1.1",
"typescript": "^5.5.4",
"vite": "^5.4.3",
"typescript": "^5.6.2",
"vite": "^5.4.6",
"vite-tsconfig-paths": "^4.3.2"
},
"packageManager": "[email protected]+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c"
Expand Down
1,866 changes: 933 additions & 933 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/apis/meta/meta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import useSWR from "swr";

import type { MetaDetailed } from "misskey-js/entities.js";

export const useMeta = () => {
const { data } = useSWR<MetaDetailed>(["/meta", { detail: true }]);

return { data };
};
34 changes: 26 additions & 8 deletions src/apis/notes/timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import { useCallback } from "react";

import type { Note } from "misskey-js/entities.js";

import { useTimeLineStore } from "@/store/timeline";
import { useCurrentTimelineStore } from "@/store/currentTimeline";
import { useTimelineStore } from "@/store/timeline";
import { fetcher } from "@/utils/fetcher";

export const useGetTimeLine = () => {
const { addNotesToBottom } = useTimeLineStore();
const getTimeLine = async (arg?: { untilId?: string }) => {
const notes = await fetcher<ReadonlyArray<Note>>(["/notes/timeline", arg]);
addNotesToBottom(notes);
};
const apiPath = {
homeTimeline: "/notes/timeline",
localTimeline: "/notes/local-timeline",
hybridTimeline: "/notes/hybrid-timeline",
globalTimeline: "/notes/global-timeline",
};

export const useGetTimeline = () => {
const { currentTimeline } = useCurrentTimelineStore();
const { addNotesToBottom } = useTimelineStore();

const getTimeline = useCallback(
async (arg?: { untilId?: string }) => {
const notes = await fetcher<ReadonlyArray<Note>>([
apiPath[currentTimeline],
arg,
]);
addNotesToBottom(notes);
},
[addNotesToBottom, currentTimeline],
);

return { getTimeLine };
return { getTimeline };
};
81 changes: 59 additions & 22 deletions src/apis/websocket/timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
import { useEffect, useRef } from "react";
import ReconnectingWebSocket from "reconnecting-websocket";

import { useGetTimeLine } from "../notes/timeline";
import { useGetTimeline } from "../notes/timeline";

import type { Timelines } from "@/store/currentTimeline";
import type { Note } from "misskey-js/entities.js";

import { useCurrentTimelineStore } from "@/store/currentTimeline";
import { useLoginStore } from "@/store/login";
import { useTimeLineStore } from "@/store/timeline";
import { useTimelineStore } from "@/store/timeline";

const id = "homeTimeLine";

type ChannelNote = {
type ChannelNote<T> = {
type: "channel";
body: {
id: typeof id;
id: T;
type: "note";
body: Note;
};
};

const connectHomeTimeLineObject = JSON.stringify({
type: "connect",
body: {
channel: "homeTimeline",
id: id,
},
});
const streamTimelineObject = ({
type,
channel,
}: {
type: "connect" | "disconnect";
channel: Timelines;
}) =>
JSON.stringify({
type: type,
body: {
channel: channel,
id: channel,
},
});

export const useTimeLine = () => {
export const useTimeline = () => {
const { instance, token } = useLoginStore();
const { addNoteToTop } = useTimeLineStore();
const { getTimeLine } = useGetTimeLine();
const { addNoteToTop } = useTimelineStore();
const { currentTimeline } = useCurrentTimelineStore();
const { clear } = useTimelineStore();
const { getTimeline } = useGetTimeline();

const prevTimeline = useRef(currentTimeline);
const socketRef = useRef<ReconnectingWebSocket>();

useEffect(() => {
Expand All @@ -42,16 +52,43 @@ export const useTimeLine = () => {
socketRef.current = socket;

socket.onopen = () => {
if (useTimeLineStore.getState().notes.length === 0) {
getTimeLine();
}
socket.send(connectHomeTimeLineObject);
getTimeline().then(() => {
socket.send(
streamTimelineObject({
type: "connect",
channel: currentTimeline,
}),
);
});
};

socket.onmessage = (event: MessageEvent) => {
const response: ChannelNote = JSON.parse(event.data);
const response: ChannelNote<typeof currentTimeline> = JSON.parse(
event.data,
);
addNoteToTop(response.body.body);
};
}
}, [instance, token, addNoteToTop, getTimeLine]);
}, [instance, token, addNoteToTop, currentTimeline, getTimeline]);

useEffect(() => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(
streamTimelineObject({
type: "disconnect",
channel: prevTimeline.current,
}),
);
clear();
prevTimeline.current = currentTimeline;
getTimeline().then(() => {
socketRef.current?.send(
streamTimelineObject({
type: "connect",
channel: currentTimeline,
}),
);
});
}
}, [currentTimeline, getTimeline, clear]);
};
2 changes: 1 addition & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ReactDOM from "react-dom/client";
import { SWRConfig } from "swr";

import { Router } from "./Router";
import { theme, config } from "./theme/theme";
import { config, theme } from "./theme/theme";
import { fetcher } from "./utils/fetcher";

const injectThemeSchemeScript = () => {
Expand Down
24 changes: 22 additions & 2 deletions src/pages/-components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link } from "@tanstack/react-router";
import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import { Avatar, HStack, Heading } from "@yamada-ui/react";

import { HeaderMenu } from "./HeaderMenu";
Expand All @@ -8,6 +8,8 @@ import { useMySelfStore } from "@/store/user";

export const Header = () => {
const { mySelf } = useMySelfStore();
const navigate = useNavigate();
const matchRoute = useMatchRoute();

return (
<HStack
Expand All @@ -16,8 +18,26 @@ export const Header = () => {
py="2"
bg="bg"
color="darkText"
pos="sticky"
top="0"
zIndex="1"
>
<Heading size="lg" isTruncated fontWeight="light" as={Link} to="/">
<Heading
size="lg"
isTruncated
fontWeight="light"
cursor="pointer"
onClick={() => {
if (matchRoute({ to: "/" })) {
window.scroll({
top: 0,
behavior: "smooth",
});
} else {
navigate({ to: "/" });
}
}}
>
at Dusk.
</Heading>
{mySelf ? (
Expand Down
5 changes: 3 additions & 2 deletions src/pages/-components/HeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Menu,
MenuButton,
MenuList,
VStack,
useDisclosure,
} from "@yamada-ui/react";

Expand All @@ -19,9 +20,9 @@ export const HeaderMenu = () => {
<DotsNine size={20} weight="bold" />
</MenuButton>

<MenuList alignItems="center" gap="md">
<MenuList as={VStack} px="2">
<LogoutButton />
<HStack>
<HStack justify="center">
<ToggleThemeButton />
<ToggleColorModeButton />
</HStack>
Expand Down
8 changes: 7 additions & 1 deletion src/pages/-components/LogoutDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Dialog } from "@yamada-ui/react";

import { useLoginStore } from "@/store/login";
import { useNavigate } from "@tanstack/react-router";

export const LogoutDialog = ({
isOpen,
Expand All @@ -10,12 +11,17 @@ export const LogoutDialog = ({
onClose: () => void;
}) => {
const { logout } = useLoginStore();
const navigate = useNavigate();

return (
<Dialog
isOpen={isOpen}
header="ログアウトしますか?"
success="する"
onSuccess={logout}
onSuccess={() => {
logout();
navigate({ to: "/login" });
}}
cancel="しない"
onCancel={onClose}
/>
Expand Down
72 changes: 72 additions & 0 deletions src/pages/-components/TimelineTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Campfire, Lamp, Meteor, Park } from "@phosphor-icons/react";
import { Button, ButtonGroup } from "@yamada-ui/react";
import { useMemo } from "react";

import { useMeta } from "@/apis/meta/meta";
import {
useCurrentTimelineStore,
type Timelines,
} from "@/store/currentTimeline";

const optionalTimelines = (
localTimelineEnabled?: boolean,
globalTimelineEnabled?: boolean,
) => {
const t: Array<Timelines> = [];
if (localTimelineEnabled) {
t.push("localTimeline", "hybridTimeline");
}
if (globalTimelineEnabled) {
t.push("globalTimeline");
}
return t;
};

const timelineLabel = {
homeTimeline: "ホーム",
localTimeline: "ローカル",
hybridTimeline: "ソーシャル",
globalTimeline: "グローバル",
};

const timelineIcon = {
homeTimeline: <Lamp size={24} weight="fill" />,
localTimeline: <Campfire size={24} weight="fill" />,
hybridTimeline: <Park size={24} weight="fill" />,
globalTimeline: <Meteor size={24} weight="fill" />,
};

export const TimelineTab = () => {
const { currentTimeline, setCurrentTimeline } = useCurrentTimelineStore();
const features = useMeta().data?.features;
const timelines = useMemo<Array<Timelines>>(
() => [
"homeTimeline",
...optionalTimelines(features?.localTimeline, features?.globalTimeline),
],
[features],
);

if (!features) {
return <Button isLoading variant="outline" disabled />;
}

return (
<ButtonGroup gap="4">
{timelines.map((t) => (
<Button
key={t}
variant={currentTimeline === t ? "solid" : "outline"}
onClick={() => {
if (currentTimeline !== t) {
setCurrentTimeline(t);
}
}}
>
{timelineIcon[t]}
{timelineLabel[t]}
</Button>
))}
</ButtonGroup>
);
};
Loading