feat(notification): 新增notification系统通知 (#127)

优化一些提示和逻辑
This commit is contained in:
Dawn
2024-12-26 21:29:12 +08:00
committed by GitHub
parent 187071b026
commit 77373e3782
24 changed files with 842 additions and 227 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Run tests with coverage
run: pnpm coverage
- name: Upload coverage to Codecov
- name: Upload coverage to Codecov
uses: ./
with:
file: ./coverage/coverage-final.json
+1
View File
@@ -55,6 +55,7 @@
"@tauri-apps/plugin-clipboard-manager": "2.2.0",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-http": "2.0.1",
"@tauri-apps/plugin-notification": "^2.2.0",
"@tauri-apps/plugin-os": "2.0.0",
"@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
+12 -2
View File
@@ -29,6 +29,9 @@ importers:
'@tauri-apps/plugin-http':
specifier: 2.0.1
version: 2.0.1
'@tauri-apps/plugin-notification':
specifier: ^2.2.0
version: 2.2.0
'@tauri-apps/plugin-os':
specifier: 2.0.0
version: 2.0.0
@@ -146,7 +149,7 @@ importers:
version: 4.1.1(vite@6.0.5(@types/node@20.17.10)(jiti@2.4.2)(sass@1.83.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.7.2))
'@vitest/coverage-v8':
specifier: ^2.1.8
version: 2.1.8(vitest@2.1.8(@types/node@20.17.10)(@vitest/ui@2.1.8)(happy-dom@15.11.7)(sass@1.83.0)(terser@5.37.0))
version: 2.1.8(vitest@2.1.8)
'@vitest/ui':
specifier: ^2.1.8
version: 2.1.8(vitest@2.1.8)
@@ -1763,6 +1766,9 @@ packages:
'@tauri-apps/plugin-http@2.0.1':
resolution: {integrity: sha512-j6IA3pVBybSCwPpsihpX4z8bs6PluuGtr06ahL/xy4D8HunNBTmRmadJrFOQi0gOAbaig4MkQ15nzNLBLy8R1A==}
'@tauri-apps/plugin-notification@2.2.0':
resolution: {integrity: sha512-uhE3a2gFlqwiWhY/JbhXOF13K4iILEYxUk0D2Y+q69HP6tnfPqKCuNTHxDM0H+oFAakXESNmJVnYw0Vi4IrMMQ==}
'@tauri-apps/plugin-os@2.0.0':
resolution: {integrity: sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==}
@@ -6634,6 +6640,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/plugin-notification@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/plugin-os@2.0.0':
dependencies:
'@tauri-apps/api': 2.1.1
@@ -6898,7 +6908,7 @@ snapshots:
vite: 6.0.5(@types/node@20.17.10)(jiti@2.4.2)(sass@1.83.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
vue: 3.5.13(typescript@5.7.2)
'@vitest/coverage-v8@2.1.8(vitest@2.1.8(@types/node@20.17.10)(@vitest/ui@2.1.8)(happy-dom@15.11.7)(sass@1.83.0)(terser@5.37.0))':
'@vitest/coverage-v8@2.1.8(vitest@2.1.8)':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 0.2.3
+155 -4
View File
@@ -1151,6 +1151,16 @@ dependencies = [
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
@@ -1174,6 +1184,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -2171,6 +2192,7 @@ dependencies = [
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-shell",
@@ -2677,6 +2699,19 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mac-notification-sys"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91"
dependencies = [
"cc",
"dirs-next",
"objc-foundation",
"objc_id",
"time",
]
[[package]]
name = "mach2"
version = "0.4.2"
@@ -2887,6 +2922,19 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "notify-rust"
version = "4.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5134a72dc570b178bff81b01e81ab14a6fcc015391ed4b3b14853090658cd3a3"
dependencies = [
"log",
"mac-notification-sys",
"serde",
"tauri-winrt-notification",
"zbus",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@@ -2981,6 +3029,17 @@ dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -3200,6 +3259,15 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.36.3"
@@ -3726,6 +3794,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.32.0"
@@ -5267,6 +5344,25 @@ dependencies = [
"urlpattern",
]
[[package]]
name = "tauri-plugin-notification"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46ab803095f14ac6521fdb6477210a49e86fed6623c3c97d8e4b2b35e045e922"
dependencies = [
"log",
"notify-rust",
"rand 0.8.5",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"time",
"url",
]
[[package]]
name = "tauri-plugin-os"
version = "2.0.1"
@@ -5509,6 +5605,17 @@ dependencies = [
"toml 0.7.8",
]
[[package]]
name = "tauri-winrt-notification"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871"
dependencies = [
"quick-xml 0.31.0",
"windows 0.56.0",
"windows-version",
]
[[package]]
name = "tempfile"
version = "3.12.0"
@@ -6318,8 +6425,8 @@ dependencies = [
"webview2-com-sys",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-implement",
"windows-interface",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
]
[[package]]
@@ -6439,6 +6546,16 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
dependencies = [
"windows-core 0.56.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.58.0"
@@ -6468,19 +6585,42 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
dependencies = [
"windows-implement 0.56.0",
"windows-interface 0.56.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
@@ -6492,6 +6632,17 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "windows-interface"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
+1
View File
@@ -36,6 +36,7 @@ tauri-plugin-clipboard-manager = "2.2.0"
tauri-plugin-updater = "2.3.0"
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
tauri-plugin-single-instance = "2.2.0"
tauri-plugin-notification = "2.2.0"
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-autostart = "2.2.0"
lazy_static = "1.4"
+93 -27
View File
@@ -2,7 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default-capability",
"description": "Capability for the main window",
"windows": ["*"],
"windows": [
"*"
],
"permissions": [
"core:default",
"core:window:default",
@@ -65,13 +67,29 @@
"sql:allow-close",
"shell:default",
"shell:allow-open",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify",
"notification:allow-register-action-types",
"notification:allow-register-listener",
"notification:allow-cancel",
"notification:allow-get-pending",
"notification:allow-remove-active",
"notification:allow-get-active",
"notification:allow-check-permissions",
"notification:allow-show",
"notification:allow-batch",
"notification:allow-list-channels",
"notification:allow-delete-channel",
"notification:allow-create-channel",
"notification:allow-permission-state",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "powershell",
"cmd": "powershell",
"args":true
"args": true
},
{
"name": "systemPath",
@@ -86,28 +104,52 @@
]
},
{
"identifier":"fs:write-files",
"allow": [{"path": "**"}]
"identifier": "fs:write-files",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:allow-mkdir",
"allow": [{"path": "**"}]
"identifier": "fs:allow-mkdir",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:read-dirs",
"allow": [{"path": "**"}]
"identifier": "fs:read-dirs",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:read-files",
"allow": [{"path": "**"}]
"identifier": "fs:read-files",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:allow-copy-file",
"allow": [{"path": "**"}]
"identifier": "fs:allow-copy-file",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:allow-read-text-file",
"allow": [{"path": "**"}]
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "**"
}
]
},
{
"identifier": "http:default",
@@ -127,28 +169,52 @@
]
},
{
"identifier":"fs:write-files",
"allow": [{"path": "**"}]
"identifier": "fs:write-files",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:allow-mkdir",
"allow": [{"path": "**"}]
"identifier": "fs:allow-mkdir",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:read-dirs",
"allow": [{"path": "**"}]
"identifier": "fs:read-dirs",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:read-files",
"allow": [{"path": "**"}]
"identifier": "fs:read-files",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:allow-copy-file",
"allow": [{"path": "**"}]
"identifier": "fs:allow-copy-file",
"allow": [
{
"path": "**"
}
]
},
{
"identifier":"fs:allow-read-text-file",
"allow": [{"path": "**"}]
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "**"
}
]
}
]
}
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"default-capability":{"identifier":"default-capability","description":"Capability for the main window","local":true,"windows":["*"],"permissions":["core:default","core:window:default","core:window:allow-create","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-hide","core:window:allow-center","core:window:allow-show","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-destroy","core:window:allow-is-focused","core:window:allow-is-fullscreen","core:window:allow-set-focus","core:window:allow-set-position","core:window:allow-scale-factor","core:window:allow-unminimize","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-unmaximize","core:window:allow-set-fullscreen","core:webview:allow-create-webview","core:webview:allow-create-webview-window","core:webview:allow-internal-toggle-devtools","core:webview:allow-set-webview-size","core:webview:allow-webview-size","core:event:default","core:event:allow-listen","core:image:default","core:image:allow-rgba","os:default","os:allow-os-type","os:allow-arch","os:allow-version","os:allow-hostname","process:default","process:allow-exit","clipboard-manager:allow-clear","clipboard-manager:allow-read-image","clipboard-manager:allow-read-text","clipboard-manager:allow-write-html","clipboard-manager:allow-write-image","clipboard-manager:allow-write-text","http:allow-fetch","http:allow-fetch-cancel","http:allow-fetch-read-body","http:allow-fetch-send","core:tray:default","core:tray:allow-get-by-id","autostart:allow-enable","autostart:allow-disable","autostart:allow-is-enabled","dialog:allow-confirm","core:resources:allow-close","sql:allow-load","sql:allow-execute","sql:allow-select","sql:allow-close","shell:default","shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"cmd":"powershell","name":"powershell"},{"args":["-c",{"validator":"\\S+"}],"cmd":"cmd","name":"systemPath"}]},{"identifier":"fs:write-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-mkdir","allow":[{"path":"**"}]},{"identifier":"fs:read-dirs","allow":[{"path":"**"}]},{"identifier":"fs:read-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-copy-file","allow":[{"path":"**"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"**"}]},{"identifier":"http:default","allow":[{"url":"http://**"},{"url":"https://**"},{"url":"http://*:*"},{"url":"https://*:*"}]},{"identifier":"fs:write-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-mkdir","allow":[{"path":"**"}]},{"identifier":"fs:read-dirs","allow":[{"path":"**"}]},{"identifier":"fs:read-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-copy-file","allow":[{"path":"**"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"**"}]}]},"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["home"],"permissions":["updater:default","updater:allow-check","updater:allow-download","updater:allow-install"],"platforms":["macOS","windows","linux"]}}
{"default-capability":{"identifier":"default-capability","description":"Capability for the main window","local":true,"windows":["*"],"permissions":["core:default","core:window:default","core:window:allow-create","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-hide","core:window:allow-center","core:window:allow-show","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-destroy","core:window:allow-is-focused","core:window:allow-is-fullscreen","core:window:allow-set-focus","core:window:allow-set-position","core:window:allow-scale-factor","core:window:allow-unminimize","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-unmaximize","core:window:allow-set-fullscreen","core:webview:allow-create-webview","core:webview:allow-create-webview-window","core:webview:allow-internal-toggle-devtools","core:webview:allow-set-webview-size","core:webview:allow-webview-size","core:event:default","core:event:allow-listen","core:image:default","core:image:allow-rgba","os:default","os:allow-os-type","os:allow-arch","os:allow-version","os:allow-hostname","process:default","process:allow-exit","clipboard-manager:allow-clear","clipboard-manager:allow-read-image","clipboard-manager:allow-read-text","clipboard-manager:allow-write-html","clipboard-manager:allow-write-image","clipboard-manager:allow-write-text","http:allow-fetch","http:allow-fetch-cancel","http:allow-fetch-read-body","http:allow-fetch-send","core:tray:default","core:tray:allow-get-by-id","autostart:allow-enable","autostart:allow-disable","autostart:allow-is-enabled","dialog:allow-confirm","core:resources:allow-close","sql:allow-load","sql:allow-execute","sql:allow-select","sql:allow-close","shell:default","shell:allow-open","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-register-action-types","notification:allow-register-listener","notification:allow-cancel","notification:allow-get-pending","notification:allow-remove-active","notification:allow-get-active","notification:allow-check-permissions","notification:allow-show","notification:allow-batch","notification:allow-list-channels","notification:allow-delete-channel","notification:allow-create-channel","notification:allow-permission-state",{"identifier":"shell:allow-execute","allow":[{"args":true,"cmd":"powershell","name":"powershell"},{"args":["-c",{"validator":"\\S+"}],"cmd":"cmd","name":"systemPath"}]},{"identifier":"fs:write-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-mkdir","allow":[{"path":"**"}]},{"identifier":"fs:read-dirs","allow":[{"path":"**"}]},{"identifier":"fs:read-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-copy-file","allow":[{"path":"**"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"**"}]},{"identifier":"http:default","allow":[{"url":"http://**"},{"url":"https://**"},{"url":"http://*:*"},{"url":"https://*:*"}]},{"identifier":"fs:write-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-mkdir","allow":[{"path":"**"}]},{"identifier":"fs:read-dirs","allow":[{"path":"**"}]},{"identifier":"fs:read-files","allow":[{"path":"**"}]},{"identifier":"fs:allow-copy-file","allow":[{"path":"**"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"**"}]}]},"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["home"],"permissions":["updater:default","updater:allow-check","updater:allow-download","updater:allow-install"],"platforms":["macOS","windows","linux"]}}
+165
View File
@@ -5179,6 +5179,171 @@
"type": "string",
"const": "http:deny-fetch-send"
},
{
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
"type": "string",
"const": "notification:default"
},
{
"description": "Enables the batch command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-batch"
},
{
"description": "Enables the cancel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-cancel"
},
{
"description": "Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-check-permissions"
},
{
"description": "Enables the create_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-create-channel"
},
{
"description": "Enables the delete_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-delete-channel"
},
{
"description": "Enables the get_active command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-get-active"
},
{
"description": "Enables the get_pending command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-get-pending"
},
{
"description": "Enables the is_permission_granted command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-is-permission-granted"
},
{
"description": "Enables the list_channels command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-list-channels"
},
{
"description": "Enables the notify command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-notify"
},
{
"description": "Enables the permission_state command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-permission-state"
},
{
"description": "Enables the register_action_types command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-register-action-types"
},
{
"description": "Enables the register_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-register-listener"
},
{
"description": "Enables the remove_active command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-remove-active"
},
{
"description": "Enables the request_permission command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-request-permission"
},
{
"description": "Enables the show command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-show"
},
{
"description": "Denies the batch command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-batch"
},
{
"description": "Denies the cancel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-cancel"
},
{
"description": "Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-check-permissions"
},
{
"description": "Denies the create_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-create-channel"
},
{
"description": "Denies the delete_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-delete-channel"
},
{
"description": "Denies the get_active command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-get-active"
},
{
"description": "Denies the get_pending command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-get-pending"
},
{
"description": "Denies the is_permission_granted command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-is-permission-granted"
},
{
"description": "Denies the list_channels command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-list-channels"
},
{
"description": "Denies the notify command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-notify"
},
{
"description": "Denies the permission_state command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-permission-state"
},
{
"description": "Denies the register_action_types command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-register-action-types"
},
{
"description": "Denies the register_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-register-listener"
},
{
"description": "Denies the remove_active command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-remove-active"
},
{
"description": "Denies the request_permission command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-request-permission"
},
{
"description": "Denies the show command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-show"
},
{
"description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
"type": "string",
+166 -1
View File
@@ -84,7 +84,7 @@
}
},
"permissions": {
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
@@ -5179,6 +5179,171 @@
"type": "string",
"const": "http:deny-fetch-send"
},
{
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
"type": "string",
"const": "notification:default"
},
{
"description": "Enables the batch command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-batch"
},
{
"description": "Enables the cancel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-cancel"
},
{
"description": "Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-check-permissions"
},
{
"description": "Enables the create_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-create-channel"
},
{
"description": "Enables the delete_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-delete-channel"
},
{
"description": "Enables the get_active command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-get-active"
},
{
"description": "Enables the get_pending command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-get-pending"
},
{
"description": "Enables the is_permission_granted command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-is-permission-granted"
},
{
"description": "Enables the list_channels command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-list-channels"
},
{
"description": "Enables the notify command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-notify"
},
{
"description": "Enables the permission_state command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-permission-state"
},
{
"description": "Enables the register_action_types command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-register-action-types"
},
{
"description": "Enables the register_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-register-listener"
},
{
"description": "Enables the remove_active command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-remove-active"
},
{
"description": "Enables the request_permission command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-request-permission"
},
{
"description": "Enables the show command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-show"
},
{
"description": "Denies the batch command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-batch"
},
{
"description": "Denies the cancel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-cancel"
},
{
"description": "Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-check-permissions"
},
{
"description": "Denies the create_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-create-channel"
},
{
"description": "Denies the delete_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-delete-channel"
},
{
"description": "Denies the get_active command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-get-active"
},
{
"description": "Denies the get_pending command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-get-pending"
},
{
"description": "Denies the is_permission_granted command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-is-permission-granted"
},
{
"description": "Denies the list_channels command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-list-channels"
},
{
"description": "Denies the notify command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-notify"
},
{
"description": "Denies the permission_state command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-permission-state"
},
{
"description": "Denies the register_action_types command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-register-action-types"
},
{
"description": "Denies the register_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-register-listener"
},
{
"description": "Denies the remove_active command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-remove-active"
},
{
"description": "Denies the request_permission command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-request-permission"
},
{
"description": "Denies the show command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-show"
},
{
"description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
"type": "string",
+1
View File
@@ -13,6 +13,7 @@ impl<R: Runtime> CustomInit for tauri::Builder<R> {
// 初始化插件
fn init_plugin(self) -> Self {
self.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_websocket::init())
+22 -50
View File
@@ -16,37 +16,8 @@
</n-flex>
<Transition name="chat-init" appear mode="out-in" @after-leave="handleTransitionComplete">
<!-- 初次加载的骨架屏 -->
<n-flex
v-if="messageOptions?.isLoading && !messageOptions?.cursor"
vertical
:size="18"
style="max-height: calc(100vh - 260px)"
class="relative h-100vh box-border p-20px">
<n-flex justify="end">
<n-skeleton style="border-radius: 14px" height="40px" width="46%" :sharp="false" />
<n-skeleton height="40px" circle />
</n-flex>
<n-flex>
<n-skeleton height="40px" circle />
<n-skeleton style="border-radius: 14px" height="60px" width="58%" :sharp="false" />
</n-flex>
<n-flex>
<n-skeleton height="40px" circle />
<n-skeleton style="border-radius: 14px" height="40px" width="26%" :sharp="false" />
</n-flex>
<n-flex justify="end">
<n-skeleton style="border-radius: 14px" height="40px" width="60%" :sharp="false" />
<n-skeleton height="40px" circle />
</n-flex>
</n-flex>
<!-- 中间聊天内容(使用虚拟列表) -->
<VirtualList
v-else
id="image-chat-main"
ref="virtualListInst"
:items="chatMessageList"
@@ -54,8 +25,7 @@
:buffer="5"
@scroll="handleScroll"
@scroll-direction-change="handleScrollDirectionChange"
:style="{ maxHeight: `calc(100vh - ${messageOptions?.isLoading && isLoadingMore ? '286px' : '260px'})` }"
:class="{ 'right-1px': activeItem.type === RoomTypeEnum.SINGLE }">
:style="{ maxHeight: `calc(100vh - ${messageOptions?.isLoading && isLoadingMore ? '286px' : '260px'})` }">
<template #default="{ item, index }">
<n-flex
vertical
@@ -389,14 +359,13 @@
</footer>
</template>
<script setup lang="ts">
import { EventEnum, MittEnum, MsgEnum, RoomTypeEnum, MessageStatusEnum } from '@/enums'
import { EventEnum, MittEnum, MsgEnum, MessageStatusEnum } from '@/enums'
import { type MessageType, SessionItem } from '@/services/types.ts'
import { useMitt } from '@/hooks/useMitt.ts'
import { usePopover } from '@/hooks/usePopover.ts'
import { useWindow } from '@/hooks/useWindow.ts'
import { useChatMain } from '@/hooks/useChatMain.ts'
import { delay } from 'lodash-es'
import { useCommon } from '@/hooks/useCommon.ts'
import { formatTimestamp, isDiffNow } from '@/utils/ComputedTime.ts'
import { useUserInfo, useBadgeInfo } from '@/hooks/useCached.ts'
import { useChatStore } from '@/stores/chat.ts'
@@ -410,10 +379,10 @@ import { useTauriListener } from '@/hooks/useTauriListener'
const appWindow = WebviewWindow.getCurrent()
const { addListener } = useTauriListener()
const { activeItem } = defineProps<{
const props = defineProps<{
activeItem: SessionItem
}>()
const activeItemRef = shallowRef<SessionItem>({ ...activeItem })
const activeItemRef = shallowRef<SessionItem>(props.activeItem)
const chatStore = useChatStore()
const userStore = useUserStore()
@@ -425,6 +394,7 @@ const isScrollingUp = ref(false)
// 添加标记,用于识别是否正在加载历史消息
const isLoadingMore = ref(false)
const userUid = computed(() => userStore.userInfo.uid)
const chatMessageList = computed(() => chatStore.chatMessageList)
const currentNewMsgCount = computed(() => chatStore.currentNewMsgCount)
const messageOptions = computed(() => chatStore.currentMessageOptions)
@@ -453,7 +423,6 @@ const recordEL = ref()
/** 网络连接是否正常 */
const { isOnline } = useNetwork()
const isMac = computed(() => type() === 'macos')
const { userUid } = useCommon()
const {
handleMsgClick,
handleConfirm,
@@ -468,18 +437,20 @@ const {
selectKey,
emojiList,
scrollTop
} = useChatMain(activeItem)
} = useChatMain(props.activeItem)
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main')
watch(activeItemRef, (value, oldValue) => {
if (oldValue.roomId !== value.roomId) {
// 等待 DOM 更新完成后再滚动
nextTick(() => {
// 给予足够的时间让消息加载和渲染
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
})
watch(
() => props.activeItem,
(value, oldValue) => {
if (oldValue.roomId !== value.roomId) {
requestAnimationFrame(() => {
// 给予足够的时间让消息加载和渲染
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
})
}
}
})
)
watch(
chatMessageList,
@@ -492,7 +463,7 @@ watch(
// 如果正在加载历史消息,优先处理历史消息的滚动
if (isLoadingMore.value) {
if (scrollTop.value < 10) {
nextTick(() => {
requestAnimationFrame(() => {
virtualListInst.value?.scrollTo({ index: value.length - oldValue.length })
})
}
@@ -516,6 +487,9 @@ watch(
}
// 其他情况:增加新消息计数
/**
* @see {chatStore.pushMsg}
*/
}
},
{ deep: false }
@@ -758,10 +732,8 @@ const handleReEdit = (msgId: number) => {
}
onMounted(async () => {
nextTick(() => {
setTimeout(() => {
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
}, 10)
requestAnimationFrame(() => {
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
})
useMitt.on(MittEnum.SEND_MESSAGE, async (messageType: MessageType) => {
await chatStore.pushMsg(messageType)
+71 -26
View File
@@ -55,7 +55,7 @@
<!-- // TODO popover显示的时候去改变窗口的大小、当点击了半个选项的时候也会出现原生滚动条 (nyh -> 2024-03-25 05:04:37) -->
<!-- // TODO 如果popover显示就先暂时不让滚动,因为在n-scrollbar和n-virtual-list中使用当我点击最后一个选项时候n-popover位置不够导致出现原生滚动条 (nyh -> 2024-03-24 22:46:38) -->
<!-- // TODO 如果直接使用n-virtual-list的滚动配上n-popover乎也没有这个bug,但是当点击倒数第二个的时候还是会出现滚动条 (nyh -> 2024-03-25 00:30:53) -->
<!-- // TODO 如果直接使用n-virtual-list的滚动配上n-popover乎也没有这个bug,但是当点击倒数第二个的时候还是会出现滚动条 (nyh -> 2024-03-25 00:30:53) -->
<n-virtual-list
id="image-chat-sidebar"
style="max-height: calc(100vh - 260px)"
@@ -120,9 +120,11 @@ import { useGlobalStore } from '@/stores/global.ts'
import type { UserItem } from '@/services/types.ts'
import { useDebounceFn } from '@vueuse/core'
import { AvatarUtils } from '@/utils/avatarUtils'
import { useCachedStore } from '~/src/stores/cached'
const groupStore = useGroupStore()
const globalStore = useGlobalStore()
const cachedStore = useCachedStore()
const groupUserList = computed(() => groupStore.userList)
const userList = computed(() => {
return groupUserList.value
@@ -150,41 +152,74 @@ const isCollapsed = ref(true)
const { optionsList, report, selectKey } = useChatMain()
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-sidebar')
watch(userList, (newVal) => {
// 如果正在搜索,则应用搜索过滤
if (searchRef.value) {
filteredUserList.value = newVal.filter((user) => user.name.toLowerCase().includes(searchRef.value.toLowerCase()))
} else {
filteredUserList.value = newVal
}
// 添加一个新的计算属性来合并用户列表
const mergedUserList = computed(() => {
// 创建一个Map用于去重,使用uid作为key
const userMap = new Map()
// 首先添加在线用户列表
userList.value.forEach((user) => {
userMap.set(user.uid, user)
})
// 添加缓存的用户列表
cachedStore.currentAtUsersList.forEach((cachedUser) => {
if (!userMap.has(cachedUser.uid)) {
// 如果用户不在在线列表中,添加到Map中
// 为缓存用户添加默认的activeStatus
userMap.set(cachedUser.uid, {
...cachedUser,
activeStatus: OnlineEnum.OFFLINE
})
}
})
// 转换回数组并按在线状态排序
return Array.from(userMap.values()).sort((a, b) => a.activeStatus - b.activeStatus)
})
const handleSelect = () => {
isSearch.value = !isSearch.value
nextTick(() => {
inputInstRef.value?.select()
})
}
/**
* 重置搜索状态
*/
const handleBlur = () => {
// 如果输入框有值,则不重置
if (searchRef.value) return
isSearch.value = false
searchRef.value = ''
filteredUserList.value = userList.value
}
// 修改watch监听器
watch(
[userList, () => cachedStore.currentAtUsersList],
() => {
// 如果正在搜索,则应用搜索过滤
if (searchRef.value) {
filteredUserList.value = mergedUserList.value.filter((user) =>
user.name.toLowerCase().includes(searchRef.value.toLowerCase())
)
} else {
filteredUserList.value = userList.value
}
},
{ immediate: true }
)
/**
* 监听搜索输入过滤用户
* @param value 输入值
*/
const handleSearch = useDebounceFn((value: string) => {
filteredUserList.value = userList.value.filter((user) => user.name.toLowerCase().includes(value.toLowerCase()))
if (!value) {
// 如果搜索框为空,只显示在线用户列表
filteredUserList.value = userList.value
return
}
// 从合并后的用户列表中搜索
filteredUserList.value = mergedUserList.value.filter((user) => user.name.toLowerCase().includes(value.toLowerCase()))
}, 10)
/**
* 重置搜索状态
*/
const handleBlur = () => {
if (searchRef.value) return
isSearch.value = false
searchRef.value = ''
// 重置为只显示在线用户列表
filteredUserList.value = userList.value
}
/**
* 处理滚动事件
* @param event 滚动事件
@@ -198,6 +233,16 @@ const handleScroll = (event: Event) => {
}
}
/**
* 切换搜索模式并自动聚焦输入框
*/
const handleSelect = () => {
isSearch.value = !isSearch.value
nextTick(() => {
inputInstRef.value?.select()
})
}
onMounted(() => {
useMitt.on(`${MittEnum.INFO_POPOVER}-Sidebar`, (event: any) => {
selectKey.value = event.uid
+24 -1
View File
@@ -107,8 +107,31 @@ export const useMessage = () => {
label: '从消息列表中移除',
icon: 'delete',
click: (item: SessionItem) => {
// TODO: 实现从消息列表中移除功能
const currentSessions = chatStore.sessionList
const currentIndex = currentSessions.findIndex((session) => session.roomId === item.roomId)
// 检查是否是当前选中的会话
const isCurrentSession = item.roomId === globalStore.currentSession.roomId
chatStore.removeContact(item.roomId)
// 如果不是当前选中的会话,直接返回
if (!isCurrentSession) {
return
}
const updatedSessions = chatStore.sessionList
// 如果没有会话就把右侧消息框关闭
if (updatedSessions.length === 0) {
console.log('没有会话了')
useMitt.emit(MittEnum.MSG_BOX_SHOW, { item: -1 })
return
}
// 选择下一个或上一个会话
const nextIndex = Math.min(currentIndex, updatedSessions.length - 1)
handleMsgClick(updatedSessions[nextIndex])
}
},
{ label: '屏蔽此人消息', icon: 'forbid' }
+49 -15
View File
@@ -1,8 +1,7 @@
import { LimitEnum, MittEnum, MsgEnum, MessageStatusEnum } from '@/enums'
import { useUserInfo } from '@/hooks/useCached.ts'
import apis from '@/services/apis.ts'
import { CacheUserItem } from '@/services/types.ts'
import { useCachedStore } from '@/stores/cached.ts'
import { useCachedStore, type BaseUserItem } from '@/stores/cached.ts'
import { useChatStore } from '@/stores/chat.ts'
import { useGlobalStore } from '@/stores/global.ts'
import { useSettingStore } from '@/stores/setting.ts'
@@ -13,7 +12,7 @@ import { Ref } from 'vue'
import { useCommon } from './useCommon.ts'
import { readText, readImage } from '@tauri-apps/plugin-clipboard-manager'
import Database from '@tauri-apps/plugin-sql'
import { messageStrategyMap } from '@/hooks/strategy/MessageStrategy.ts'
import { messageStrategyMap } from '@/strategy/MessageStrategy.ts'
export const useMsgInput = (messageInputDom: Ref) => {
const chatStore = useChatStore()
@@ -119,6 +118,41 @@ export const useMsgInput = (messageInputDom: Ref) => {
chat.value.sendKey = v
})
/**
* 从HTML内容中提取@用户的uid
* @param content HTML格式的消息内容
* @param userList 用户列表
* @returns 被@用户的uid数组
*/
const extractAtUserIds = (content: string, userList: BaseUserItem[]): number[] => {
const atUserIds: number[] = []
// 创建临时DOM元素来解析HTML
const tempDiv = document.createElement('div')
tempDiv.innerHTML = content
// 获取纯文本内容
const textContent = tempDiv.textContent || ''
// 使用更精确的正则表达式匹配@用户
// 匹配@后面的非空白字符,直到遇到空白字符或字符串结束
const regex = /@([^\s]+)/g
const matches = textContent.match(regex)
if (matches) {
matches.forEach((match) => {
const username = match.slice(1) // 移除@符号
const user = userList.find((u) => u.name === username)
if (user) {
atUserIds.push(user.uid)
}
})
}
// 去重并返回
return [...new Set(atUserIds)]
}
/** 去除html标签(用于鉴别回复时是否有输入内容) */
const stripHtml = (html: string) => {
try {
@@ -170,19 +204,18 @@ export const useMsgInput = (messageInputDom: Ref) => {
return
}
const msg = messageStrategy.getMsg(msgInput.value, reply.value)
// // 处理超链接
// const { hyperlinkRegex, foundHyperlinks } = RegExp.isHyperlink(msg.content)
// if (foundHyperlinks && foundHyperlinks.length > 0) {
// msg.content = msg.content.replace(hyperlinkRegex, (match) => {
// const href = match.startsWith('www.') ? 'https://' + match : match
// return `<a style="color: inherit;text-underline-offset: 4px" href="${href}" target="_blank" rel="noopener noreferrer">${match}</a>`
// })
// }
console.log(msg)
// 从消息内容中提取@用户的uid
const atUidList = extractAtUserIds(msgInput.value, cachedStore.currentAtUsersList)
// 创建临时消息ID
const tempMsgId = Date.now()
// 根据消息类型创建消息体
const messageBody = messageStrategy.buildMessageBody(msg, reply)
const messageBody = {
...messageStrategy.buildMessageBody(msg, reply),
atUidList // 添加@用户列表
}
// 创建消息对象;
const tempMsg = messageStrategy.buildMessageType(tempMsgId, messageBody, globalStore, userUid)
// 先添加到消息列表
@@ -297,7 +330,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
} else {
ait.value = false
}
}, 10) // 防抖时间过长会导致输入内容已经显示但是实际还没有进入到这里进行处理
}, 10) // 防抖时间过长会导致输入内容已经显示但是实际还没有进入到这里处理
/** input的keydown事件 */
const inputKeyDown = (e: KeyboardEvent) => {
@@ -340,7 +373,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
}
/** 处理点击@提及框事件 */
const handleAit = (item: CacheUserItem) => {
const handleAit = (item: BaseUserItem) => {
// 如果正在输入拼音,不发送消息
if (isChinese.value) {
return
@@ -447,6 +480,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
if (messageInputDom.value) {
nextTick().then(() => {
messageInputDom.value.focus()
// 插入回复框
insertNode(
MsgEnum.REPLY,
{ avatar: avatar, accountName: accountName, content: event.message.body.content },
+21
View File
@@ -25,6 +25,9 @@ import { LoginStatus, useWsLoginStore } from '@/stores/ws.ts'
import type { MarkItemType, MessageType, RevokedMsgType } from '@/services/types.ts'
import { useLogin } from '@/hooks/useLogin.ts'
import { computedToken } from '@/services/request'
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification'
import { useUserInfo } from '@/hooks/useCached.ts'
import { AvatarUtils } from '@/utils/avatarUtils.ts'
const globalStore = useGlobalStore()
const contactStore = useContactStore()
@@ -100,6 +103,17 @@ useMitt.on(WsResponseMessageType.MSG_RECALL, (data: RevokedMsgType) => {
})
useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, (data: MessageType) => {
chatStore.pushMsg(data)
const username = useUserInfo(data.fromUser.uid).value.name!
const avatarSrc = computed(() => AvatarUtils.getAvatarUrl(useUserInfo(data.fromUser.uid).value.avatar as string))
// 不是自己发的消息才通知
if (data.fromUser.uid !== userStore.userInfo.uid) {
sendNotification({
title: username,
body: data.message.body.content,
icon: avatarSrc.value
})
}
})
useMitt.on(WsResponseMessageType.REQUEST_NEW_FRIEND, (data: { uid: number; unreadCount: number }) => {
console.log(data.unreadCount)
@@ -144,5 +158,12 @@ onBeforeMount(async () => {
onMounted(async () => {
await getCurrentWebviewWindow().show()
let permissionGranted = await isPermissionGranted()
// 如果没有授权,则请求授权系统通知
if (!permissionGranted) {
const permission = await requestPermission()
permissionGranted = permission === 'granted'
}
})
</script>
+24 -41
View File
@@ -3,12 +3,8 @@ import { useSettingStore } from '@/stores/setting.ts'
import { useUserStore } from '@/stores/user.ts'
import { useCachedStore } from '@/stores/cached.ts'
import { onlineStatus } from '@/stores/onlineStatus.ts'
import { EventEnum, IsYesEnum, MittEnum, MsgEnum, PluginEnum, ThemeEnum } from '@/enums'
import { EventEnum, IsYesEnum, MittEnum, ThemeEnum } from '@/enums'
import { BadgeType, UserInfoType } from '@/services/types.ts'
import { useChatStore } from '@/stores/chat.ts'
import { useUserInfo } from '@/hooks/useCached.ts'
import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
import { formatTimestamp } from '@/utils/ComputedTime.ts'
import { useMitt } from '@/hooks/useMitt.ts'
import apis from '@/services/apis.ts'
import { delay } from 'lodash-es'
@@ -55,33 +51,6 @@ export const leftHook = () => {
const currentBadge = computed(() =>
editInfo.value.badgeList.find((item) => item.obtain === IsYesEnum.YES && item.wearing === IsYesEnum.YES)
)
const chatStore = useChatStore()
const sessionList = computed(() =>
chatStore.sessionList.map((item) => {
// 最后一条消息内容
const lastMsg = Array.from(chatStore.messageMap.get(item.roomId)?.values() || [])?.slice(-1)?.[0]
let LastUserMsg = ''
if (lastMsg) {
const lastMsgUserName = useUserInfo(lastMsg.fromUser.uid)
LastUserMsg =
lastMsg.message?.type === MsgEnum.RECALL
? `${lastMsgUserName.value.name}:'撤回了一条消息'`
: (renderReplyContent(
lastMsgUserName.value.name,
lastMsg.message?.type,
lastMsg.message?.body?.content || lastMsg.message?.body
) as string)
}
return {
...item,
lastMsg: LastUserMsg || item.text || '欢迎使用HuLa',
lastMsgTime: formatTimestamp(item?.activeTime)
}
})
)
const msgTotal = computed(() => {
return sessionList.value.reduce((total, item) => total + item.unreadCount, 0)
})
/* =================================== 方法 =============================================== */
@@ -91,11 +60,6 @@ export const leftHook = () => {
}
watchEffect(() => {
menuTop.find((item: STO.Plugins<PluginEnum>) => {
if (item.url === 'message') {
item.badge = msgTotal.value
}
})
/** 判断是否是跟随系统主题 */
if (themes.pattern === ThemeEnum.OS) {
followOS()
@@ -137,8 +101,29 @@ export const leftHook = () => {
/** 佩戴徽章 */
const toggleWarningBadge = async (badge: BadgeType) => {
if (!badge?.id) return
await apis.setUserBadge(badge.id)
window.$message.success('佩戴成功')
try {
await apis.setUserBadge(badge.id)
// 更新本地缓存中的用户徽章信息
const currentUser = userStore.userInfo.uid && cachedStore.userCachedList[userStore.userInfo.uid]
if (currentUser) {
// 更新当前佩戴的徽章ID
currentUser.wearingItemId = badge.id
// 更新用户信息中的佩戴徽章ID
userStore.userInfo.wearingItemId = badge.id
// 更新徽章列表中的佩戴状态
editInfo.value.badgeList = editInfo.value.badgeList.map((item) => ({
...item,
wearing: item.id === badge.id ? IsYesEnum.YES : IsYesEnum.NO,
obtain: item.obtain // 保持原有的obtain状态
}))
}
// 确保在状态更新后再显示成功消息
nextTick(() => {
window.$message.success('佩戴成功')
})
} catch (error) {
window.$message.error('佩戴失败,请稍后重试')
}
}
/* 打开并且创建modal */
@@ -241,8 +226,6 @@ export const leftHook = () => {
openWindowsList,
editInfo,
currentBadge,
sessionList,
msgTotal,
handleEditing,
pageJumps,
openContent,
+1 -1
View File
@@ -64,7 +64,7 @@ export default {
addEmoji: (data: { uid: number; expressionUrl: string }) => POST<MessageType>(urls.addEmoji, data),
/** 获取表情 **/
getEmoji: (params: { uid: number }) => GET<EmojiItem[]>(urls.getEmoji, { params }),
/** 删除id */
/** 删除Emoji */
deleteEmoji: (params: { id: number }) => DELETE<EmojiItem[]>(urls.deleteEmoji, params),
/** 获取联系人列表 */
getContactList: (params?: any) => GET<ListResponse<ContactItem>>(urls.getContactList, params),
+4 -6
View File
@@ -73,18 +73,16 @@ async function Http<T>(
// 拼接 API 基础路径
//url = `${import.meta.env.VITE_SERVICE_URL}${url}`
// console.log('fetch url: ', url)
// console.log('fetch options: ', fetchOptions)
console.log(url)
console.log(fetchOptions.headers)
// console.log(url)
// console.log(fetchOptions.headers)
try {
const res = await fetch(url, fetchOptions)
console.log(res)
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
const data = options.isBlob ? await res.arrayBuffer() : await res.json()
console.log(url)
console.log(data)
if (fullResponse) {
return { data, resp: res }
+2 -2
View File
@@ -165,10 +165,10 @@ export type UserInfoType = {
modifyNameChance: number
/** 性别 1为男性,2为女性 */
sex: SexEnum
/** 徽章,本地字段,有值用本地,无值用远端 */
badge?: string
/** 权限 */
power?: number
/** 佩戴的徽章 */
wearingItemId?: number
}
export type BadgeType = {
+21 -24
View File
@@ -11,6 +11,7 @@ import { useContactStore } from '@/stores/contacts.ts'
import { cloneDeep } from 'lodash-es'
import { useUserStore } from '@/stores/user.ts'
import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
import { sendNotification } from '@tauri-apps/plugin-notification'
// 定义每页加载的消息数量
export const pageSize = 20
@@ -284,6 +285,7 @@ export const useChatStore = defineStore(
// 获取用户信息缓存
const uid = msg.fromUser.uid
const cacheUser = cachedStore.userCachedList[uid]
await cachedStore.getBatchUserInfo([uid])
// 发完消息就要刷新会话列表
@@ -292,10 +294,10 @@ export const useChatStore = defineStore(
detailResponse = await apis.sessionDetail({ id: msg.message.roomId })
}
// 更新会话的文本属性以触发重新计算
// 更新会话的文本属性和未读数
const session = sessionList.find((item) => item.roomId === msg.message.roomId)
if (session) {
const lastMsgUserName = cachedStore.currentAtUsersList[uid]?.name
const lastMsgUserName = cachedStore.userCachedList[uid]?.name
const formattedText =
msg.message.type === MsgEnum.RECALL
? session.type === RoomTypeEnum.GROUP
@@ -310,18 +312,27 @@ export const useChatStore = defineStore(
session.type
)
session.text = formattedText!
// 更新未读数
if (msg.fromUser.uid !== userStore.userInfo.uid) {
if (currentRoomId.value !== msg.message.roomId) {
session.unreadCount = (session.unreadCount || 0) + 1
} else if (route?.path !== '/message') {
session.unreadCount = (session.unreadCount || 0) + 1
}
}
}
updateSessionLastActiveTime(msg.message.roomId, detailResponse)
console.log(msg.message.body.atUidList)
// 如果收到的消息里面是艾特自己的就发送系统通知
// if (msg.message.body.atUidList?.includes(userStore.userInfo.uid) && cacheUser) {
// notify({
// name: cacheUser.name as string,
// text: msg.message.body.content,
// icon: cacheUser.avatar as string
// })
// }
if (msg.message.body.atUidList?.includes(userStore.userInfo.uid) && cacheUser) {
sendNotification({
title: cacheUser.name as string,
body: msg.message.body.content,
icon: cacheUser.avatar as string
})
}
// tab 在后台获得新消息,就开始闪烁!
// if (document.hidden && !shakeTitle.isShaking) {
@@ -333,20 +344,6 @@ export const useChatStore = defineStore(
return
}
// 聊天记录计数
if (currentRoomId.value !== msg.message.roomId) {
const item = sessionList.find((item) => item.roomId === msg.message.roomId)
if (item) {
item.unreadCount += 1
}
// 如果新消息的 roomId 和 当前显示的 room 的 Id 一致,就更新已读
} else {
// 且当前路由在 聊天 内
if (route?.path && route?.path === '/message') {
await apis.markMsgRead({ roomId: currentRoomId.value })
}
}
// 如果当前路由不是聊天,就开始计数
if (route?.path !== '/message') {
globalStore.unReadMark.newMsgUnreadCount++
@@ -421,7 +418,7 @@ export const useChatStore = defineStore(
const cacheUser = cachedStore.userCachedList[data.recallUid]
// 如果撤回者的 id 不等于消息发送人的 id, 或者你本人就是管理员,那么显示管理员撤回的。
if (data.recallUid !== message.fromUser.uid) {
message.message.body = `管理员"${cacheUser.name}"撤回了一条成员消息` // 后期根据本地用户数据修改
message.message.body = `管理员"${cacheUser.name}"撤回了一条消息` // 后期根据本地用户数据修改
} else {
// 如果被撤回的消息是消息发送者撤回,正常显示
message.message.body = `"${cacheUser.name}"撤回了一条消息` // 后期根据本地用户数据修改
-18
View File
@@ -27,24 +27,6 @@
}
}
/**! TransitionGroup过渡样式 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.6s cubic-bezier(0.55, 0, 0.1, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
.fade-leave-active {
position: absolute;
}
/**! end */
:deep(.n-badge .n-badge-sup) {
font-weight: bold;
font-size: 10px;
+6 -6
View File
@@ -1,8 +1,8 @@
<template>
<!-- 消息列表 // TODO 使用虚拟列表组件就用不了动画和拖动了 (nyh -> 2024-03-28 06:01:00) -->
<n-scrollbar ref="scrollbar" style="max-height: calc(100vh - 70px)">
<n-scrollbar ref="msg-scrollbar" style="max-height: calc(100vh - 70px)">
<!-- 右键菜单组件 -->
<TransitionGroup class="sort-target p-[4px_10px_0px_8px]" name="fade" tag="div" type="transition">
<div class="p-[4px_10px_0px_8px]">
<ContextMenu
v-for="item in sessionList"
:key="item.roomId"
@@ -46,7 +46,7 @@
</n-flex>
</n-flex>
</ContextMenu>
</TransitionGroup>
</div>
</n-scrollbar>
</template>
<script lang="ts" setup>
@@ -67,7 +67,7 @@ const chatStore = useChatStore()
const globalStore = useGlobalStore()
const groupStore = useGroupStore()
const { userUid } = useCommon()
// const scrollbar = useTemplateRef('scrollbar')
// const msgScrollbar = useTemplateRef('msg-scrollbar')
const { handleMsgClick, menuList, specialMenuList, handleMsgDblclick } = useMessage()
const currentSession = computed(() => globalStore.currentSession)
// TODO 艾特我提醒
@@ -107,12 +107,12 @@ watch(
console.log('newVal', newVal)
handleMsgClick(newVal as SessionItem)
// nextTick(() => {
// requestAnimationFrame(() => {
// const index = sessionList.value.findIndex((item) => item.roomId === newVal.roomId)
// if (index !== -1) {
// scrollbar.value?.scrollTo({
// top: index * (75 + 5) + 4, // height of msg-box (75px) + margin-bottom (5px) + top padding (4px)
// behavior: 'smooth'
// behavior: 'auto'
// })
// }
// })