diff --git a/shared/hooks/useAsyncRequest.ts b/shared/hooks/useAsyncRequest.ts index 68cb71d0..00f06639 100644 --- a/shared/hooks/useAsyncRequest.ts +++ b/shared/hooks/useAsyncRequest.ts @@ -15,5 +15,5 @@ export function useAsyncRequest( } }, deps); - return [{ loading }, call] as const; + return [{ loading }, call as T] as const; } diff --git a/shared/model/group.ts b/shared/model/group.ts index 2bee5f30..7ddae357 100644 --- a/shared/model/group.ts +++ b/shared/model/group.ts @@ -116,10 +116,12 @@ export async function quitGroup(groupId: string) { * @param groupId 群组id */ export async function createGroupInviteCode( - groupId: string + groupId: string, + inviteType: 'normal' | 'permanent' ): Promise { const { data } = await request.post('/api/group/invite/createGroupInvite', { groupId, + inviteType, }); return data; diff --git a/web/src/components/InviteCodeExpiredAt.tsx b/web/src/components/InviteCodeExpiredAt.tsx new file mode 100644 index 00000000..16586f70 --- /dev/null +++ b/web/src/components/InviteCodeExpiredAt.tsx @@ -0,0 +1,34 @@ +import { Tooltip } from 'antd'; +import React from 'react'; +import { + datetimeFromNow, + formatFullTime, + GroupInvite, + t, + Trans, +} from 'tailchat-shared'; + +interface InviteCodeExpiredAtProps { + invite: Pick; +} +export const InviteCodeExpiredAt: React.FC = + React.memo((props) => { + const { invite } = props; + + if (!invite.expiredAt) { + return t('该邀请码永不过期'); + } + + return ( + + 该邀请将于{' '} + + + {{ date: datetimeFromNow(invite.expiredAt) }} + {' '} + + 过期 + + ); + }); +InviteCodeExpiredAt.displayName = 'InviteCodeExpiredAt'; diff --git a/web/src/components/modals/CreateGroupInvite/CreateInviteCode.module.less b/web/src/components/modals/CreateGroupInvite/CreateInviteCode.module.less new file mode 100644 index 00000000..5fae3cf0 --- /dev/null +++ b/web/src/components/modals/CreateGroupInvite/CreateInviteCode.module.less @@ -0,0 +1,8 @@ +.createInviteBtn { + display: flex; + :global { + .ant-btn:not(.ant-dropdown-trigger) { + flex: 1; + } + } +} diff --git a/web/src/components/modals/CreateGroupInvite/CreateInviteCode.tsx b/web/src/components/modals/CreateGroupInvite/CreateInviteCode.tsx new file mode 100644 index 00000000..428dcb6a --- /dev/null +++ b/web/src/components/modals/CreateGroupInvite/CreateInviteCode.tsx @@ -0,0 +1,75 @@ +import { InviteCodeExpiredAt } from '@/components/InviteCodeExpiredAt'; +import { Menu, Typography, Dropdown } from 'antd'; +import React, { useState } from 'react'; +import { + useAsyncRequest, + createGroupInviteCode, + t, + GroupInvite, +} from 'tailchat-shared'; +import styles from './CreateInviteCode.module.less'; + +enum InviteCodeType { + Normal = 'normal', + Permanent = 'permanent', +} + +interface CreateInviteCodeProps { + groupId: string; + onInviteCreated?: () => void; +} +export const CreateInviteCode: React.FC = React.memo( + ({ groupId, onInviteCreated }) => { + const [createdInvite, setCreateInvite] = useState(null); + const [{ loading }, handleCreateInviteLink] = useAsyncRequest( + async (inviteType: InviteCodeType) => { + const invite = await createGroupInviteCode(groupId, inviteType); + setCreateInvite(invite); + onInviteCreated?.(); + }, + [groupId, onInviteCreated] + ); + + const menu = ( + + handleCreateInviteLink(InviteCodeType.Permanent)} + > + {t('创建永久邀请码')} + + + ); + + return ( +
+ {createdInvite ? ( +
+ + {`${location.origin}/invite/${createdInvite.code}`} + +

+ +

+
+ ) : ( + handleCreateInviteLink(InviteCodeType.Normal)} + overlay={menu} + trigger={['click']} + > + {t('创建链接')} + + )} +
+ ); + } +); +CreateInviteCode.displayName = 'CreateInviteCode'; diff --git a/web/src/components/modals/CreateGroupInvite/index.tsx b/web/src/components/modals/CreateGroupInvite/index.tsx new file mode 100644 index 00000000..807a3d5e --- /dev/null +++ b/web/src/components/modals/CreateGroupInvite/index.tsx @@ -0,0 +1,60 @@ +import { Icon } from '@/components/Icon'; +import React from 'react'; +import { useGroupInfo, t } from 'tailchat-shared'; +import { ModalWrapper } from '../../Modal'; +import { CreateInviteCode } from './CreateInviteCode'; + +/** + * 群组邀请 + */ + +interface CreateGroupInviteProps { + groupId: string; + onInviteCreated?: () => void; +} +export const CreateGroupInvite: React.FC = React.memo( + (props) => { + const groupId = props.groupId; + const groupInfo = useGroupInfo(groupId); + // const [searchName, setSearchName] = useState(''); + + // const handleSearch = useCallback(() => { + // console.log('searchName', searchName); + // }, []); + + if (!groupInfo) { + return
{t('异常')}
; + } + + return ( + + {/*
邀请好友加入群组 {groupInfo.name}
+ +
+ setSearchName(e.target.value)} + onSearch={handleSearch} + /> +
+ + 或者创建链接并发送给外部好友 */} + + + +
+ {t('创建链接并发送给外部好友')} +
+ + +
+ ); + } +); +CreateGroupInvite.displayName = 'GroupInvite'; diff --git a/web/src/components/modals/GroupDetail/Invite.tsx b/web/src/components/modals/GroupDetail/Invite.tsx index 896f4c6c..f2354734 100644 --- a/web/src/components/modals/GroupDetail/Invite.tsx +++ b/web/src/components/modals/GroupDetail/Invite.tsx @@ -1,4 +1,3 @@ -import { LoadingSpinner } from '@/plugin/component'; import React, { useCallback, useMemo } from 'react'; import { getAllGroupInviteCode, @@ -13,7 +12,8 @@ import { Button, Table, Tooltip } from 'antd'; import type { ColumnType } from 'antd/lib/table'; import { UserName } from '@/components/UserName'; import { Loading } from '@/components/Loading'; -import { openReconfirmModalP } from '@/components/Modal'; +import { openModal, openReconfirmModalP } from '@/components/Modal'; +import { CreateGroupInvite } from '../CreateGroupInvite'; export const GroupInvite: React.FC<{ groupId: string; @@ -22,9 +22,20 @@ export const GroupInvite: React.FC<{ const { loading, value, refresh } = useAsyncRefresh(async () => { const list = await getAllGroupInviteCode(groupId); - return list; + return list.reverse(); // 倒序返回 }, [groupId]); + const handleCreateInvite = useCallback(() => { + openModal( + { + refresh(); + }} + /> + ); + }, [groupId, refresh]); + const handleDeleteInvite = useCallback( async (inviteId: string) => { if (await openReconfirmModalP()) { @@ -53,11 +64,17 @@ export const GroupInvite: React.FC<{ { title: t('过期时间'), dataIndex: 'expiredAt', - render: (date) => ( - - {datetimeFromNow(date)} - - ), + render: (date) => { + if (!date) { + return t('永不过期'); + } + + return ( + + {datetimeFromNow(date)} + + ); + }, }, { title: t('创建者'), @@ -87,6 +104,12 @@ export const GroupInvite: React.FC<{ return ( +
+ +
+ = React.memo((props) => { - const groupId = props.groupId; - const groupInfo = useGroupInfo(groupId); - // const [searchName, setSearchName] = useState(''); - const [inviteLink, setInviteLink] = useState(''); - - // const handleSearch = useCallback(() => { - // console.log('searchName', searchName); - // }, []); - - const [{ loading }, handleCreateInviteLink] = useAsyncRequest(async () => { - const invite = await createGroupInviteCode(groupId); - setInviteLink(`${location.origin}/invite/${invite.code}`); - }, [groupId]); - - if (!groupInfo) { - return
{t('异常')}
; - } - - return ( - - {/*
邀请好友加入群组 {groupInfo.name}
- -
- setSearchName(e.target.value)} - onSearch={handleSearch} - /> -
- - 或者创建链接并发送给外部好友 */} - - - -
- {t('创建链接并发送给外部好友')} -
- -
- {isValidStr(inviteLink) ? ( -
- - {inviteLink} - - {/* TODO: 显示过期天数 */} -

- {t('该邀请链接将会于7天后过期')} -

-
- ) : ( - - )} -
-
- ); -}); -GroupInvite.displayName = 'GroupInvite'; diff --git a/web/src/routes/Invite/InviteInfo.tsx b/web/src/routes/Invite/InviteInfo.tsx index 31682aee..baff8c1c 100644 --- a/web/src/routes/Invite/InviteInfo.tsx +++ b/web/src/routes/Invite/InviteInfo.tsx @@ -1,4 +1,5 @@ import { Avatar } from '@/components/Avatar'; +import { InviteCodeExpiredAt } from '@/components/InviteCodeExpiredAt'; import { LoadingSpinner } from '@/components/LoadingSpinner'; import { UserName } from '@/components/UserName'; import React from 'react'; @@ -66,15 +67,10 @@ export const InviteInfo: React.FC = React.memo((props) => { {t('当前成员数')}: {value.group.memberCount} + {/* 永久邀请码不显示过期时间 */} {value.expired && (
- - 该邀请将于{' '} - - {{ date: datetimeFromNow(value.expired) }} - {' '} - 过期 - +
)} diff --git a/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx b/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx index 332de083..96669a94 100644 --- a/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx +++ b/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx @@ -1,6 +1,6 @@ import { closeModal, openModal } from '@/components/Modal'; import { GroupDetail } from '@/components/modals/GroupDetail'; -import { GroupInvite } from '@/components/modals/GroupInvite'; +import { CreateGroupInvite } from '@/components/modals/CreateGroupInvite'; import React from 'react'; import { useCallback } from 'react'; import { useHistory } from 'react-router'; @@ -25,7 +25,7 @@ export function useGroupHeaderAction(groupId: string) { }, [groupId]); const handleInviteUser = useCallback(() => { - openModal(); + openModal(); }, [groupId]); const handleQuitGroup = useCallback(() => {