├── .gitignore
├── LICENSE
├── README.md
├── biome.json
├── eslint.config.js
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── images
│ ├── filetype_any.svg
│ ├── logo.svg
│ └── wrapped-2024
│ │ ├── cover-logo.svg
│ │ ├── footer-logo.svg
│ │ ├── widget-message-background.png
│ │ ├── widget-music-background.png
│ │ ├── widget-new-friends-background.png
│ │ └── widget-voice-background.png
└── wxemoji
│ ├── Expression_100@2x.png
│ ├── Expression_101@2x.png
│ ├── Expression_102@2x.png
│ ├── Expression_103@2x.png
│ ├── Expression_104@2x.png
│ ├── Expression_105@2x.png
│ ├── Expression_10@2x.png
│ ├── Expression_11@2x.png
│ ├── Expression_12@2x.png
│ ├── Expression_13@2x.png
│ ├── Expression_14@2x.png
│ ├── Expression_15@2x.png
│ ├── Expression_16@2x.png
│ ├── Expression_17@2x.png
│ ├── Expression_18@2x.png
│ ├── Expression_19@2x.png
│ ├── Expression_1@2x.png
│ ├── Expression_20@2x.png
│ ├── Expression_21@2x.png
│ ├── Expression_22@2x.png
│ ├── Expression_23@2x.png
│ ├── Expression_24@2x.png
│ ├── Expression_25@2x.png
│ ├── Expression_26@2x.png
│ ├── Expression_27@2x.png
│ ├── Expression_28@2x.png
│ ├── Expression_29@2x.png
│ ├── Expression_2@2x.png
│ ├── Expression_30@2x.png
│ ├── Expression_31@2x.png
│ ├── Expression_32@2x.png
│ ├── Expression_33@2x.png
│ ├── Expression_34@2x.png
│ ├── Expression_35@2x.png
│ ├── Expression_36@2x.png
│ ├── Expression_37@2x.png
│ ├── Expression_38@2x.png
│ ├── Expression_39@2x.png
│ ├── Expression_3@2x.png
│ ├── Expression_40@2x.png
│ ├── Expression_41@2x.png
│ ├── Expression_42@2x.png
│ ├── Expression_43@2x.png
│ ├── Expression_44@2x.png
│ ├── Expression_45@2x.png
│ ├── Expression_46@2x.png
│ ├── Expression_47@2x.png
│ ├── Expression_48@2x.png
│ ├── Expression_49@2x.png
│ ├── Expression_4@2x.png
│ ├── Expression_50@2x.png
│ ├── Expression_51@2x.png
│ ├── Expression_52@2x.png
│ ├── Expression_53@2x.png
│ ├── Expression_54@2x.png
│ ├── Expression_55@2x.png
│ ├── Expression_56@2x.png
│ ├── Expression_57@2x.png
│ ├── Expression_58@2x.png
│ ├── Expression_59@2x.png
│ ├── Expression_5@2x.png
│ ├── Expression_60@2x.png
│ ├── Expression_61@2x.png
│ ├── Expression_62@2x.png
│ ├── Expression_63@2x.png
│ ├── Expression_64@2x.png
│ ├── Expression_65@2x.png
│ ├── Expression_66@2x.png
│ ├── Expression_67@2x.png
│ ├── Expression_68@2x.png
│ ├── Expression_69@2x.png
│ ├── Expression_6@2x.png
│ ├── Expression_70@2x.png
│ ├── Expression_71@2x.png
│ ├── Expression_72@2x.png
│ ├── Expression_73@2x.png
│ ├── Expression_74@2x.png
│ ├── Expression_75@2x.png
│ ├── Expression_76@2x.png
│ ├── Expression_77@2x.png
│ ├── Expression_78@2x.png
│ ├── Expression_79@2x.png
│ ├── Expression_7@2x.png
│ ├── Expression_80@2x.png
│ ├── Expression_81@2x.png
│ ├── Expression_82@2x.png
│ ├── Expression_83@2x.png
│ ├── Expression_84@2x.png
│ ├── Expression_85@2x.png
│ ├── Expression_86@2x.png
│ ├── Expression_87@2x.png
│ ├── Expression_88@2x.png
│ ├── Expression_89@2x.png
│ ├── Expression_8@2x.png
│ ├── Expression_90@2x.png
│ ├── Expression_91@2x.png
│ ├── Expression_92@2x.png
│ ├── Expression_93@2x.png
│ ├── Expression_94@2x.png
│ ├── Expression_95@2x.png
│ ├── Expression_96@2x.png
│ ├── Expression_97@2x.png
│ ├── Expression_98@2x.png
│ ├── Expression_99@2x.png
│ ├── Expression_9@2x.png
│ └── new
│ ├── 2_02.png
│ ├── 2_04.png
│ ├── 2_05.png
│ ├── 2_06.png
│ ├── 2_07.png
│ ├── 2_09.png
│ ├── 2_10.png
│ ├── 2_11.png
│ ├── 2_12.png
│ ├── 2_14.png
│ ├── 2_15.png
│ ├── 2_16.png
│ ├── 2_17.png
│ ├── 666.png
│ ├── Addoil.png
│ ├── Boring.png
│ ├── Broken.png
│ ├── Cold.png
│ ├── Duh.png
│ ├── Firecracker.png
│ ├── Fireworks.png
│ ├── Flushed.png
│ ├── Happy.png
│ ├── Hurt.png
│ ├── KeepFighting.png
│ ├── LetDown.png
│ ├── LetMeSee.png
│ ├── Lol.png
│ ├── NoProb.png
│ ├── Party.png
│ ├── Shocked.png
│ ├── Sick.png
│ ├── Sigh.png
│ ├── Slap.png
│ ├── Social.png
│ ├── Sweat.png
│ ├── Terror.png
│ ├── Watermelon.png
│ ├── Worship.png
│ ├── Wow.png
│ ├── Yellowdog.png
│ ├── smiley_39b.png
│ └── smiley_83b.png
├── src
├── App.tsx
├── assets
│ ├── gh_prefixUserNames.csv
│ ├── needCheckDatabaseUsernames.csv
│ └── specialBrandUserNames.csv
├── components
│ ├── account-select-dialog.tsx
│ ├── central-icon.tsx
│ ├── chat-details.tsx
│ ├── chat-list.tsx
│ ├── contact-list.tsx
│ ├── filetype-icon.tsx
│ ├── icon.tsx
│ ├── image.tsx
│ ├── link-card.tsx
│ ├── link.tsx
│ ├── local-image.tsx
│ ├── local-video.tsx
│ ├── local-voice.tsx
│ ├── media-message-list.tsx
│ ├── media-viewer-dialog.tsx
│ ├── message-bubble-group.tsx
│ ├── message-list.tsx
│ ├── message
│ │ ├── app-message.tsx
│ │ ├── app-message
│ │ │ ├── announcement-message.tsx
│ │ │ ├── attach-2-message.tsx
│ │ │ ├── attach-message.tsx
│ │ │ ├── channel-message.tsx
│ │ │ ├── channel-video-message.tsx
│ │ │ ├── forward-message-2.tsx
│ │ │ ├── forward-message.tsx
│ │ │ ├── game-message.tsx
│ │ │ ├── link-message-2.tsx
│ │ │ ├── live-message.tsx
│ │ │ ├── miniapp-message-2.tsx
│ │ │ ├── miniapp-message.tsx
│ │ │ ├── music-message.tsx
│ │ │ ├── note-message.tsx
│ │ │ ├── pat-message.tsx
│ │ │ ├── realtime-location-message.tsx
│ │ │ ├── red-envelope-message.tsx
│ │ │ ├── refer-message.tsx
│ │ │ ├── ringtone-message.tsx
│ │ │ ├── scan-result-message.tsx
│ │ │ ├── solitaire-message.tsx
│ │ │ ├── sticker-message.tsx
│ │ │ ├── sticker-set-message.tsx
│ │ │ ├── store-message.tsx
│ │ │ ├── store-product-message.tsx
│ │ │ ├── text-message.tsx
│ │ │ ├── ting-message.tsx
│ │ │ ├── transfer-message.tsx
│ │ │ ├── url-message.tsx
│ │ │ ├── video-message.tsx
│ │ │ └── voice-message.tsx
│ │ ├── chatroom-voip-message.tsx
│ │ ├── contact-message.tsx
│ │ ├── image-message.tsx
│ │ ├── location-message.tsx
│ │ ├── mail-message.tsx
│ │ ├── message-inline.tsx
│ │ ├── message.tsx
│ │ ├── micro-video-message.tsx
│ │ ├── sticker-message.tsx
│ │ ├── system-extended-message.tsx
│ │ ├── system-message.tsx
│ │ ├── text-message.tsx
│ │ ├── verify-message.tsx
│ │ ├── video-message.tsx
│ │ ├── voice-message.tsx
│ │ ├── voip-message.tsx
│ │ └── wecom-contact-message.tsx
│ ├── record
│ │ ├── attatch-record.tsx
│ │ ├── channel-record.tsx
│ │ ├── channel-video-record.tsx
│ │ ├── contact-record.tsx
│ │ ├── forward-record.tsx
│ │ ├── image-record.tsx
│ │ ├── link-record.tsx
│ │ ├── live-record.tsx
│ │ ├── location-record.tsx
│ │ ├── miniapp-record.tsx
│ │ ├── music-record.tsx
│ │ ├── note-record.tsx
│ │ ├── record.tsx
│ │ ├── text-record.tsx
│ │ └── ting-record.tsx
│ ├── statistic
│ │ └── wrapped-2024
│ │ │ ├── daily-message-count-chart.tsx
│ │ │ ├── section-message-count-description.tsx
│ │ │ ├── section-most-message-chat.tsx
│ │ │ ├── section-most-used-wxemoji.tsx
│ │ │ ├── section-new-user-added.tsx
│ │ │ ├── section-sent-media-messages.tsx
│ │ │ ├── section-sent-message-count-description.tsx
│ │ │ ├── section-sent-message-count.tsx
│ │ │ ├── section-sent-message-word-count.tsx
│ │ │ ├── section-summary.tsx
│ │ │ ├── wrapped-2024-trigger.tsx
│ │ │ └── wrapped-2024.tsx
│ ├── ui
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── chart.tsx
│ │ ├── dialog.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── radio-group.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── tabs.tsx
│ │ ├── tooltip.tsx
│ │ └── typography.tsx
│ ├── user.tsx
│ └── wechat-emoji.tsx
├── index.css
├── index.tsx
├── lib
│ ├── controllers
│ │ ├── attach.ts
│ │ ├── chat.ts
│ │ ├── contact.ts
│ │ ├── image.ts
│ │ ├── message.ts
│ │ ├── statistic.ts
│ │ ├── video.ts
│ │ ├── voice.ts
│ │ └── wrapped-2024.ts
│ ├── global.ts
│ ├── hooks
│ │ ├── appProvider.tsx
│ │ ├── databaseProvider.tsx
│ │ ├── useQuery.ts
│ │ └── workerProvider.tsx
│ ├── schema.ts
│ ├── utils.ts
│ ├── wechat-emojis.ts
│ └── worker.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.app.tsbuildinfo
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.node.tsbuildinfo
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Chclt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OhMyWeChat:微信备份 & 数据报告
2 |
3 | 这是一个为微信设计的备份阅读器,总体上还原了微信,但经过了无数的重新设计,看起来焕然一新。
4 |
5 | 以及,年度数据报告。
6 |
7 | [关注我的 Telegram 频道](https://t.me/chclt_hi) | [关注我的 Twitter](https://twitter.com/realChclt)
8 |
9 | [注意事项](#注意) | [使用说明](#使用说明)
10 |
11 | ## 已知问题
12 |
13 | - [ ] 微信在新版本(大约从 8.0.55 开始)对数据进行了压缩,导致新版本的微信用户会看到很多消息解析失败,正在解决这个问题...
14 | - [x] ~~部分用户会在打开文件夹后没有反应,同时控制台会报出一个 RangeError 错误。~~(应该是解决了,请试试)
15 | - [ ] OhMyWeChat 仍处于测试阶段,欢迎关注,也感谢宽容,欢迎随时[通过 Telegram 联系我](https://t.me/realchclt),帮助我调试、改进,我们一起把这个项目变得更好~
16 |
17 | ## 特性
18 |
19 | - (几乎)完整的消息类型支持
20 |
21 | - [x] 文本、微信表情、图片、视频、语音、回复消息
22 | - [ ] 表情包(可用但仍有问题)
23 | - [x] 合并转发
24 | - [ ] 收藏笔记(未完成)
25 | - [x] 位置、实时位置共享
26 | - [x] 红包、转账、AA 收款(可用但仍有问题)
27 | - [x] 分享链接、分享音乐等
28 | - [x] 通话记录
29 | - [x] 微信名片、公众号名片、视频号名片、微信小店名片等
30 | - [x] 群接龙、群公告
31 | - [x] 拍一拍、系统消息
32 | - [x] 等等近 50 种消息类型……
33 |
34 | - 熟悉但焕然一新的 UI 界面
35 |
36 | 
37 |
38 | 
39 |
40 | - 2024 微信年度数据报告
41 |
42 | 
43 |
44 |
45 | ## 注意
46 |
47 | - 所有数据均在本地处理。
48 | - 部分图片资源(如头像等)通过网址从微信自己的服务器获取,如果你介意这一部分请求可能造成的隐私泄露,你可以在页面加载完成后断开网络,所有功能依然能够正常使用。
49 | - Safari 和 Arc 浏览器因为一些技术原因暂无法适配,软件在最新版 Chrome 以及 FireFox 下测试可用。
50 | - 为了防止可能发生的浏览器插件造成的隐私泄露,建议在无痕模式下打开本产品。
51 |
52 | ## 使用说明
53 |
54 | 1. 连接你的 iPhone / iPad 到电脑,第一步是通过苹果官方的 iTunes 备份你的设备到电脑上。如果你使用 Mac,备份的功能已经交给访达,你不需要安装任何额外软件。备份的时候记得勾选“不加密”。
55 |
56 | > 注:并不是使用微信自带的“迁移聊天记录到电脑”的功能,而是使用 Windows 下的 iTunes 或 Mac 下的访达,所以使用 OhMyWeChat 需要一台 iPhone/iPad 和一台 Windows/Mac 电脑。
57 |
58 |
59 |
60 | 2. 等待备份完成后,你的备份文件应该位于 Windows 下的 `C:\用户\(用户名)\AppData\Roaming\Apple Computer\MobileSync\Backup\` 或 Mac 下的 `~/Library/Application Support/MobileSync/Backup`(在访达中你可以使用快捷键 Command + Shift + G 快速打开这个文件夹),我们需要的文件夹是其中名如 `xxxxxxxx-xxxxxxxxxxxxxxxx` 的那一个。
61 | 3. 出于安全考虑,浏览器应该不会允许你在网页中直接打开上面这两个文件夹,所以你需要把所需的 `xxxxxxxx-xxxxxxxxxxxxxxxx` 文件夹移动到系统目录以外的地方。
62 | 4. 在 OhMyWeChat 中打开刚才准备好的文件夹,出于不同浏览器中不同的接口调用,Chrome 浏览器会询问你是否允许网页访问该文件夹,而 FireFox 会询问你是否要上传整个文件夹,请选择允许。事实上 OhMyWeChat 并不会“上传”任何数据,所有数据不会离开本地,这里的“上传”只是浏览器对于网页操作文件的一种广义描述,请放心~
63 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "ignore": []
11 | },
12 | "formatter": {
13 | "enabled": true,
14 | "indentStyle": "space"
15 | },
16 | "organizeImports": {
17 | "enabled": true
18 | },
19 | "linter": {
20 | "enabled": true,
21 | "rules": {
22 | "recommended": true
23 | }
24 | },
25 | "javascript": {
26 | "formatter": {
27 | "quoteStyle": "double"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Oh My Wechat
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oh-my-wechat",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@ffmpeg/core": "^0.12.6",
14 | "@ffmpeg/ffmpeg": "^0.12.10",
15 | "@ffmpeg/types": "^0.12.2",
16 | "@ffmpeg/util": "^0.12.1",
17 | "@radix-ui/react-dialog": "^1.1.2",
18 | "@radix-ui/react-label": "^2.1.0",
19 | "@radix-ui/react-popover": "^1.1.4",
20 | "@radix-ui/react-radio-group": "^1.2.1",
21 | "@radix-ui/react-scroll-area": "^1.2.1",
22 | "@radix-ui/react-select": "^2.1.2",
23 | "@radix-ui/react-slot": "^1.1.0",
24 | "@radix-ui/react-tabs": "^1.1.1",
25 | "@radix-ui/react-tooltip": "^1.1.6",
26 | "@radix-ui/react-visually-hidden": "^1.1.0",
27 | "@tanstack/react-virtual": "^3.11.2",
28 | "class-variance-authority": "^0.7.0",
29 | "clsx": "^2.1.1",
30 | "crypto-js": "^4.2.0",
31 | "date-fns": "^4.1.0",
32 | "embla-carousel-fade": "^8.5.1",
33 | "embla-carousel-react": "^8.5.1",
34 | "fast-xml-parser": "^4.5.0",
35 | "lucide-react": "^0.459.0",
36 | "protobufjs": "^7.4.0",
37 | "react": "^18.3.1",
38 | "react-day-picker": "^8.10.1",
39 | "react-dom": "^18.3.1",
40 | "react-error-boundary": "^4.1.2",
41 | "react-resizable-panels": "^2.1.6",
42 | "recharts": "^2.15.0",
43 | "silk-wasm": "^3.6.3",
44 | "sql.js": "^1.11.0",
45 | "tailwind-merge": "^2.5.4",
46 | "tailwindcss-animate": "^1.0.7"
47 | },
48 | "devDependencies": {
49 | "@biomejs/biome": "1.9.4",
50 | "@eslint/js": "^9.11.1",
51 | "@types/crypto-js": "^4.2.2",
52 | "@types/node": "^22.8.1",
53 | "@types/react": "^18.3.10",
54 | "@types/react-dom": "^18.3.0",
55 | "@types/sql.js": "^1.4.9",
56 | "@types/wicg-file-system-access": "^2023.10.5",
57 | "@vitejs/plugin-react": "^4.3.2",
58 | "autoprefixer": "^10.4.20",
59 | "eslint": "^9.11.1",
60 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
61 | "eslint-plugin-react-refresh": "^0.4.12",
62 | "globals": "^15.9.0",
63 | "postcss": "^8.4.47",
64 | "postcss-loader": "^8.1.1",
65 | "tailwindcss": "^3.4.14",
66 | "typescript": "^5.5.3",
67 | "typescript-eslint": "^8.7.0",
68 | "vite": "^5.4.8",
69 | "vite-plugin-wasm": "^3.3.0"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/filetype_any.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/wrapped-2024/widget-message-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/images/wrapped-2024/widget-message-background.png
--------------------------------------------------------------------------------
/public/images/wrapped-2024/widget-music-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/images/wrapped-2024/widget-music-background.png
--------------------------------------------------------------------------------
/public/images/wrapped-2024/widget-new-friends-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/images/wrapped-2024/widget-new-friends-background.png
--------------------------------------------------------------------------------
/public/images/wrapped-2024/widget-voice-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/images/wrapped-2024/widget-voice-background.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_100@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_100@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_101@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_101@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_102@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_102@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_103@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_103@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_104@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_104@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_105@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_105@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_10@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_10@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_11@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_11@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_12@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_12@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_13@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_13@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_14@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_14@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_15@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_15@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_16@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_17@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_17@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_18@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_18@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_19@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_19@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_1@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_1@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_20@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_21@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_21@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_22@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_22@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_23@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_23@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_24@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_24@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_25@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_25@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_26@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_26@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_27@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_27@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_28@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_28@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_29@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_2@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_2@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_30@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_30@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_31@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_31@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_32@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_33@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_33@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_34@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_34@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_35@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_35@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_36@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_36@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_37@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_37@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_38@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_38@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_39@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_39@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_3@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_3@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_40@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_41@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_41@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_42@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_42@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_43@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_43@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_44@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_44@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_45@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_45@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_46@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_46@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_47@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_47@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_48@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_48@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_49@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_49@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_4@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_4@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_50@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_51@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_51@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_52@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_52@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_53@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_53@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_54@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_54@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_55@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_55@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_56@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_56@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_57@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_57@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_58@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_58@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_59@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_59@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_5@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_60@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_61@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_61@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_62@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_62@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_63@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_63@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_64@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_64@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_65@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_65@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_66@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_66@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_67@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_67@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_68@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_68@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_69@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_69@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_6@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_6@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_70@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_70@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_71@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_71@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_72@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_73@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_73@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_74@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_74@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_75@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_75@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_76@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_77@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_77@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_78@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_78@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_79@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_79@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_7@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_7@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_80@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_80@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_81@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_81@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_82@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_82@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_83@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_83@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_84@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_84@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_85@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_85@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_86@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_86@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_87@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_87@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_88@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_88@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_89@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_89@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_8@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_8@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_90@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_90@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_91@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_91@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_92@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_92@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_93@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_93@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_94@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_94@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_95@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_95@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_96@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_96@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_97@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_97@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_98@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_98@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_99@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_99@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/Expression_9@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/Expression_9@2x.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_02.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_04.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_05.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_06.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_07.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_09.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_10.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_11.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_12.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_14.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_15.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_16.png
--------------------------------------------------------------------------------
/public/wxemoji/new/2_17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/2_17.png
--------------------------------------------------------------------------------
/public/wxemoji/new/666.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/666.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Addoil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Addoil.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Boring.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Boring.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Broken.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Cold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Cold.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Duh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Duh.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Firecracker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Firecracker.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Fireworks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Fireworks.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Flushed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Flushed.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Happy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Happy.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Hurt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Hurt.png
--------------------------------------------------------------------------------
/public/wxemoji/new/KeepFighting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/KeepFighting.png
--------------------------------------------------------------------------------
/public/wxemoji/new/LetDown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/LetDown.png
--------------------------------------------------------------------------------
/public/wxemoji/new/LetMeSee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/LetMeSee.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Lol.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Lol.png
--------------------------------------------------------------------------------
/public/wxemoji/new/NoProb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/NoProb.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Party.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Party.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Shocked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Shocked.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Sick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Sick.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Sigh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Sigh.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Slap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Slap.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Social.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Social.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Sweat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Sweat.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Terror.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Terror.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Watermelon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Watermelon.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Worship.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Worship.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Wow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Wow.png
--------------------------------------------------------------------------------
/public/wxemoji/new/Yellowdog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/Yellowdog.png
--------------------------------------------------------------------------------
/public/wxemoji/new/smiley_39b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/smiley_39b.png
--------------------------------------------------------------------------------
/public/wxemoji/new/smiley_83b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chclt/oh-my-wechat/e5a277aeb66463921664697eeb09fd8775d17a90/public/wxemoji/new/smiley_83b.png
--------------------------------------------------------------------------------
/src/components/contact-list.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import { useApp } from "@/lib/hooks/appProvider";
3 | import useQuery from "@/lib/hooks/useQuery.ts";
4 | import type { Chat, Chatroom, ControllerResult, User } from "@/lib/schema.ts";
5 | import { cn } from "@/lib/utils.ts";
6 | import type React from "react";
7 | import { useEffect } from "react";
8 |
9 | interface ContactListProps extends React.HTMLAttributes {}
10 |
11 | export default function ContactList({ className, ...props }: ContactListProps) {
12 | const [query, isQuerying, result, error] = useQuery>(
13 | {
14 | data: [],
15 | },
16 | );
17 |
18 | useEffect(() => {
19 | query("/contacts");
20 | }, []);
21 |
22 | return (
23 |
24 | {result.data.map((contact) => (
25 |
26 | ))}
27 |
28 | );
29 | }
30 |
31 | interface ContactItemProps extends React.HTMLAttributes {
32 | contact: User | Chatroom;
33 | }
34 |
35 | export function ContactItem({
36 | contact,
37 |
38 | className,
39 | ...props
40 | }: ContactItemProps) {
41 | const { setChat } = useApp();
42 |
43 | const [query, isQuerying, result, error] = useQuery>(
44 | {
45 | data: [],
46 | },
47 | );
48 |
49 | useEffect(() => {
50 | query("/chats/in", {
51 | ids: [contact.id],
52 | });
53 | }, [contact.id]);
54 |
55 | return (
56 |
60 | {contact.photo ? (
61 |
62 |
68 |
69 | ) : (
70 |
71 | )}
72 |
73 |
74 |
75 |
76 | {/* @ts-ignore */}
77 | {contact.remark ?? contact.username ?? contact.title}
78 | {contact.is_openim && (
79 |
80 | @企业微信
81 |
82 | )}
83 |
84 |
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/filetype-icon.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils.ts";
2 |
3 | export default function FileTypeIcon({
4 | className,
5 | ...props
6 | }: React.SVGProps) {
7 | return (
8 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/image.tsx:
--------------------------------------------------------------------------------
1 | export default function Image({ src, alt, className, ...props }: React.ImgHTMLAttributes) {
2 | return (
3 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/link-card.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardFooter,
5 | CardIndicator,
6 | CardTitle,
7 | } from "@/components/ui/card.tsx";
8 | import { cn } from "@/lib/utils";
9 | import { Slot } from "@radix-ui/react-slot";
10 | import type * as React from "react";
11 | import { ArrowShareRightSolid } from "./central-icon";
12 | import Link from "./link";
13 |
14 | export interface LinkCardProps
15 | extends React.AnchorHTMLAttributes {
16 | heading?: string;
17 | abstract?: string;
18 | preview?: React.ReactElement;
19 | from?: string;
20 | icon?: React.ReactNode;
21 | }
22 |
23 | const LinkCard = ({
24 | href,
25 | heading,
26 | abstract,
27 | preview,
28 | from,
29 | icon,
30 | ...props
31 | }: LinkCardProps) => {
32 | return (
33 |
34 |
35 |
36 | {heading}
37 |
42 | {preview && (
43 |
44 | {preview}
45 |
46 | )}
47 |
48 | {abstract}
49 |
50 |
51 |
52 |
53 | {from && from.length > 0 ? from : "\u200B"}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export { LinkCard };
65 |
--------------------------------------------------------------------------------
/src/components/link.tsx:
--------------------------------------------------------------------------------
1 | export default function Link({
2 | href,
3 | target = "_blank",
4 | className,
5 | children,
6 | ...props
7 | }: React.AnchorHTMLAttributes) {
8 | return (
9 |
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/local-image.tsx:
--------------------------------------------------------------------------------
1 | import type { RecordVM } from "@/components/record/record.tsx";
2 | import _global from "@/lib/global.ts";
3 | import { useApp } from "@/lib/hooks/appProvider.tsx";
4 | import useQuery from "@/lib/hooks/useQuery.ts";
5 | import type { Chat, MessageVM, PhotpSize } from "@/lib/schema.ts";
6 | import type React from "react";
7 | import { forwardRef, useEffect, useRef } from "react";
8 |
9 | type LocalImageProps = React.ImgHTMLAttributes & {
10 | chat: Chat;
11 | message: MessageVM;
12 | record?: RecordVM;
13 | size?: "origin" | "thumb"; // 期望的尺寸,可能因为没有指定尺寸而使用另一尺寸
14 | domain?: "image" | "opendata" | "video"; // 图片资源默认从 Img 文件夹获取,如果消息里有 appattach 字段,图片在 OpenData 文件夹,如果是视频,缩略图会和视频一样在 Video 文件夹
15 | };
16 |
17 | const LocalImage = forwardRef(
18 | (
19 | {
20 | chat,
21 | message,
22 | record,
23 | size = "origin",
24 | domain = "image",
25 | alt,
26 | className,
27 | ...props
28 | },
29 | ref,
30 | ) => {
31 | const { registerIntersectionObserver } = useApp();
32 |
33 | const internalRef = useRef(null);
34 | const imgRef = (ref as React.RefObject) || internalRef;
35 |
36 | const [query, isQuerying, result, error] = useQuery([]);
37 |
38 | useEffect(() => {
39 | if (imgRef.current) {
40 | registerIntersectionObserver(imgRef.current, () => {
41 | query("/images", {
42 | chat,
43 | message,
44 | record,
45 | size,
46 | domain,
47 | });
48 | });
49 | }
50 | }, [imgRef]);
51 |
52 | useEffect(() => {
53 | return () => {
54 | if (result.length)
55 | result.map((photo) => {
56 | if (_global.enableDebug) console.log("revoke image", photo.src);
57 | URL.revokeObjectURL(photo.src);
58 | });
59 | };
60 | });
61 |
62 | return (
63 |
73 | );
74 | },
75 | );
76 |
77 | export default LocalImage;
78 |
--------------------------------------------------------------------------------
/src/components/local-video.tsx:
--------------------------------------------------------------------------------
1 | import _global from "@/lib/global.ts";
2 | import { useApp } from "@/lib/hooks/appProvider.tsx";
3 | import useQuery from "@/lib/hooks/useQuery.ts";
4 | import type {
5 | Chat,
6 | MicroVideoMessage,
7 | VideoInfo,
8 | VideoMessage,
9 | } from "@/lib/schema.ts";
10 | import type React from "react";
11 | import { forwardRef, useEffect, useRef } from "react";
12 |
13 | interface LocalVideoProps extends React.VideoHTMLAttributes {
14 | chat: Chat;
15 | message: VideoMessage | MicroVideoMessage;
16 | }
17 |
18 | const LocalVideo = forwardRef(
19 | ({ chat, message, ...props }, ref) => {
20 | const { registerIntersectionObserver } = useApp();
21 |
22 | const internalRef = useRef(null);
23 | const videoRef = (ref as React.RefObject) || internalRef;
24 |
25 | const [query, isQuerying, result, error] = useQuery(
26 | undefined,
27 | );
28 |
29 | useEffect(() => {
30 | if (videoRef.current) {
31 | registerIntersectionObserver(videoRef.current, () => {
32 | query("/videos", {
33 | chat,
34 | message,
35 | });
36 | });
37 | }
38 | }, [videoRef]);
39 |
40 | useEffect(() => {
41 | return () => {
42 | if (result?.poster) {
43 | if (_global.enableDebug)
44 | console.log("revoke video poster", result.poster);
45 | URL.revokeObjectURL(result.poster);
46 | }
47 | if (result?.src) {
48 | if (_global.enableDebug) console.log("revoke video", result.src);
49 | URL.revokeObjectURL(result.src);
50 | }
51 | };
52 | });
53 |
54 | return (
55 |
64 | );
65 | },
66 | );
67 |
68 | export default LocalVideo;
69 |
--------------------------------------------------------------------------------
/src/components/local-voice.tsx:
--------------------------------------------------------------------------------
1 | import _global from "@/lib/global.ts";
2 | import { useApp } from "@/lib/hooks/appProvider.tsx";
3 | import useQuery from "@/lib/hooks/useQuery.ts";
4 | import type { Chat, VoiceInfo, VoiceMessage } from "@/lib/schema.ts";
5 | import type React from "react";
6 | import { forwardRef, useEffect, useRef } from "react";
7 |
8 | interface LocalVoiceProps extends React.ImgHTMLAttributes {
9 | chat: Chat;
10 | message: VoiceMessage;
11 | }
12 |
13 | const LocalVoice = forwardRef(
14 | ({ chat, message, ...props }, ref) => {
15 | const { registerIntersectionObserver } = useApp();
16 |
17 | const internalRef = useRef(null);
18 | const voiceRef = (ref as React.RefObject) || internalRef;
19 |
20 | const [query, isQuerying, result, error] = useQuery(
21 | undefined,
22 | );
23 |
24 | useEffect(() => {
25 | if (voiceRef.current) {
26 | registerIntersectionObserver(voiceRef.current, () => {
27 | query("/voices", {
28 | chat,
29 | message,
30 |
31 | scope: "transcription",
32 | });
33 | });
34 | }
35 | }, [voiceRef]);
36 |
37 | useEffect(() => {
38 | return () => {
39 | if (result?.src) {
40 | if (_global.enableDebug) console.log("revoke audio", result.src);
41 | URL.revokeObjectURL(result.src);
42 | }
43 | };
44 | });
45 |
46 | return (
47 |
55 | //
56 | // {result && (
57 | // <>
58 | // {result.raw_aud_src && (
59 | //
63 | // download aud
64 | //
65 | // )}
66 | // {result.transcription &&
{result.transcription}
}
67 | // >
68 | // )}
69 | //
70 | );
71 | },
72 | );
73 |
74 | export default LocalVoice;
75 |
--------------------------------------------------------------------------------
/src/components/media-message-list.tsx:
--------------------------------------------------------------------------------
1 | import Message from "@/components/message/message.tsx";
2 | import { useApp } from "@/lib/hooks/appProvider.tsx";
3 | import useQuery from "@/lib/hooks/useQuery.ts";
4 | import {
5 | type Chat,
6 | type ControllerPaginatorResult,
7 | type ImageMessage as ImageMessageVM,
8 | MessageType,
9 | type MessageVM,
10 | } from "@/lib/schema.ts";
11 | import type React from "react";
12 | import { useState } from "react";
13 | import { useEffect } from "react";
14 |
15 | interface MediaMessageListProps extends React.HTMLAttributes {
16 | chat: Chat;
17 | isChatroom: boolean;
18 | }
19 |
20 | export default function MediaMessageList({
21 | chat,
22 | isChatroom,
23 | className,
24 | ...props
25 | }: MediaMessageListProps) {
26 | const { setIsOpenMediaViewer } = useApp();
27 |
28 | const [messageList, setMessageList] = useState<{
29 | [key: number]: MessageVM[];
30 | }>({});
31 | const [query, isQuerying, result, error] = useQuery<
32 | ControllerPaginatorResult
33 | >({
34 | data: [],
35 | meta: {},
36 | });
37 |
38 | useEffect(() => {
39 | setMessageList({});
40 | query("/messages", {
41 | chat,
42 | type: [MessageType.IMAGE, MessageType.VIDEO, MessageType.MICROVIDEO],
43 | });
44 | }, [chat]);
45 |
46 | useEffect(() => {
47 | if (result.meta.cursor) {
48 | setMessageList((old) => ({
49 | // @ts-ignore TODO
50 | [result.meta.cursor]: result.data,
51 | ...old,
52 | }));
53 | }
54 | }, [result]);
55 |
56 | return (
57 | *]:basis-20 [&>*]:grow"}>
58 | {Object.keys(messageList)
59 | .map(Number)
60 | .sort((a, b) => b - a)
61 | .flatMap((key) => messageList[key])
62 | .map((message) => (
63 |
64 | {
77 | // setIsOpenMediaViewer(true)
78 | }}
79 | />
80 |
81 | ))}
82 |
83 | {Array.from(new Array(8)).map(() => (
84 |
85 | ))}
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/message-bubble-group.tsx:
--------------------------------------------------------------------------------
1 | import User from "@/components/user.tsx";
2 | import { MessageDirection, type User as UserVM } from "@/lib/schema.ts";
3 |
4 | import _global from "@/lib/global.ts";
5 | import type { MessageVM } from "@/lib/schema.ts";
6 | import { cn } from "@/lib/utils.ts";
7 | import { ErrorBoundary } from "react-error-boundary";
8 | import Message from "./message/message.tsx";
9 |
10 | interface BubbleGroupProps extends React.HTMLAttributes {
11 | user: UserVM;
12 | messages?: MessageVM[];
13 |
14 | showPhoto?: boolean;
15 | showUsername?: boolean;
16 | }
17 |
18 | export function MessageBubbleGroup({
19 | user,
20 | messages = [],
21 | showPhoto = true,
22 | showUsername = false,
23 |
24 | className,
25 | children,
26 | ...props
27 | }: BubbleGroupProps) {
28 | const messageDirection = messages[0]?.direction ?? MessageDirection.incoming;
29 |
30 | return (
31 | {
33 | console.error(error);
34 | }}
35 | fallback={
36 | {
38 | if (_global.enableDebug) console.log(messages);
39 | }}
40 | >
41 | 解析失败:消息组
42 |
43 | }
44 | >
45 |
52 | {showPhoto && (
53 |
58 | )}
59 |
65 | {showUsername && (
66 |
73 | )}
74 |
*:nth-child(n+2).bubble-tail-l]:bubble-tail-none [&>*:nth-child(n+2).bubble-tail-r]:bubble-tail-none",
79 | className,
80 | )}
81 | {...props}
82 | >
83 | {messages.map((message, index) => (
84 |
85 | ))}
86 | {children}
87 |
88 |
89 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/message/app-message/announcement-message.tsx:
--------------------------------------------------------------------------------
1 | import { MegaphoneSolid } from "@/components/central-icon.tsx";
2 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
3 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
4 | import {
5 | FormatTextMessageContent,
6 | textMessageVariants,
7 | } from "@/components/message/text-message.tsx";
8 | import type { AppMessageType } from "@/lib/schema.ts";
9 | import { cn } from "@/lib/utils.ts";
10 |
11 | export interface AnnouncementMessageEntity {
12 | type: AppMessageType.ANNOUNCEMENT;
13 | url: string;
14 | announcement: string; // xml
15 | textannouncement: string;
16 | xmlpuretext: number;
17 | }
18 |
19 | type AnnouncementMessageProps = AppMessageProps;
20 |
21 | export default function AnnouncementMessage({
22 | message,
23 | variant = "default",
24 | ...props
25 | }: AnnouncementMessageProps) {
26 | if (variant === "default")
27 | return (
28 |
37 |
42 |
43 | 公告
44 |
45 |
48 |
49 | );
50 |
51 | return (
52 |
53 | [公告] {message.message_entity.msg.appmsg.textannouncement})
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/message/app-message/attach-2-message.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import AttachMessage, {
3 | type AttachMessageEntity,
4 | } from "@/components/message/app-message/attach-message.tsx";
5 | import type {
6 | AppMessageType,
7 | AppMessage as AppMessageVM,
8 | } from "@/lib/schema.ts";
9 |
10 | export interface AttachMessage2Entity {
11 | type: AppMessageType.ATTACH_2;
12 | title: string;
13 | des: string;
14 | md5: string;
15 | laninfo: string;
16 | appattach: {
17 | totallen: number;
18 | fileext: string;
19 | fileuploadtoken: string;
20 | status: number;
21 | };
22 | }
23 |
24 | type AttachMessage2Props = AppMessageProps;
25 |
26 | export default function Attach2Message({
27 | message,
28 | ...props
29 | }: AttachMessage2Props) {
30 | return (
31 | }
33 | {...props}
34 | />
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/message/app-message/channel-message.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
3 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
4 | import {
5 | Card,
6 | CardContent,
7 | CardFooter,
8 | CardTitle,
9 | } from "@/components/ui/card.tsx";
10 | import type { AppMessageType } from "@/lib/schema.ts";
11 |
12 | export interface ChannelMessageEntity {
13 | type: AppMessageType.CHANNEL;
14 | title: string;
15 | url: string;
16 | findernamecard: {
17 | username: string;
18 | avatar: string;
19 | nickname: string;
20 | auth_job: string;
21 | auth_icon: number;
22 | auth_icon_url: string;
23 | ecSource: string;
24 | content_type: number;
25 | lastGMsgID: string;
26 | };
27 | }
28 |
29 | type ChannelMessageProps = AppMessageProps;
30 |
31 | export default function ChannelMessage({
32 | message,
33 | variant = "default",
34 | ...props
35 | }: ChannelMessageProps) {
36 | if (variant === "default")
37 | return (
38 |
39 |
40 |
44 |
45 | {message.message_entity.msg.appmsg.findernamecard.nickname}
46 |
47 |
48 | 频道名片
49 |
50 | );
51 |
52 | return (
53 |
54 | [视频号名片] {message.message_entity.msg.appmsg.findernamecard.nickname}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/message/app-message/forward-message-2.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
3 | import type { AppMessageType } from "@/lib/schema.ts";
4 | import { cn, decodeUnicodeReferences } from "@/lib/utils.ts";
5 |
6 | export interface ForwardMessage2Entity {
7 | type: AppMessageType.FORWARD_MESSAGE_2;
8 | title: string;
9 | des: string;
10 | thumburl: "";
11 | xmlfulllen: 120685;
12 | realinnertype: 19;
13 | }
14 |
15 | type ForwardMessage2Props = AppMessageProps;
16 |
17 | export default function ForwardMessage2({
18 | message,
19 | variant = "default",
20 | ...props
21 | }: ForwardMessage2Props) {
22 | if (variant === "default")
23 | return (
24 |
35 |
36 | {decodeUnicodeReferences(message.message_entity.msg.appmsg.title)}
37 |
38 |
47 | {message.message_entity.msg.appmsg.des}
48 |
49 |
50 | );
51 |
52 | return (
53 |
54 | {decodeUnicodeReferences(message.message_entity.msg.appmsg.title)}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/message/app-message/game-message.tsx:
--------------------------------------------------------------------------------
1 | import LocalImage from "@/components/local-image.tsx";
2 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
3 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
4 | import { useApp } from "@/lib/hooks/appProvider.tsx";
5 | import type { AppMessageType } from "@/lib/schema.ts";
6 | import { cn } from "@/lib/utils.ts";
7 |
8 | export interface GameMessageEntity {
9 | type: AppMessageType.GAME;
10 | title: string;
11 | des: string;
12 | appattach: {
13 | attachid: string;
14 | cdnthumburl: string;
15 | cdnthumbmd5: string;
16 | cdnthumblength: number;
17 | cdnthumbheight: number;
18 | cdnthumbwidth: number;
19 | cdnthumbaeskey: string;
20 | aeskey: string;
21 | encryver: number;
22 | fileext: string;
23 | islargefilemsg: number;
24 | };
25 | gameshare: {
26 | liteappext: {
27 | liteappbizdata: string;
28 | priority: 1;
29 | };
30 | appbrandext: {
31 | litegameinfo: string;
32 | priority: -1;
33 | };
34 | gameshareid: string;
35 | sharedata: string;
36 | isvideo: 0;
37 | duration: 0;
38 | isexposed: 0;
39 | readtext: string;
40 | };
41 | liteapp: {
42 | id: string;
43 | path: string;
44 | query: string;
45 | };
46 | }
47 |
48 | type GameMessageProps = AppMessageProps;
49 |
50 | export default function GameMessage({
51 | message,
52 | variant = "default",
53 | ...props
54 | }: GameMessageProps) {
55 | const chat = message.chat;
56 | if (variant === "default")
57 | return (
58 |
64 |
65 |
66 | {message.message_entity.msg.appmsg.title}
67 |
68 |
69 | {message.message_entity.msg.appmsg.appattach.cdnthumbmd5 && (
70 |
77 | )}
78 |
79 |
80 |
81 | );
82 |
83 | return (
84 |
85 | [游戏] {message.message_entity.msg.appmsg.title}
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/message/app-message/link-message-2.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import type {
3 | AppMessageType,
4 | AppMessage as AppMessageVM,
5 | } from "@/lib/schema.ts";
6 | import UrlMessage, { type UrlMessageEntity } from "./url-message";
7 |
8 | export interface LinkMessage2Entity {
9 | type: AppMessageType.LINK_2;
10 | title: string;
11 | des: string;
12 | url: string;
13 | appattach: {
14 | attachid: string;
15 | cdnthumburl: string;
16 | cdnthumbmd5: string;
17 | cdnthumblength: number;
18 | cdnthumbheight: number;
19 | cdnthumbwidth: number;
20 | cdnthumbaeskey: string;
21 | aeskey: string;
22 | encryver: number;
23 | fileext: string;
24 | islargefilemsg: number;
25 | };
26 | sourceusername: string;
27 | sourcedisplayname: string;
28 | md5: string;
29 | weappinfo: {
30 | pagepath: string;
31 | username: string;
32 | appid: string;
33 | version: number;
34 | type: number;
35 | weappiconurl: string;
36 | shareId: string;
37 | appservicetype: number;
38 | secflagforsinglepagemode: number;
39 | videopageinfo: {
40 | thumbwidth: number;
41 | thumbheight: number;
42 | fromopensdk: number;
43 | };
44 | };
45 | }
46 |
47 | type LinkMessage2Props = AppMessageProps;
48 |
49 | export default function LinkMessage2({ message, ...props }: LinkMessage2Props) {
50 | return (
51 | }
53 | {...props}
54 | />
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/message/app-message/miniapp-message-2.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import MiniappMessage, {
3 | type MiniappMessageEntity,
4 | } from "@/components/message/app-message/miniapp-message.tsx";
5 | import type {
6 | AppMessageType,
7 | AppMessage as AppMessageVM,
8 | } from "@/lib/schema.ts";
9 |
10 | export interface MiniappMessage2Entity {
11 | type: AppMessageType.MINIAPP_2;
12 | title: string;
13 | des: string;
14 | url: string;
15 | appattach: {
16 | attachid: string;
17 | cdnthumburl: string;
18 | cdnthumbmd5: string;
19 | cdnthumblength: number;
20 | cdnthumbheight: number;
21 | cdnthumbwidth: number;
22 | cdnthumbaeskey: string;
23 | aeskey: string;
24 | encryver: number;
25 | fileext: string;
26 | islargefilemsg: number;
27 | };
28 | sourceusername: string;
29 | sourcedisplayname: string;
30 | md5: string;
31 | weappinfo: {
32 | pagepath: string;
33 | username: string;
34 | appid: string;
35 | version: number;
36 | type: number;
37 | weappiconurl: string;
38 | shareId: string;
39 | appservicetype: number;
40 | secflagforsinglepagemode: number;
41 | videopageinfo: {
42 | thumbwidth: number;
43 | thumbheight: number;
44 | fromopensdk: number;
45 | };
46 | };
47 | }
48 |
49 | type MiniappMessage2Props = AppMessageProps;
50 |
51 | export default function MiniappMessage2({
52 | message,
53 | ...props
54 | }: MiniappMessage2Props) {
55 | return (
56 | }
58 | {...props}
59 | />
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/message/app-message/realtime-location-message.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
3 | import type { AppMessageType } from "@/lib/schema.ts";
4 |
5 | export interface RealtimeLocationMessageEntity {
6 | type: AppMessageType.REALTIME_LOCATION;
7 | title: string; // eg. "我发起了位置共享"
8 | }
9 |
10 | type RealtimeLocationMessageProps =
11 | AppMessageProps;
12 |
13 | export default function RealtimeLocationMessage({
14 | message,
15 | variant = "default",
16 | ...props
17 | }: RealtimeLocationMessageProps) {
18 | if (variant === "default")
19 | return (
20 |
24 |
25 |
26 |
27 | {message.from.remark ?? message.from.username}发起了位置共享
28 |
29 |
30 |
31 | );
32 |
33 | return (
34 |
35 | [位置共享] {message.from.remark ?? message.from.username}发起了位置共享)
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/message/app-message/refer-message.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
3 | import Message from "@/components/message/message.tsx";
4 | import {
5 | FormatTextMessageContent,
6 | textMessageVariants,
7 | } from "@/components/message/text-message.tsx";
8 | import User from "@/components/user.tsx";
9 | import type { AppMessageType } from "@/lib/schema.ts";
10 | import { cn } from "@/lib/utils";
11 |
12 | export interface ReferMessageEntity {
13 | type: AppMessageType.REFER;
14 | title: string;
15 | refermsg: {
16 | type: number;
17 | svrid: string;
18 | fromusr: string;
19 | chatusr: string;
20 | displayname: string;
21 | msgsource: string; // xml
22 | content: string;
23 | };
24 | }
25 |
26 | type ReferMessageProps = AppMessageProps;
27 |
28 | export default function ReferMessage({
29 | message,
30 | variant = "default",
31 | ...props
32 | }: ReferMessageProps) {
33 | if (variant === "default")
34 | return (
35 |
44 |
47 |
48 |
57 | {message.reply_to_message ? (
58 |
63 | ) : (
64 | // TODO 当引用了一个不存在的消息(比如加入群之前的消息),content 是一个 xml
65 |
68 | )}
69 |
70 |
71 | );
72 |
73 | if (variant === "referenced")
74 | return (
75 |
76 |
77 | :
78 |
79 | {" "}
83 |
84 |
85 | );
86 |
87 | return (
88 |
89 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/message/app-message/ringtone-message.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import type { AppMessageType } from "@/lib/schema.ts";
3 |
4 | export interface RingtoneMessageEntity {
5 | type: AppMessageType.RINGTONE;
6 | title: string;
7 | des: string;
8 | }
9 |
10 | type RingtoneMessageProps = AppMessageProps;
11 |
12 | export default function RingtoneMessage({
13 | message,
14 | variant = "default",
15 | ...props
16 | }: RingtoneMessageProps) {
17 | if (variant === "default")
18 | return (
19 |
23 |
24 | 朋友使用的铃声 {message.message_entity.msg.appmsg.title}
25 |
26 |
27 | );
28 | return (
29 |
30 |
朋友使用的铃声 {message.message_entity.msg.appmsg.title})
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/message/app-message/scan-result-message.tsx:
--------------------------------------------------------------------------------
1 | import { LinkCard } from "@/components/link-card.tsx";
2 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
3 | import type { AppMessageType } from "@/lib/schema.ts";
4 |
5 | export interface ScanResultMessageEntity {
6 | type: AppMessageType.SCAN_RESULT;
7 | title: string;
8 | scanhistory: {
9 | url: string;
10 | time: string; // e.g. "2024-08-04 18:49"
11 | scene: number; // e.g. 1
12 | type: string | "QR_CODE" | "CODE_128";
13 | version: number;
14 | isfromalbum: number;
15 | network: number;
16 | isfromcombinetab: number;
17 | };
18 | }
19 |
20 | type ScanResultMessageProps = AppMessageProps;
21 |
22 | export default function ScanResultMessage({
23 | message,
24 | variant = "default",
25 | ...props
26 | }: ScanResultMessageProps) {
27 | if (variant === "default")
28 | return (
29 |
39 | );
40 | return (
41 |
42 |
扫码结果通知 {message.message_entity.msg.appmsg.title})
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/message/app-message/solitaire-message.tsx:
--------------------------------------------------------------------------------
1 | import { TripleCircleIcon } from "@/components/icon.tsx";
2 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
3 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
4 | import {
5 | FormatTextMessageContent,
6 | textMessageVariants,
7 | } from "@/components/message/text-message.tsx";
8 | import type { AppMessageType } from "@/lib/schema.ts";
9 | import { cn } from "@/lib/utils.ts";
10 |
11 | export interface SolitaireMessageEntity {
12 | type: AppMessageType.SOLITAIRE;
13 | title: string;
14 | des: string;
15 | extinfo: {
16 | solitaire_info: string; // xml
17 | };
18 | }
19 |
20 | type SolitaireProps = AppMessageProps;
21 |
22 | export default function SolitaireMessage({
23 | message,
24 | variant = "default",
25 | ...props
26 | }: SolitaireProps) {
27 | if (variant === "default")
28 | return (
29 |
38 |
43 |
44 | 接龙
45 |
46 |
47 |
50 |
51 | );
52 |
53 | return (
54 |
55 | {message.message_entity.msg.appmsg.title}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/message/app-message/sticker-message.tsx:
--------------------------------------------------------------------------------
1 | import LocalImage from "@/components/local-image.tsx";
2 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
3 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
4 | import { useApp } from "@/lib/hooks/appProvider.tsx";
5 | import type { AppMessageType } from "@/lib/schema.ts";
6 |
7 | export interface StickerMessageEntity {
8 | type: AppMessageType.STICKER;
9 | title: string;
10 | appattach: {
11 | totallen: number;
12 | attachid: string;
13 | cdnattachurl: string;
14 | emoticonmd5: string;
15 | aeskey: string;
16 | fileext: string;
17 | islargefilemsg: number;
18 | cdnthumburl: string;
19 | cdnthumbaeskey: string;
20 | cdnthumblength: number;
21 | cdnthumbwidth: number;
22 | cdnthumbheight: number;
23 | cdnthumbmd5: string;
24 | };
25 | }
26 |
27 | type StickerMessageProps = AppMessageProps;
28 |
29 | export default function StickerMessage({
30 | message,
31 | variant = "default",
32 | ...props
33 | }: StickerMessageProps) {
34 | const chat = message.chat;
35 | if (variant === "default")
36 | return (
37 |
38 |
45 |
46 | );
47 |
48 | return (
49 |
50 | [表情]
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/message/app-message/sticker-set-message.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import { LinkCard } from "@/components/link-card.tsx";
3 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
4 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
5 | import type { AppMessageType } from "@/lib/schema.ts";
6 | import { decodeUnicodeReferences } from "@/lib/utils.ts";
7 |
8 | export interface StickerSetMessageEntity {
9 | type: AppMessageType.STICKER_SET;
10 | title: string;
11 | des: string;
12 | url: string;
13 | thumburl: string;
14 | emoticonshared: {
15 | packageflag: 0;
16 | packageid: string;
17 | };
18 | }
19 |
20 | type StickerSetMessageProps = AppMessageProps;
21 |
22 | export default function StickerSetMessage({
23 | message,
24 | variant = "default",
25 | ...props
26 | }: StickerSetMessageProps) {
27 | const heading = decodeUnicodeReferences(
28 | message.message_entity.msg.appmsg.title,
29 | );
30 | const preview = message.message_entity.msg.appmsg.thumburl ? (
31 |
32 | ) : undefined;
33 |
34 | if (variant === "default")
35 | return (
36 | >}
43 | {...props}
44 | />
45 | );
46 |
47 | return (
48 |
49 | [表情包] {heading}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/message/app-message/store-message.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
3 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
4 | import type { AppMessageType } from "@/lib/schema.ts";
5 | import { cn } from "@/lib/utils";
6 |
7 | export interface StoreMessageEntity {
8 | type: AppMessageType.STORE;
9 | title: string;
10 | url: string;
11 | appattach: {
12 | cdnthumbaeskey: "";
13 | aeskey: "";
14 | };
15 | finderShopWindowShare: {
16 | finderUsername: string;
17 | query: string;
18 | liteAppId: string;
19 | liteAppPath: string;
20 | liteAppQuery: string;
21 | avatar: string;
22 | nickname: string;
23 | reputationInfo: string;
24 | saleWording: string;
25 | productImageURLList: {
26 | productImageURL: string;
27 | };
28 | profileTypeWording: string;
29 | isWxShop: 1;
30 | platformIconUrl: string;
31 | platformIconUrlDarkmode: string;
32 | };
33 | "@_appid": "";
34 | "@_sdkver": "0";
35 | }
36 |
37 | type StoreMessageProps = AppMessageProps;
38 |
39 | export default function StoreMessage({
40 | message,
41 | variant = "default",
42 | ...props
43 | }: StoreMessageProps) {
44 | if (variant === "default")
45 | return (
46 |
52 |
53 |
57 |
58 | {message.message_entity.msg.appmsg.finderShopWindowShare.nickname}
59 |
60 |
61 |
62 |
63 | 微信小店
64 |
65 |
71 |
72 |
73 |
74 | );
75 |
76 | return (
77 |
78 | [微信小店]{" "}
79 | {message.message_entity.msg.appmsg.finderShopWindowShare.nickname}
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/message/app-message/text-message.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
3 | import { textMessageVariants } from "@/components/message/text-message.tsx";
4 | import type { AppMessageType } from "@/lib/schema.ts";
5 | import { cn } from "@/lib/utils.ts";
6 |
7 | export interface AppTextMessageEntity {
8 | type: AppMessageType.TEXT;
9 | title: string;
10 | des: string; // eg. a link
11 | }
12 |
13 | type TextMessageProps = AppMessageProps;
14 |
15 | export default function TextMessage({
16 | message,
17 | variant = "default",
18 | ...props
19 | }: TextMessageProps) {
20 | if (variant === "default") {
21 | return (
22 |
31 | {message.message_entity.msg.appmsg.title}
32 |
33 | );
34 | }
35 |
36 | return (
37 |
38 | {message.message_entity.msg.appmsg.title}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/message/app-message/video-message.tsx:
--------------------------------------------------------------------------------
1 | import { LinkCard } from "@/components/link-card.tsx";
2 | import LocalImage from "@/components/local-image.tsx";
3 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
4 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
5 | import type { AppMessageType } from "@/lib/schema.ts";
6 | import { decodeUnicodeReferences } from "@/lib/utils.ts";
7 |
8 | export interface VideoMessageEntity {
9 | type: AppMessageType.VIDEO;
10 | title: string;
11 | des: string;
12 | url: string;
13 | lowurl: string;
14 | appattach: {
15 | totallen: number;
16 | attachid: string;
17 | emoticonmd5: string;
18 | fileext: string;
19 | cdnthumburl: string;
20 | cdnthumbmd5: string;
21 | cdnthumblength: number;
22 | cdnthumbwidth: number;
23 | cdnthumbheight: number;
24 | cdnthumbaeskey: string;
25 | aeskey: string;
26 | encryver: 0 | 1;
27 | filekey: string;
28 | };
29 | }
30 |
31 | type VideoMessageProps = AppMessageProps;
32 |
33 | export default function VideoMessage({
34 | message,
35 | variant = "default",
36 | ...props
37 | }: VideoMessageProps) {
38 | const chat = message.chat;
39 | const heading = decodeUnicodeReferences(
40 | message.message_entity.msg.appmsg.title,
41 | );
42 | const preview = message.message_entity.msg.appmsg.appattach.cdnthumbmd5 ? (
43 |
49 | ) : undefined;
50 |
51 | if (variant === "default")
52 | return (
53 |
64 | );
65 |
66 | return (
67 |
68 | [链接] {heading}
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/message/app-message/voice-message.tsx:
--------------------------------------------------------------------------------
1 | import type { AppMessageProps } from "@/components/message/app-message.tsx";
2 | import UrlMessage, {
3 | type UrlMessageEntity,
4 | } from "@/components/message/app-message/url-message.tsx";
5 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
6 | import type {
7 | AppMessageType,
8 | AppMessage as AppMessageVM,
9 | } from "@/lib/schema.ts";
10 | import { decodeUnicodeReferences } from "@/lib/utils.ts";
11 |
12 | export interface VoiceMessageEntity {
13 | type: AppMessageType.VOICE;
14 | title: string;
15 | des: string;
16 | username: string;
17 | action: string;
18 | url: string;
19 | lowurl: string;
20 | dataurl: string;
21 | lowdataurl: string;
22 | statextstr: string;
23 | songalbumurl: string;
24 | songlyric: string;
25 | musicShareItem: {
26 | mvCoverUrl: string;
27 | musicDuration: number;
28 | mid: string;
29 | };
30 | }
31 |
32 | type VoiceMessageProps = AppMessageProps;
33 |
34 | export default function VoiceMessage({
35 | message,
36 | variant = "default",
37 | ...props
38 | }: VoiceMessageProps) {
39 | if (variant === "default") {
40 | return (
41 | }
43 | variant={variant}
44 | {...props}
45 | />
46 | );
47 | }
48 | return (
49 |
50 | [音频] {decodeUnicodeReferences(message.message_entity.msg.appmsg.title)}
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/message/chatroom-voip-message.tsx:
--------------------------------------------------------------------------------
1 | import { CallIncoming, CallOutgoing } from "@/components/icon.tsx";
2 | import type { MessageProp } from "@/components/message/message.tsx";
3 | import {
4 | type ChatroomVoipMessage as ChatroomVoipMessageVM,
5 | MessageDirection,
6 | } from "@/lib/schema.ts";
7 |
8 | type ChatroomVoipMessageProps = MessageProp;
9 |
10 | export interface ChatroomVoipMessageEntity {
11 | msgLocalID: number;
12 | clientGroupID: string;
13 | groupID: string;
14 | lastMsgID: number;
15 | msgContent: string; // eg. "XXX has started a voice call"
16 | }
17 | export default function ChatroomVoipMessage({
18 | message,
19 | variant = "default",
20 | ...props
21 | }: ChatroomVoipMessageProps) {
22 | if (variant === "default") {
23 | if (message.from) {
24 | return (
25 |
29 | {message.direction === MessageDirection.outgoing ? (
30 |
31 | ) : (
32 |
33 | )}
34 |
35 |
{message.message_entity.msgContent}
36 |
37 |
38 | );
39 | }
40 |
41 | return (
42 |
46 |
47 | {message.message_entity.msgContent}
48 |
49 |
50 | );
51 | }
52 |
53 | return (
54 |
55 |
{message.message_entity.msgContent}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/message/location-message.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageProp } from "@/components/message/message.tsx";
2 | import type { LocationMessage as LocationMessageVM } from "@/lib/schema.ts";
3 | import { cn } from "@/lib/utils.ts";
4 | import { cva } from "class-variance-authority";
5 | import { LocationIcon } from "../icon";
6 | import MessageInlineWrapper from "./message-inline";
7 |
8 | type LocationMessageProps = MessageProp;
9 |
10 | export interface LocationMessageEntity {
11 | msg: {
12 | location: {
13 | "@_x": string; // 纬度
14 | "@_y": string; // 经度
15 | "@_scale": string; // ?缩放级别
16 | "@_label": string;
17 | "@_maptype": string;
18 | "@_poiname": string;
19 | "@_poiid": string;
20 | "@_buildingId": string;
21 | "@_floorName": string;
22 | "@_poiCategoryTips": string;
23 | "@_poiBusinessHour": string;
24 | "@_poiPhone": string;
25 | "@_poiPriceTips": string;
26 | "@_isFromPoiList": "true" | "false";
27 | "@_adcode": string;
28 | "@_cityname": string;
29 | };
30 | };
31 | }
32 |
33 | export const locationMessageVariants = cva(
34 | "max-w-[20em] py-3 ps-3 pe-[1.375rem] flex items-center gap-3 bg-white rounded-2xl [&_svg]:shrink-0 text-pretty",
35 | );
36 |
37 | export default function LocationMessage({
38 | message,
39 | variant = "default",
40 | ...props
41 | }: LocationMessageProps) {
42 | if (variant === "default")
43 | return (
44 |
45 |
46 |
47 |
48 | {message.message_entity.msg.location["@_poiname"]}
49 |
50 |
51 | {message.message_entity.msg.location["@_label"]}
52 |
53 |
54 |
55 | );
56 |
57 | return (
58 |
59 | [位置] {message.message_entity.msg.location["@_poiname"]}
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/message/mail-message.tsx:
--------------------------------------------------------------------------------
1 | import { LinkCard } from "@/components/link-card.tsx";
2 | import type { MessageProp } from "@/components/message/message.tsx";
3 | import type { MailMessage as MailMessageVM } from "@/lib/schema.ts";
4 | import MessageInlineWrapper from "./message-inline";
5 |
6 | type MailMessageProps = MessageProp;
7 |
8 | export interface MailMessageEntity {
9 | msg: {
10 | pushmail: {
11 | content: {
12 | subject: string; // 邮件主题
13 | attach: boolean;
14 | sender: string; // 发件人
15 | digest: string; // e.g. "点击查看全文"
16 | date: string; // e.g. "2021-03-10 17:17:43"
17 | fromlist: {
18 | item: {
19 | name: string; // 发件人名字
20 | addr: string; // 发件人邮箱
21 | };
22 | "@_count": string; // e.g. "1"
23 | };
24 | tolist: {
25 | item: {
26 | name: string; // 收件人名字
27 | addr: string; // 收件人邮箱
28 | };
29 | "@_count": string; // e.g. "1"
30 | };
31 | cclist: {
32 | "@_count": string; // e.g. "0"
33 | };
34 | };
35 | mailid: string;
36 | waplink: string; // 打开邮箱的链接
37 | };
38 | };
39 | }
40 |
41 | export default function MailMessage({
42 | message,
43 | variant = "default",
44 | ...props
45 | }: MailMessageProps) {
46 | if (variant === "default")
47 | return (
48 |
54 | );
55 |
56 | return (
57 |
58 | [邮件] {message.message_entity.msg.pushmail.content.subject}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/message/message-inline.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageVM } from "@/lib/schema.ts";
2 | import User from "../user";
3 |
4 | interface MessageInlineProps
5 | extends React.HTMLAttributes {
6 | message: MessageVM;
7 |
8 | showUsername?: boolean;
9 | showPhoto?: boolean;
10 | }
11 |
12 | export default function MessageInlineWrapper({
13 | message,
14 | showUsername = true,
15 | showPhoto = true,
16 |
17 | children,
18 | className,
19 | ...props
20 | }: MessageInlineProps) {
21 | return (
22 |
23 | {showUsername && (
24 |
25 | )}
26 | {showUsername && ": "}
27 | {children}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/message/micro-video-message.tsx:
--------------------------------------------------------------------------------
1 | import LocalVideo from "@/components/local-video.tsx";
2 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
3 | import type { MessageProp } from "@/components/message/message.tsx";
4 | import { useApp } from "@/lib/hooks/appProvider.tsx";
5 | import type { MicroVideoMessage as MicroVideoMessageVM } from "@/lib/schema.ts";
6 |
7 | type MicroVideoMessageProps = MessageProp;
8 |
9 | export interface MicroVideoMessageEntity {
10 | msg: {
11 | videomsg: {
12 | "@_clientmsgid": string;
13 | "@_playlength": string;
14 | "@_length": string;
15 | "@_type": string;
16 | "@_status": string;
17 | "@_fromusername": string;
18 | "@_aeskey": string;
19 | "@_cdnvideourl": string;
20 | "@_cdnthumburl": string;
21 | "@_cdnthumblength": string;
22 | "@_cdnthumbwidth": string;
23 | "@_cdnthumbheight": string;
24 | "@_cdnthumbaeskey": string;
25 | "@_encryver": string;
26 | "@_isplaceholder": string;
27 | "@_rawlength": string;
28 | "@_cdnrawvideourl": string;
29 | "@_cdnrawvideoaeskey": string;
30 | };
31 | };
32 | }
33 |
34 | export default function MicroVideoMessage({
35 | message,
36 | variant = "default",
37 | ...props
38 | }: MicroVideoMessageProps) {
39 | const chat = message.chat;
40 | if (variant === "default")
41 | return (
42 |
43 |
44 |
45 | );
46 |
47 | return (
48 |
49 | [视频]
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/message/sticker-message.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
3 | import type { MessageProp } from "@/components/message/message.tsx";
4 | import type { StickerMessage as StickerMessageVM } from "@/lib/schema.ts";
5 |
6 | type StickerMessageProps = MessageProp;
7 |
8 | export interface StickerMessageEntity {
9 | msg: {
10 | emoji: {
11 | "@_fromusername": string;
12 | "@_tousername": string;
13 | "@_type": string;
14 | "@_idbuffer": string;
15 | "@_md5": string;
16 | "@_len": string;
17 | "@_productid": string;
18 | "@_androidmd5": string;
19 | "@_androidlen": string;
20 |
21 | "@_s60v3md5": string;
22 | "@_s60v3len": string;
23 | "@_s60v5md5": string;
24 | "@_s60v5len": string;
25 |
26 | "@_cdnurl": string;
27 | "@_designerid": string;
28 | "@_thumburl": string;
29 | "@_encrypturl": string;
30 | "@_aeskey": string;
31 |
32 | "@_externurl": string;
33 | "@_externmd5": string;
34 |
35 | "@_width": string;
36 | "@_height": string;
37 |
38 | "@_tpurl": string;
39 | "@_tpauthkey": string;
40 | "@_attachedtext": string;
41 | "@_attachedtextcolor": string;
42 | "@_lensid": string;
43 | "@_emojiattr": string;
44 | "@_linkid": string;
45 | "@_desc": string;
46 | };
47 |
48 | gameext: {
49 | "@_type": string;
50 | "@_content": string;
51 | };
52 | };
53 | }
54 |
55 | export default function StickerMessage({
56 | message,
57 | variant = "default",
58 | ...props
59 | }: StickerMessageProps) {
60 | if (variant === "default")
61 | return (
62 |
63 |
89 |
90 | );
91 |
92 | return (
93 |
94 | [表情]
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/message/system-message.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageProp } from "@/components/message/message.tsx";
2 | import type { SystemMessage as SystemMessageVM } from "@/lib/schema.ts";
3 |
4 | type SystemMessageProp = MessageProp;
5 |
6 | export type SystemMessageEntity = string;
7 |
8 | export default function SystemMessage({
9 | message,
10 | variant = "default",
11 | ...props
12 | }: SystemMessageProp) {
13 | const content = message.message_entity
14 | .split(/<[^>]+?>/)
15 | .map((s) => s)
16 | .join("");
17 |
18 | if (variant === "default")
19 | return (
20 |
26 | );
27 |
28 | return {content}
;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/message/verify-message.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageProp } from "@/components/message/message.tsx";
2 | import User from "@/components/user.tsx";
3 | import type { VerityMessage as VerityMessageVM } from "@/lib/schema.ts";
4 |
5 | type VerifyMessageProps = MessageProp;
6 |
7 | export interface VerityMessageEntity {
8 | msg: {
9 | "@_fromusername": "wxid";
10 | "@_fromnickname": "nickname";
11 | "@_fullpy": string;
12 | "@_shortpy": string;
13 | "@_encryptusername": string;
14 | "@_content": string; // eg. 我是...
15 | "@_sign": string; // bio
16 | "@_imagestatus": "3";
17 | "@_scene": "17";
18 | "@_country": string;
19 | "@_province": string;
20 | "@_city": string;
21 | "@_percard": "1";
22 | "@_sex": "1";
23 | "@_alias": string; // user_id;
24 | "@_weibo": "";
25 | "@_albumflag": "0";
26 | "@_albumstyle": "0";
27 | "@_albumbgimgid": string;
28 | "@_snsflag": "305";
29 | "@_snsbgimgid": "http://.../0";
30 | "@_snsbgobjectid": string;
31 | "@_mhash": string;
32 | "@_mfullhash": string;
33 | "@_bigheadimgurl": "http://.../0";
34 | "@_smallheadimgurl": "http://.../132";
35 | "@_ticket": "v4_000b708f0b04000001000000000017fe2c7893ec3a09430a7e876f641000000050ded0b020927e3c97896a09d47e6e9e70a4918dd744da06402bf2dd5f023e06ea8c8594c0bb9345926e254136bcffeab273b40c3a40eceba5a9a1df9cfb2eb9228d016049e7df9122af3597815aad5c9d170ef956240a80e120603b92812478baae87677d1eabbdf6684c003abd3ef654d4cb65051775f0e4e4d8e89fb42723041c1bd5ab43070bbfbe106239acdc48ee9b4414cb200962aeca2271ac749c57169fef259d28998e25a278e44b19c4df0ec72b9fd133306ee3bc3f30033993f5f381aa0192f335c12c5596802c08c5e35d13bd5750d025de9eab78497ced9e46de8ab4656b25eb88d846b82ed0a52e8debe5737b9f57e9164d96b9e5bbe22118715351870e8f668d343e53fe1d7da1a96d5f98d97f4808f298a9d772a9ea76ada8eee4b19d879148f1@stranger";
36 | "@_opcode": "2";
37 | "@_googlecontact": "";
38 | "@_qrticket": "";
39 | "@_chatroomusername": "";
40 | "@_sourceusername": "wxid";
41 | "@_sourcenickname": "nickname";
42 | "@_sharecardusername": "wxid";
43 | "@_sharecardnickname": "nickname";
44 | "@_cardversion": "0";
45 | "@_extflag": "0";
46 | };
47 | }
48 |
49 | export default function VerityMessage({
50 | message,
51 | variant = "default",
52 | ...props
53 | }: VerifyMessageProps) {
54 | return (
55 |
56 |
57 |
61 | {message.message_entity.msg["@_fromnickname"]}:{" "}
62 | {message.message_entity.msg["@_content"]}
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/message/voice-message.tsx:
--------------------------------------------------------------------------------
1 | import LocalVoice from "@/components/local-voice.tsx";
2 | import MessageInlineWrapper from "@/components/message/message-inline.tsx";
3 | import type { MessageProp } from "@/components/message/message.tsx";
4 | import { useApp } from "@/lib/hooks/appProvider.tsx";
5 | import type { VoiceMessage as VoiceMessageVM } from "@/lib/schema.ts";
6 | import { cn } from "@/lib/utils.ts";
7 |
8 | type VoiceMessageProps = MessageProp;
9 |
10 | export interface VoiceMessageEntity {
11 | msg: {
12 | voicemsg: {
13 | "@_endflag": "0" | "1";
14 | "@_cancelflag": "0" | "1";
15 | "@_forwardflag": "0" | "1";
16 | "@_voiceformat": string;
17 | "@_voicelength": string;
18 | "@_length": string;
19 | "@_bufid": string;
20 | "@_aeskey": string;
21 | "@_voiceurl": string;
22 | "@_voicemd5": string;
23 | "@_clientmsgid": string;
24 | "@_fromusername": string;
25 | };
26 | };
27 | }
28 |
29 | export default function VoiceMessage({
30 | message,
31 | variant = "default",
32 | ...props
33 | }: VoiceMessageProps) {
34 | const chat = message.chat;
35 | if (variant === "default")
36 | return (
37 |
38 |
39 |
40 | );
41 |
42 | return (
43 |
44 | [语音]{" "}
45 | {Math.floor(
46 | Number.parseInt(message.message_entity.msg.voicemsg["@_voicelength"]) /
47 | 1000,
48 | )}
49 | ″
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/message/voip-message.tsx:
--------------------------------------------------------------------------------
1 | import { CallIncoming, CallOutgoing } from "@/components/icon.tsx";
2 | import type { MessageProp } from "@/components/message/message.tsx";
3 | import {
4 | MessageDirection,
5 | type VoipMessage as VoipMessageVM,
6 | } from "@/lib/schema.ts";
7 |
8 | type VoipMessageProps = MessageProp;
9 | export interface VoipMessageEntity {
10 | voipmsg?: {
11 | "@_type": "VoIPBubbleMsg" | string; // eg. VoIPBubbleMsg
12 | VoIPBubbleMsg: {
13 | msg: string; // eg. 通话时长 00:31
14 | room_type: string; // unknown eg. 1
15 | red_dot: "true" | "false";
16 | roomid: string;
17 | roomkey: string;
18 | inviteid: string;
19 | msg_type: string; // eg. 0
20 | timestamp: string;
21 | identity: string;
22 | duration: string;
23 | inviteid64: string;
24 | business: string;
25 | };
26 | };
27 | voipinvitemsg?: {
28 | roomid: 88320233;
29 | key: "7240177537323914205";
30 | status: 2;
31 | invitetype: 1;
32 | };
33 | voipextinfo?: {
34 | recvtime: 1685271014;
35 | };
36 | }
37 |
38 | export default function VoipMessage({
39 | message,
40 | variant = "default",
41 | ...props
42 | }: VoipMessageProps) {
43 | if (variant === "default")
44 | return (
45 | <>
46 | {message.message_entity.voipmsg && (
47 |
51 | {message.direction === MessageDirection.outgoing ? (
52 |
53 | ) : (
54 |
55 | )}
56 |
57 |
58 | {message.from.remark ?? message.from.username}发起了语音通话
59 |
60 |
61 | {message.message_entity.voipmsg["@_type"] === "VoIPBubbleMsg" &&
62 | message.message_entity.voipmsg[
63 | message.message_entity.voipmsg["@_type"]
64 | ].msg}
65 |
66 |
67 |
68 | )}
69 |
70 | {message.message_entity.voipinvitemsg && (
71 |
74 | )}
75 | >
76 | );
77 |
78 | return (
79 |
80 | {message.message_entity.voipmsg && (
81 |
82 | [语音通话]{" "}
83 | {message.message_entity.voipmsg["@_type"] === "VoIPBubbleMsg" &&
84 | message.message_entity.voipmsg[
85 | message.message_entity.voipmsg["@_type"]
86 | ].msg}
87 |
88 | )}
89 |
90 | {message.message_entity.voipinvitemsg && 通话邀请}
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/message/wecom-contact-message.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import type { MessageProp } from "@/components/message/message.tsx";
3 | import type { WeComContactMessage as WeComContactMessageVM } from "@/lib/schema.ts";
4 | import type * as React from "react";
5 | import MessageInlineWrapper from "./message-inline";
6 |
7 | type WeComContactMessageProps = MessageProp;
8 |
9 | export interface WeComContactMessageEntity {
10 | msg: {
11 | "@_username": string;
12 | "@_nickname": string;
13 | "@_sex": string;
14 | "@_smallheadimgurl": string;
15 | "@_bigheadimgurl": string;
16 | "@_openimappid": string;
17 | "@_openimdesc": string;
18 | "@_openimdescicon": string;
19 | };
20 | }
21 |
22 | export default function WeComContactMessage({
23 | message,
24 | variant = "default",
25 | ...props
26 | }: WeComContactMessageProps) {
27 | if (variant === "default")
28 | return (
29 |
30 |
31 |
36 |
37 |
38 | {message.message_entity.msg["@_nickname"]}
39 |
40 |
41 | @{message.message_entity.msg["@_openimdesc"]}
42 |
43 |
44 |
45 |
46 |
51 | 企业微信名片
52 |
53 |
54 | );
55 |
56 | return (
57 |
58 | [企业微信名片] {message.message_entity.msg["@_nickname"]}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/record/attatch-record.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
2 | import type { RecordVM } from "./record";
3 |
4 | interface AttatchRecordProps extends React.HTMLAttributes {
5 | message: MessageVM;
6 | record: AttatchRecordEntity;
7 | variant: "default" | string;
8 | }
9 |
10 | export interface AttatchRecordEntity extends RecordVM {
11 | "@_datatype": RecordType.ATTACH;
12 | datatitle: string;
13 | datasize: number;
14 | }
15 |
16 | export default function AttatchRecord({
17 | message,
18 | record,
19 | variant = "default",
20 | className,
21 | ...props
22 | }: AttatchRecordProps) {
23 | if (variant === "default")
24 | return (
25 |
26 | {record.datatitle}
27 |
28 | );
29 |
30 | return {record.datatitle}
;
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/record/channel-record.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import {
3 | Card,
4 | CardContent,
5 | CardFooter,
6 | CardTitle,
7 | } from "@/components/ui/card.tsx";
8 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
9 | import type { RecordVM } from "./record";
10 |
11 | interface ChannelRecordProps extends React.HTMLAttributes {
12 | message: MessageVM;
13 | record: ChannelRecordEntity;
14 | variant: "default" | string;
15 | }
16 |
17 | export interface ChannelRecordEntity extends RecordVM {
18 | "@_datatype": RecordType.CHANNEL;
19 | datatitle: string;
20 | datadesc: string;
21 | finderShareNameCard: {
22 | username: string;
23 | nickname: string;
24 | avatar: string;
25 | };
26 | }
27 |
28 | export default function ChannelRecord({
29 | message,
30 | record,
31 | variant = "default",
32 | ...props
33 | }: ChannelRecordProps) {
34 | if (variant === "default")
35 | return (
36 |
37 |
38 |
43 | {record.datatitle}
44 |
45 | 视频号名片
46 |
47 | );
48 |
49 | return [视频号名片] {record.datatitle}
;
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/record/contact-record.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import {
3 | Card,
4 | CardContent,
5 | CardFooter,
6 | CardTitle,
7 | } from "@/components/ui/card.tsx";
8 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
9 | import { XMLParser } from "fast-xml-parser";
10 | import type { RecordVM } from "./record";
11 |
12 | interface ContactRecordProps extends React.HTMLAttributes {
13 | message: MessageVM;
14 | record: ContactRecordEntity;
15 | variant: "default" | string;
16 | }
17 |
18 | export interface ContactRecordEntity extends RecordVM {
19 | "@_datatype": RecordType.CONTACT;
20 | datasize: number;
21 | datadesc: string; // xml
22 | }
23 |
24 | export default function ContactRecord({
25 | message,
26 | record,
27 | variant = "default",
28 | ...props
29 | }: ContactRecordProps) {
30 | const xmlParser = new XMLParser({
31 | ignoreAttributes: false,
32 | });
33 | const dataEntity = xmlParser.parse(record.datadesc);
34 | console.log(dataEntity);
35 |
36 | if (dataEntity.msg["@_certflag"] === "0") {
37 | return [名片] {dataEntity.msg["@_nickname"]}
;
38 | }
39 |
40 | if (variant === "default")
41 | return (
42 |
43 |
44 |
49 |
50 |
51 | {dataEntity.msg["@_nickname"]}
52 |
53 |
54 | {dataEntity.msg["@_certinfo"]}
55 |
56 |
57 |
58 | 公众号名片
59 |
60 | );
61 |
62 | return [公众号名片] {dataEntity.msg["@_nickname"]}
;
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/record/image-record.tsx:
--------------------------------------------------------------------------------
1 | import LocalImage from "@/components/local-image";
2 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
3 | import { cn } from "@/lib/utils.ts";
4 | import type { RecordVM } from "./record";
5 |
6 | interface ImageRecordProps extends React.HTMLAttributes {
7 | message: MessageVM;
8 | record: ImageRecordEntity;
9 | variant: "default" | string;
10 | }
11 |
12 | export interface ImageRecordEntity extends RecordVM {
13 | "@_datatype": RecordType.IMAGE;
14 |
15 | datafmt: string; // e.g. "pic"
16 |
17 | thumbsize: number;
18 | thumbfullmd5: string;
19 |
20 | datasize: number;
21 | fullmd5: string;
22 | }
23 |
24 | export default function ImageRecord({
25 | message,
26 | record,
27 | variant = "default",
28 | className,
29 | ...props
30 | }: ImageRecordProps) {
31 | const { chat } = message;
32 |
33 | if (variant === "default")
34 | return (
35 |
36 |
47 |
48 | );
49 |
50 | return (
51 |
52 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/record/link-record.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
2 | import Image from "../image";
3 | import { LinkCard } from "../link-card";
4 | import type { RecordVM } from "./record";
5 |
6 | interface LinkRecordProps extends React.HTMLAttributes {
7 | message: MessageVM;
8 | record: LinkRecordEntity;
9 | variant: "default" | string;
10 | }
11 |
12 | export interface LinkRecordEntity extends RecordVM {
13 | "@_datatype": RecordType.LINK;
14 | datatitle: string;
15 | datasize: number;
16 | link: string;
17 | weburlitem: {
18 | thumburl?: string;
19 | title: string;
20 | desc: string;
21 | link: string;
22 | appmsgshareitem?: {
23 | pubtime: number; // Unix时间戳
24 | srcdisplayname: string; // 公众号名称
25 | srcusername: string; // 公众号id
26 | cover: string;
27 | };
28 | };
29 | }
30 |
31 | export default function LinkRecord({
32 | message,
33 | record,
34 | variant = "default",
35 | className,
36 | ...props
37 | }: LinkRecordProps) {
38 | if (variant === "default")
39 | return (
40 |
47 | ) : undefined
48 | }
49 | from={record.weburlitem.appmsgshareitem?.srcdisplayname}
50 | {...props}
51 | />
52 | );
53 |
54 | return [链接] {record.datatitle}
;
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/record/location-record.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
2 | import { cn } from "@/lib/utils";
3 | import { LocationIcon } from "../icon";
4 | import { locationMessageVariants } from "../message/location-message";
5 | import type { RecordVM } from "./record";
6 |
7 | interface LocationRecordProps extends React.HTMLAttributes {
8 | message: MessageVM;
9 | record: LocationRecordEntity;
10 | variant: "default" | string;
11 | }
12 |
13 | export interface LocationRecordEntity extends RecordVM {
14 | "@_datatype": RecordType.LOCATION;
15 | locitem: {
16 | lng: number; // 经度
17 | buildingid: number; // 建筑ID?
18 | poiname: string;
19 | floorname: string; // e.g. "F2"
20 | label: string;
21 | isfrompoilist: number; // e.g. 1
22 | poiid: string; // e.g. "qqmap_00000000000000000000"
23 | lat: number; // 纬度
24 | scale: number; // 地图缩放比例
25 | };
26 | }
27 |
28 | export default function LocationRecord({
29 | message,
30 | record,
31 | variant = "default",
32 | ...props
33 | }: LocationRecordProps) {
34 | if (variant === "default")
35 | return (
36 |
37 |
38 |
39 |
{record.locitem.poiname}
40 |
41 | {record.locitem.label}
42 |
43 |
44 |
45 | );
46 |
47 | return (
48 |
49 | [位置] {record.locitem.poiname} {record.locitem.label}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/record/miniapp-record.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import LocalImage from "@/components/local-image.tsx";
3 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
4 | import { cn } from "@/lib/utils.ts";
5 |
6 | import { CardTitle } from "@/components/ui/card.tsx";
7 | import type { RecordVM } from "./record";
8 |
9 | interface MiniAppRecordProps extends React.HTMLAttributes {
10 | message: MessageVM;
11 | record: MiniAppRecordEntity;
12 | variant: "default" | string;
13 | }
14 |
15 | export interface MiniAppRecordEntity extends RecordVM {
16 | "@_datatype": RecordType.MINIAPP;
17 | datatitle: string;
18 | appbranditem: {
19 | iconurl: string;
20 | type: number;
21 | sourcedisplayname: string;
22 | username: string;
23 | pagepath: string; // 小程序路径
24 | };
25 | }
26 |
27 | export default function MiniAppRecord({
28 | message,
29 | record,
30 | variant = "default",
31 | className,
32 | ...props
33 | }: MiniAppRecordProps) {
34 | const { chat } = message;
35 |
36 | if (variant === "default")
37 | return (
38 |
42 |
43 |
48 |
53 |
{record.appbranditem.sourcedisplayname}
54 |
55 |
56 | {record.datatitle.length > 0 && (
57 |
58 | {record.datatitle}
59 |
60 | )}
61 |
62 |
63 |
72 |
73 |
89 |
90 | );
91 |
92 | return [小程序] {record.datatitle}
;
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/record/music-record.tsx:
--------------------------------------------------------------------------------
1 | import Link from "@/components/link.tsx";
2 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
3 | import type { RecordVM } from "./record";
4 |
5 | interface MusicRecordProps extends React.HTMLAttributes {
6 | message: MessageVM;
7 | record: MusicRecordEntity;
8 | variant: "default" | string;
9 | }
10 |
11 | export interface MusicRecordEntity extends RecordVM {
12 | "@_datatype": RecordType.MUSIC;
13 | datatitle: string;
14 | streamweburl: string;
15 | streamdataurl: string;
16 | }
17 |
18 | export default function MusicRecord({
19 | message,
20 | record,
21 |
22 | ...props
23 | }: MusicRecordProps) {
24 | return (
25 |
26 | [音乐] {record.datatitle}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/record/note-record.tsx:
--------------------------------------------------------------------------------
1 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
2 | import { cn, decodeUnicodeReferences } from "@/lib/utils.ts";
3 | import type { RecordVM } from "./record";
4 |
5 | interface NoteRecordProps extends React.HTMLAttributes {
6 | message: MessageVM;
7 | record: NoteRecordEntity;
8 | variant: "default" | string;
9 | }
10 |
11 | export interface NoteRecordEntity extends RecordVM {
12 | "@_datatype": RecordType.NOTE;
13 | datatitle: string;
14 | datadesc: string;
15 | recordxml: unknown;
16 | }
17 |
18 | export default function NoteRecord({
19 | message,
20 | record,
21 | variant = "default",
22 | ...props
23 | }: NoteRecordProps) {
24 | if (variant === "default")
25 | return (
26 |
32 |
33 | {decodeUnicodeReferences(record.datadesc)
34 | .split("\n")
35 | .map((segment, index) => (
36 |
{segment}
37 | ))}
38 |
39 |
40 |
45 | 笔记
46 |
47 |
48 | );
49 |
50 | return (
51 |
52 | [笔记] {record.datadesc}
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/record/text-record.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FormatTextMessageContent,
3 | textMessageVariants,
4 | } from "@/components/message/text-message.tsx";
5 | import {
6 | MessageDirection,
7 | type MessageVM,
8 | type RecordType,
9 | } from "@/lib/schema.ts";
10 | import { cn } from "@/lib/utils.ts";
11 | import type { RecordVM } from "./record";
12 |
13 | interface TextRecordProps extends React.HTMLAttributes {
14 | message: MessageVM;
15 | record: TextRecordEntity;
16 | variant: "default" | string;
17 | }
18 |
19 | export interface TextRecordEntity extends RecordVM {
20 | "@_datatype": RecordType.TEXT;
21 | datadesc: string;
22 | }
23 |
24 | export default function TextRecord({
25 | message,
26 | record,
27 | variant = "default",
28 | className,
29 | ...props
30 | }: TextRecordProps) {
31 | if (variant === "default")
32 | return (
33 |
43 |
44 |
45 | );
46 |
47 | if (variant === "note")
48 | return (
49 |
50 |
51 |
52 | );
53 |
54 | return (
55 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/record/ting-record.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import Link from "@/components/link.tsx";
3 | import type { MessageVM, RecordType } from "@/lib/schema.ts";
4 | import { cn } from "@/lib/utils";
5 | import type { RecordVM } from "./record";
6 | interface TingRecordProps extends React.HTMLAttributes {
7 | message: MessageVM;
8 | record: TingRecordEntity;
9 | variant: "default" | string;
10 | }
11 |
12 | export interface TingRecordEntity extends RecordVM {
13 | "@_datatype": RecordType.TING;
14 |
15 | datatitle: string;
16 | datadesc: string;
17 |
18 | streamweburl: string; // 如果是音乐,这个链接一般是前往QQ音乐,如果是音频,这个链接前往公众号文章
19 | songalbumurl: string;
20 | weburlitem: {
21 | thumburl: string;
22 | title: string;
23 | };
24 |
25 | // 如果是音乐会有下面的数据
26 | streamdataurl?: string;
27 | streamlowbandurl?: string;
28 | musicShareItem?: {
29 | mvSingerName: string;
30 | mid: string;
31 | };
32 | }
33 |
34 | export default function TingRecord({
35 | message,
36 | record,
37 | variant = "default",
38 | className,
39 | ...props
40 | }: TingRecordProps) {
41 | if (variant === "default")
42 | return (
43 |
44 |
51 | {record.songalbumurl ? (
52 |
56 | ) : null}
57 |
58 |
63 | {record.songalbumurl ? (
64 |
68 | ) : null}
69 |
70 |
71 |
72 | {record.datatitle}
73 |
74 |
79 | {record.datadesc}
80 |
81 |
82 |
83 |
84 |
85 | );
86 |
87 | return (
88 |
89 | {record.musicShareItem ? "[音乐]" : "[音频]"} {record.datatitle}
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/daily-message-count-chart.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | type ChartConfig,
3 | ChartContainer,
4 | ChartTooltip,
5 | ChartTooltipContent,
6 | } from "@/components/ui/chart.tsx";
7 | import type React from "react";
8 | import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
9 |
10 | export interface DailyMessageCountChartProps
11 | extends React.HTMLAttributes {
12 | data: {
13 | date: string;
14 | sent_message_count: number;
15 | received_message_count: number;
16 | }[];
17 | }
18 |
19 | const chartConfig = {
20 | sent_message_count: {
21 | label: "发送消息",
22 | color: "#26DC22",
23 | },
24 | received_message_count: {
25 | label: "接受消息",
26 | color: "#FFB75C",
27 | },
28 | } satisfies ChartConfig;
29 |
30 | export function DailyMessageCountChart({
31 | data,
32 | ...props
33 | }: DailyMessageCountChartProps) {
34 | return (
35 |
36 |
37 |
38 |
44 | } />
45 |
46 |
47 |
52 |
57 |
58 |
59 |
64 |
69 |
70 |
71 |
72 |
82 |
92 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/section-message-count-description.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import User from "@/components/user.tsx";
3 | import { useApp } from "@/lib/hooks/appProvider.tsx";
4 | import type React from "react";
5 |
6 | import footer_logo from "/images/wrapped-2024/footer-logo.svg?url";
7 |
8 | export default function SectionMessageCountDescription({
9 | data,
10 | }: {
11 | data: {
12 | message_count_description: string;
13 | };
14 | }) {
15 | const { user } = useApp();
16 |
17 | return (
18 |
26 |
27 |
28 |
29 |
30 | 读完过去一年的所有消息
31 |
32 | 你的手指需要滑动
33 |
34 | {data.message_count_description}
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
{" "}
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/section-most-used-wxemoji.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import User from "@/components/user.tsx";
3 | import { useApp } from "@/lib/hooks/appProvider.tsx";
4 | import WechatEmojiTable from "@/lib/wechat-emojis.ts";
5 | import type React from "react";
6 |
7 | import footer_logo from "/images/wrapped-2024/footer-logo.svg?url";
8 |
9 | export default function SectionMostUsedWxemoji({
10 | data,
11 | }: {
12 | data: {
13 | sent_wxemoji_usage: { key: string; count: number }[];
14 | };
15 | }) {
16 | const { user } = useApp();
17 |
18 | const mostUsedWxemoji = data.sent_wxemoji_usage.sort(
19 | (a, b) => b.count - a.count,
20 | )[0];
21 | const mostUsedWxemojiKey = mostUsedWxemoji ? mostUsedWxemoji.key : undefined;
22 | const mostUsedWxemojiSrc = mostUsedWxemojiKey
23 | ? `/wxemoji/${WechatEmojiTable[mostUsedWxemojiKey]}`
24 | : undefined;
25 |
26 | return (
27 |
35 |
36 |
发送表情
37 |
38 | 过去一年,你使用最多的微信表情是
39 |
40 |
41 |
42 |
43 |
46 | {mostUsedWxemoji && (
47 |
48 |
54 |
59 |
60 | )}
61 |
62 | {mostUsedWxemoji && (
63 |
66 | 累计使用 {mostUsedWxemoji.count} 次
67 |
68 | )}
69 |
70 |
71 |
72 |
73 |
74 |
78 |
79 |
{" "}
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/section-new-user-added.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import { ScrollArea } from "@/components/ui/scroll-area.tsx";
3 | import User from "@/components/user.tsx";
4 | import { useApp } from "@/lib/hooks/appProvider.tsx";
5 | import type { User as UserVM } from "@/lib/schema.ts";
6 | import { format } from "date-fns";
7 | import type React from "react";
8 |
9 | import footer_logo from "/images/wrapped-2024/footer-logo.svg?url";
10 |
11 | export default function SectionNewUserAdded({
12 | data,
13 | }: {
14 | data: {
15 | user_dates_contact_added: { user: UserVM; date: string }[];
16 | };
17 | }) {
18 | const { user } = useApp();
19 |
20 | return (
21 |
29 |
30 |
新朋友们
31 |
32 | 过去一年,你认识了 {data.user_dates_contact_added.length}{" "}
33 | 位新好友,还记得你们的第一个话题吗{" "}
34 |
35 |
36 |
37 |
38 |
39 | {data.user_dates_contact_added.map((i) => (
40 | -
41 |
46 |
51 |
52 |
53 | {}
54 |
55 |
56 | {format(new Date(i.date), "MM月dd日")}
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/section-sent-message-count-description.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import User from "@/components/user.tsx";
3 | import { useApp } from "@/lib/hooks/appProvider.tsx";
4 | import type React from "react";
5 |
6 | import footer_logo from "/images/wrapped-2024/footer-logo.svg?url";
7 |
8 | export default function SectionSentMessageCountDescription({
9 | data,
10 | }: {
11 | data: {
12 | sent_message_count_description: string;
13 | };
14 | }) {
15 | const { user } = useApp();
16 |
17 | return (
18 |
26 |
27 |
28 |
29 |
30 | 发送过去一年的所有消息
31 |
32 | 你需要消耗能量 {data.sent_message_count_description} 卡
33 |
34 |
35 |
36 |
37 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/section-sent-message-count.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import { DailyMessageCountChart } from "@/components/statistic/wrapped-2024/daily-message-count-chart.tsx";
3 | import User from "@/components/user.tsx";
4 | import { useApp } from "@/lib/hooks/appProvider.tsx";
5 | import { differenceInMonths, format } from "date-fns";
6 | import React from "react";
7 |
8 | import footer_logo from "/images/wrapped-2024/footer-logo.svg?url";
9 |
10 | export default function SectionSentMessageCount({
11 | data,
12 | }: {
13 | data: {
14 | startTime: Date;
15 | endTime: Date;
16 | sent_message_count: number;
17 | daily_sent_message_count: { date: string; message_count: number }[];
18 | daily_received_message_count: { date: string; message_count: number }[];
19 | };
20 | }) {
21 | const { user } = useApp();
22 |
23 | const { startTime, endTime } = data;
24 |
25 | const combinedDate = Array.from(
26 | new Array(differenceInMonths(endTime, startTime)),
27 | ).map((_, i) => ({
28 | date: `${(i + 1).toString()}月`,
29 | sent_message_count: 0,
30 | received_message_count: 0,
31 | }));
32 |
33 | data.daily_sent_message_count.map((i) => {
34 | const monthIndex = Number.parseInt(format(new Date(i.date), "MM")) - 1;
35 | combinedDate[monthIndex].sent_message_count += i.message_count;
36 | });
37 |
38 | data.daily_received_message_count.map((i) => {
39 | const monthIndex = Number.parseInt(format(new Date(i.date), "MM")) - 1;
40 | combinedDate[monthIndex].received_message_count += i.message_count;
41 | });
42 |
43 | return (
44 |
52 |
53 |
消息统计
54 |
55 | 过去一年,你一共发送了 {data.sent_message_count} 条消息
56 |
57 |
58 |
59 |
60 |
61 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/section-sent-message-word-count.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import User from "@/components/user.tsx";
3 | import { useApp } from "@/lib/hooks/appProvider.tsx";
4 | import type React from "react";
5 |
6 | import footer_logo from "/images/wrapped-2024/footer-logo.svg?url";
7 |
8 | export default function SectionSentMessageWordCount({
9 | data,
10 | }: {
11 | data: {
12 | sent_message_word_count: number;
13 | sent_message_word_count_description: string;
14 | };
15 | }) {
16 | const { user } = useApp();
17 |
18 | return (
19 |
27 |
28 |
发送文字
29 |
30 |
31 |
32 |
33 | 过去一年
34 |
35 | 你一共发送了 {data.sent_message_word_count} 字
36 |
37 | {/*约等于写了*/}
38 | {/*{data.sent_message_word_count_description}*/}
39 | {/*
*/}
40 |
41 |
42 |
43 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/statistic/wrapped-2024/wrapped-2024-trigger.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CelebrateSolid,
3 | ChevronRightSmallLine,
4 | } from "@/components/central-icon.tsx";
5 | import Wrapped2024 from "@/components/statistic/wrapped-2024/wrapped-2024.tsx";
6 | import { Button } from "@/components/ui/button.tsx";
7 | import {
8 | Dialog,
9 | DialogClose,
10 | DialogContent,
11 | DialogDescription,
12 | DialogHeader,
13 | DialogTitle,
14 | DialogTrigger,
15 | } from "@/components/ui/dialog.tsx";
16 | import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
17 | import { X } from "lucide-react";
18 | import { useState } from "react";
19 |
20 | export default function Wrapped2024Trigger() {
21 | const [isOpen, setIsOpen] = useState(false);
22 | return (
23 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronLeft, ChevronRight } from "lucide-react";
2 | import type * as React from "react";
3 | import { DayPicker } from "react-day-picker";
4 |
5 | import { buttonVariants } from "@/components/ui/button";
6 | import { cn } from "@/lib/utils";
7 |
8 | export type CalendarProps = React.ComponentProps;
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: CalendarProps) {
16 | return (
17 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
41 | : "[&:has([aria-selected])]:rounded-md",
42 | ),
43 | day: cn(
44 | buttonVariants({ variant: "ghost" }),
45 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100",
46 | ),
47 | day_range_start: "day-range-start",
48 | day_range_end: "day-range-end",
49 | day_selected:
50 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
51 | day_today: "bg-accent text-accent-foreground",
52 | day_outside:
53 | "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
54 | day_disabled: "text-muted-foreground opacity-50",
55 | day_range_middle:
56 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
57 | day_hidden: "invisible",
58 | ...classNames,
59 | }}
60 | components={{
61 | IconLeft: ({ className, ...props }) => (
62 |
63 | ),
64 | IconRight: ({ className, ...props }) => (
65 |
66 | ),
67 | }}
68 | {...props}
69 | />
70 | );
71 | }
72 | Calendar.displayName = "Calendar";
73 |
74 | export { Calendar };
75 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
10 | ));
11 | Card.displayName = "Card";
12 |
13 | const CardHeader = React.forwardRef<
14 | HTMLDivElement,
15 | React.HTMLAttributes
16 | >(({ className, ...props }, ref) => (
17 |
22 | ));
23 | CardHeader.displayName = "CardHeader";
24 |
25 | const CardTitle = React.forwardRef<
26 | HTMLDivElement,
27 | React.HTMLAttributes
28 | >(({ className, ...props }, ref) => (
29 |
34 | ));
35 | CardTitle.displayName = "CardTitle";
36 |
37 | const CardContent = React.forwardRef<
38 | HTMLDivElement,
39 | React.HTMLAttributes
40 | >(({ className, ...props }, ref) => (
41 |
42 | ));
43 | CardContent.displayName = "CardContent";
44 |
45 | const CardFooter = React.forwardRef<
46 | HTMLDivElement,
47 | React.HTMLAttributes
48 | >(({ className, ...props }, ref) => (
49 |
57 | ));
58 | CardFooter.displayName = "CardFooter";
59 |
60 | const CardIndicator = React.forwardRef<
61 | HTMLDivElement,
62 | React.HTMLAttributes
63 | >(({ className, ...props }, ref) => (
64 |
72 | ));
73 | CardIndicator.displayName = "CardIndicator";
74 |
75 | export { Card, CardHeader, CardFooter, CardTitle, CardContent, CardIndicator };
76 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as LabelPrimitive from "@radix-ui/react-label";
2 | import { type VariantProps, cva } from "class-variance-authority";
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
9 | );
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ));
22 | Label.displayName = LabelPrimitive.Root.displayName;
23 |
24 | export { Label };
25 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as PopoverPrimitive from "@radix-ui/react-popover";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Popover = PopoverPrimitive.Root;
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger;
9 |
10 | const PopoverAnchor = PopoverPrimitive.Anchor;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
32 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
2 | import { Circle } from "lucide-react";
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return (
12 |
17 | );
18 | });
19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
20 |
21 | const RadioGroupItem = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => {
25 | return (
26 |
34 |
35 |
36 |
37 |
38 | );
39 | });
40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
41 |
42 | export { RadioGroup, RadioGroupItem };
43 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | // import { GripVertical } from "lucide-react";
2 | import * as ResizablePrimitive from "react-resizable-panels";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const ResizablePanelGroup = ({
7 | className,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
17 | );
18 |
19 | const ResizablePanel = ResizablePrimitive.Panel;
20 |
21 | const ResizableHandle = ({
22 | withHandle,
23 | className,
24 | ...props
25 | }: React.ComponentProps & {
26 | withHandle?: boolean;
27 | }) => (
28 | div]:rotate-90",
31 | className,
32 | )}
33 | {...props}
34 | >
35 | {withHandle && (
36 |
37 | {/**/}
38 |
39 | )}
40 |
41 | );
42 |
43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
44 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { useEffect, useRef } from "react";
6 |
7 | type ScrollAreaProps = {
8 | setScrollRef?: (
9 | ref: React.ElementRef
10 | ) => void;
11 | };
12 |
13 | const ScrollArea = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | ScrollAreaProps
17 | >(({ setScrollRef, className, children, ...props }, ref) => {
18 | const scrollRef = useRef | null>(null);
21 |
22 | useEffect(() => {
23 | if (!scrollRef.current || !setScrollRef) {
24 | return;
25 | }
26 |
27 | setScrollRef(scrollRef.current);
28 | }, [scrollRef.current, setScrollRef]);
29 |
30 | return (
31 |
36 |
40 | {children}
41 |
42 |
43 |
44 |
45 | );
46 | });
47 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
48 |
49 | const ScrollBar = React.forwardRef<
50 | React.ElementRef,
51 | React.ComponentPropsWithoutRef
52 | >(({ className, orientation = "vertical", ...props }, ref) => (
53 |
66 |
67 |
68 | ));
69 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
70 |
71 | export { ScrollArea, ScrollBar };
72 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as TabsPrimitive from "@radix-ui/react-tabs";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Tabs = TabsPrimitive.Root;
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | TabsList.displayName = TabsPrimitive.List.displayName;
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ));
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ));
51 | TabsContent.displayName = TabsPrimitive.Content.displayName;
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent };
54 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider;
7 |
8 | const Tooltip = TooltipPrimitive.Root;
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger;
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
17 |
26 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/src/components/ui/typography.tsx:
--------------------------------------------------------------------------------
1 | export function TypographyP() {
2 | return (
3 |
4 | The king, seeing how much happier his subjects were, realized the error of
5 | his ways and repealed the joke tax.
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/user.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import type { User as UserVM } from "@/lib/schema.ts";
3 | import { cn } from "@/lib/utils.ts";
4 |
5 | interface UserProps extends React.HTMLProps {
6 | user: UserVM;
7 | showPhoto?: boolean;
8 | showUsername?: boolean;
9 | variant?: "default" | "inline";
10 | }
11 |
12 | const userVariants = {
13 | container: {
14 | default: "",
15 | inline: "inline font-medium cursor-pointer hover:underline",
16 | },
17 | photo: {
18 | default:
19 | "shrink-0 size-11 aspect-square rounded-[18.18%] clothoid-corner-[18.18%] bg-neutral-200",
20 | inline:
21 | "relative inline-block size-[1.5em] align-top [&_img]:inline [&_img]:absolute [&_img]:inset-0 [&_img]:m-auto [&_img]:size-[1.25em] [&_img]:rounded-[3px]",
22 | },
23 | username: {
24 | default: "",
25 | inline: "",
26 | },
27 | };
28 |
29 | export default function User({
30 | user,
31 | showPhoto = true,
32 | showUsername = true,
33 | variant = "default",
34 |
35 | className,
36 | ...props
37 | }: UserProps) {
38 | const Container = variant === "inline" ? "span" : "div";
39 |
40 | return (
41 |
45 | {showPhoto && (
46 |
51 | )}
52 | {showUsername && (
53 |
58 | )}
59 |
60 | );
61 | }
62 |
63 | interface UserPhotoProps extends React.HTMLAttributes {
64 | user: UserVM;
65 | variant: "default" | "inline";
66 | }
67 |
68 | function Photo({
69 | user,
70 | variant = "default",
71 | className,
72 | ...props
73 | }: UserPhotoProps) {
74 | if (variant === "inline") {
75 | return (
76 |
77 | {user.photo && }
78 |
79 | );
80 | }
81 | return user.photo ? (
82 |
87 | ) : (
88 |
89 | );
90 | }
91 |
92 | User.Photo = Photo;
93 |
94 | interface UserNameProps extends React.HTMLAttributes {
95 | user: UserVM;
96 |
97 | variant: "default" | "inline";
98 | }
99 |
100 | function Username({
101 | user,
102 | variant = "default",
103 | className,
104 | ...props
105 | }: UserNameProps) {
106 | return (
107 |
108 | {user.remark ?? user.username}
109 |
110 | );
111 | }
112 |
113 | User.Username = Username;
114 |
--------------------------------------------------------------------------------
/src/components/wechat-emoji.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/image.tsx";
2 | import WechatEmojiTable from "@/lib/wechat-emojis.ts";
3 |
4 | export default function WechatEmoji({ emojiName }: { emojiName: string }) {
5 | return (
6 |
11 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App";
4 | import "./index.css";
5 | import { AppProvider } from "@/lib/hooks/appProvider.tsx";
6 | import { DatabaseProvider } from "@/lib/hooks/databaseProvider";
7 | import { WorkerProvider } from "@/lib/hooks/workerProvider.tsx";
8 | import { ErrorBoundary } from "react-error-boundary";
9 |
10 | const rootEl = document.getElementById("root");
11 | if (rootEl) {
12 | const root = ReactDOM.createRoot(rootEl);
13 | root.render(
14 |
15 |
16 | 你的浏览器不支持 Web Worker,请检查浏览器版本或浏览器设置
20 | )
21 | }
22 | >
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ,
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/controllers/attach.ts:
--------------------------------------------------------------------------------
1 | import type { RecordVM } from "@/components/record/record";
2 | import type { Chat, FileInfo, MessageVM, WCDatabases } from "@/lib/schema.ts";
3 | import CryptoJS from "crypto-js";
4 | import { getFilesFromManifast } from "../utils";
5 |
6 | export const AttachController = {
7 | get: async (
8 | directory: FileSystemDirectoryHandle,
9 | databases: WCDatabases,
10 | {
11 | chat,
12 | message,
13 | record,
14 |
15 | type,
16 | }: {
17 | chat: Chat;
18 | message: MessageVM;
19 | record?: RecordVM;
20 |
21 | type: string;
22 | },
23 | ): Promise => {
24 | const db = databases.manifest;
25 | if (!db) throw new Error("manifest database is not found");
26 |
27 | const files = await getFilesFromManifast(
28 | db,
29 | directory,
30 | record
31 | ? `%/OpenData/${CryptoJS.MD5(chat.id).toString()}/${message.local_id}/${record["@_dataid"]}.%`
32 | : `%/OpenData/${CryptoJS.MD5(chat.id).toString()}/${message.local_id}.%`,
33 | );
34 |
35 | const result = [];
36 |
37 | for (const file of files) {
38 | if (type) {
39 | const fileBuffer = await file.file.arrayBuffer();
40 | const fileBlob = new Blob([fileBuffer], { type });
41 | result.push({
42 | src: URL.createObjectURL(fileBlob),
43 | });
44 | } else {
45 | result.push({
46 | src: URL.createObjectURL(file.file),
47 | });
48 | }
49 | }
50 |
51 | return result;
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/src/lib/controllers/image.ts:
--------------------------------------------------------------------------------
1 | import type { RecordVM } from "@/components/record/record";
2 | import type { Chat, MessageVM, PhotpSize, WCDatabases } from "@/lib/schema.ts";
3 | import CryptoJS from "crypto-js";
4 | import { getFilesFromManifast } from "../utils";
5 |
6 | export const ImageController = {
7 | get: async (
8 | directory: FileSystemDirectoryHandle,
9 | databases: WCDatabases,
10 | {
11 | chat,
12 | message,
13 | record,
14 |
15 | size = "origin",
16 | domain = "image",
17 | }: {
18 | chat: Chat;
19 | message: MessageVM;
20 | record?: RecordVM;
21 |
22 | size: "origin" | "thumb";
23 | domain: "image" | "opendata" | "video";
24 | },
25 | ): Promise => {
26 | const db = databases.manifest;
27 | if (!db) throw new Error("manifest database is not found");
28 |
29 | const files = await getFilesFromManifast(
30 | db,
31 | directory,
32 | record
33 | ? `%/OpenData/${CryptoJS.MD5(chat.id).toString()}/${message.local_id}/${record["@_dataid"]}.%`
34 | : `%/${
35 | { image: "Img", opendata: "OpenData", video: "Video" }[domain]
36 | }/${CryptoJS.MD5(chat.id).toString()}/${message.local_id}.%`,
37 | );
38 |
39 | const result: PhotpSize[] = [];
40 |
41 | for (const file of files) {
42 | if (file.filename.endsWith(".pic")) {
43 | const newPhoto: PhotpSize = {
44 | size: "origin",
45 | src: URL.createObjectURL(file.file),
46 | };
47 | if (size === "origin") result.push(newPhoto);
48 | else result.unshift(newPhoto);
49 | }
50 |
51 | if (
52 | file.filename.endsWith(".pic_thum") ||
53 | file.filename.endsWith(".record_thum")
54 | ) {
55 | const newPhoto: PhotpSize = {
56 | size: "thumb",
57 | src: URL.createObjectURL(file.file),
58 | // width: Number.parseInt(messageEntity.msg.img["@_cdnthumbwidth"]),
59 | // height: Number.parseInt(messageEntity.msg.img["@_cdnthumbheight"]),
60 | };
61 | if (size === "origin") result.push(newPhoto);
62 | else result.unshift(newPhoto);
63 | }
64 |
65 | if (file.filename.endsWith(".video_thum")) {
66 | const newPhoto: PhotpSize = {
67 | size: "thumb",
68 | src: URL.createObjectURL(file.file),
69 | // width: Number.parseInt(messageEntity.msg.img["@_cdnthumbwidth"]),
70 | // height: Number.parseInt(messageEntity.msg.img["@_cdnthumbheight"]),
71 | };
72 | if (size === "origin") result.push(newPhoto);
73 | else result.unshift(newPhoto);
74 | }
75 | }
76 |
77 | return result;
78 | },
79 | };
80 |
--------------------------------------------------------------------------------
/src/lib/controllers/video.ts:
--------------------------------------------------------------------------------
1 | import type { Chat, MessageVM, VideoInfo, WCDatabases } from "@/lib/schema.ts";
2 | import CryptoJS from "crypto-js";
3 | import { getFilesFromManifast } from "../utils";
4 |
5 | export const VideoController = {
6 | get: async (
7 | directory: FileSystemDirectoryHandle,
8 | databases: WCDatabases,
9 | {
10 | chat,
11 | message,
12 | }: {
13 | chat: Chat;
14 | message: MessageVM;
15 | },
16 | ): Promise => {
17 | const db = databases.manifest;
18 | if (!db) throw new Error("manifest database is not found");
19 |
20 | const files = await getFilesFromManifast(
21 | db,
22 | directory,
23 | `%/Video/${CryptoJS.MD5(chat.id).toString()}/${message.local_id}.%`,
24 | );
25 |
26 | let result: VideoInfo = {
27 | poster: "",
28 | };
29 |
30 | for (const file of files) {
31 | if (file.filename.endsWith(".mp4")) {
32 | result = {
33 | ...result,
34 | src: URL.createObjectURL(file.file),
35 | };
36 | }
37 |
38 | if (file.filename.endsWith(".video_thum")) {
39 | result = {
40 | ...result,
41 | poster: URL.createObjectURL(file.file),
42 |
43 | // poster_width: Number.parseInt(
44 | // messageEntity.msg.videomsg["@_cdnthumbwidth"],
45 | // ),
46 | // poster_height: Number.parseInt(
47 | // messageEntity.msg.videomsg["@_cdnthumbheight"],
48 | // ),
49 | };
50 | }
51 | }
52 |
53 | return result;
54 | },
55 | };
56 |
--------------------------------------------------------------------------------
/src/lib/controllers/voice.ts:
--------------------------------------------------------------------------------
1 | import type { Chat, MessageVM, VoiceInfo, WCDatabases } from "@/lib/schema.ts";
2 | import { FFmpeg } from "@ffmpeg/ffmpeg";
3 | import { toBlobURL } from "@ffmpeg/util";
4 | import CryptoJS from "crypto-js";
5 | import { decode } from "silk-wasm";
6 | import { getFilesFromManifast } from "../utils";
7 |
8 | const ffmpegCoreURL =
9 | "https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.js";
10 | const ffmpegWasmURL =
11 | "https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.wasm";
12 |
13 | const ffmpeg = new FFmpeg();
14 |
15 | let isFFmpegLoading = false; // 防止重复加载, TODO: 更好应该是写一个事件广播
16 |
17 | export const VoiceController = {
18 | get: async (
19 | directory: FileSystemDirectoryHandle,
20 | databases: WCDatabases,
21 | {
22 | chat,
23 | message,
24 |
25 | scope = "all",
26 | }: {
27 | chat: Chat;
28 | message: MessageVM;
29 |
30 | scope: "all" | "transcription";
31 | },
32 | ): Promise => {
33 | const db = databases.manifest;
34 | if (!db) throw new Error("manifest database is not found");
35 |
36 | const files = await getFilesFromManifast(
37 | db,
38 | directory,
39 | `%/Audio/${CryptoJS.MD5(chat.id).toString()}/${message.local_id}.%`,
40 | );
41 |
42 | let result: VoiceInfo = {
43 | raw_aud_src: "",
44 | };
45 |
46 | for (const file of files) {
47 | if (file.filename.endsWith(".aud")) {
48 | result = {
49 | ...result,
50 | raw_aud_src: URL.createObjectURL(file.file),
51 | src: await (async () => {
52 | const silk = await decode(await file.file.arrayBuffer(), 24000);
53 |
54 | if (!ffmpeg.loaded) {
55 | if (!isFFmpegLoading) {
56 | isFFmpegLoading = true;
57 | await ffmpeg.load({
58 | coreURL: await toBlobURL(ffmpegCoreURL, "text/javascript"),
59 | wasmURL: await toBlobURL(ffmpegWasmURL, "application/wasm"),
60 | });
61 | }
62 |
63 | // TODO
64 | while (!ffmpeg.loaded) {
65 | await new Promise((resolve) => setTimeout(resolve, 200));
66 | }
67 | }
68 |
69 | const ffmpegInputFilename = `${message.chat.id}|${message.local_id}.pcm`;
70 | const ffmpegOutputFilename = `${message.chat.id}|${message.local_id}.wav`;
71 |
72 | await ffmpeg.writeFile(ffmpegInputFilename, silk.data);
73 | await ffmpeg.exec([
74 | "-y",
75 | "-f",
76 | "s16le",
77 | "-ar",
78 | "24000",
79 | "-ac",
80 | "1",
81 | "-i",
82 | ffmpegInputFilename,
83 | ffmpegOutputFilename,
84 | ]);
85 | const wav = await ffmpeg.readFile(ffmpegOutputFilename);
86 |
87 | ffmpeg.deleteFile(ffmpegInputFilename);
88 | ffmpeg.deleteFile(ffmpegOutputFilename);
89 |
90 | return URL.createObjectURL(new Blob([wav], { type: "audio/wav" }));
91 | })(),
92 | };
93 | }
94 |
95 | if (file.filename.endsWith(".txt")) {
96 | result = {
97 | ...result,
98 | transcription: await file.file.text(),
99 | };
100 | }
101 | }
102 |
103 | return result;
104 | },
105 | };
106 |
--------------------------------------------------------------------------------
/src/lib/global.ts:
--------------------------------------------------------------------------------
1 | import type { User } from "@/lib/schema.ts";
2 |
3 | /**
4 | * TODO 全局变量
5 | * 有些数据,比如登录的微信用户自己的用户数据不存在数据库中,
6 | * 因为控制器的函数是普通函数,不能通过访问 content 获取上层数据
7 | * 所以这里使用一个全局变量储存数据
8 | */
9 |
10 | const _global: {
11 | user?: User;
12 | ChatDatabaseTable: { [key: string]: number };
13 |
14 | enableDebug: boolean;
15 | } = {
16 | ChatDatabaseTable: {},
17 |
18 | enableDebug: true,
19 | };
20 |
21 | export default _global;
22 |
--------------------------------------------------------------------------------
/src/lib/hooks/appProvider.tsx:
--------------------------------------------------------------------------------
1 | import _global from "@/lib/global.ts";
2 | import type { Chat, User } from "@/lib/schema.ts";
3 | import {
4 | createContext,
5 | useCallback,
6 | useContext,
7 | useEffect,
8 | useRef,
9 | useState,
10 | } from "react";
11 |
12 | interface AppProviderContext {
13 | user: User | undefined;
14 | setUser: (user: User) => void;
15 | chat: Chat | undefined;
16 | setChat: (chat: Chat) => void;
17 |
18 | isOpenMediaViewer: boolean;
19 | setIsOpenMediaViewer: (isOpenMediaViewer: boolean) => void;
20 |
21 | registerIntersectionObserver: (
22 | element: Element,
23 | callback: () => void,
24 | ) => void;
25 | }
26 |
27 | const AppContext = createContext({
28 | user: undefined,
29 | setUser: () => {},
30 | chat: undefined,
31 | setChat: () => {},
32 |
33 | isOpenMediaViewer: false,
34 | setIsOpenMediaViewer: () => {},
35 |
36 | registerIntersectionObserver: () => {},
37 | });
38 |
39 | interface AppProviderProps {
40 | children: React.ReactNode;
41 | }
42 |
43 | export const AppProvider = ({ children, ...props }: AppProviderProps) => {
44 | // const [userList, setUserList] = useState();
45 | const [user, setUser] = useState();
46 | useEffect(() => {
47 | if (user) {
48 | _global.user = user;
49 | } else {
50 | delete _global.user;
51 | }
52 | }, [user]);
53 |
54 | const [chat, setChat] = useState();
55 |
56 | const [isOpenMediaViewer, setIsOpenMediaViewer] = useState(false);
57 |
58 | // Intersection Observer
59 | const intersectionObserverRef = useRef();
60 | const entriesMap = useRef(new Map());
61 | const intersectionObserver = () => {
62 | if (!intersectionObserverRef.current) {
63 | intersectionObserverRef.current = new IntersectionObserver(
64 | (entries) => {
65 | for (const entry of entries) {
66 | if (entry.isIntersecting) {
67 | const callback = entriesMap.current.get(entry.target);
68 | if (callback) {
69 | callback();
70 | intersectionObserverRef.current?.unobserve(entry.target);
71 | entriesMap.current.delete(entry.target);
72 | }
73 | }
74 | }
75 | },
76 | { rootMargin: "0px" },
77 | );
78 | }
79 | return intersectionObserverRef.current;
80 | };
81 | const registerIntersectionObserver = useCallback(
82 | (element: Element, callback: () => void) => {
83 | if (element) {
84 | entriesMap.current.set(element, callback);
85 | intersectionObserver().observe(element);
86 | }
87 | },
88 | [],
89 | );
90 |
91 | return (
92 |
106 | {children}
107 |
108 | );
109 | };
110 |
111 | export const useApp = () => {
112 | const context = useContext(AppContext);
113 | if (!context) {
114 | throw new Error("useApp must be used within a AppProvider");
115 | }
116 | return context;
117 | };
118 |
--------------------------------------------------------------------------------
/src/lib/hooks/databaseProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useApp } from "@/lib/hooks/appProvider.tsx";
2 | import { useCommand } from "@/lib/hooks/useQuery.ts";
3 | import type { User, WCDatabases } from "@/lib/schema.ts";
4 | import type {
5 | WorkerRequestLoadDatabases,
6 | WorkerRequestLoadDirectory,
7 | WorkerResponseLoadDatabases,
8 | WorkerResponseLoadDirectory,
9 | } from "@/lib/worker.ts";
10 | import { createContext, useContext, useEffect, useState } from "react";
11 |
12 | const DatabaseContext = createContext<{
13 | loadDirectory: (
14 | directory: WorkerRequestLoadDirectory["payload"]["directory"],
15 | ) => Promise;
16 | directory:
17 | | WorkerResponseLoadDirectory["payload"]["data"]["directory"]
18 | | undefined;
19 | accountList: User[];
20 | loadDatabases: (account: User) => Promise;
21 | databases: WCDatabases | undefined;
22 | }>({
23 | loadDirectory: () => Promise.resolve(),
24 | directory: undefined,
25 | accountList: [],
26 | loadDatabases: () => Promise.resolve(),
27 | databases: undefined,
28 | });
29 |
30 | export const DatabaseProvider = ({
31 | children,
32 | }: { children: React.ReactNode }) => {
33 | const { setUser } = useApp();
34 | const [accountList, setAccountList] = useState([]);
35 |
36 | // load directory
37 | const [directory, setDirectory] =
38 | useState();
39 | const [
40 | _loadDirectory,
41 | isLoadingDirectory,
42 | loadDirectoryResult,
43 | loadDirectoryError,
44 | ] = useCommand<
45 | WorkerRequestLoadDirectory,
46 | WorkerResponseLoadDirectory["payload"] | undefined
47 | >(undefined);
48 | const loadDirectory = async (
49 | directory: WorkerResponseLoadDirectory["payload"]["data"]["directory"],
50 | ) => {
51 | await _loadDirectory("load_directory", { directory });
52 | };
53 | useEffect(() => {
54 | if (loadDirectoryResult) {
55 | setDirectory(loadDirectoryResult.data.directory);
56 | setAccountList(loadDirectoryResult.data.accounts);
57 | }
58 | }, [loadDirectoryResult]);
59 |
60 | // load databases
61 | const [databases, setDatabases] = useState();
62 | const [
63 | _loadDatabases,
64 | isLoadingDatabases,
65 | loadDatabasesResult,
66 | loadDatabasesError,
67 | ] = useCommand<
68 | WorkerRequestLoadDatabases,
69 | WorkerResponseLoadDatabases["payload"] | undefined
70 | >(undefined);
71 | const loadDatabases = async (account: User) => {
72 | await _loadDatabases("load_databases", { account });
73 | // setUser(account);
74 | };
75 | useEffect(() => {
76 | if (loadDatabasesResult) {
77 | setDatabases(loadDatabasesResult.data.databases);
78 | setUser(loadDatabasesResult.data.account);
79 | }
80 | }, [loadDatabasesResult]);
81 |
82 | return (
83 |
92 | {children}
93 |
94 | );
95 | };
96 |
97 | export const useDatabase = () => {
98 | const context = useContext(DatabaseContext);
99 | if (!context) {
100 | throw new Error("useDatabase must be used within a DatabaseProvider");
101 | }
102 | return context;
103 | };
104 |
--------------------------------------------------------------------------------
/src/lib/hooks/useQuery.ts:
--------------------------------------------------------------------------------
1 | import type { WorkerRequest, WorkerRequestQuery } from "@/lib/worker.ts";
2 |
3 | import _global from "@/lib/global.ts";
4 | import { useWorker } from "@/lib/hooks/workerProvider.tsx";
5 | import { useCallback, useState } from "react";
6 |
7 | export function useCommand(
8 | initialState: Response,
9 | ): [
10 | (type: Request["type"], payload: Request["payload"]) => Promise,
11 | boolean,
12 | Response,
13 | unknown,
14 | ] {
15 | const { worker, pendingQueries } = useWorker();
16 |
17 | const [isQuerying, setIsQuerying] = useState(false);
18 | const [result, setResult] = useState(initialState);
19 | const [error, setError] = useState(null);
20 |
21 | const query = useCallback(
22 | async (type: Request["type"], payload: Request["payload"]) => {
23 | setIsQuerying(true);
24 | const id = crypto.randomUUID();
25 |
26 | return new Promise((resolve, reject) => {
27 | pendingQueries.set(id, (response) => {
28 | setIsQuerying(false);
29 |
30 | if (_global.enableDebug) {
31 | console.groupCollapsed(`command ${type}`);
32 | console.log(payload);
33 | console.log(response);
34 | console.groupEnd();
35 | }
36 |
37 | setResult(response);
38 | setError(null);
39 | resolve(response);
40 | });
41 |
42 | worker.postMessage({ id, type, payload } as WorkerRequest<
43 | Request["type"],
44 | Request["payload"]
45 | >);
46 | });
47 | },
48 | [],
49 | );
50 |
51 | return [query, isQuerying, result, error];
52 | }
53 |
54 | export default function useQuery(
55 | initialState: Response,
56 | ): [
57 | (endpoint: string, ...args: unknown[]) => Promise,
58 | boolean,
59 | Response,
60 | unknown,
61 | ] {
62 | const { worker, pendingQueries } = useWorker();
63 |
64 | const [isQuerying, setIsQuerying] = useState(false);
65 | const [result, setResult] = useState(initialState);
66 | const [error, setError] = useState(null);
67 |
68 | const query = useCallback(async (endpoint: string, ...args: unknown[]) => {
69 | setIsQuerying(true);
70 | const id = crypto.randomUUID();
71 |
72 | return new Promise((resolve, reject) => {
73 | pendingQueries.set(id, (response) => {
74 | setIsQuerying(false);
75 |
76 | if (_global.enableDebug) {
77 | console.groupCollapsed(`query ${endpoint}`);
78 | console.log(...args);
79 | console.log(response);
80 | console.groupEnd();
81 | }
82 |
83 | setResult(response);
84 | setError(null);
85 | resolve(response);
86 | });
87 |
88 | worker.postMessage({
89 | id,
90 | type: "query",
91 | payload: [endpoint, ...args],
92 | } as WorkerRequestQuery);
93 | });
94 | }, []);
95 |
96 | return [query, isQuerying, result, error];
97 | }
98 |
--------------------------------------------------------------------------------
/src/lib/hooks/workerProvider.tsx:
--------------------------------------------------------------------------------
1 | import type { WorkerResponse } from "@/lib/worker.ts";
2 | import { createContext, useContext, useRef } from "react";
3 |
4 | import AppWorker from "@/lib/worker.ts?worker";
5 |
6 | interface WorkerProviderContext {
7 | worker: Worker;
8 | pendingQueries: Map void>;
9 | }
10 |
11 | const WorkerContext = createContext(
12 | undefined,
13 | );
14 |
15 | interface WorkerProviderProps {
16 | children: React.ReactNode;
17 | }
18 |
19 | export const WorkerProvider = ({ children }: WorkerProviderProps) => {
20 | const pendingQueries = useRef