diff --git a/client/shared/model/user.ts b/client/shared/model/user.ts index 13900283..b6e0d79a 100644 --- a/client/shared/model/user.ts +++ b/client/shared/model/user.ts @@ -13,6 +13,7 @@ export interface UserBaseInfo { discriminator: string; avatar: string | null; temporary: boolean; + extra?: Record; } export interface UserLoginInfo extends UserBaseInfo { @@ -267,6 +268,18 @@ export async function modifyUserField( return data; } +export async function modifyUserExtra( + fieldName: string, + fieldValue: unknown +): Promise { + const { data } = await request.post('/api/user/updateUserExtra', { + fieldName, + fieldValue, + }); + + return data; +} + /** * 获取用户设置 */ diff --git a/client/shared/redux/slices/user.ts b/client/shared/redux/slices/user.ts index e2eff122..419e8af1 100644 --- a/client/shared/redux/slices/user.ts +++ b/client/shared/redux/slices/user.ts @@ -1,4 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import _set from 'lodash/set'; import type { UserLoginInfo } from '../../model/user'; import type { FriendRequest } from '../../model/friend'; @@ -29,9 +30,19 @@ const userSlice = createSlice({ if (state.info === null) { return; } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - state.info[fieldName] = fieldValue; + + _set(state.info, [fieldName], fieldValue); + }, + setUserInfoExtra( + state, + action: PayloadAction<{ fieldName: string; fieldValue: any }> + ) { + const { fieldName, fieldValue } = action.payload; + if (state.info === null) { + return; + } + + _set(state.info, ['extra', fieldName], fieldValue); }, setFriendList(state, action: PayloadAction) { state.friends = action.payload; diff --git a/client/web/plugins/com.msgbyte.user.location/manifest.json b/client/web/plugins/com.msgbyte.user.location/manifest.json new file mode 100644 index 00000000..e33762cd --- /dev/null +++ b/client/web/plugins/com.msgbyte.user.location/manifest.json @@ -0,0 +1,9 @@ +{ + "label": "用户地理位置", + "name": "com.msgbyte.user.location", + "url": "/plugins/com.msgbyte.user.location/index.js", + "version": "0.0.0", + "author": "moonrailgun", + "description": "为用户信息增加地理位置记录", + "requireRestart": true +} diff --git a/client/web/plugins/com.msgbyte.user.location/package.json b/client/web/plugins/com.msgbyte.user.location/package.json new file mode 100644 index 00000000..b93547bc --- /dev/null +++ b/client/web/plugins/com.msgbyte.user.location/package.json @@ -0,0 +1,16 @@ +{ + "name": "@plugins/com.msgbyte.user.location", + "main": "src/index.tsx", + "version": "0.0.0", + "description": "为用户信息增加地理位置记录", + "private": true, + "scripts": { + "sync:declaration": "tailchat declaration github" + }, + "dependencies": {}, + "devDependencies": { + "@types/styled-components": "^5.1.26", + "react": "18.2.0", + "styled-components": "^5.3.6" + } +} diff --git a/client/web/plugins/com.msgbyte.user.location/src/index.tsx b/client/web/plugins/com.msgbyte.user.location/src/index.tsx new file mode 100644 index 00000000..d70b7142 --- /dev/null +++ b/client/web/plugins/com.msgbyte.user.location/src/index.tsx @@ -0,0 +1,6 @@ +import { regUserExtraInfo, localTrans } from '@capital/common'; + +regUserExtraInfo({ + name: 'location', + label: localTrans({ 'zh-CN': '所在城市', 'en-US': 'City' }), +}); diff --git a/client/web/plugins/com.msgbyte.user.location/tsconfig.json b/client/web/plugins/com.msgbyte.user.location/tsconfig.json new file mode 100644 index 00000000..d9b47ed0 --- /dev/null +++ b/client/web/plugins/com.msgbyte.user.location/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + } +} diff --git a/client/web/plugins/com.msgbyte.user.location/types/tailchat.d.ts b/client/web/plugins/com.msgbyte.user.location/types/tailchat.d.ts new file mode 100644 index 00000000..49f524ae --- /dev/null +++ b/client/web/plugins/com.msgbyte.user.location/types/tailchat.d.ts @@ -0,0 +1,2 @@ +declare module '@capital/common'; +declare module '@capital/component'; diff --git a/client/web/registry.json b/client/web/registry.json index 05edd7dd..f743983a 100644 --- a/client/web/registry.json +++ b/client/web/registry.json @@ -75,5 +75,14 @@ "author": "moonrailgun", "description": "快捷打开 files.fm 以支持传输文件", "requireRestart": true + }, + { + "label": "用户地理位置", + "name": "com.msgbyte.user.location", + "url": "/plugins/com.msgbyte.user.location/index.js", + "version": "0.0.0", + "author": "moonrailgun", + "description": "为用户信息增加地理位置记录", + "requireRestart": true } ] diff --git a/client/web/src/components/FullModal/Field.tsx b/client/web/src/components/FullModal/Field.tsx index 52dcc088..f983f4f3 100644 --- a/client/web/src/components/FullModal/Field.tsx +++ b/client/web/src/components/FullModal/Field.tsx @@ -99,15 +99,28 @@ const FullModalFieldEditor: React.FC = React.memo(
{!isEditing ? ( - + ) : ( - + - + )} diff --git a/client/web/src/components/modals/SettingsView/Account.tsx b/client/web/src/components/modals/SettingsView/Account.tsx index 45993055..1490f8ad 100644 --- a/client/web/src/components/modals/SettingsView/Account.tsx +++ b/client/web/src/components/modals/SettingsView/Account.tsx @@ -1,19 +1,21 @@ -import { Avatar } from '@/components/Avatar'; import { AvatarUploader } from '@/components/AvatarUploader'; import { DefaultFullModalInputEditorRender, FullModalField, } from '@/components/FullModal/Field'; import { openModal } from '@/components/Modal'; -import { closeModal } from '@/plugin/common'; +import { closeModal, pluginUserExtraInfo } from '@/plugin/common'; import { getGlobalSocket } from '@/utils/global-state-helper'; import { setUserJWT } from '@/utils/jwt-helper'; import { setGlobalUserLoginInfo } from '@/utils/user-helper'; import { Button, Divider, Typography } from 'antd'; import React, { useCallback } from 'react'; import { useNavigate } from 'react-router'; +import { Avatar } from 'tailchat-design'; import { + model, modifyUserField, + showSuccessToasts, showToasts, t, UploadFileResult, @@ -28,6 +30,7 @@ export const SettingsAccount: React.FC = React.memo(() => { const userInfo = useUserInfo(); const dispatch = useAppDispatch(); const navigate = useNavigate(); + const userExtra = userInfo?.extra ?? {}; const [, handleUserAvatarChange] = useAsyncRequest( async (fileInfo: UploadFileResult) => { @@ -57,6 +60,20 @@ export const SettingsAccount: React.FC = React.memo(() => { [] ); + const [, handleUpdateExtraInfo] = useAsyncRequest( + async (fieldName: string, fieldValue: unknown) => { + await model.user.modifyUserExtra(fieldName, fieldValue); + dispatch( + userActions.setUserInfoExtra({ + fieldName, + fieldValue, + }) + ); + showSuccessToasts(t('修改成功')); + }, + [] + ); + const handleUpdatePassword = useCallback(() => { const key = openModal( closeModal(key)} />); }, []); @@ -93,6 +110,30 @@ export const SettingsAccount: React.FC = React.memo(() => { renderEditor={DefaultFullModalInputEditorRender} onSave={handleUpdateNickName} /> + + {pluginUserExtraInfo.map((item, i) => { + if (item.component && item.component.editor) { + const Component = item.component.editor; + return ( + handleUpdateExtraInfo(item.name, val)} + /> + ); + } + + return ( + handleUpdateExtraInfo(item.name, val)} + /> + ); + })}
diff --git a/client/web/src/components/popover/GroupUserPopover.tsx b/client/web/src/components/popover/GroupUserPopover.tsx index 880816c9..30322f9f 100644 --- a/client/web/src/components/popover/GroupUserPopover.tsx +++ b/client/web/src/components/popover/GroupUserPopover.tsx @@ -1,3 +1,4 @@ +import { pluginUserExtraInfo } from '@/plugin/common'; import { fetchImagePrimaryColor } from '@/utils/image-helper'; import { Tag } from 'antd'; import React, { useEffect } from 'react'; @@ -8,6 +9,7 @@ export const GroupUserPopover: React.FC<{ userInfo: UserBaseInfo; }> = React.memo((props) => { const { userInfo } = props; + const userExtra = userInfo.extra ?? {}; useEffect(() => { if (userInfo.avatar) { @@ -28,6 +30,24 @@ export const GroupUserPopover: React.FC<{
{userInfo.temporary && {t('游客')}}
+ +
+ {pluginUserExtraInfo.map((item, i) => { + const Component = item.component?.render; + return ( +
+
{item.label}:
+
+ {Component ? ( + + ) : ( + String(userExtra[item.name]) + )} +
+
+ ); + })} +
); diff --git a/client/web/src/plugin/builtin.ts b/client/web/src/plugin/builtin.ts index 152de159..b8acb1df 100644 --- a/client/web/src/plugin/builtin.ts +++ b/client/web/src/plugin/builtin.ts @@ -49,6 +49,7 @@ export const builtinPlugins: PluginManifest[] = _compact([ description: '为应用首次打开介绍应用的能力', requireRestart: true, }, + // isOffical isOffical && { label: 'Posthog', name: 'com.msgbyte.posthog', @@ -69,4 +70,13 @@ export const builtinPlugins: PluginManifest[] = _compact([ description: 'Sentry 错误处理', requireRestart: true, }, + isOffical && { + label: '用户地理位置', + name: 'com.msgbyte.user.location', + url: '/plugins/com.msgbyte.user.location/index.js', + version: '0.0.0', + author: 'moonrailgun', + description: '为用户信息增加地理位置记录', + requireRestart: true, + }, ]); diff --git a/client/web/src/plugin/common/reg.ts b/client/web/src/plugin/common/reg.ts index 98b54478..70e1907e 100644 --- a/client/web/src/plugin/common/reg.ts +++ b/client/web/src/plugin/common/reg.ts @@ -230,3 +230,27 @@ export const [ pluginGroupTextPanelExtraMenus, regPluginGroupTextPanelExtraMenu, ] = buildRegList(); + +interface PluginUserExtraInfo { + name: string; + label: string; + /** + * 自定义渲染函数 + * 可选 + */ + component?: { + render?: React.ComponentType<{ + value: unknown; + }>; + editor?: React.ComponentType<{ + value: unknown; + onSave: (val: unknown) => void; + }>; + }; +} + +/** + * 注册用户自定义信息(比如社交账号,地理位置,手机号这些非必须的信息) + */ +export const [pluginUserExtraInfo, regUserExtraInfo] = + buildRegList();