feat: 增加自定义用户信息

并增加用户所在城市信息插件
pull/64/head
moonrailgun 2 years ago
parent b5cc18fbe1
commit 1ad880b948

@ -13,6 +13,7 @@ export interface UserBaseInfo {
discriminator: string;
avatar: string | null;
temporary: boolean;
extra?: Record<string, unknown>;
}
export interface UserLoginInfo extends UserBaseInfo {
@ -267,6 +268,18 @@ export async function modifyUserField(
return data;
}
export async function modifyUserExtra(
fieldName: string,
fieldValue: unknown
): Promise<UserBaseInfo> {
const { data } = await request.post('/api/user/updateUserExtra', {
fieldName,
fieldValue,
});
return data;
}
/**
*
*/

@ -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<string[]>) {
state.friends = action.payload;

@ -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
}

@ -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"
}
}

@ -0,0 +1,6 @@
import { regUserExtraInfo, localTrans } from '@capital/common';
regUserExtraInfo({
name: 'location',
label: localTrans({ 'zh-CN': '所在城市', 'en-US': 'City' }),
});

@ -0,0 +1,7 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"importsNotUsedAsValues": "error"
}
}

@ -0,0 +1,2 @@
declare module '@capital/common';
declare module '@capital/component';

@ -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
}
]

@ -99,15 +99,28 @@ const FullModalFieldEditor: React.FC<FullModalFieldProps> = React.memo(
<div className="ml-2">
{!isEditing ? (
<DelayTip title={t('编辑')}>
<IconBtn icon="mdi:square-edit-outline" onClick={handleEditing} />
<IconBtn
size="small"
icon="mdi:square-edit-outline"
onClick={handleEditing}
/>
</DelayTip>
) : (
<Space>
<DelayTip title={t('取消')}>
<IconBtn icon="mdi:close" onClick={handleEditing} />
<IconBtn
size="small"
icon="mdi:close"
onClick={handleEditing}
/>
</DelayTip>
<DelayTip title={t('保存变更')}>
<IconBtn type="primary" icon="mdi:check" onClick={handleSave} />
<IconBtn
type="primary"
size="small"
icon="mdi:check"
onClick={handleSave}
/>
</DelayTip>
</Space>
)}

@ -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(<ModifyPassword onSuccess={() => 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 (
<Component
key={item.name + i}
value={userExtra[item.name]}
onSave={(val) => handleUpdateExtraInfo(item.name, val)}
/>
);
}
return (
<FullModalField
key={item.name + i}
title={item.label}
value={userExtra[item.name] ? String(userExtra[item.name]) : ''}
editable={true}
renderEditor={DefaultFullModalInputEditorRender}
onSave={(val) => handleUpdateExtraInfo(item.name, val)}
/>
);
})}
</div>
</div>

@ -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<{
<div>
{userInfo.temporary && <Tag color="processing">{t('游客')}</Tag>}
</div>
<div className="pt-2">
{pluginUserExtraInfo.map((item, i) => {
const Component = item.component?.render;
return (
<div key={item.name + i} className="flex">
<div className="w-1/4 text-gray-500">{item.label}:</div>
<div className="w-3/4">
{Component ? (
<Component value={userExtra[item.name]} />
) : (
String(userExtra[item.name])
)}
</div>
</div>
);
})}
</div>
</UserProfileContainer>
</div>
);

@ -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,
},
]);

@ -230,3 +230,27 @@ export const [
pluginGroupTextPanelExtraMenus,
regPluginGroupTextPanelExtraMenu,
] = buildRegList<PluginPanelMenu>();
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<PluginUserExtraInfo>();

Loading…
Cancel
Save