Compare commits

...

5 Commits

Author SHA1 Message Date
CrescentLeaf
937af27698 feat: 删除收藏的对话 2025-10-26 23:04:12 +08:00
CrescentLeaf
5469ff6826 若factor: addContact -> s 2025-10-26 23:02:56 +08:00
CrescentLeaf
f8e6fcac46 chore: 客户端的 deno.jsonc: patch -> links
* 与此同时, 会导致旧版本 Deno 不认
2025-10-26 22:07:49 +08:00
CrescentLeaf
ab96ef889d fix: 当用户名没有被修改时, 忽略修改操作 2025-10-26 21:20:11 +08:00
CrescentLeaf
b1e618e07c feat: 修改密码 (UI) 2025-10-26 21:18:31 +08:00
8 changed files with 157 additions and 48 deletions

View File

@@ -12,7 +12,7 @@ export type CallMethod =
"User.getInfo" | "User.getInfo" |
"User.getMyContacts" | "User.getMyContacts" |
"User.addContact" | "User.addContacts" |
"User.removeContacts" | "User.removeContacts" |
"User.getMyRecentChats" | "User.getMyRecentChats" |

View File

@@ -9,11 +9,10 @@
"jsxImportSource": "react", "jsxImportSource": "react",
"jsxImportSourceTypes": "@types/react" "jsxImportSourceTypes": "@types/react"
}, },
"patch": [ "links": [
"./mdui_patched", "./mdui_patched",
], ],
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"unstable": ["npm-patch"],
"imports": { "imports": {
"@deno/vite-plugin": "npm:@deno/vite-plugin@1.0.5", "@deno/vite-plugin": "npm:@deno/vite-plugin@1.0.5",
"@types/react": "npm:@types/react@18.3.1", "@types/react": "npm:@types/react@18.3.1",

View File

@@ -18,14 +18,14 @@ export default function AddContactDialog({
const inputTargetRef = React.useRef<TextField>(null) const inputTargetRef = React.useRef<TextField>(null)
async function addContact() { async function addContact() {
const re = await Client.invoke("User.addContact", { const re = await Client.invoke("User.addContacts", {
target: inputTargetRef.current!.value, targets: [inputTargetRef.current!.value],
token: data.access_token, token: data.access_token,
}) })
if (checkApiSuccessOrSncakbar(re, "添加失敗")) return if (checkApiSuccessOrSncakbar(re, "添加失敗")) return
snackbar({ snackbar({
message: "添加成功!", message: re.msg,
placement: "top", placement: "top",
}) })
EventBus.emit('ContactsList.updateContacts') EventBus.emit('ContactsList.updateContacts')

View File

@@ -42,6 +42,12 @@ export default function MyProfileDialog({
const editNickNameRef = React.useRef<TextField>(null) const editNickNameRef = React.useRef<TextField>(null)
const editUserNameRef = React.useRef<TextField>(null) const editUserNameRef = React.useRef<TextField>(null)
const accountSettingsDialogRef = React.useRef<Dialog>(null)
const editPasswordDialogRef = React.useRef<Dialog>(null)
const editPasswordOldInputRef = React.useRef<TextField>(null)
const editPasswordNewInputRef = React.useRef<TextField>(null)
return (<> return (<>
{ {
// 公用 - 資料卡 // 公用 - 資料卡
@@ -66,14 +72,10 @@ export default function MyProfileDialog({
<mdui-list> <mdui-list>
<mdui-list-item icon="edit" rounded onClick={() => userProfileEditDialogRef.current!.open = true}></mdui-list-item> <mdui-list-item icon="edit" rounded onClick={() => userProfileEditDialogRef.current!.open = true}></mdui-list-item>
<mdui-list-item icon="settings" rounded></mdui-list-item> <mdui-list-item icon="settings" rounded onClick={() => accountSettingsDialogRef.current!.open = true}></mdui-list-item>
{/* {/*
<mdui-list-item icon="lock" rounded>隱私設定</mdui-list-item> <mdui-list-item icon="lock" rounded>隱私設定</mdui-list-item>
*/} */}
<mdui-divider style={{
marginTop: "10px",
marginBottom: "10px",
}}></mdui-divider>
<mdui-list-item icon="logout" rounded onClick={() => dialog({ <mdui-list-item icon="logout" rounded onClick={() => dialog({
headline: "退出登录", headline: "退出登录",
description: "请确保在退出登录前, 设定了用户名或者已经记录下了用户 ID, 以免无法登录账号", description: "请确保在退出登录前, 设定了用户名或者已经记录下了用户 ID, 以免无法登录账号",
@@ -101,9 +103,35 @@ export default function MyProfileDialog({
{ {
// 账号设定 // 账号设定
} }
<mdui-dialog close-on-overlay-click close-on-esc ref={userProfileEditDialogRef} headline="账号设定"> <mdui-dialog close-on-overlay-click close-on-esc ref={accountSettingsDialogRef} headline="账号设定">
<mdui-list-item icon="edit" rounded onClick={() => editPasswordDialogRef.current!.open = true}></mdui-list-item>
<mdui-button slot="action" variant="text" onClick={() => userProfileEditDialogRef.current!.open = false}></mdui-button> <mdui-button slot="action" variant="text" onClick={() => accountSettingsDialogRef.current!.open = false}></mdui-button>
</mdui-dialog>
<mdui-dialog close-on-overlay-click close-on-esc ref={editPasswordDialogRef} headline="修改密码">
<mdui-text-field label="旧密码" type="password" toggle-password ref={editPasswordOldInputRef as any}></mdui-text-field>
<div style={{
height: "10px",
}}></div>
<mdui-text-field label="新密码" type="password" toggle-password ref={editPasswordNewInputRef as any}></mdui-text-field>
<mdui-button slot="action" variant="text" onClick={() => editPasswordDialogRef.current!.open = false}></mdui-button>
<mdui-button slot="action" variant="text" onClick={async () => {
const re = await Client.invoke("User.resetPassword", {
token: data.access_token,
old_password: CryptoJS.SHA256(editPasswordOldInputRef.current?.value || '').toString(CryptoJS.enc.Hex),
new_password: CryptoJS.SHA256(editPasswordNewInputRef.current?.value || '').toString(CryptoJS.enc.Hex),
})
if (checkApiSuccessOrSncakbar(re, "修改失败")) return
snackbar({
message: "修改成功 (其他客户端需要重新登录)",
placement: "top",
})
data.access_token = re.data!.access_token as string
data.refresh_token = re.data!.refresh_token as string
data.apply()
editPasswordDialogRef.current!.open = false
}}></mdui-button>
</mdui-dialog> </mdui-dialog>
{ {
// 個人資料編輯 // 個人資料編輯

View File

@@ -1,11 +1,10 @@
import React from "react" import React from "react"
import ContactsListItem from "./ContactsListItem.tsx" import ContactsListItem from "./ContactsListItem.tsx"
import useEventListener from "../useEventListener.ts" import useEventListener from "../useEventListener.ts"
import { Dialog, ListItem, TextField } from "mdui" import { dialog, Dialog, TextField } from "mdui"
import useAsyncEffect from "../useAsyncEffect.ts"
import Client from "../../api/Client.ts" import Client from "../../api/Client.ts"
import data from "../../Data.ts" import data from "../../Data.ts"
import { checkApiSuccessOrSncakbar } from "../snackbar.ts" import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts"
import Chat from "../../api/client_data/Chat.ts" import Chat from "../../api/client_data/Chat.ts"
import EventBus from "../../EventBus.ts" import EventBus from "../../EventBus.ts"
@@ -27,24 +26,29 @@ export default function ContactsList({
const [isMultiSelecting, setIsMultiSelecting] = React.useState(false) const [isMultiSelecting, setIsMultiSelecting] = React.useState(false)
const [searchText, setSearchText] = React.useState('') const [searchText, setSearchText] = React.useState('')
const [contactsList, setContactsList] = React.useState<Chat[]>([]) const [contactsList, setContactsList] = React.useState<Chat[]>([])
const [checkedList, setCheckedList] = React.useState<{ [key: string]: boolean }>({})
useEventListener(searchRef, 'input', (e) => { useEventListener(searchRef, 'input', (e) => {
setSearchText((e.target as unknown as TextField).value) setSearchText((e.target as unknown as TextField).value)
}) })
useAsyncEffect(async () => { React.useEffect(() => {
async function updateContacts() { async function updateContacts() {
const re = await Client.invoke("User.getMyContacts", { const re = await Client.invoke("User.getMyContacts", {
token: data.access_token, token: data.access_token,
}) })
if (re.code != 200) if (re.code != 200)
return checkApiSuccessOrSncakbar(re, "获取所有对话列表失败") return checkApiSuccessOrSncakbar(re, "获取所有对话列表失败")
setContactsList(re.data!.contacts_list as Chat[]) setContactsList(re.data!.contacts_list as Chat[])
} }
updateContacts() updateContacts()
EventBus.on('ContactsList.updateContacts', () => updateContacts()) EventBus.on('ContactsList.updateContacts', () => updateContacts())
}) return () => {
EventBus.off('ContactsList.updateContacts')
}
// 警告: 不添加 deps 導致無限執行
}, [])
return <mdui-list style={{ return <mdui-list style={{
overflowY: 'auto', overflowY: 'auto',
@@ -68,12 +72,66 @@ export default function ContactsList({
<mdui-list-item rounded style={{ <mdui-list-item rounded style={{
width: '100%', width: '100%',
marginTop: '13px', marginTop: '13px',
marginBottom: '15px',
}} icon="refresh" onClick={() => EventBus.emit('ContactsList.updateContacts')}></mdui-list-item> }} icon="refresh" onClick={() => EventBus.emit('ContactsList.updateContacts')}></mdui-list-item>
{/* <mdui-list-item rounded style={{ <mdui-list-item rounded style={{
width: '100%', width: '100%',
marginBottom: '15px', }} icon={isMultiSelecting ? "done" : "edit"} onClick={() => {
}} icon={ isMultiSelecting ? "done" : "edit"} onClick={() => setIsMultiSelecting(!isMultiSelecting)}>{ isMultiSelecting ? "關閉多選" : "多選模式" }</mdui-list-item> */} if (isMultiSelecting)
setCheckedList({})
setIsMultiSelecting(!isMultiSelecting)
}}>{isMultiSelecting ? "关闭多选" : "多选模式"}</mdui-list-item>
{
isMultiSelecting && <>
<mdui-list-item rounded style={{
width: '100%',
}} icon="delete" onClick={() => dialog({
headline: "删除所选",
description: "确定要删除所选的收藏对话吗? 这并不会删除您的聊天记录, 也不会丢失对话成员身份",
actions: [
{
text: "取消",
onClick: () => {
return true
},
},
{
text: "确定",
onClick: async () => {
const ls = Object.keys(checkedList).filter((chatId) => checkedList[chatId] == true)
const re = await Client.invoke("User.removeContacts", {
token: data.access_token,
targets: ls,
})
if (re.code != 200)
checkApiSuccessOrSncakbar(re, "删除所选收藏失败")
else {
setCheckedList({})
setIsMultiSelecting(false)
EventBus.emit('ContactsList.updateContacts')
snackbar({
message: "已删除所选",
placement: "top",
action: "撤销操作",
onActionClick: async () => {
const re = await Client.invoke("User.addContacts", {
token: data.access_token,
targets: ls,
})
if (re.code != 200)
checkApiSuccessOrSncakbar(re, "恢复所选收藏失败")
EventBus.emit('ContactsList.updateContacts')
}
})
}
},
}
],
})}></mdui-list-item>
</>
}
<div style={{
height: "15px",
}}></div>
{ {
contactsList.filter((chat) => contactsList.filter((chat) =>
@@ -82,14 +140,16 @@ export default function ContactsList({
chat.id.includes(searchText) chat.id.includes(searchText)
).map((v) => ).map((v) =>
<ContactsListItem <ContactsListItem
// active={!isMultiSelecting && false} active={checkedList[v.id] == true}
onClick={(e) => { onClick={() => {
const self = (e.target as ListItem) if (isMultiSelecting)
/*if (isMultiSelecting) setCheckedList({
self.active = !self.active ...checkedList,
else*/ [v.id]: !checkedList[v.id],
openChatInfoDialog(v) })
}} else
openChatInfoDialog(v)
}}
key={v.id} key={v.id}
contact={v} /> contact={v} />
) )

View File

@@ -12,7 +12,7 @@ export type CallMethod =
"User.getInfo" | "User.getInfo" |
"User.getMyContacts" | "User.getMyContacts" |
"User.addContact" | "User.addContacts" |
"User.removeContacts" | "User.removeContacts" |
"User.getMyRecentChats" | "User.getMyRecentChats" |

View File

@@ -339,8 +339,41 @@ export default class UserApi extends BaseApi {
} }
}) })
// 添加聯絡人 // 添加聯絡人
this.registerEvent("User.addContact", (args, { deviceId }) => { this.registerEvent("User.addContacts", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'targets'])) return {
msg: "参数缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌无效",
}
let fail = 0
const user = User.findById(token.author) as User
for (const target of (args.targets as string[])) {
const chat = Chat.findById(target) || Chat.findByName(target)
const targetUser = User.findByAccount(target) as User
if (chat)
user!.addContact(chat.bean.id)
else if (targetUser) {
const privChat = ChatPrivate.findOrCreateForPrivate(user, targetUser)
user!.addContact(privChat.bean.id)
} else {
fail++
}
}
return {
msg: "添加成功 (失败 " + fail + " 个)",
code: 200,
}
})
// 添加聯絡人
this.registerEvent("User.removeContacts", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'targets'])) return {
msg: "参数缺失", msg: "参数缺失",
code: 400, code: 400,
} }
@@ -352,19 +385,7 @@ export default class UserApi extends BaseApi {
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
const chat = Chat.findById(args.target as string) || Chat.findByName(args.target as string) user.removeContacts(args.targets as string[])
const targetUser = User.findByAccount(args.target as string) as User
if (chat)
user!.addContact(chat.bean.id)
else if (targetUser) {
const privChat = ChatPrivate.findOrCreateForPrivate(user, targetUser)
user!.addContact(privChat.bean.id)
} else {
return {
msg: "找不到目标",
code: 404,
}
}
return { return {
msg: "成功", msg: "成功",

View File

@@ -115,6 +115,7 @@ export default class User {
return this.bean.username return this.bean.username
} }
setUserName(userName: string) { setUserName(userName: string) {
if (this.getUserName() == userName) return
if (User.findAllBeansByCondition('username = ?', userName).length > 0) if (User.findAllBeansByCondition('username = ?', userName).length > 0)
throw new DataWrongError(`用户名 ${userName} 已存在`) throw new DataWrongError(`用户名 ${userName} 已存在`)
this.setAttr("username", userName) this.setAttr("username", userName)