Skip to content

Commit 20d2678

Browse files
Merge pull request #148 from MaaAssistantArknights/dev
Feat: comment & bullet time
2 parents 1339ff7 + d0b3d60 commit 20d2678

23 files changed

+1271
-469
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"react-router-dom": "6",
5353
"react-spring": "^9.4.5",
5454
"react-use": "^17.4.0",
55+
"remark-breaks": "^3.0.2",
5556
"remark-gfm": "^3.0.1",
5657
"snakecase-keys": "^5.4.4",
5758
"swr": "^2.0.0-rc.3",
@@ -83,7 +84,7 @@
8384
"prettier": "^2.7.1",
8485
"tailwindcss": "^3.1.4",
8586
"typescript": "^4.6.3",
86-
"vite": "^2.9.9"
87+
"vite": "^4.2.1"
8788
},
8889
"resolutions": {
8990
"react-rating/@types/react": "^18.0.0"

src/apis/comment.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { isNil } from 'lodash-es'
2+
import { useEffect } from 'react'
3+
import useSWRInfinite from 'swr/infinite'
4+
5+
import { Response } from 'models/network'
6+
import { jsonRequest } from 'utils/fetcher'
7+
8+
import { CommentRating, MainCommentInfo } from '../models/comment'
9+
import { Operation, PaginatedResponse } from '../models/operation'
10+
11+
export interface CommentsQueryParams {
12+
copilotId: number
13+
page?: number
14+
limit?: number
15+
desc?: boolean
16+
orderBy?: string
17+
}
18+
19+
export interface UseCommentsParams
20+
extends Omit<CommentsQueryParams, 'page' | 'copilotId'> {
21+
suspense?: boolean
22+
operationId: Operation['id']
23+
}
24+
25+
export const useComments = ({
26+
operationId,
27+
limit,
28+
desc,
29+
orderBy = 'uploadTime',
30+
suspense,
31+
}: UseCommentsParams) => {
32+
const {
33+
data: listData,
34+
size,
35+
setSize,
36+
mutate,
37+
isValidating,
38+
} = useSWRInfinite<Response<PaginatedResponse<MainCommentInfo>>>(
39+
(pageIndex, previousPageData) => {
40+
if (previousPageData && !previousPageData?.data.hasNext) {
41+
return null // reached the end
42+
}
43+
44+
if (!isFinite(+operationId)) {
45+
throw new Error('operationId is not a valid number')
46+
}
47+
48+
const params: CommentsQueryParams = {
49+
page: pageIndex + 1,
50+
copilotId: +operationId,
51+
limit,
52+
desc,
53+
orderBy,
54+
}
55+
56+
const searchParams = new URLSearchParams()
57+
58+
Object.entries(params).forEach(([key, value]) => {
59+
if (!isNil(value)) {
60+
searchParams.append(key, value.toString())
61+
}
62+
})
63+
64+
return `/comments/query?${searchParams.toString()}`
65+
},
66+
{
67+
suspense,
68+
focusThrottleInterval: 1000 * 60 * 30,
69+
},
70+
)
71+
72+
const isReachingEnd = listData?.some((el) => !el.data.hasNext)
73+
74+
const comments: MainCommentInfo[] =
75+
listData?.map((el) => el.data.data).flat() || []
76+
77+
useEffect(() => {
78+
setSize(1)
79+
}, [orderBy, limit, desc, operationId])
80+
81+
return { comments, size, setSize, mutate, isValidating, isReachingEnd }
82+
}
83+
84+
export const requestAddComment = (
85+
message: string,
86+
operationId: Operation['id'],
87+
fromCommentId?: string,
88+
) => {
89+
return jsonRequest<Response<string>>('/comments/add', {
90+
method: 'POST',
91+
json: {
92+
copilot_id: operationId,
93+
message,
94+
from_comment_id: fromCommentId,
95+
},
96+
})
97+
}
98+
99+
export const requestDeleteComment = (commentId: string) => {
100+
return jsonRequest<Response<string>>('/comments/delete', {
101+
method: 'POST',
102+
json: {
103+
comment_id: commentId,
104+
},
105+
})
106+
}
107+
108+
export const requestRateComment = (
109+
commentId: string,
110+
rating: CommentRating,
111+
) => {
112+
return jsonRequest<Response<string>>('/comments/rating', {
113+
method: 'POST',
114+
json: {
115+
comment_id: commentId,
116+
rating,
117+
},
118+
})
119+
}

src/components/ActionCard.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Card, Elevation } from '@blueprintjs/core'
2+
3+
import clsx from 'clsx'
4+
import { FC } from 'react'
5+
import { ReactNode } from 'react-markdown/lib/ast-to-react'
6+
import { FCC } from 'types'
7+
8+
import { CardTitle } from 'components/CardTitle'
9+
import { FactItem } from 'components/FactItem'
10+
import type { CopilotDocV1 } from 'models/copilot.schema'
11+
import { findActionType } from 'models/types'
12+
13+
import {
14+
findOperatorDirection,
15+
findOperatorSkillUsage,
16+
} from '../models/operator'
17+
import { formatDuration } from '../utils/times'
18+
19+
interface ActionCardProps {
20+
className?: string
21+
action: CopilotDocV1.Action
22+
title?: ReactNode
23+
}
24+
25+
export const ActionCard: FC<ActionCardProps> = ({
26+
className,
27+
action,
28+
title,
29+
}) => {
30+
const type = findActionType(action.type)
31+
32+
title ??= (
33+
<div className="flex items-center">
34+
<CardTitle className="mb-0 flex-grow" icon={type.icon}>
35+
<span className="mr-2">{type.title}</span>
36+
</CardTitle>
37+
</div>
38+
)
39+
40+
return (
41+
<Card
42+
elevation={Elevation.TWO}
43+
className={clsx(className, 'flex mb-2 last:mb-0 border-l-4', type.accent)}
44+
>
45+
<div className="flex-grow">
46+
{title}
47+
48+
<div className="flex flex-wrap gap-x-8 gap-y-2 mt-6 w-full">
49+
{'name' in action && action.name && (
50+
<FactItem
51+
dense
52+
title={action.name}
53+
icon="mugshot"
54+
className="font-bold"
55+
/>
56+
)}
57+
58+
{'skillUsage' in action && (
59+
<FactItem
60+
dense
61+
title={findOperatorSkillUsage(action.skillUsage).title}
62+
icon="swap-horizontal"
63+
/>
64+
)}
65+
66+
{'location' in action && action.location && (
67+
<FactItem dense title="坐标" icon="map-marker">
68+
<span className="font-mono">{action.location.join(', ')}</span>
69+
</FactItem>
70+
)}
71+
72+
{'direction' in action && (
73+
<FactItem dense title="朝向" icon="compass">
74+
<span className="font-mono">
75+
{findOperatorDirection(action.direction).title}
76+
</span>
77+
</FactItem>
78+
)}
79+
80+
{'distance' in action && action.distance && (
81+
<FactItem dense title="距离" icon="camera">
82+
<span className="font-mono">{action.distance.join(', ')}</span>
83+
</FactItem>
84+
)}
85+
</div>
86+
</div>
87+
88+
{/* direction:rtl is for the grid to place columns from right to left; need to set it back to ltr for the children */}
89+
<div className="grid grid-flow-row grid-cols-2 gap-y-2 text-right [direction:rtl] [&>*]:[direction:ltr]">
90+
<InlineCondition title="击杀">{action.kills || '-'}</InlineCondition>
91+
<InlineCondition title="冷却中">
92+
{action.cooling || '-'}
93+
</InlineCondition>
94+
<InlineCondition title="费用">{action.costs || '-'}</InlineCondition>
95+
<InlineCondition title="费用变化">
96+
{action.costChanges || '-'}
97+
</InlineCondition>
98+
<InlineCondition title="前置">
99+
{action.preDelay ? formatDuration(action.preDelay) : '-'}
100+
</InlineCondition>
101+
<InlineCondition title="后置">
102+
{action.rearDelay ? formatDuration(action.rearDelay) : '-'}
103+
</InlineCondition>
104+
</div>
105+
</Card>
106+
)
107+
}
108+
109+
const InlineCondition: FCC<{
110+
title?: string
111+
}> = ({ title, children }) => (
112+
<div className="min-w-[5em] text-lg leading-none">
113+
<span className="text-zinc-500 mr-0.5 tabular-nums font-bold">
114+
{children}
115+
</span>
116+
<span className="text-zinc-400 text-xs">{title}</span>
117+
</div>
118+
)

src/components/Markdown.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import clsx from 'clsx'
2+
import { uniq } from 'lodash-es'
3+
import ReactMarkdown from 'react-markdown'
4+
import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown'
5+
import remarkBreaks from 'remark-breaks'
6+
import remarkGfm from 'remark-gfm'
7+
8+
interface MarkdownProps extends ReactMarkdownOptions {
9+
className?: string
10+
}
11+
12+
export function Markdown({
13+
className,
14+
remarkPlugins,
15+
components,
16+
children,
17+
...props
18+
}: MarkdownProps) {
19+
return (
20+
<ReactMarkdown
21+
className={clsx(
22+
className,
23+
'markdown-body !text-sm !bg-transparent [&_img]:!bg-transparent',
24+
)}
25+
remarkPlugins={uniq([remarkGfm, remarkBreaks, ...(remarkPlugins ?? [])])}
26+
components={{
27+
...components,
28+
29+
a: ({ node, children, ...props }) => {
30+
// set target="_blank" for external links, see: https://github.com/remarkjs/react-markdown/issues/12#issuecomment-1479195975
31+
if (props.href?.startsWith('http')) {
32+
props.target = '_blank'
33+
props.rel = 'noopener noreferrer'
34+
}
35+
36+
// by default, ReactMarkdown already handles dangerous URIs and replaces them with "javascript:void(0)",
37+
// but React warns about passing such a string to the href, so we'll handle this
38+
if (props.href?.startsWith('javascript:')) {
39+
props.href = '#'
40+
props.onClick = (e) => e.preventDefault()
41+
}
42+
43+
return <a {...props}>{children}</a>
44+
},
45+
}}
46+
{...props}
47+
>
48+
{children}
49+
</ReactMarkdown>
50+
)
51+
}

src/components/OutlinedIcon.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Icon, IconProps } from '@blueprintjs/core'
2+
3+
import clsx from 'clsx'
4+
5+
export const OutlinedIcon = ({
6+
className,
7+
outlined = true,
8+
...iconProps
9+
}: IconProps & {
10+
// default is true, set to false to disable the outline effect
11+
outlined?: boolean
12+
}) => (
13+
<Icon
14+
{...iconProps}
15+
className={clsx(
16+
className,
17+
(outlined ?? true) && '[&_path]:fill-transparent [&_path]:stroke-current',
18+
)}
19+
/>
20+
)

src/components/announcement/AnnDialog.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import {
88
} from '@blueprintjs/core'
99

1010
import { FC } from 'react'
11-
import ReactMarkdown, { Components } from 'react-markdown'
12-
import remarkGfm from 'remark-gfm'
11+
import { Components } from 'react-markdown'
1312

1413
import { announcementBaseURL } from '../../apis/announcement'
1514
import {
1615
AnnouncementSection,
1716
AnnouncementSectionMeta,
1817
} from '../../models/announcement'
1918
import { formatDateTime, formatRelativeTime } from '../../utils/times'
19+
import { Markdown } from '../Markdown'
2020

2121
interface AnnDialogProps extends DialogProps {
2222
sections?: AnnouncementSection[]
@@ -68,9 +68,7 @@ export const AnnDialog: FC<AnnDialogProps> = ({ sections, ...dialogProps }) => {
6868
<Dialog className="" title="公告" icon="info-sign" {...dialogProps}>
6969
<DialogBody className="">
7070
{content ? (
71-
<ReactMarkdown
72-
className="markdown-body !text-sm !bg-transparent [&_img]:!bg-transparent"
73-
remarkPlugins={[remarkGfm]}
71+
<Markdown
7472
rehypePlugins={[attachMetaPlugin]}
7573
components={{
7674
h1: Heading,
@@ -84,7 +82,7 @@ export const AnnDialog: FC<AnnDialogProps> = ({ sections, ...dialogProps }) => {
8482
transformImageUri={transformUri}
8583
>
8684
{content || ''}
87-
</ReactMarkdown>
85+
</Markdown>
8886
) : (
8987
<NonIdealState icon="help" title="暂无公告" />
9088
)}

src/components/editor/OperationEditor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,11 +360,11 @@ export const OperationEditor: FC<OperationEditorProps> = ({
360360

361361
<div className="h-[1px] w-full bg-gray-200 mt-4 mb-6" />
362362

363-
<div className="flex flex-wrap md:flex-nowrap min-h-[calc(100vh-6rem)]">
364-
<div className="w-full md:w-1/3 md:mr-8 flex flex-col pb-8">
363+
<div className="flex flex-col min-h-[calc(100vh-6rem)]">
364+
<div className="w-full flex flex-col pb-8">
365365
<EditorPerformerPanel control={control} />
366366
</div>
367-
<div className="w-full md:w-2/3 pb-8">
367+
<div className="w-full pb-8">
368368
<H4>动作序列</H4>
369369
<HelperText className="mb-4">
370370
<span>拖拽以重新排序</span>

0 commit comments

Comments
 (0)