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.getMyContacts" |
"User.addContact" |
"User.addContacts" |
"User.removeContacts" |
"User.getMyRecentChats" |

View File

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

View File

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

View File

@@ -42,6 +42,12 @@ export default function MyProfileDialog({
const editNickNameRef = 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 (<>
{
// 公用 - 資料卡
@@ -66,14 +72,10 @@ export default function MyProfileDialog({
<mdui-list>
<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-divider style={{
marginTop: "10px",
marginBottom: "10px",
}}></mdui-divider>
<mdui-list-item icon="logout" rounded onClick={() => dialog({
headline: "退出登录",
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={() => 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={() => userProfileEditDialogRef.current!.open = false}></mdui-button>
<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>
{
// 個人資料編輯

View File

@@ -1,11 +1,10 @@
import React from "react"
import ContactsListItem from "./ContactsListItem.tsx"
import useEventListener from "../useEventListener.ts"
import { Dialog, ListItem, TextField } from "mdui"
import useAsyncEffect from "../useAsyncEffect.ts"
import { dialog, Dialog, TextField } from "mdui"
import Client from "../../api/Client.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 EventBus from "../../EventBus.ts"
@@ -27,12 +26,13 @@ export default function ContactsList({
const [isMultiSelecting, setIsMultiSelecting] = React.useState(false)
const [searchText, setSearchText] = React.useState('')
const [contactsList, setContactsList] = React.useState<Chat[]>([])
const [checkedList, setCheckedList] = React.useState<{ [key: string]: boolean }>({})
useEventListener(searchRef, 'input', (e) => {
setSearchText((e.target as unknown as TextField).value)
})
useAsyncEffect(async () => {
React.useEffect(() => {
async function updateContacts() {
const re = await Client.invoke("User.getMyContacts", {
token: data.access_token,
@@ -44,7 +44,11 @@ export default function ContactsList({
}
updateContacts()
EventBus.on('ContactsList.updateContacts', () => updateContacts())
})
return () => {
EventBus.off('ContactsList.updateContacts')
}
// 警告: 不添加 deps 導致無限執行
}, [])
return <mdui-list style={{
overflowY: 'auto',
@@ -68,12 +72,66 @@ export default function ContactsList({
<mdui-list-item rounded style={{
width: '100%',
marginTop: '13px',
marginBottom: '15px',
}} icon="refresh" onClick={() => EventBus.emit('ContactsList.updateContacts')}></mdui-list-item>
{/* <mdui-list-item rounded style={{
<mdui-list-item rounded style={{
width: '100%',
marginBottom: '15px',
}} icon={ isMultiSelecting ? "done" : "edit"} onClick={() => setIsMultiSelecting(!isMultiSelecting)}>{ isMultiSelecting ? "關閉多選" : "多選模式" }</mdui-list-item> */}
}} icon={isMultiSelecting ? "done" : "edit"} onClick={() => {
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) =>
@@ -82,12 +140,14 @@ export default function ContactsList({
chat.id.includes(searchText)
).map((v) =>
<ContactsListItem
// active={!isMultiSelecting && false}
onClick={(e) => {
const self = (e.target as ListItem)
/*if (isMultiSelecting)
self.active = !self.active
else*/
active={checkedList[v.id] == true}
onClick={() => {
if (isMultiSelecting)
setCheckedList({
...checkedList,
[v.id]: !checkedList[v.id],
})
else
openChatInfoDialog(v)
}}
key={v.id}

View File

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

View File

@@ -339,8 +339,41 @@ export default class UserApi extends BaseApi {
}
})
// 添加聯絡人
this.registerEvent("User.addContact", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return {
this.registerEvent("User.addContacts", (args, { deviceId }) => {
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: "参数缺失",
code: 400,
}
@@ -352,19 +385,7 @@ export default class UserApi extends BaseApi {
}
const user = User.findById(token.author) as User
const chat = Chat.findById(args.target as string) || Chat.findByName(args.target 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,
}
}
user.removeContacts(args.targets as string[])
return {
msg: "成功",

View File

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