88 Commits

Author SHA1 Message Date
MoonLeeeaf
b38e156c79 docs: 停止维护v0版本 2024-07-11 20:30:26 +08:00
MoonLeeeaf
2281e92909 feat: 前端群聊部分 这将是铃之椅V1的最后一个版本发布 2024-07-11 20:20:17 +08:00
MoonLeeeaf
2a9708bca3 chore: 合并 2024-07-10 23:41:09 +08:00
MoonLeeeaf
2bde59e5cc chore: 重构, 日志函数装库, 修改启动文本
使用反引号来重构字符串相关代码行
将log函数分出到 log.js 中
重新编写了启动的提示文本
2024-07-10 23:40:50 +08:00
MoonLeeeaf
afcec52b79 feat: Pink for console text 2024-07-10 23:12:00 +08:00
满月叶
d38baca35f docs: 在注释添加色情文本以防止GitCode搬运本项目
<!-- 致某国内搬运GitHub的平台:别搬我,否则等死。色情小说在线看!链接:https://www.book18.org/哈!我只在Gi‌t‌H‌ub‌(Gay‌Hub)哦,不要亲信其它平台伪造的虚假账号!为防止本人账号被莫名搬运到GxxCode(某SDN的代码平台),因此特地收汇了很多黄色词汇!希望看到此注释的人不要太惊讶,我也不想被莫名搬运啊(),好,正文:爱女人爱液按摩棒拔出来爆草包二奶暴干暴奸暴乳爆乳暴淫屄被插被操被干逼奸仓井空插暴操逼操黑操烂肏你肏死操死操我厕奴插比插b插逼插进插你插我插阴潮吹潮喷成人dv成人电影成人论坛成人小说成人电成人电影成人卡通成人聊成人片成人视成人图成人文成人小成人电影成人论坛成人色情成人网站成人文学成人小说艳情小说成人游戏吃精赤裸抽插扌由插抽一插春药大波大力抽送大乳荡妇荡女盗撮多人轮发浪放尿肥逼粉穴封面女郎风月大陆干死你干穴肛交肛门龟头裹本国产av好嫩豪乳黑逼后庭后穴虎骑花花公子换妻俱乐部黄片几吧鸡吧鸡巴鸡奸寂寞男寂寞女妓女激情集体淫奸情叫床脚交金鳞岂是池中物金麟岂是池中物精液就去日巨屌菊花洞菊门巨奶巨乳菊穴开苞口爆口活口交口射口淫裤袜狂操狂插浪逼浪妇浪叫浪女狼友聊性流淫铃木麻凌辱漏乳露b乱交乱伦轮暴轮操轮奸裸陪买春美逼美少妇美乳美腿美穴美幼秘唇迷奸密穴蜜穴蜜液摸奶摸胸母奸奈美奶子男奴内射嫩逼嫩女嫩穴捏弄女优炮友砲友喷精屁眼品香堂前凸后翘强jian强暴强奸处女情趣用品情色拳交全裸群交惹火身材人妻人兽日逼日烂肉棒肉逼肉唇肉洞肉缝肉棍肉茎肉具揉乳肉穴肉欲乳爆乳房乳沟乳交乳头三级片骚逼骚比骚女骚水骚穴色逼色界色猫色盟色情网站色区色色色诱色欲色b少年阿宾少修正射爽射颜食精释欲兽奸兽交手淫兽欲熟妇熟母熟女爽片爽死我了双臀死逼丝袜丝诱松岛枫酥痒汤加丽套弄体奸体位舔脚舔阴调教偷欢偷拍推油脱内裤文做我就色无码舞女无修正吸精夏川纯相奸小逼校鸡小穴小xue写真性感妖娆性感诱惑 https://cn.pornhub.com https://zh.xhamster.com 来看色情罢!(喜) 针对国内的采取措施 希望各GitHub访友以及工作人员能理解 因为现在某SDN搬仓库越来越严重了,我们必须采取措施 -->
2024-07-06 16:58:59 +08:00
MoonLeeeaf
410fa1439d Merge branch 'main' of github.com:LingChair/LingChair 2024-06-30 12:29:00 +08:00
MoonLeeeaf
2f9c18d2b2 ui: 未登录用户的昵称修改为 未知用户 2024-06-30 12:28:46 +08:00
MoonLeeeaf
5bc4d1a01c ui: 未登录用户的昵称修改为 未登录用户 2024-06-30 12:27:43 +08:00
MoonLeeeaf
d39cf69902 docs: 修正小写为大写 2024-06-30 12:27:25 +08:00
满月叶
129ba2d27a docs: "~".toUpperCase() 2024-06-29 22:23:31 +08:00
MoonLeeeaf
81c314c734 docs: docs 2024-06-29 22:05:45 +08:00
MoonLeeeaf
81fde561b2 docs: 添加说明 2024-06-29 21:57:56 +08:00
MoonLeeeaf
097e9280d2 docs: 修正语法 2024-06-29 21:51:04 +08:00
MoonLeeeaf
95e27be9eb docs: NO CLONING 2024-06-29 21:40:58 +08:00
MoonLeeeaf
521d7590af docs: NO CLONING 2024-06-29 21:38:42 +08:00
MoonLeeeaf
6b5a33462f docs: NO CLONING 2024-06-29 21:35:19 +08:00
MoonLeeeaf
0baaa5b574 chore: 关于文本 2024-06-29 21:31:38 +08:00
MoonLeeeaf
80fd1157dd docs: 暂时移除语言切换 2024-06-29 19:55:45 +08:00
MoonLeeeaf
1a86464a32 style: html, body 定义处删去一个空格 2024-06-29 19:53:12 +08:00
MoonLeeeaf
31d78b39f3 fix: 再再再再次修Tab, 自动移除 div.mdui-tab-indicator 2024-06-29 19:52:04 +08:00
MoonLeeeaf
fb89952bcb fix: 再次修复Tab菜单右键关闭的问题
修复了:
当 a, b 时, 选中 a, 关闭 a, 造成 b 没有被选中
2024-06-29 19:29:04 +08:00
MoonLeeeaf
70b314947d docs: 排版中英文切换, 英文添加提示 2024-06-29 19:28:34 +08:00
MoonLeeeaf
27369097eb chore: 删除测试代码 2024-06-29 17:30:11 +08:00
MoonLeeeaf
76acd890f6 other: Tab栏右键事件添加相应注释 2024-06-29 17:28:00 +08:00
MoonLeeeaf
ebe5b43622 fix: 彻底修复 Tab 栏的关闭功能 2024-06-29 17:25:59 +08:00
MoonLeeeaf
19a685ca8b fix: Cannot read properties of null (reading 'previousElementSibling')
将测试代码注释即可
2024-06-29 17:18:37 +08:00
MoonLeeeaf
65bcc768d7 chore: add banner 2024-06-29 17:15:58 +08:00
MoonLeeeaf
dba5cbba5a fix: Tab右键菜单相关问题(未完成) 2024-06-28 22:04:54 +08:00
MoonLeeeaf
9d0eb8c4f8 refactor: CachedData.addToList(str -> obj) 2024-06-28 21:54:30 +08:00
MoonLeeeaf
7f1479ffda fix: Tab菜单的右键菜单事件
1. 如题
2. CachedString 重命名为 CachedData
2024-06-28 21:42:22 +08:00
MoonLeeeaf
4406b9c016 depend & style: see changelog
1. 文件注释头小修改
2. 迁移魔改版 mdui.css 到 MDUI-Modified 仓库
2024-06-28 21:17:41 +08:00
MoonLeeeaf
4c32b5235e depend & style: see changelog
1. 更换所有依赖文件的 CDN 为 jsDelivr
2. 分离魔改版 mdui.css 到 MDUI-Modified 仓库, 使用 jsDelivr CDN 供应
3. 注释头处小修改
2024-06-28 21:15:30 +08:00
MoonLeeeaf
8c709bf420 style: 备注头的小修改 2024-06-28 21:14:37 +08:00
MoonLeeeaf
98d36d1079 fix: MDUI css 导致的右键菜单的一系列问题
1. 彻底移除了导致右键菜单无法显示的 overflow-x: auto; 以及 overflow-y: hidden;
2. 使用 .mdui-tab > a 来代替 .mdui-tab a 防止误匹配导致右键菜单文本样式有误
2024-06-25 22:23:47 +08:00
MoonLeeeaf
d96f3163a2 fix: Material Icons (see changelog)
1. 换回未经压缩的 css 文件以方便修改
2. 修改 Material Icons 的依赖链接, 从本地相对路径指向 Unpkg CDN
2024-06-25 21:52:15 +08:00
MoonLeeeaf
b8e00858d3 chore: 迁移MDUI的css依赖到本地以便于魔改来满足复杂需求 2024-06-25 21:45:28 +08:00
MoonLeeeaf
baa6c084be fix(msg): 修复由于右键菜单获取的文本导致的HTML代码出错的问题 2024-06-25 21:40:20 +08:00
满月叶
81e9fb994d Merge pull request #1 from LingChair/dependabot/npm_and_yarn/multi-e091cc75b0
chore(deps): bump ws, engine.io and socket.io-adapter
2024-06-23 14:05:57 +08:00
dependabot[bot]
add555237a chore(deps): bump ws, engine.io and socket.io-adapter
Bumps [ws](https://github.com/websockets/ws), [engine.io](https://github.com/socketio/engine.io) and [socket.io-adapter](https://github.com/socketio/socket.io-adapter). These dependencies needed to be updated together.

Updates `ws` from 8.11.0 to 8.17.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.11.0...8.17.1)

Updates `engine.io` from 6.5.4 to 6.5.5
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.5.4...6.5.5)

Updates `socket.io-adapter` from 2.5.4 to 2.5.5
- [Release notes](https://github.com/socketio/socket.io-adapter/releases)
- [Changelog](https://github.com/socketio/socket.io-adapter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-adapter/compare/2.5.4...2.5.5)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
- dependency-name: engine.io
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-23 06:04:41 +00:00
MoonLeeeaf
20990ed3b1 fix(chat): 移动端输入体验优化 2024-06-23 14:00:02 +08:00
MoonLeeeaf
d4e3104ddf fix: auto resize on mobile 2024-06-23 13:44:49 +08:00
MoonLeeeaf
39b9127df8 fix: chat tab(progress: 40%) 2024-06-15 23:36:35 +08:00
MoonLeeeaf
45f557cd94 ui: 整合侧边栏功能到设置中, 更改设置入口为头像 2024-06-15 22:55:16 +08:00
MoonLeeeaf
bf224acb4b chore: remove useless code block 2024-06-15 22:44:45 +08:00
MoonLeeeaf
045ed60211 fix: tab menu 2024-06-15 22:41:41 +08:00
MoonLeeeaf
facf7cb3fe ui: 使项目链接颜色更深 2024-06-15 19:05:21 +08:00
MoonLeeeaf
5d7d7e7209 chore: 删除无用的测试文件 2024-06-15 18:37:30 +08:00
MoonLeeeaf
8446ba09c2 test: Tab Menu 2024-06-15 18:35:00 +08:00
MoonLeeeaf
d450797895 Merge branch 'main' of github.com:LingChair/LingChair 2024-06-15 18:32:50 +08:00
MoonLeeeaf
a326c16f1c fix: 异常数量的Tab栏指示器 2024-06-15 18:05:38 +08:00
MoonLeeeaf
3df9df6d85 style: ~ 2024-06-15 17:53:31 +08:00
MoonLeeeaf
585ea41831 change: 切换标签页不会滚动到底部 2024-06-15 12:44:57 +08:00
满月叶
12de793b26 docs: 使用h2代替h1 2024-06-15 10:34:26 +08:00
满月叶
84a5e52fbf docs: 修正英文readme语法错误 2024-06-15 10:33:57 +08:00
满月叶
436358e7c1 docs: add non-Chinese tip 2024-06-15 10:28:59 +08:00
满月叶
25d61b3a78 docs: remove a duplication language switch 2024-06-15 10:23:12 +08:00
满月叶
f2c9e51fd3 docs: readme_en 2024-06-15 10:19:34 +08:00
满月叶
cc5fcc1b02 docs: readme 2024-06-15 10:18:18 +08:00
满月叶
bf9ba20ede docs: English readme 2024-06-15 10:13:48 +08:00
满月叶
dcc4e040a5 docs: English readme 2024-06-15 10:09:42 +08:00
满月叶
733d5f76c3 docs: readme 2024-06-15 10:05:07 +08:00
MoonLeeeaf
1cb0dd3885 feat: 多标签页聊天, v0.8.0 2024-06-15 00:05:33 +08:00
MoonLeeeaf
d42caea57a feat: 多标签页聊天(实验性) 2024-06-14 23:50:33 +08:00
MoonLeeeaf
1a5afc8ad0 chore: rename symbols, add "原文" menu 2024-06-14 19:31:42 +08:00
MoonLeeeaf
4bdfad340f fix: image 2024-06-12 22:07:50 +08:00
MoonLeeeaf
6ac1b460bb feat: Markdown Code Dialog 2024-06-12 21:48:39 +08:00
MoonLeeeaf
29e224f87a test 2024-06-12 21:05:24 +08:00
MoonLeeeaf
a39973bb5c feat: markdown support 2024-06-11 22:03:41 +08:00
MoonLeeeaf
47afacbba3 chore: make limit happy 2024-06-01 14:33:40 +08:00
MoonLeeeaf
3a4d733c13 chore: 修改哈希输出(Base64改为Hex) 2024-06-01 14:33:26 +08:00
MoonLeeeaf
89263e6e2a chore: 命名 2024-06-01 14:09:45 +08:00
MoonLeeeaf
b2c8c86689 chore: 模块化前端js脚本 2024-05-31 00:08:55 +08:00
MoonLeeeaf
6654141c18 chore: 文件头, 输入框UI优化, 部分头像优化, 其他 2024-05-29 23:40:13 +08:00
MoonLeeeaf
822a4ad4da chore: 规范命名 2024-05-29 17:40:10 +08:00
MoonLeeeaf
9f456b95c1 chore: 0.7.1 released 2024-05-29 16:30:57 +08:00
MoonLeeeaf
7d2798d4fd fix: .gitignore 2024-05-26 00:56:50 +08:00
MoonLeeeaf
0ccee91b3e chore: go back 2024-05-26 00:29:56 +08:00
MoonLeeeaf
48bad65df5 rebase 2024-05-25 16:41:27 +08:00
MoonLeeeaf
5b55ca77ec rebase 2024-05-25 16:34:17 +08:00
MoonLeeeaf
71b5b5b2df test: webpack 2024-05-25 10:44:35 +08:00
MoonLeeeaf
9c1dc4c540 chore: readme 2024-05-24 20:54:35 +08:00
MoonLeeeaf
f3bf5f88b5 chore: 软键盘调整布局(测试中) 2024-05-19 23:11:31 +08:00
MoonLeeeaf
971b8aff85 chore: Settings dialog UI 2024-05-19 22:41:28 +08:00
MoonLeeeaf
ddfbe547e1 chore: readme 2024-05-19 13:47:08 +08:00
MoonLeeeaf
b4bbe08ff2 fix: 无法设置昵称 2024-05-19 12:17:00 +08:00
MoonLeeeaf
6ec0005122 chore: ui 2024-05-19 12:14:47 +08:00
MoonLeeeaf
541f043531 feat: 新的界面布局方式(WIP) 2024-05-18 16:13:29 +08:00
29 changed files with 2268 additions and 1135 deletions

39
.github/QA.md vendored Normal file
View File

@@ -0,0 +1,39 @@
### Q&A
#### 1. 协助项目
1. 若修改代码,请务必添加注释或者尽可能让方法名能被人所理解
2. 修改 readme 相关时,请不要留废话
3. 其它非代码注明性注释会不定期清理
4. 待补充
#### 2. 商业用途
不建议,不推荐,不赞同用于商业用途, 本项目的设计初衷是为了个人和团队使用, 而非用于圈钱, 因此如果确实需要商业化, 请考虑其他项目
因为这个项目还在初级阶段, 还有很多地方不够完善, 甚至有很多漏洞被利用, 造成不必要的损失
#### 3. 提问
1. 禁止人身攻击性回复
2. 请详细说明你的问题
3. 漏洞、BUG 类请给出代码位置
#### 4. 功能请求
目前不考虑,因为每一个做开源的人都应该以生活,以自己的感受为本
就像 weishu 大佬所说的:
> 我发现很多搞开源的开发者都把自己弄得很累
> 其实你把它当作是钓鱼,摩托,音响,单反那样的兴趣爱好就好多了
> 既然是爱好,就不要搞得像打第二份工一样,开心了就玩,玩腻了就扔一边
另外如果你提交功能 PR我有可能会直接 Close我没有太多精力去维护一个用不上的功能
#### 5. 待补充

1
.gitignore vendored
View File

@@ -1,3 +1,2 @@
node_modules/
ling_chair_data/
ling_chair_config/

View File

@@ -1,36 +1,21 @@
/*
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 消息! 消息!
*/
.chat-message-right {
.chat-message.right {
display: flex;
justify-content: flex-end;
align-items: flex-start;
margin: 13px;
margin: 13px 13px 13px 10%;
}
.chat-message-left {
.chat-message.left {
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin: 13px;
margin: 13px 10% 13px 13px;
}
.message-content {
@@ -39,9 +24,10 @@
margin-left: 5px;
margin-right: 5px;
max-width: 100%;
min-width: 0%;
white-space: normal;
word-break: break-all;
font-size: medium;
/* font-size: medium; */
/* 使用了 CardView 就不需要边框了 */
/* border: 1.3px solid; */
padding: 15px;
@@ -70,16 +56,22 @@
/* 左对齐元素 */
}
.chat-message-left .message-content-with-nickname-left .nickname,
.chat-message-right .message-content-with-nickname-right .nickname {
.chat-message .message-content-with-nickname-left .nickname,
.chat-message .message-content-with-nickname-right .nickname {
margin-right: 5px;
font-size: medium;
margin-top: 3px;
}
.chat-message-left > .avatar,
.chat-message-right > .avatar {
width: 45px;
height: 45px;
.chat-message > .avatar {
width: 50px;
height: 50px;
border-radius: 50%;
}
}
.message-image {
max-width: 40%;
max-height: 40%;
margin: -5px;
border-radius: 15px;
}

View File

@@ -0,0 +1,97 @@
/*
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 最终执行的杂项
*/
// 感觉 window.attr 比那一堆 import 好用多了
// ================================
// 正文开始
// ================================
// 没有刷新令牌需要重新登录 或者初始化
if (!localStorage.refreshToken || localStorage.refreshToken === "")
localStorage.isSignIn = false
if (!localStorage.server || localStorage.server === "")
setUpClient()
else
setUpClient(localStorage.server)
// 登录到账号
let dialogSignIn
// 谨防 localStorage 字符串数据大坑
if (localStorage.isSignIn == "false")
dialogSignIn = new mdui.Dialog(viewBinding.dialogSignIn.get(0), {
modal: true,
closeOnEsc: false,
history: false,
}).open()
else {
(async () => viewBinding.userNick.text(await NickCache.getNick(localStorage.userName)))()
let hello
let nowHour = new Date().getHours()
if (nowHour >= 6 && nowHour <= 11) hello = "早安"
else if (nowHour == 12) hello = "中午好"
else if (nowHour >= 13 && nowHour <= 18) hello = "下午好"
else if (nowHour >= 19 && nowHour < 22) hello = "晚上好"
else hello = "晚安"
viewBinding.helloText.text(hello)
viewBinding.userHead.attr("src", CurrentUser.getUserHeadUrl(localStorage.userName))
ContactsList.reloadList()
CurrentUser.registerCallback()
}
// 感谢AI的力量
Stickyfill.add($("*").filter((a, b) => $(b).css('position') === 'sticky'))
ChatMsgAdapter.initMsgElementEvents()
ChatMsgAdapter.initInputResizer()
ChatTabManager.initTabElementEvents()
const showLinkDialog = (link) => mdui.alert(decodeURI(link) + "<br/>如果你确认此链接是安全的, 那么请<a class=\"mdui-text-color-theme\" href=\"" + link + "\">点我</a>", '链接', () => { }, { confirmText: "关闭" })
const showImageDialog = (link, id, alt) => mdui.alert(`此图片链接来源未知: ${decodeURI(link)}<br/>如果你希望加载, 请<a class="mdui-text-color-theme" mdui-dialog-close onclick="$('#${id}').html('<img src=\\'${link}\\' alt=\\'${decodeURI(alt)}\\' class=\\'message-image\\'></img>')">点我</a>`, '外部图片', () => { }, { confirmText: "关闭" })
const showCodeDialog = (code) => mdui.alert(`<pre><code>${decodeURI(code)}</code></pre>`, '代码块', () => { }, { confirmText: "关闭" })
const renderer = {
heading(text, level) {
return text
},
paragraph(text) {
return text
},
blockquote(text) {
return text
},
link(href, title, text) {
return `<a class="mdui-text-color-theme" onclick="showLinkDialog('${encodeURI(href)}')">[链接] ${text}</a>`
},
image(href, title, text) {
let h = Hash.sha256(href)
let out = true
try {
out = new URL(href).hostname === new URL(location.href)
} catch(e) {}
if (out)
return `<img src="${encodeURI(href)}" alt="${text}" class="message-image"></img>`
else
return `<div id="${h}"><a class="mdui-text-color-theme" onclick="showImageDialog('${encodeURI(href)}', '${h}', '${encodeURI(text)}')">[外部图片] ${text}</a></div>`
},
code(src) {
return `<a class="mdui-text-color-theme" onclick="showCodeDialog(\`${encodeURI(src)}\`)">[代码块]</a>`
},
}
marked.use({
gfm: true,
renderer: renderer,
async: true,
})

732
ling_chair_http/handler.js Normal file
View File

@@ -0,0 +1,732 @@
/*
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 业务逻辑
*/
// ================================
// 当前用户
// ================================
class CurrentUser {
/** @type { String } */
static myAccessToken
/**
* 登录账号
* @param { String } name
* @param { String } passwd
* @param { Function } callback
*/
static signIn(name, passwd, cb) {
client.emit("user.signIn", {
name: name,
passwd: Hash.sha256(passwd) + Hash.md5(passwd),
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
cb(re)
})
}
/**
* 注册账号
* @param { String } name
* @param { String } passwd
* @param { Function } callback
*/
static signUp(name, passwd, cb) {
client.emit("user.signUp", {
name: name,
passwd: Hash.sha256(passwd) + Hash.md5(passwd),
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
cb(re)
})
}
/**
* 登录对话框中的登录逻辑
* @param { String } name
* @param { String } passwd
*/
static signInWithDialog(name, passwd) {
this.signIn(name, passwd, (re) => {
localStorage.refreshToken = re.data.refreshToken
localStorage.isSignIn = true
location.reload()
})
}
/**
* 设置昵称
* @param { String } nick
* @param { Function } callback
*/
static async setNick(nick, cb) {
client.emit("user.setNick", {
name: localStorage.userName,
accessToken: await this.getAccessToken(),
nick: nick,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
if (cb) cb()
})
}
/**
* 获取用户头像的链接
* @param { String } name
* @returns { String } headImageUrl
*/
static getUserHeadUrl(name) {
return client.io.uri + "/users_head/" + name + ".png"
}
/**
* 获取访问密钥
* @param { String } name
* @returns { Promise<String> } accessToken
*/
static async getAccessToken(er) {
if (this.myAccessToken == null)
this.myAccessToken = await new Promise((res) => {
client.emit("user.getAccessToken", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (r) => {
if (r.data != null) res(r.data.accessToken)
if (er != null) er(r.msg)
})
})
return this.myAccessToken
}
/**
* 请求上传头像
*/
static uploadHeadImage() {
viewBinding.uploadHeadImage.click()
}
/**
* 上传头像回调事件
* @param { Element } element
*/
static async uploadHeadImageCallback(self) {
let img = self.files[0]
client.emit("user.setHeadImage", {
name: localStorage.userName,
accessToken: await CurrentUser.getAccessToken(),
headImage: img,
}, (re) => mdui.snackbar(re.msg))
}
/**
* 验证用户
*/
static auth() {
client.emit("user.auth", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (re) => {
if (re.code !== 0) {
console.error(re)
if (!re.invalid)
return mdui.snackbar("验证用户失败!")
mdui.alert("账号刷新令牌已过期, 请重新登录哦", "提示", () => CurrentUser.signOutAndReload(), {
confirmText: "确定",
closeOnConfirm: false,
closeOnEsc: false,
modal: true,
})
}
})
}
/**
* 登出并重载页面
*/
static signOutAndReload() {
localStorage.refreshToken = ""
localStorage.isSignIn = false
setTimeout(() => location.reload(), 300)
}
/**
* 注册客户端回调事件
*/
static registerCallback() {
client.on("msg.receive", async (a) => {
if (checkEmpty([a.target, a.msg, a.type]))
return
let currentPage = ChatPage.getCurrentChatPage()
if ((currentPage.chatTarget === a.target) && (currentPage.chatType === a.type)) {
let i = ChatMsgAdapter.isAtBottom()
await currentPage.addMsg(a.target, a.msg.msg, a.msg.time, false, a.msg.msgid)
if (i) ChatMsgAdapter.scrollToBottom()
}
if (currentPage.chatTarget !== localStorage.userName) {
let n = new 通知().setTitle("" + await NickCache.getNick(a.target)).setMessage(a.msg.msg).setIcon(CurrentUser.getUserHeadUrl(a.target)).show(async () => {
await ChatMsgAdapter.switchTo(a.target, a.type)
location.replace("#msgid_" + a.msg.msgid)
n.close()
})
}
})
}
/**
* 打开资料卡
* @param { String } name
*/
static async openProfileDialog(name) {
viewBinding.dialogProfileHead.attr("src", CurrentUser.getUserHeadUrl(name))
viewBinding.dialogProfileNick.text(await NickCache.getNick(name))
new mdui.Dialog(viewBinding.dialogProfile).open()
}
}
// ================================
// 昵称缓存
// ================================
class NickCache {
static data = {}
/**
* 获取昵称
* @param { String } name
* @returns { String } nick
*/
static async getNick(name) {
return await new Promise((res, _rej) => {
// 这个this别摆着不放啊 不然两下就会去世
let nick = NickCache.data[name]
if (nick == null)
client.emit("user.getNick", { name: name }, (re) => {
let nk = re.data != null ? re.data.nick : name
if (nk == null) nk = name
NickCache.data[name] = nk
res(nk)
})
else
res(nick)
})
}
}
// ================================
// 联系人
// ================================
class ContactsList {
/**
* 重载联系人列表
*/
static async reloadList() {
client.emit("user.getFriends", {
name: localStorage.userName,
accessToken: await CurrentUser.getAccessToken(),
}, async (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
viewBinding.contactsList.empty()
let ls = re.data.friends
for (let index in ls) {
let name = ls[index]
let dick = await NickCache.getNick(name)
$($.parseHTML(`<li class="mdui-list-item mdui-ripple" mdui-drawer-close><div class="mdui-list-item-avatar"><img src="${CurrentUser.getUserHeadUrl(name)}" onerror="this.src='res/default_head.png'" /></div><div class="mdui-list-item-content">` + dick + `</div></li>`)).appendTo(viewBinding.contactsList).click(() => {
ChatMsgAdapter.switchTo(name, "single")
})
}
})
client.emit("user.getGroups", {
name: localStorage.userName,
accessToken: await CurrentUser.getAccessToken(),
}, async (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
viewBinding.groupsList.empty()
let ls = re.data.groups
for (let index in ls) {
let name = ls[index]
let dick = await NickCache.getNick(name)
$($.parseHTML(`<li class="mdui-list-item mdui-ripple" mdui-drawer-close><div class="mdui-list-item-avatar"><img src="${CurrentUser.getUserHeadUrl(name)}" onerror="this.src='res/default_head.png'" /></div><div class="mdui-list-item-content">` + dick + `</div></li>`)).appendTo(viewBinding.groupsList).click(() => {
ChatMsgAdapter.switchTo(name, "single")
})
}
})
}
/**
* 添加联系人/群峦
* @param { String } nameOrId
*/
static async add(name, type) {
if (type == "single") {
client.emit("user.addFriend", {
name: localStorage.userName,
target: name,
accessToken: await CurrentUser.getAccessToken(),
}, async (re) => {
// if (re.code !== 0)
return mdui.snackbar(re.msg)
})
}
}
/**
* 打开添加联系人的对话框
*/
static openAddDialog() {
new mdui.Dialog(viewBinding.dialogNewContact.get(0)).open()
}
}
// ================================
// 消息核心
// ================================
// 自古框架BUG多, 各种麻烦遭不住
class ChatTabManager {
static tabs = {}
/**
* 添加Tab
* @param { String } title
* @param { String } target
*/
static add(title, target) {
if (this.tabs[target]) return
let tabElement = $($.parseHTML(`<a onclick="ChatMsgAdapter.switchTo('${target}');" tag="chatTab" id="chatTab_${target}" target="${target}" class="mdui-ripple" style="text-transform: none;">${title}</a>`))
tabElement.appendTo(viewBinding.chatTab)
// 就你MDUI的B事最多 加Tab还多一个下划线 删掉就解决了
$(".mdui-tab-indicator").remove()
new mdui.Tab(viewBinding.chatTab).handleUpdate()
this.tabs[target] = tabElement
if (Object.keys(this.tabs).length == 1) tabElement.addClass("mdui-tab-active")
}
/**
* 寻找Tab
* @param { String } target
* @returns { jQuery } element
*/
static find(target) {
return this.tabs[target]
}
/**
* 点击Tab
* @param { String } target
*/
static click(target) {
this.find(target).get(0).click()
}
/**
* 删除Tab
* @param { String } target
*/
static remove(target) {
this.find(target).remove()
delete this.tabs[target]
if(Object.keys(this.tabs).length == 0)
viewBinding.chatTab.find('.mdui-tab-indicator').remove()
}
static initTabElementEvents() {
let menu
let callback = (e) => {
if (menu) menu.close()
// 切到 chatTab
// document.getElementById("").nextElementSibling
let ele = e.get(0)
while ($(ele).attr("tag") != "chatTab")
ele = ele.parentNode
// ele.previousElementSibling 是 Menu 的 Element, 因此改写成 ele.previousElementSibling.previousElementSibling
let menuHtml = $.parseHTML(`<ul class="mdui-menu">
<li class="mdui-menu-item">
<a onclick="let ele=CachedData.getAndRecycle('${CachedData.addToList(ele)}');let elenp=ele.previousElementSibling.previousElementSibling;if(!elenp){elenp=ele.nextElementSibling};let canclick=$(elenp).attr('target');if(canclick){ChatTabManager.click(canclick);}ChatPage.getChatSeesion($(ele).attr('target')).remove();if(canclick){ChatTabManager.click(canclick);}" class="mdui-ripple">关闭</a>
</li>
</ul>`)
let $menu = $(menuHtml)
e.before($menu)
menu = new mdui.Menu(e.get(0), menuHtml, {
position: "bottom",
align: "auto",
// covered: true,
})
$menu.on('closed.mdui.menu', () => {
$(menuHtml).remove()
})
menu.open()
}
viewBinding.chatTab.on('contextmenu dblclick', 'a[tag=chatTab]', (e) => {
let eventType = e.type
let self = $(e.target)
// 根据事件类型执行不同操作
switch (eventType) {
case 'contextmenu':
e.preventDefault() // 阻止默认行为
callback(self)
break
case 'dblclick':
//if (!isMobile()) return
callback(self)
break
}
})
}
}
class ChatPage {
static cached = {}
constructor(name, title, type) {
this.chatTarget = name
this.chatType = type
ChatTabManager.add(title, this.chatTarget)
this.chatPageElement = $($.parseHTML(`<div class="chat-seesion" id="chatPageTargetIs${this.chatTarget}" target="${this.chatTarget}"></div>`))
this.chatPageElement.hide()
this.chatPageElement.appendTo(viewBinding.pageChatSeesion)
;(async () => await this.loadMore())()
}
/**
* 获取某个聊天栏
* @param { String } target
* @returns { jQuery }
*/
static getChatSeesion(target) {
return ChatPage.cached[target]
}
/**
* 获取当前的聊天栏
* @returns { jQuery }
*/
static getCurrentChatSeesion() {
return $(".chat-seesion[actived=true]")
}
/**
* 获取当前聊天页面
* @returns { ChatPage }
*/
static getCurrentChatPage() {
return this.getChatSeesion($(".chat-seesion[actived=true]").attr("target"))
}
/**
* 切换选择的聊天对象
*/
async show() {
ChatTabManager.click(this.chatTarget)
this.minMsgId = null
for (let k of Object.keys(ChatPage.cached)) {
let cpe = ChatPage.getChatSeesion(k).chatPageElement
cpe.attr("actived", null)
cpe.hide()
}
$(this.chatPageElement).attr("actived", "true")
ChatTabManager.click(this.chatTarget)
$(this.chatPageElement).show()
}
/**
* 连带Tab一起销毁
*/
remove() {
ChatTabManager.remove(this.chatTarget)
ChatPage.cached[this.chatTarget].chatPageElement.remove()
delete ChatPage.cached[this.chatTarget]
}
/**
* 加载更多聊天记录
* @param { int } 加载数量
*/
async loadMore(limit) {
let histroy = await this.getHistroy(this.minMsgId, limit == null ? 13 : limit)
let chatPager = viewBinding.chatPager.get(0)
if (histroy.length == 0)
return mdui.snackbar("已经加载完了~")
let doReverse = this.minMsgId != null
this.minMsgId = histroy[0].msgid - 1
// 英语水平不够(
let scroll幅度 = 0
if (doReverse) histroy = histroy.reverse()
for (let index in histroy) {
let i = histroy[index]
let msgElement = await this.addMsg(i.name, i.msg, i.time, doReverse, i.msgid)
// 因为某些因素直接DEBUG到吐血 断点继续都不报错 原因不明
scroll幅度 = scroll幅度 + (msgElement == null ? 35 : getOffsetTop(chatPager, msgElement.get(0)))
}
chatPager.scrollBy({
top: scroll幅度,
behavior: 'smooth'
})
}
/**
* 获取聊天消息记录
* @param { int } 起始点
* @param { int } 获取数量
*/
async getHistroy(start, limit) {
if (this.chatType == "single")
return new Promise(async (res, _rej) => {
client.emit("user.getSingleChatHistroy", {
name: localStorage.userName,
target: this.chatTarget,
limit: limit,
accessToken: await CurrentUser.getAccessToken(),
startId: start,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
res(re.data.histroy)
})
})
throw new TypeError("Unsupported chat type!")
}
/**
* 发送消息
* @param { String } msg
*/
async send(msg) {
if (this.chatType == "single")
client.emit("user.sendSingleMsg", {
name: localStorage.userName,
target: this.chatTarget,
msg: msg,
accessToken: await CurrentUser.getAccessToken(),
}, async (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
viewBinding.inputMsg.val("")
// 微机课闲的没事干玩玩 发现私聊会多发一个(一个是本地的, 另一个是发送成功的) 选择一个关掉就好了
// 这里我选择服务端不发送回调, 不然多设备同步会吵死
// 错了 应该是客户端少发条才对 不然不能多设备同步
if (this.chatTarget !== localStorage.userName) {
let i = ChatMsgAdapter.isAtBottom()
await this.addMsg(localStorage.userName, msg, re.data.time, false, re.data.msgid)
if (i) ChatMsgAdapter.scrollToBottom()
}
})
throw new TypeError("Unsupported chat type!")
}
/**
* 添加系统消息
* @param { String } 消息
* @param { Boolean } 是否加到顶部
* @returns { jQuery } 消息元素
*/
addSystemMsg(msg, addToTop) {
let element
if (addToTop)
// 加到头部
element = $($.parseHTML(msg)).prependTo(this.chatPageElement)
else
// 加到尾部
element = $($.parseHTML(msg)).appendTo(this.chatPageElement)
return element
}
/**
* 添加聊天记录
* @param { String } name
* @param { String } msg
* @param { String } type
* @param { Boolean } 是否加到头部
* @param { String || int } 消息id
* @returns { jQuery } 消息元素
*/
async addMsg(name, preMsg, time, addToTop, msgid) {
let nick = await NickCache.getNick(name) // re.data == null ? name : re.data.nick
let msg
try {
msg = await marked.parse(preMsg)
} catch (error) {
console.log("解析消息失败: " + error)
msg = escapeHTML(preMsg)
}
let temp
if (name === localStorage.userName)
temp = `<div class="chat-message right">
<div class="message-content-with-nickname-right">
<span class="nickname">${nick}</span>
<div class="message-content mdui-card" tag="msg-card" id="msgid_${msgid}">
<span id="msg-content">${msg}</span>
<pre class="mdui-hidden" id="raw-msg-content">${preMsg}</pre>
</div>
</div>
<img class="avatar" src="${CurrentUser.getUserHeadUrl(name)}" onerror="this.src='res/default_head.png'" />
</div>`
else
temp = `<div class="chat-message left">
<img class="avatar" src="${CurrentUser.getUserHeadUrl(name)}" onerror="this.src='res/default_head.png'" />
<div class="message-content-with-nickname-left">
<span class="nickname">${nick}</span>
<div class="message-content mdui-card" tag="msg-card" id="msgid_${msgid}">
<span id="msg-content">${msg}</span>
<pre class="mdui-hidden" id="raw-msg-content">${preMsg}</pre>
</div>
</div>
</div>`
let nowMinutes = new Date(time).getMinutes()
let msgElement
if (addToTop) {
this.addSystemMsg(temp, addToTop)
if (this.minutesCache != nowMinutes) {
msgElement = this.addSystemMsg(`<div class="mdui-center">${new Date().format(time == null ? Date.parse("1000-1-1 00:00:00") : time, "yyyy年MM月dd日 hh:mm:ss")}</div>`, addToTop)
this.time = nowMinutes
}
} else {
if (this.minutesCache != nowMinutes) {
msgElement = this.addSystemMsg(`<div class="mdui-center">${new Date().format(time == null ? Date.parse("1000-1-1 00:00:00") : time, "yyyy年MM月dd日 hh:mm:ss")}</div>`, addToTop)
this.time = nowMinutes
}
this.addSystemMsg(temp, addToTop)
}
this.minutesCache = new Date(time).getMinutes()
return msgElement
}
}
class ChatMsgAdapter {
static type
static target
static resizeDick
/**
* 切换到某一个聊天对象
* @param { String } name
* @param { String } type
*/
static async switchTo(name, type) {
if (!ChatPage.cached[name])
ChatPage.cached[name] = new ChatPage(name, await NickCache.getNick(name), type)
ChatPage.cached[name].show()
}
/**
* 是否在底部
* @returns { Boolean } 是否在底部
*/
static isAtBottom() {
let elementRect = viewBinding.pageChatSeesion.get(0).getBoundingClientRect()
return (elementRect.bottom <= window.innerHeight)
}
/**
* 滑到底部
*/
static scrollToBottom() {
// 吐了啊 原来这样就行了 我何必在子element去整啊
viewBinding.chatPager.get(0).scrollBy({
top: 1145141919810,
behavior: 'smooth'
})
}
/**
* 初始化输入框位置调整器
*/
static initInputResizer() {
// 实验表面移动端切出输入法时会触发1-2次resize事件
// 可以利用这个特性来实现自动滚动文本
let resize = () => {
// CSS 牵一发而动全身 因此这个减少的数值是每天都要更改的
viewBinding.chatPager.height(window.innerHeight - viewBinding.inputToolbar.height() - $("header.mdui-appbar").height() - viewBinding.chatTab.height() - 17)
let ledi = this.resizeDick - window.innerHeight
let h = $('.chat-seesion[actived=true] > .chat-message:last-child').height()
if (isMobile()) viewBinding.chatPager.get(0).scrollBy({
// 5.19晚1056分调配出来的秘方
// < 0 为窗口变大
// cnm的调试十万次就你tm检测不到底是吧就你语法天天错误是吧
// 欺负我现在用不了电脑
top: (ledi > 0 ? (this.isAtBottom() ? viewBinding.inputToolbar.height() : -h * ledi / 20) : -h * ledi / 20),
behavior: 'smooth'
})
this.resizeDick = window.innerHeight
}
window.initInputResizerResize = resize
window.addEventListener("resize", resize)
resize()
}
/**
* 初始化消息框右击事件
*/
static initMsgElementEvents() {
let listeners = {}
let menu
let callback = (e) => {
if (menu) menu.close()
// 切到 div.message-content
let ele = e.get(0)
while ($(ele).attr("tag") != "msg-card")
ele = ele.parentNode
e = $(ele)
let rawText = e.find("#raw-msg-content").text()
let text = e.find("#msg-content").text()
let menuHtml = $.parseHTML(`<ul class="mdui-menu menu-on-message">
<li class="mdui-menu-item">
<a onclick="copyText(CachedString.getAndRecycle('${CachedData.addToList(text)}'))" class="mdui-ripple">复制</a>
</li>
<li class="mdui-menu-item">
<a onclick="mdui.alert(CachedString.getAndRecycle('${CachedData.addToList(rawText)}'), '消息原文', () => { }, { confirmText: '关闭' })" class="mdui-ripple">原文</a>
</li>
<li class="mdui-menu-item">
<a onclick="mdui.alert('未制作功能', '提示', () => { }, { confirmText: '关闭' })" class="mdui-ripple">转发</a>
</li>
</ul>`)
let $menu = $(menuHtml)
e.before($menu)
menu = new mdui.Menu(e.get(0), menuHtml, {
position: "bottom",
align: "right",
// covered: true,
})
$menu.on('closed.mdui.menu', () => {
$(menuHtml).remove()
})
menu.open()
}
viewBinding.pageChatSeesion.on('contextmenu mousedown mouseup', '.message-content', (e) => {
let eventType = e.type
let self = $(e.target)
// 根据事件类型执行不同操作
switch (eventType) {
case 'contextmenu':
e.preventDefault() // 阻止默认行为
callback(self)
break
case 'mousedown':
if (!isMobile()) return
listeners[self + ""] = setTimeout(() => {
callback(self)
}, 300) // 300颗够吗 应该够吧
break
case 'mouseup':
if (!isMobile()) return
clearTimeout(listeners[self + ""])
listeners[self + ""] = null
break
}
})
}
}
/**
* 刷新联系人列表以及昵称缓存
*/
function refreshAll() {
ContactsList.reloadList()
delete NickCache.data
NickCache.data = {}
}
window.User = CurrentUser
window.ContactsList = ContactsList
window.NickCache = NickCache
window.ChatPage = ChatPage
window.ChatMsgAdapter = ChatMsgAdapter
window.refreshAll = refreshAll

View File

@@ -1,29 +1,14 @@
/*
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
/*
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 铃之椅 网页端
*/
html, body {
html, body {
max-height: 100%;
margin: 0;
padding: 0;
/* overflow: hidden; */
overflow: hidden;
/*font: initial;*/
}
body {
@@ -49,6 +34,25 @@ body {
position: sticky;
bottom: 0;
display: block;
height: var(--pseudo-height); /* 设置伪元素的高度 */
z-index: -1; /* 防止遮挡实际内容 */
}
align-self: center;
}
.chat-seesion {
flex: 1 1 auto;
display: flex;
flex-direction: column;
position: relative;
}
/* https://segmentfault.com/q/1010000010391524 */
img {
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: -o-crisp-edges; /* Opera */
image-rendering: -webkit-optimize-contrast; /* Webkit (non-standard naming) */
image-rendering: crisp-edges;
-ms-interpolation-mode: nearest-neighbor; /* IE (non-standard property) */
}
img.round {
border-radius: 50%;
}

View File

@@ -1,26 +1,10 @@
<!doctype html>
<html lang="zh-cmn-Hans">
<!--
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
<!--
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 铃之椅 网页端
-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no" />
@@ -28,21 +12,22 @@
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<!-- 给老旧的设备提供支持 支持不了, 照样没法运行 -->
<!-- <script src='https://polyfill.io/v3/polyfill.min.js?features=default%2Cdom4%2Ces2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022%2Ces5%2Ces6%2Ces7'></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> -->
<!-- Maybe it can run :D -->
<script src='https://polyfill.io/v3/polyfill.min.js?features=default%2Cdom4%2Ces2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022%2Ces5%2Ces6%2Ces7'></script>
<!-- Styles -->
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" />
<!-- <link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" /> -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/LingChair/MDUI-Modified/dist/mdui.min.css" />
<link rel="stylesheet" href="index.css" />
<link rel="stylesheet" href="chat-message.css" />
<link rel="stylesheet" href="mdui-prettier.css" />
<!-- 代替私人 fixed 并提供更好的兼容性 -->
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/gh/wilddeer/stickyfill@2.1.0/dist/stickyfill.min.js"></script>
<script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></script>
<link rel="icon" href="icon.ico" />
<script src="https://cdn.jsdelivr.net/npm/marked@13.0.1/marked.min.js"></script>
<link rel="icon" href="res/icon.ico" />
<title>铃之椅</title>
</head>
@@ -55,36 +40,30 @@
<div id="lingchair-app" style="height: 100%;">
<!-- 侧滑栏 -->
<div class="mdui-drawer" id="main-drawer">
<ul class="mdui-list">
<li class="mdui-list-item mdui-ripple">
<ul class="mdui-list" mdui-collapse="{accordion: true}">
<li class="mdui-list-item mdui-ripple" onclick="new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()">
<div class="mdui-list-item-avatar">
<img src="default_head.png" n-id="userHead" onerror="this.src='default_head.png'" />
<img src="default_head.png" n-id="userHead" onerror="this.src='res/default_head.png'" />
</div>
<div class="mdui-list-item-content"><a n-id="helloText">早安</a>, <a n-id="userNick">Unknown</a></div>
<div class="mdui-list-item-content"><a n-id="helloText">早安</a>, <a n-id="userNick">未知用户</a></div>
</li>
<li class="mdui-subheader">个人</li>
<li class="mdui-list-item mdui-ripple" onclick="new mdui.Dialog(viewBinding.dialogMyProfile.get(0)).open()">
<i class="mdui-list-item-icon mdui-icon material-icons">account_circle</i>
<div class="mdui-list-item-content">资料</div>
<div class="mdui-subheader">聊天</div>
<li class="mdui-collapse-item">
<div class="mdui-collapse-item-header mdui-list-item mdui-ripple"><i class="mdui-list-item-icon mdui-icon material-icons">contacts</i>
<div class="mdui-list-item-content">联系人</div><i class="mdui-collapse-item-arrow mdui-icon material-icons">keyboard_arrow_down</i>
</div>
<div class="mdui-collapse-item-body mdui-list" n-id="contactsList">
</div>
</li>
<li class="mdui-list-item mdui-ripple" onclick="new mdui.Dialog(viewBinding.dialogMyProfile.get(0)).open()">
<i class="mdui-list-item-icon mdui-icon material-icons">person_add</i>
<div class="mdui-list-item-content">新的好友</div>
</li>
<li class="mdui-subheader">客户端</li>
<li class="mdui-list-item mdui-ripple">
<i class="mdui-list-item-icon mdui-icon material-icons">settings</i>
<div class="mdui-list-item-content">设置</div>
</li>
<li class="mdui-list-item mdui-ripple" n-id="drawerChangeServer">
<i class="mdui-list-item-icon mdui-icon material-icons">cloud_circle</i>
<div class="mdui-list-item-content">更换服务器</div>
</li>
<li class="mdui-list-item mdui-ripple" n-id="drawerSignOut">
<i class="mdui-list-item-icon mdui-icon material-icons">exit_to_app</i>
<div class="mdui-list-item-content">退出登录</div>
<li class="mdui-collapse-item">
<div class="mdui-collapse-item-header mdui-list-item mdui-ripple"><i class="mdui-list-item-icon mdui-icon material-icons">group</i>
<div class="mdui-list-item-content">群聊</div><i class="mdui-collapse-item-arrow mdui-icon material-icons">keyboard_arrow_down</i>
</div>
<div class="mdui-collapse-item-body mdui-list" n-id="groupsList">
</div>
</li>
</ul>
</div>
<!-- 应用栏 -->
@@ -96,7 +75,7 @@
</a>
<a class="mdui-typo-title" n-id="appTitle"></a>
<div class="mdui-toolbar-spacer"></div>
<a onclick="ContactsList.reloadList()" n-id="contactsRefresh" class="mdui-btn mdui-btn-icon mdui-ripple">
<a onclick="refreshAll()" n-id="contactsRefresh" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">refresh</i>
</a>
<a onclick="ContactsList.openAddDialog()" n-id="contactsAdd" class="mdui-btn mdui-btn-icon mdui-ripple">
@@ -120,38 +99,23 @@
<div class="mdui-tab mdui-accent-theme mdui-theme-color-auto" style="position: fixed; z-index: 114;width: 100%;"
mdui-tab n-id="chatTab">
<!-- 侧滑栏的 z-index 是2000, 在移动端会直接覆盖 -->
<a href="#page-chat-list" n-id="tabChatList" class="mdui-ripple">常用</a>
<a href="#page-contacts" n-id="tabContacts" class="mdui-ripple">通讯录</a>
<a href="#page-chat-seesion" n-id="tabChatSeesion" class="mdui-ripple" style="text-transform: none;"></a>
</div>
<div id="page-chat-list" class="mdui-p-a-2 container">
<div class="mdui-valign content" style="margin-top: 40px;">
<span class="mdui-center">欢迎回来! (^▽^。)</span>
</div>
</div>
<div id="page-contacts" class="mdui-p-a-2">
<ul class="mdui-list" style="margin-top: 30px;">
<li class="mdui-subheader">好友</li>
<div n-id="contactsList">
</div>
</ul>
</div>
<!-- 滚动到底部咋这么难写... -->
<div id="page-chat-seesion" class="mdui-p-a-2" style="display: flex;flex-direction: column;">
<div style="display: flex;flex-direction: column;">
<!-- 写时间居中写到吐了 这样式表不能要了 -->
<div
style="margin-top: 30px;overflow: auto;width: 100%;max-width: 100%;height: 100%;max-height: 100%;min-height: 0;margin-bottom: 40px;flex: 1 1 auto;display: flex;flex-direction: column;">
<div class="mdui-center" style="margin: 15px;"><a href="javascript:;" onclick="ChatMsgAdapter.loadMore()"
class="mdui-text-color-theme">点我</a>继续加载前面的聊天记录, 或者<a href="javascript:;"
style="margin-top: 50px;overflow: auto;width: 100%;max-width: 100%;height: 100%;max-height: 100%;min-height: 0;flex: 1 1 auto;display: flex;flex-direction: column;"
n-id="chatPager">
<div class="mdui-center" style="margin: 15px;"><a href="javascript:;" onclick="ChatPage.getCurrentChatPage().loadMore()"
class="mdui-text-color-theme">加载更多</a> | <a href="javascript:;"
onclick="ChatMsgAdapter.scrollToBottom()" class="mdui-text-color-theme">回到底部</a></div>
<div n-id="pageChatSeesion" style="flex: 1 1 auto;display: flex;flex-direction: column;position: relative;">
</div>
<div n-id="pageChatSeesion" class="chat-seesion" id="page-chat-seesion"></div>
<!-- 输入框和聊天消息重叠的原因就是死人 scrollbar, 把自动调整的距离调小, margin调大就行了 -->
</div>
<!-- 妈的黑化了 私人玩意这么难整 早知道 z-index 弄死它得了 浪费我时间 我就没试过这么离谱的样式表 第三方库真难写CSS 就应该先写后端的 啊啊啊啊啊啊 -->
<!-- 不黑化了 因为 stickyfill -->
<div class="mdui-toolbar mdui-theme-color-auto"
style="position: sticky;max-width: 100%;margin-bottom: -30px;bottom: 0;z-index: 101;"
n-id="inputToolbar">
style="position: sticky;max-width: 100%;margin-top: 1px;bottom: 0;z-index: 101;padding-top: 7px;" n-id="inputToolbar">
<ul class="mdui-menu" id="msg-input-more">
<li class="mdui-menu-item">
<a class="mdui-ripple">插入图片</a>
@@ -197,9 +161,9 @@
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple"
onclick="User.signUp(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val(), () => mdui.snackbar('注册成功, 请直接点击登录即可~'))">注册</button>
onclick="CurrentUser.signUp(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val(), () => mdui.snackbar('注册成功, 请直接点击登录即可~'))">注册</button>
<button class="mdui-btn mdui-ripple" n-id="dialogSignInEnter"
onclick="User.signInWithDialog(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val())">登录</button>
onclick="CurrentUser.signInWithDialog(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val())">登录</button>
</div>
</div>
</div>
@@ -219,29 +183,6 @@
</div>
</div>
<!-- 资料编辑对话框 -->
<div class="mdui-dialog" n-id="dialogMyProfile">
<div class="mdui-dialog-title">
资料
</div>
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<ul class="mdui-list">
<li class="mdui-list-item mdui-ripple" mdui-dialog-close
onclick="(async () => {viewBinding.dialogEditNickNick.val(await NickCache.getNick(localStorage.userName));new mdui.Dialog(viewBinding.dialogEditNick.get(0)).open()})()">
<i class="mdui-list-item-icon mdui-icon material-icons">edit</i>
<div class="mdui-list-item-content">修改昵称</div>
</li>
<li class="mdui-list-item mdui-ripple" onclick="User.uploadHeadImage()">
<i class="mdui-list-item-icon mdui-icon material-icons">account_circle</i>
<div class="mdui-list-item-content">上传头像</div>
</li>
</ul>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" mdui-dialog-close>关闭</button>
</div>
</div>
<!-- 编辑昵称对话框 -->
<div class="mdui-dialog" n-id="dialogEditNick">
<div class="mdui-dialog-title">
@@ -255,9 +196,9 @@
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" n-id="dialogEditNickClose" mdui-dialog-close
onclick="new mdui.Dialog(viewBinding.dialogMyProfile.get(0)).open()">关闭</button>
onclick="new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()">关闭</button>
<button class="mdui-btn mdui-ripple"
onclick="User.setNick(viewBinding.dialogEditNickNick.val(), () => {mdui.snackbar('已保存, 刷新页面生效');viewBinding.dialogEditNickClose.click()})">保存</button>
onclick="CurrentUser.setNick(viewBinding.dialogEditNickNick.val(), () => {mdui.snackbar('已保存, 刷新页面生效');viewBinding.dialogEditNickClose.click()})">保存</button>
</div>
</div>
@@ -269,7 +210,7 @@
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<div class="mdui-textfield">
<label class="mdui-textfield-label">昵称</label>
<input n-id="dialogEditNickNick" class="mdui-textfield-input" maxlength="30" type="text" />
<input n-id="" class="mdui-textfield-input" maxlength="30" type="text" />
</div>
</div>
<div class="mdui-dialog-actions">
@@ -284,7 +225,7 @@
</div>
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<div class="mdui-textfield">
<label class="mdui-textfield-label">好友/群的ID (不是名称)</label>
<label class="mdui-textfield-label">账号/群的ID (不是名称)</label>
<input n-id="dialogNewContactID" class="mdui-textfield-input" maxlength="30" type="text" />
</div>
<select class="mdui-select" mdui-select="{position: 'top'}" n-id="dialogNewContactType">
@@ -293,13 +234,46 @@
</select>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" mdui-dialog-close onclick="ContactsList.add(viewBinding.dialogNewContactID.val(), viewBinding.dialogNewContactType.val())">确认并关闭</button>
<button class="mdui-btn mdui-ripple" mdui-dialog-close
onclick="ContactsList.add(viewBinding.dialogNewContactID.val(), viewBinding.dialogNewContactType.val())">确认并关闭</button>
<button class="mdui-btn mdui-ripple" mdui-dialog-close>取消</button>
</div>
</div>
<!-- 懂得都懂 -->
<div class="mdui-dialog" n-id="dialogSettings">
<div class="mdui-dialog-title">
设置
</div>
<div class="mdui-dialog-content">
<ul class="mdui-list">
<div class="mdui-subheader">我的资料</div>
<li class="mdui-list-item mdui-ripple" mdui-dialog-close onclick="(async () => {viewBinding.dialogEditNickNick.val(await NickCache.getNick(localStorage.userName));new mdui.Dialog(viewBinding.dialogEditNick.get(0)).open()})()">
<i class="mdui-list-item-icon mdui-icon material-icons">edit</i>
<div class="mdui-list-item-content">修改昵称</div>
</li>
<li class="mdui-list-item mdui-ripple" onclick="CurrentUser.uploadHeadImage()">
<i class="mdui-list-item-icon mdui-icon material-icons">account_circle</i>
<div class="mdui-list-item-content">上传头像</div>
</li>
<div class="mdui-subheader">客户端</div>
<li class="mdui-list-item mdui-ripple" n-id="drawerChangeServer" mdui-dialog-close>
<i class="mdui-list-item-icon mdui-icon material-icons">cloud_circle</i>
<div class="mdui-list-item-content">更换服务器</div>
</li>
<li class="mdui-list-item mdui-ripple" n-id="settingsDialogSignOut" mdui-dialog-close>
<i class="mdui-list-item-icon mdui-icon material-icons">exit_to_app</i>
<div class="mdui-list-item-content">登出</div>
</li>
</ul>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" mdui-dialog-close>关闭</button>
</div>
</div>
<div class="mdui-hidden">
<input type="file" n-id="uploadHeadImage" name="选择头像" onchange="User.uploadHeadImageCallback(this)"
<input type="file" n-id="uploadHeadImage" name="选择头像" onchange="CurrentUser.uploadHeadImageCallback(this)"
accept="image/png, image/jpeg" />
</div>
@@ -307,12 +281,12 @@
<script src="https://unpkg.com/crypto-js@4.2.0/crypto-js.js"></script>
<script src="https://unpkg.com/socket.io-client@4.7.4/dist/socket.io.min.js"></script>
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
<!-- 加了babel也许能解决下浏览器兼容问题 -->
<!-- <script type="module" src="index.js"></script>
<script nomodule type="text/babel" src="index.js"></script> -->
<!-- 就算弄语法兼容其他老旧设备照样用不了, 比如我的 iPad4 -->
<script src="index.js"></script>
<!-- 核心脚本部分 -->
<script src="utils.js"></script>
<script src="manager.js"></script>
<script src="ui.js"></script>
<script src="handler.js"></script>
<script src="finally.js"></script>
</body>
</html>

View File

@@ -1,770 +0,0 @@
/*
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const UrlArgs = new URL(location.href).searchParams
function setOnRightClick(e, cb) {
if (!(e instanceof jQuery))
e = $(e)
let longPressTimer
if (!cb) throw new Error("定义回调!!!!")
e.on('contextmenu', function (e) {
e.preventDefault() // 阻止默认右键菜单
cb()
})
e.on('mousedown', function () {
longPressTimer = setTimeout(function () {
cb()
}, 1000)
})
e.on('mouseup', function () {
clearTimeout(longPressTimer)
});
}
if (UrlArgs.get("debug")) {
let script = document.createElement('script')
script.src = "//cdn.jsdelivr.net/npm/eruda"
document.body.appendChild(script)
script.onload = () => eruda.init()
}
// 经常会因为这个指定ID为位置导致一些莫名BUG
if (location.href.includes("#")) location.replace(location.href.substring(0, location.href.indexOf("#")))
const mdui_snackbar = mdui.snackbar
mdui.snackbar = (m) => {
let t = m
if (m instanceof Object)
t = JSON.stringify(m)
mdui_snackbar(t)
}
const checkEmpty = (i) => {
if (i instanceof Array) {
for (let k of i) {
if (checkEmpty(k)) return true
}
}
return (i == null) || ("" === i) || (0 === i)
}
function escapeHTML(str) {
return str.replace(/[<>&"']/g, function (match) {
switch (match) {
case '<':
return '&lt;'
case '>':
return '&gt;'
case '&':
return '&amp;'
case '"':
return '&quot;'
case "'":
return '&#39;'
default:
return match
}
})
}
class NData {
static mount(node) {
// 便捷获得指定组件
let es = node.querySelectorAll("[n-id]")
let ls = {}
es.forEach((i) => ls[$(i).attr("n-id")] = $(i))
// input 组件与 localStorage 绑定
es = node.querySelectorAll("[n-input-ls]")
es.forEach((e) => {
let j = $(e)
j.val(localStorage.getItem(j.attr("n-input-ls")))
j.blur(() => localStorage.setItem(j.attr("n-input-ls"), j.val()))
})
return ls
}
}
// 快捷获取指定视图
let viewBinding = NData.mount($("#app").get(0))
$.ajax({
url: "config.json",
dataType: "json",
success: (c) => {
viewBinding.appTitle.text(c.appTitle)
if (!c.canChangeServer) {
viewBinding.dialogSignInServerLabel.hide()
viewBinding.drawerChangeServer.hide()
}
},
})
// Toolbar 快捷按钮绑定
viewBinding.contactsRefresh.hide()
viewBinding.contactsAdd.hide()
viewBinding.tabChatList.on("show.mdui.tab", () => {
viewBinding.contactsRefresh.hide()
viewBinding.contactsAdd.hide()
})
viewBinding.tabContacts.on("show.mdui.tab", () => {
viewBinding.contactsRefresh.show()
viewBinding.contactsAdd.show()
})
viewBinding.tabChatSeesion.on("show.mdui.tab", () => {
viewBinding.contactsRefresh.hide()
viewBinding.contactsAdd.hide()
})
viewBinding.tabChatSeesion.hide()
// 关于页面
viewBinding.menuAbout.click(() => mdui.alert('GitHub: MoonLeeeaf<br/><br/>欢迎各位大佬访问我们的<a class="mdui-text-color-theme-accent" href="https://github.com/LingChair/LingChair">项目主页</a>', '关于 铃之椅', () => { }, { confirmText: "关闭" }))
viewBinding.drawerChangeServer.click(() => {
mdui.prompt('输入服务器地址...(为空则使用当前页面地址)', (value) => {
localStorage.server = value
mdui.snackbar("更新成功, 刷新页面生效")
}, () => { }, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.drawerSignOut.click(() => {
mdui.confirm('确定要登出账号吗', () => {
User.signOutAndReload()
}, () => { }, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.sendMsg.click((a) => {
let text = viewBinding.inputMsg.val()
if (text.trim() !== "")
ChatMsgAdapter.send(text)
})
viewBinding.inputMsg.keydown((e) => {
if (e.ctrlKey && e.keyCode === 13)
viewBinding.sendMsg.click()
})
viewBinding.dialogSignInPasswd.keydown((e) => {
if (e.keyCode === 13)
viewBinding.dialogSignInEnter.click()
})
viewBinding.switchNotifications.click((a) => {
if ((localStorage.useNotifications == "true" || localStorage.useNotifications != null) && localStorage.useNotifications != "false") {
localStorage.useNotifications = "false"
viewBinding.switchNotificationsIcon.text("notifications_off")
} else {
localStorage.useNotifications = "true"
viewBinding.switchNotificationsIcon.text("notifications")
}
})
if (localStorage.useNotifications == "true")
viewBinding.switchNotificationsIcon.text("notifications")
// https://www.runoob.com/w3cnote/javascript-copy-clipboard.html
function copyText(t) {
let btn = viewBinding.textCopierBtn
btn.attr("data-clipboard-text", t)
new ClipboardJS(btn.get(0)).on('success', (e) => {
e.clearSelection()
})
btn.click()
}
// https://zhuanlan.zhihu.com/p/162910462
Date.prototype.format = function (tms, format) {
let tmd = new Date(tms)
/*
* 例子: format="YYYY-MM-dd hh:mm:ss";
*/
var o = {
"M+": tmd.getMonth() + 1, // month
"d+": tmd.getDate(), // day
"h+": tmd.getHours(), // hour
"m+": tmd.getMinutes(), // minute
"s+": tmd.getSeconds(), // second
"q+": Math.floor((tmd.getMonth() + 3) / 3), // quarter
"S": tmd.getMilliseconds()
// millisecond
}
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (tmd.getFullYear() + "")
.substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k]
: ("00" + o[k]).substr(("" + o[k]).length));
}
}
return format;
}
// new mdui.Drawer('#main-drawer').close()
class NickCache {
static data = {}
static async getNick(name) {
return await new Promise((res, rej) => {
// 这个this别摆着不放啊 不然两下就会去世
let nick = this.data[name]
if (nick == null)
client.emit("user.getNick", { name: localStorage.userName }, (re) => {
let nk = re.data != null ? re.data.nick : name
if (nk == null) nk = name
this.data[name] = nk
res(nk)
})
else
res(nick)
})
}
}
// 既然已经有 Notification 了, 那用回中文也不过分吧 :)
class 通知 {
constructor() {
this.args = {}
this.title = ""
}
static checkAvailable() {
return ("Notification" in window)
}
static async request() {
if (!this.checkAvailable()) return false
return (await Notification.requestPermission())
}
setId(id) {
this.args.tag = id
return this
}
setTitle(t) {
this.title = t
return this
}
setMessage(m) {
this.args.body = m
return this
}
setIcon(i) {
this.args.icon = i
return this
}
setImage(i) {
this.args.image = i
return this
}
setData(data) {
this.args.data = data
}
show(onclick/*, onclose*/) {
if (!通知.checkAvailable()) return
if (localStorage.useNotifications !== "true") return
let n = new Notification(this.title, this.args)
n.onclick = onclick == null ? () => n.close() : (n) => onclick(n)
// n.onclose = onclose
// n.close()
return n
}
}
class ContactsList {
static async reloadList() {
client.emit("user.getFriends", {
name: localStorage.userName,
accessToken: await User.getAccessToken(),
}, async (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
viewBinding.contactsList.empty()
let ls = re.data.friends
for (let index in ls) {
let name = ls[index]
let dick = await NickCache.getNick(name)
/*client.emit("user.getNick", { name: localStorage.userName }, (re) => {
let nick = re.data == null ? re.data.nick : null
let name = ls[index]*/
$($.parseHTML(`<li class="mdui-list-item mdui-ripple"><div class="mdui-list-item-avatar"><img src="` + User.getUserHeadUrl(name) + `" onerror="this.src='default_head.png'" /></div><div class="mdui-list-item-content">` + dick + `</div></li>`)).appendTo(viewBinding.contactsList).click(() => {
ChatMsgAdapter.switchTo(name, "single")
})
//})
}
})
}
static add(name, type) {
if (type == "single") {
}
}
static openAddDialog() {
new mdui.Dialog(viewBinding.dialogNewContact.get(0)).open()
}
}
// 第一次写前端的消息加载, 代码很乱, 还请原谅~
class ChatMsgAdapter {
static type
static target
// static msgList
static minMsgId
static time
static bbn
// 切换聊天对象
static async switchTo(name, type) {
viewBinding.tabChatSeesion.show()
viewBinding.tabChatSeesion.text(await NickCache.getNick(name))
viewBinding.tabChatSeesion.get(0).click()
this.type = type
this.target = name
// this.msgList = []
this.minMsgId = null
viewBinding.pageChatSeesion.empty()
await this.loadMore()
this.scrollToBottom()
}
// 发送消息
static async send(msg) {
client.emit("user.sendSingleMsg", {
name: localStorage.userName,
target: this.target,
msg: msg,
accessToken: await User.getAccessToken(),
}, async (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
viewBinding.inputMsg.val("")
// 微机课闲的没事干玩玩 发现私聊会多发一个(一个是本地的, 另一个是发送成功的) 选择一个关掉就好了
// 这里我选择服务端不发送回调, 不然多设备同步会吵死
// 错了 应该是客户端少发条才对 不然不能多设备同步
if (ChatMsgAdapter.target !== localStorage.userName && ChatMsgAdapter.type === "single") {
let i = ChatMsgAdapter.isAtBottom()
await ChatMsgAdapter.addMsg(localStorage.userName, msg, re.data.time, re.data.msgid)
if (i) ChatMsgAdapter.scrollToBottom()
}
})
}
static async getHistroy(start, limit) {
return new Promise(async (res, rej) => {
client.emit("user.getSingleChatHistroy", {
name: localStorage.userName,
target: this.target,
limit: limit,
accessToken: await User.getAccessToken(),
startId: start,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
res(re.data.histroy)
})
})
}
static async loadMore(limit) {
let histroy = await this.getHistroy(this.minMsgId, limit == null ? 13 : limit)
if (histroy.length == 0)
return mdui.snackbar("已经加载完了~")
let re = this.minMsgId != null
this.minMsgId = histroy[0].msgid - 1
let sc = 0
if (re) histroy = histroy.reverse()
for (let index in histroy) {
let i = histroy[index]
let e = await this.addMsg(i.name, i.msg, i.time, re, i.msgid)
// 因为某些因素直接DEBUG到吐血 断点继续都不报错 原因不明
sc = sc + (e == null ? 25 : e.get(0).offsetTop)
}
window.scrollBy({
top: sc,
behavior: 'smooth'
})
}
static addSystemMsg(m, re) {
let e
if (re)
// 加到头部
e = $($.parseHTML(m)).prependTo(viewBinding.pageChatSeesion)
else
// 加到尾部
e = $($.parseHTML(m)).appendTo(viewBinding.pageChatSeesion)
return e
}
static isAtBottom() {
let elementRect = viewBinding.pageChatSeesion.get(0).getBoundingClientRect()
return (elementRect.bottom <= window.innerHeight)
}
// 添加消息 返回消息的JQ对象
// name: 用户id m: 消息 t: 时间戳 re: 默认加到尾部 msgid: 消息id
static async addMsg(name, m, t, re, msgid) {
let nick = await NickCache.getNick(name) // re.data == null ? name : re.data.nick
let msg = escapeHTML(m)
let temp
if (name === localStorage.userName)
temp = `<div class="chat-message-right">
<div class="message-content-with-nickname-right">
<span class="nickname">` + nick + `</span>
<div class="message-content mdui-card" id="msgid_` + msgid + `">
<span id="msg-content">` + msg + `</span>
</div>
</div>
<img class="avatar" src="` + User.getUserHeadUrl(name) + `" onerror="this.src='default_head.png'" />
</div>`
else
temp = `<div class="chat-message-left">
<img class="avatar" src="` + User.getUserHeadUrl(name) + `" onerror="this.src='default_head.png'" />
<div class="message-content-with-nickname-left">
<span class="nickname">` + nick + `</span>
<div class="message-content mdui-card" id="msgid_` + msgid + `">
<span id="msg-content">` + msg + `</span>
</div>
</div>
</div>`
let bn = new Date(t).getMinutes()
let e
if (re) {
this.addSystemMsg(temp, re)
if (this.bbn != bn) {
e = this.addSystemMsg(`<div class="mdui-center">` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `</div>`, re)
this.time = bn
}
} else {
if (this.bbn != bn) {
e = this.addSystemMsg(`<div class="mdui-center">` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `</div>`, re)
this.time = bn
}
this.addSystemMsg(temp, re)
}
this.bbn = new Date(t).getMinutes()
return e
}
// 添加消息记录 作用在 UI 和 msgList
/* static async addMsgLocal(name, m, t, msgid) {
this.msgList.push({
name: name,
msg: m,
msgid: msgid,
})
this.addMsg(name, m, t)
} */
// 从服务器加载一些聊天记录, limit默认=13
static async loadMsgs(limit) {
let histroy = await this.getHistroy(this.msgList[0] == null ? null : this.msgList[0].msgid - 1, limit == null ? 13 : limit)
this.msgList = histroy
}
/* static async loadMsgsFromList(lst) {
for (let index in lst) {
let i = lst[index]
await this.addMsg(i.name, i.msg, i.time)
}
} */
static scrollToBottom() {
// 吐了啊 原来这样就行了 我何必在子element去整啊
window.scrollBy({
top: 1145141919810,
behavior: 'smooth'
});
}
// 从本地加载
/*static loadMsgsFromLocal(target) {
let data = localStorage["chat_msg_" + target]
if (data == null || data === "[]")
return []
return JSON.parse(data)
}
// 把当前聊天记录储存到本地
static saveToLocal() {
localStorage["chat_msg_" + this.target] = JSON.stringify(this.msgList)
}*/
// 自动调整使输入框置底 CSS真tm靠不住啊
static initInputResizer() {
let resize = () => viewBinding.pageChatSeesion.height(window.innerHeight - viewBinding.inputToolbar.height() - $("header.mdui-appbar").height() - viewBinding.chatTab.height() - 90)
window.addEventListener("resize", resize)
resize()
}
// 为消息设置长按/右键事件
static initMsgElementEvents() {
let listeners = {}
let menu
let callback = (e) => {
if (menu) menu.close()
// 从 span 切到 div
if (e.get(0).tagName.toLowerCase() != "div") e = $(e.get(0).parentNode)
// 从 消息框 切到 更上层
e = $(e.get(0).parentNode)
let menuHtml = $.parseHTML(`<ul class="mdui-menu menu-on-message">
<li class="mdui-menu-item">
<a onclick="copyText(\`` + e.find("#msg-content").text() + `\`)" class="mdui-ripple">复制</a>
</li>
<li class="mdui-menu-item">
<a onclick="mdui.alert('未制作功能', '提示', () => { }, { confirmText: '关闭' })" class="mdui-ripple">转发</a>
</li>
</ul>`)
let $menu = $(menuHtml)
e.before($menu)
menu = new mdui.Menu(e.get(0), menuHtml, {
position: "bottom",
align: "right",
// covered: true,
})
$menu.on('closed.mdui.menu', () => {
$(menuHtml).remove()
})
menu.open()
}
viewBinding.pageChatSeesion.on('contextmenu mousedown mouseup', '.message-content', (e) => {
let eventType = e.type
let self = $(e.target)
// 根据事件类型执行不同操作
switch (eventType) {
case 'contextmenu':
e.preventDefault() // 阻止默认行为
callback(self)
break
case 'mousedown':
listeners[self + ""] = setTimeout(() => {
callback(self)
}, 300) // 300颗够吗 应该够吧
break
case 'mouseup':
clearTimeout(listeners[self + ""])
listeners[self + ""] = null
break
}
})
}
}
class Hash {
static md5(data) {
return CryptoJS.MD5(data).toString(CryptoJS.enc.Base64)
}
static sha256(data) {
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Base64)
}
}
class User {
static myAccessToken
// 登录账号 通过回调函数返回刷新令牌
static signIn(name, passwd, cb) {
client.emit("user.signIn", {
name: name,
passwd: Hash.sha256(passwd) + Hash.md5(passwd),
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
cb(re)
})
}
static signUp(name, passwd, cb) {
client.emit("user.signUp", {
name: name,
passwd: Hash.sha256(passwd) + Hash.md5(passwd),
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
cb(re)
})
}
// 为登录对话框编写的
static signInWithDialog(name, passwd) {
this.signIn(name, passwd, (re) => {
localStorage.refreshToken = re.data.refreshToken
localStorage.isSignIn = true
location.reload()
})
}
static async setNick(nick, cb) {
client.emit("user.setNick", {
name: localStorage.userName,
accessToken: await this.getAccessToken(),
nick: nick,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
if (cb) cb()
})
}
// 获取头像链接
static getUserHeadUrl(name) {
return client.io.uri + "/users_head/" + name + ".png"
}
static async getAccessToken(er) {
if (this.myAccessToken == null)
this.myAccessToken = await new Promise((res) => {
client.emit("user.getAccessToken", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (r) => {
if (r.data != null) res(r.data.accessToken)
if (er != null) er(r.msg)
})
})
return this.myAccessToken
}
static uploadHeadImage() {
viewBinding.uploadHeadImage.click()
}
static async uploadHeadImageCallback(self) {
let img = self.files[0]
client.emit("user.setHeadImage", {
name: localStorage.userName,
accessToken: await User.getAccessToken(),
headImage: img,
}, (re) => mdui.snackbar(re.msg))
}
static auth() {
client.emit("user.auth", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (re) => {
if (re.code !== 0) {
console.error(re)
if (!re.invalid)
return mdui.snackbar("验证用户失败!")
mdui.alert("账号刷新令牌已过期, 请重新登录哦", "提示", () => User.signOutAndReload(), {
confirmText: "确定",
closeOnConfirm: false,
closeOnEsc: false,
modal: true,
})
}
})
}
static signOutAndReload() {
localStorage.refreshToken = ""
localStorage.isSignIn = false
setTimeout(() => location.reload(), 300)
}
static registerCallback() {
client.on("msg.receive", async (a) => {
if (checkEmpty([a.target, a.msg, a.type]))
return
if ((ChatMsgAdapter.target === a.target) && (ChatMsgAdapter.type === a.type)) {
let i = ChatMsgAdapter.isAtBottom()
await ChatMsgAdapter.addMsg(a.target, a.msg.msg, a.msg.time)
if (i) ChatMsgAdapter.scrollToBottom()
}
let n = new 通知().setTitle("新消息 - " + await NickCache.getNick(a.target)).setMessage(a.msg.msg).setIcon(User.getUserHeadUrl(a.target)).show(async () => {
await ChatMsgAdapter.switchTo(a.target, a.type)
location.replace("#msgid_" + a.msg.msgid)
n.close()
})
})
}
static async openProfileDialog(name) {
viewBinding.dialogProfileHead.attr("src", User.getUserHeadUrl(name))
viewBinding.dialogProfileNick.text(await NickCache.getNick(name))
new mdui.Dialog(viewBinding.dialogProfile).open()
}
}
// 没有刷新令牌需要重新登录 或者初始化
if (!localStorage.refreshToken || localStorage.refreshToken === "")
localStorage.isSignIn = false
let client
function setUpClient(server) {
if (server && server !== "")
client = new io(server, {
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
else
client = new io({
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
client.on("connect", () => {
User.auth()
})
}
if (!localStorage.server || localStorage.server === "")
setUpClient()
else
setUpClient(localStorage.server)
// 登录到账号
let dialogSignIn
// 谨防 localStorage 字符串数据大坑
if (localStorage.isSignIn == "false")
dialogSignIn = new mdui.Dialog(viewBinding.dialogSignIn.get(0), {
modal: true,
closeOnEsc: false,
history: false,
}).open()
else {
(async () => viewBinding.userNick.text(await NickCache.getNick(localStorage.userName)))()
let hello
let nowHour = new Date().getHours()
if (nowHour >= 6 && nowHour <= 11) hello = "早安"
else if (nowHour == 12) hello = "中午好"
else if (nowHour >= 13 && nowHour <= 18) hello = "下午好"
else if (nowHour >= 19 && nowHour < 22) hello = "晚上好"
else hello = "晚安"
viewBinding.helloText.text(hello)
viewBinding.userHead.attr("src", User.getUserHeadUrl(localStorage.userName))
ContactsList.reloadList()
User.registerCallback()
}
// 感谢AI的力量
Stickyfill.add($("*").filter((a, b) => $(b).css('position') === 'sticky'))
ChatMsgAdapter.initMsgElementEvents()
ChatMsgAdapter.initInputResizer()

View File

@@ -0,0 +1,36 @@
/*
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 资源类
*/
const viewBinding = NData.mount($("#app").get(0))
let client
/**
* 初始化客户端
* @param {String} 服务器地址
*/
function setUpClient(server) {
if (server && server !== "")
client = new io(server, {
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
else
client = new io({
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
client.on("connect", () => {
User.auth()
})
}
window.viewBinding = viewBinding
window.setUpClient = setUpClient
window.client = client

View File

@@ -1,20 +1,20 @@
/*
* ©2024 满月叶
* GitHub: MoonLeeeaf
* GitHub @MoonLeeeaf
* 是 UI 美化,好耶!
*/
/* 美化UI */
/* 恢复系统字体 */
body {
font-family: -apple-system, system-ui, -webkit-system-font;
}
/* 圆角化 */
.mdui-dialog {
border-radius: 23px;
}
.mdui-snackbar {
border-radius: 10px;
}
.mdui-menu {
border-radius: 10px;
}
@@ -37,13 +37,22 @@ body {
.mdui-select-open {
border-radius: 10px;
}
@media screen and (min-width: 768px) {
.mdui-snackbar {
border-radius: 10px;
}
}
/* 配色方案 */
.mdui-theme-color-auto {
background-color: #fff;
.mdui-list-item-avatar {
background-color: rgba(0, 0, 0, 0) !important;
}
/* 背景底色 */
.mdui-theme-color-auto {
background-color: rgba(0, 0, 0, 0);
}
@media (prefers-color-scheme: dark) {
.mdui-theme-color-auto {
background-color: #303030;

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

76
ling_chair_http/ui.js Normal file
View File

@@ -0,0 +1,76 @@
/*
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 界面逻辑
*/
$.ajax({
url: "res/config.json",
dataType: "json",
success: (c) => {
viewBinding.appTitle.text(c.appTitle)
if (!c.canChangeServer) {
viewBinding.dialogSignInServerLabel.hide()
viewBinding.drawerChangeServer.hide()
}
},
})
// 关于页面
viewBinding.menuAbout.click(() => mdui.alert('为人民服务<br/>GitHub @MoonLeeeaf<br/>欢迎访问我们的<a class="mdui-text-color-theme" href="https://github.com/LingChair/LingChair">项目主页</a>', '关于 铃之椅', () => { }, { confirmText: "关闭" }))
viewBinding.drawerChangeServer.click(() => {
mdui.prompt('输入服务器地址...(为空则使用当前地址)', (value) => {
localStorage.server = value
mdui.snackbar("更新成功, 刷新页面生效")
new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()
}, () => {
new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()
}, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.settingsDialogSignOut.click(() => {
mdui.confirm('确定要登出账号吗', () => {
User.signOutAndReload()
}, () => {
new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()
}, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.sendMsg.click((a) => {
let text = viewBinding.inputMsg.val()
if (text.trim() !== "")
ChatPage.getCurrentChatPage().send(text)
})
viewBinding.inputMsg.keydown((e) => {
if (e.ctrlKey && e.keyCode === 13)
viewBinding.sendMsg.click()
})
viewBinding.dialogSignInPasswd.keydown((e) => {
if (e.keyCode === 13)
viewBinding.dialogSignInEnter.click()
})
viewBinding.switchNotifications.click((a) => {
if ((localStorage.useNotifications == "true" || localStorage.useNotifications != null) && localStorage.useNotifications != "false") {
localStorage.useNotifications = "false"
viewBinding.switchNotificationsIcon.text("notifications_off")
} else {
localStorage.useNotifications = "true"
viewBinding.switchNotificationsIcon.text("notifications")
}
})
if (localStorage.useNotifications == "true")
viewBinding.switchNotificationsIcon.text("notifications")
viewBinding.inputMsg.blur(() => {
window.initInputResizerResize()
})

258
ling_chair_http/utils.js Normal file
View File

@@ -0,0 +1,258 @@
/*
* ©2024 满月叶
* GitHub @MoonLeeeaf
* 辅助添加
*/
// 2024.5.28 睡着了
const sleep = (t) => new Promise((res) => setTimeout(res, t))
const UrlArgs = new URL(location.href).searchParams
// https://www.ruanyifeng.com/blog/2021/09/detecting-mobile-browser.html
function isMobile() {
return ('ontouchstart' in document.documentElement);
}
if (UrlArgs.get("debug")) {
let script = document.createElement('script')
script.src = "//cdn.jsdelivr.net/npm/eruda"
document.body.appendChild(script)
script.onload = () => eruda.init()
}
// 经常会因为这个指定ID为位置导致一些莫名BUG
if (location.href.includes("#")) location.replace(location.href.substring(0, location.href.indexOf("#")))
const mdui_snackbar = mdui.snackbar
mdui.snackbar = (m) => {
let t = m
if (m instanceof Object)
t = JSON.stringify(m)
mdui_snackbar(t)
}
const checkEmpty = (i) => {
if (i instanceof Array) {
for (let k of i) {
if (checkEmpty(k)) return true
}
}
return (i == null) || ("" === i) || (0 === i)
}
// AI的力量太强了
function getOffsetTop(parent, child) {
let top = 0
while (child && child !== parent) {
top += child.offsetTop
child = child.offsetParent
}
return top
}
function escapeHTML(str) {
return str.replace(/[<>&"']/g, function (match) {
switch (match) {
case '<':
return '&lt;'
case '>':
return '&gt;'
case '&':
return '&amp;'
case '"':
return '&quot;'
case "'":
return '&#39;'
default:
return match
}
})
}
class NData {
/**
* 获取 MD5sum
* @param {String} 数据
*/
static mount(node) {
// 便捷获得指定组件
let es = node.querySelectorAll("[n-id]")
let ls = {}
es.forEach((i) => ls[$(i).attr("n-id")] = $(i))
// input 组件与 localStorage 绑定
es = node.querySelectorAll("[n-input-ls]")
es.forEach((e) => {
let j = $(e)
j.val(localStorage.getItem(j.attr("n-input-ls")))
j.blur(() => localStorage.setItem(j.attr("n-input-ls"), j.val()))
})
return ls
}
}
// https://www.runoob.com/w3cnote/javascript-copy-clipboard.html
/**
* 复制文字
* @param {String} 欲复制的文本
*/
function copyText(t) {
let btn = $("[n-id=textCopierBtn]")
btn.attr("data-clipboard-text", t)
new ClipboardJS(btn.get(0)).on('success', (e) => {
e.clearSelection()
})
btn.click()
}
// https://zhuanlan.zhihu.com/p/162910462
/**
* 格式化日期
* @param {int} 时间戳
* @param {String} 欲格式化的文本
* @returns {String} 格式后的文本
*/
Date.prototype.format = function (tms, format) {
let tmd = new Date(tms)
/*
* 例子: format="YYYY-MM-dd hh:mm:ss";
*/
var o = {
"M+": tmd.getMonth() + 1, // month
"d+": tmd.getDate(), // day
"h+": tmd.getHours(), // hour
"m+": tmd.getMinutes(), // minute
"s+": tmd.getSeconds(), // second
"q+": Math.floor((tmd.getMonth() + 3) / 3), // quarter
"S": tmd.getMilliseconds()
// millisecond
}
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (tmd.getFullYear() + "")
.substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k]
: ("00" + o[k]).substr(("" + o[k]).length));
}
}
return format;
}
// 既然已经有 Notification 了, 那用回中文也不过分吧 :)
class 通知 {
constructor() {
this.args = {}
this.title = ""
}
static checkAvailable() {
return ("Notification" in window)
}
static async request() {
if (!this.checkAvailable()) return false
return (await Notification.requestPermission())
}
setId(id) {
this.args.tag = id
return this
}
setTitle(t) {
this.title = t
return this
}
setMessage(m) {
this.args.body = m
return this
}
setIcon(i) {
this.args.icon = i
return this
}
setImage(i) {
this.args.image = i
return this
}
setData(data) {
this.args.data = data
}
show(onclick/*, onclose*/) {
if (!通知.checkAvailable()) return
if (localStorage.useNotifications !== "true") return
let n = new Notification(this.title, this.args)
n.onclick = onclick == null ? () => n.close() : (n) => onclick(n)
return n
}
}
class Hash {
/**
* 获取 MD5sum
* @param {String} 数据
* @returns {String} Hex化的哈希值
*/
static md5(data) {
return CryptoJS.MD5(data).toString(CryptoJS.enc.Hex)
}
/**
* 获取 SHA256sum
* @param {String} 数据
* @returns {String} Hex化的哈希值
*/
static sha256(data) {
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex)
}
}
class CachedData {
static cache = {}
/**
* 添加缓存对象
* @param {Object} 欲缓存的对象
* @returns {String} 该对象的ID
*/
static addToList(obj) {
let id = Hash.sha256(obj)
this.cache[id] = obj
return id
}
/**
* 回收字符
* @param {String} 该对象的ID
*/
static recycle(id) {
this.cache[id] = null
}
/**
* 根据ID获取文本
* @param {String} 该对象的ID
* @returns {Object} 对象
*/
static get(id) {
return this.cache[id]
}
/**
* 根据ID获取文本并回收
* @param {String} 该文本的ID
* @returns {Object} 对象
*/
static getAndRecycle(id) {
let t = this.get(id)
this.recycle(id)
return t
}
}
window.copyText = copyText
window.NData = NData
window.escapeHTML = escapeHTML
window.isMobile = isMobile
window.checkEmpty = checkEmpty
window.sleep = sleep
window.Hash = Hash
window.通知 = 通知
window.CachedString = CachedData

845
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "lingchair",
"description": "A simple, lightweight and powerful Instant Messaging application.",
"version": "0.6.1",
"version": "0.7.0",
"license": "Apache License 2.0",
"author": {
"name": "MoonLeeeaf",
@@ -11,7 +11,8 @@
"start": "node ./server_src/main.js"
},
"dependencies": {
"mime": "^4.0.1",
"express": "^4.19.2",
"sharp": "^0.33.4",
"socket.io": "^4.7.5"
},
"type": "commonjs"

View File

@@ -1,29 +1,48 @@
## 铃之椅
<div align="center">
<h2> 铃之椅 </h2>
</div>
欢迎来到铃之椅! 这是一个即时通讯项目, 为通讯提供更多的选择, 为人民服务
> [!WARNING]
> 该版本因存在过多复杂且难以理解的代码写法以及杂乱不堪的前端代码而停止开发, 同时也在准备开发 v1 版本, 不建议大规模将此实验版本投入使用, 否则后果自负
> [!NOTE]
> 拒绝一切未经授权的项目搬运!铃之椅只会在 GitHub 上发布, 不可能在 Gitee、GitCode 等平台发布!
>
> 本项目仍在实验阶段, [点我](final.md)可查看进展
>
> 如果发现有任何疑问, 欢迎提问
>
> 另外 Android 客户端也在开发, 但因为从头开始写且生地中考临近, 因此暂时搁置
> 欢迎各位提出项目修改意见
### 衍生项目
客户端 & 服务端:
* LingChair-Node.js (本仓库)
客户端:
* 暂无
服务端:
* 暂无
### 使用
服务端:
1. 克隆本仓库源代码到本地
0. 安装 Node.js
2. 运行 run.sh 或 run.bat
1. 克隆或下载本仓库源代码,执行`npm install`,再执行`npm run start` 或者运行 run.bat / run.sh
网页端:
客户端:
* 直接使用服务端集成的网页 (推荐)
* 克隆本仓库到本地并运行本地 HTTP 服务端
* 使用服务端提供的网页 (推荐)
* 静态网页 (不推荐, 有跨域问题)
* GitHub Pages (可能导致跨域问题)
### [Q&A](.github/QA.md)
### 鸣谢
@@ -31,10 +50,4 @@ WIP
### [你知道吗](.github/do_you_know.md)
### 温馨提示
不建议,不推荐,不赞同用于商业用途, 本项目的设计初衷是为了个人和团队使用, 而非用于圈钱, 因此如果确实需要商业化, 请考虑其他项目
为什么? 因为这个项目还在初级阶段, 还有很多地方不够完善, 甚至有可能被一些 Hacker 抓到漏洞, 造成不必要的损失
因此我更建议个人及团队内部使用, 亦或者加密后放在公网使用
<!-- 致某国内搬运GitHub的平台别搬我否则等死。色情小说在线看链接https://www.book18.org/哈我只在GitHubGayHub不要亲信其它平台伪造的虚假账号为防止本人账号被莫名搬运到GxxCode某SDN的代码平台因此特地收汇了很多黄色词汇希望看到此注释的人不要太惊讶我也不想被莫名搬运啊正文爱女人爱液按摩棒拔出来爆草包二奶暴干暴奸暴乳爆乳暴淫屄被插被操被干逼奸仓井空插暴操逼操黑操烂肏你肏死操死操我厕奴插比插b插逼插进插你插我插阴潮吹潮喷成人dv成人电影成人论坛成人小说成人电成人电影成人卡通成人聊成人片成人视成人图成人文成人小成人电影成人论坛成人色情成人网站成人文学成人小说艳情小说成人游戏吃精赤裸抽插扌由插抽一插春药大波大力抽送大乳荡妇荡女盗撮多人轮发浪放尿肥逼粉穴封面女郎风月大陆干死你干穴肛交肛门龟头裹本国产av好嫩豪乳黑逼后庭后穴虎骑花花公子换妻俱乐部黄片几吧鸡吧鸡巴鸡奸寂寞男寂寞女妓女激情集体淫奸情叫床脚交金鳞岂是池中物金麟岂是池中物精液就去日巨屌菊花洞菊门巨奶巨乳菊穴开苞口爆口活口交口射口淫裤袜狂操狂插浪逼浪妇浪叫浪女狼友聊性流淫铃木麻凌辱漏乳露b乱交乱伦轮暴轮操轮奸裸陪买春美逼美少妇美乳美腿美穴美幼秘唇迷奸密穴蜜穴蜜液摸奶摸胸母奸奈美奶子男奴内射嫩逼嫩女嫩穴捏弄女优炮友砲友喷精屁眼品香堂前凸后翘强jian强暴强奸处女情趣用品情色拳交全裸群交惹火身材人妻人兽日逼日烂肉棒肉逼肉唇肉洞肉缝肉棍肉茎肉具揉乳肉穴肉欲乳爆乳房乳沟乳交乳头三级片骚逼骚比骚女骚水骚穴色逼色界色猫色盟色情网站色区色色色诱色欲色b少年阿宾少修正射爽射颜食精释欲兽奸兽交手淫兽欲熟妇熟母熟女爽片爽死我了双臀死逼丝袜丝诱松岛枫酥痒汤加丽套弄体奸体位舔脚舔阴调教偷欢偷拍推油脱内裤文做我就色无码舞女无修正吸精夏川纯相奸小逼校鸡小穴小xue写真性感妖娆性感诱惑 https://cn.pornhub.com https://zh.xhamster.com 来看色情罢!(喜) 针对国内的采取措施 希望各GitHub访友以及工作人员能理解 因为现在某SDN搬仓库挺严重的我们必须采取措施 -->

41
readme_en.md Normal file
View File

@@ -0,0 +1,41 @@
<div align="center">
<h2> LingChair </h2>
</div>
Welcome to LingChair! This is an IM project that it provide a new communication way. And it serves people.
> [!WARNING]
> 该版本因存在过多复杂且难以理解的代码写法以及杂乱不堪的前端代码而停止开发, 同时也在准备开发 v1 版本, 不建议大规模将此实验版本投入使用, 否则后果自负
> [!NOTE]
> THIS PROJECT IS ON GITHUB ONLY,**EVERYONE MUSTN'T COPY ANYTHING AND UPLOAD TO ANYWHERE WITHOUT MY PERMISSION.** <!-- GitCode, 说的就是你, 到时候如果被我发现你搬我仓库我迟早要找你算账 -->
>
> My English is not so good as a normal Englishmen, so you need a translator to read Chinese texts. And sometimes this document is outdated, I'm sorry with that.
>
> This project is still in progress, [Click me](final.md) to view progress. (Chinese)
>
> Comments and BUG report welcome
### Usage
Server:
0. Install Node.js
1. Clone this repo or download the source code, then run `npm install`, after that, run `npm run start` or run.bat / run.sh
Client:
* Use the pages that the server provide (Recommend)
* GitHub Pages (It may cause CORS security error)
### [Q&A (Chinese)](.github/QA.md)
### Credits
WIP
### [Do you know? (Chinese)](.github/do_you_know.md)
<!-- 致某国内搬运GitHub的平台别搬我否则等死。色情小说在线看链接https://www.book18.org/哈我只在GitHubGayHub不要亲信其它平台伪造的虚假账号为防止本人账号被莫名搬运到GxxCode某SDN的代码平台因此特地收汇了很多黄色词汇希望看到此注释的人不要太惊讶我也不想被莫名搬运啊正文爱女人爱液按摩棒拔出来爆草包二奶暴干暴奸暴乳爆乳暴淫屄被插被操被干逼奸仓井空插暴操逼操黑操烂肏你肏死操死操我厕奴插比插b插逼插进插你插我插阴潮吹潮喷成人dv成人电影成人论坛成人小说成人电成人电影成人卡通成人聊成人片成人视成人图成人文成人小成人电影成人论坛成人色情成人网站成人文学成人小说艳情小说成人游戏吃精赤裸抽插扌由插抽一插春药大波大力抽送大乳荡妇荡女盗撮多人轮发浪放尿肥逼粉穴封面女郎风月大陆干死你干穴肛交肛门龟头裹本国产av好嫩豪乳黑逼后庭后穴虎骑花花公子换妻俱乐部黄片几吧鸡吧鸡巴鸡奸寂寞男寂寞女妓女激情集体淫奸情叫床脚交金鳞岂是池中物金麟岂是池中物精液就去日巨屌菊花洞菊门巨奶巨乳菊穴开苞口爆口活口交口射口淫裤袜狂操狂插浪逼浪妇浪叫浪女狼友聊性流淫铃木麻凌辱漏乳露b乱交乱伦轮暴轮操轮奸裸陪买春美逼美少妇美乳美腿美穴美幼秘唇迷奸密穴蜜穴蜜液摸奶摸胸母奸奈美奶子男奴内射嫩逼嫩女嫩穴捏弄女优炮友砲友喷精屁眼品香堂前凸后翘强jian强暴强奸处女情趣用品情色拳交全裸群交惹火身材人妻人兽日逼日烂肉棒肉逼肉唇肉洞肉缝肉棍肉茎肉具揉乳肉穴肉欲乳爆乳房乳沟乳交乳头三级片骚逼骚比骚女骚水骚穴色逼色界色猫色盟色情网站色区色色色诱色欲色b少年阿宾少修正射爽射颜食精释欲兽奸兽交手淫兽欲熟妇熟母熟女爽片爽死我了双臀死逼丝袜丝诱松岛枫酥痒汤加丽套弄体奸体位舔脚舔阴调教偷欢偷拍推油脱内裤文做我就色无码舞女无修正吸精夏川纯相奸小逼校鸡小穴小xue写真性感妖娆性感诱惑 https://cn.pornhub.com https://zh.xhamster.com 来看色情罢!(喜) 针对国内的采取措施 希望各GitHub访友以及工作人员能理解 因为现在某SDN搬仓库挺严重的我们必须采取措施 -->

View File

@@ -12,7 +12,7 @@ const users = require("./api-users")
let getSameHashedValue = (a, b) => {
let _a = [hash.md5(a) + hash.sha256(a), hash.md5(b) + hash.sha256(b)].sort()
let [_1, _2] = _a
return hash.sha256hex(hash.sha256hex(_1) + hash.sha256hex(_2))
return hash.sha256(hash.sha256(_1) + hash.sha256(_2))
}
let getSingleChatDir = (a, b) => {

View File

@@ -29,7 +29,7 @@ let apis = {
// 账号文件结构: {uid: 10000, name: "GenShin", nick: "Impact", passwd: "SHA-256 + MD5"}
// 注意: 密码在客户端也应该经过哈希处理(SHA256 + MD5)
// @APi
signUp: (name, passwd) => {
signUp(name, passwd) {
if (passwd == null || name == null)
return { msg: "必须输入 账号和密码", code: -1 }
@@ -56,7 +56,7 @@ let apis = {
// 登录账号: 账号, 密码 返回刷新令牌 失败返回 null 和原因
// 注意: 密码在客户端应该经过哈希处理(SHA256 + MD5)
// @API
signIn: (name, passwd) => {
signIn(name, passwd) {
if (passwd == null || name == null)
return { msg: "必须输入 账号和密码", code: -1 }
@@ -73,7 +73,7 @@ let apis = {
// 注意: 密码在客户端也应该经过哈希处理(SHA256 + MD5)
// 刷新令牌算法: 哈希(用户ID + 当前年 + 当前月 + 密码 + 盐)
// 有效期: 一个月
getRefreshToken: (name, passwd) => {
getRefreshToken(name, passwd) {
let d = new Date()
let raw = name + d.getFullYear() + d.getMonth() + passwd + "LINGCHAIR-TEST-DEMO"
return hash.sha256(raw) + hash.md5(raw)
@@ -83,7 +83,7 @@ let apis = {
// 注意: 密码在客户端也应该经过哈希处理(SHA256 + MD5)
// 刷新令牌算法: 哈希(用户ID + 当前年 + 当前月 + 密码 + 盐)
// 有效期: 一天
getAccessTokenNonApi: (name, rt) => {
getAccessTokenNonApi(name, rt) {
if (!apis.checkRefreshToken(name, rt))
return null
let date = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric' })
@@ -94,25 +94,25 @@ let apis = {
// 在密码被设置前已经被哈希过,不需要重复
// 算法: (SHA256 + MD5)
// 警告: 这是经过二次哈希的
getPassWordHashed: (name) => {
getPassWordHashed(name) {
return hash.sha256(apis.getPassWordHashedRaw(name)) + hash.md5(apis.getPassWordHashedRaw(name))
},
// 请勿与上面的混淆
// 上面的是经过第二次哈希的
getPassWordHashedRaw: (name) => {
getPassWordHashedRaw(name) {
return io.open(getUserPath(name) + "/user.json").readJson().passwd
},
// 检测刷新令牌是否正确: 账号, 刷新令牌 返回布尔值
// 密码在服务端经过哈希保存 不需要重复输入密码
checkRefreshToken: (name, rt) => {
checkRefreshToken(name, rt) {
return apis.getRefreshToken(name, apis.getPassWordHashed(name)) === rt
},
// 检测访问令牌是否正确: 账号, 访问令牌 返回布尔值
// 密码在服务端经过哈希保存 不需要重复输入密码
checkAccessToken: (name, at) => {
checkAccessToken(name, at) {
return apis.getAccessTokenNonApi(name, apis.getRefreshToken(name, apis.getPassWordHashed(name /* 就是你这个傻逼害得我找两年BUG */))) === at
},
@@ -124,7 +124,7 @@ let apis = {
// 有效期: 一天
// 算法: SHA256(name) + MD5(rt + 盐)
// @Api
getAccessToken: (name, rt) => {
getAccessToken(name, rt) {
if (!apis.checkRefreshToken(name, rt))
return { msg: "刷新令牌不正确!", code: -1 }
@@ -133,7 +133,7 @@ let apis = {
// 设置头像: 账号, 访问令牌, 头像数据 返回结果
// @API
setHeadImage: (name, at, head) => {
setHeadImage(name, at, head) {
if (!apis.checkAccessToken(name, at))
return { msg: "访问令牌不正确!", code: -1 }
@@ -144,7 +144,7 @@ let apis = {
// 修改昵称
// @APi
setNick: (name, at, nick) => {
setNick(name, at, nick) {
if (!apis.checkAccessToken(name, at))
return { msg: "访问令牌不正确!", code: -1 }
@@ -160,8 +160,8 @@ let apis = {
return { msg: "成功", code: 0 }
},
// 取联系人列表(好友): 账号, 访问令牌 返回好友列表
getFriendsNonApi: (name, at) => {
// 取联系人列表(好友): 账号 返回好友列表
getFriendsNonApi(name) {
let file = getUserPath(name) + "/friends.json"
if (!io.exists(file))
io.open(file, "w").writeJson({list: [name]}).close()
@@ -169,8 +169,19 @@ let apis = {
return io.open(file, "r").readJson().list
},
// 加好友: 账号, 欲添加对象
addFriendNonApi(name, target) {
let file = getUserPath(name) + "/friends.json"
if (!io.exists(file))
io.open(file, "w").writeJson({list: [name]}).close()
let friends = io.open(file, "r").readJson()
friends.list.push(target)
io.open(file, "r").writeJson(friends).close()
},
// 取用户昵称: 账号 返回昵称
getNickNonApi: (name) => {
getNickNonApi(name) {
let file = getUserPath(name) + "/user.json"
return io.open(file, "r").readJson().nick
@@ -178,18 +189,29 @@ let apis = {
// 取昵称: 账号 返回昵称
// @API
getNick: (name, at) => {
getNick(name, at) {
return { msg: "成功", code: 0, nick: apis.getNickNonApi(name)}
},
// 取联系人列表(好友): 账号, 访问令牌 返回好友列表
// @API
getFriends: (name, at) => {
getFriends(name, at) {
if (!apis.checkAccessToken(name, at))
return { msg: "访问令牌不正确!", code: -1 }
return { msg: "成功", code: 0, friends: apis.getFriendsNonApi(name, at)}
},
// 加到好友列表: 账号, 欲加的好友, 访问令牌
// @API
addFriend(name, target, at) {
if (!apis.checkAccessToken(name, at))
return { msg: "访问令牌不正确!", code: -1 }
apis.addFriendNonApi(name, target, at)
return { msg: "成功", code: 0 }
},
}
module.exports = apis

View File

@@ -7,6 +7,7 @@
module.exports = {
none: "\033[0m",
red: "\033[1;31m",
pink: "\033[1;35m",
green: "\033[1;32m",
yellow: "\033[1;33m",
blue: "\033[1;34m",

View File

@@ -7,10 +7,8 @@
const crypto = require("crypto")
let apis = {
sha256: (data) => crypto.createHash("sha256").update(data).digest("base64"),
md5: (data) => crypto.createHash("md5").update(data).digest("base64"),
sha256hex: (data) => crypto.createHash("sha256").update(data).digest("hex"),
md5hex: (data) => crypto.createHash("md5").update(data).digest("hex"),
sha256: (data) => crypto.createHash("sha256").update(data).digest("hex"),
md5: (data) => crypto.createHash("md5").update(data).digest("hex"),
}
module.exports = apis

19
server_src/log.js Normal file
View File

@@ -0,0 +1,19 @@
const color = require("./color")
const log = (t) => {
console.log("[" + new Date().toLocaleTimeString('en-US', { hour12: false }) + "] " + t)
}
const loge = (t) => {
log(`[E] ${color.red + t + color.none}`)
}
const logw = (t) => {
log(`[W] ${color.yellow + t + color.none}`)
}
module.exports = {
log: log,
loge: loge,
logw, logw,
}

View File

@@ -6,9 +6,7 @@
console.log("正在初始化...")
const log = (t) => {
console.log("[" + new Date().toLocaleTimeString('en-US', { hour12: false }) + "] " + t)
}
const { log, loge, logw } = require("./log")
const sio = require("socket.io")
const http = require("http")
@@ -19,10 +17,10 @@ const vals = require("./val")
const color = require("./color")
//定义 Http 服务器回调
let httpServerCallback = require("./httpApi")
let httpServerCallback = require("./http-api")
// 定义 Socket.io 服务器回调
let wsServerCallback = require("./wsApi")
let wsServerCallback = require("./ws-api")
let httpServer
if (vals.LINGCHAIR_SERVER_CONFIG.useHttps)
@@ -49,19 +47,22 @@ let checkEmpty = (i) => {
wsServer.on("connect", (client) => {
log("客户端 " + client.handshake.address + " 已连接, 用户名(未经验证): " + client.handshake.auth.name)
logw(`客户端 ${client.handshake.address} 已连接, 用户名(未经验证): ${client.handshake.auth.name}`)
for (const cb in wsServerCallback) {
client.on(cb, (...args) => {
log("客户端 " + client.handshake.address + " 对接口 [" + cb + "] 发起请求,参数为 " + JSON.stringify(args[0]))
log(`客户端 ${client.handshake.address} 对接口 [${cb}] 发起请求,参数为 ${JSON.stringify(args[0])}`)
let callback = args[args.length - 1]
try {
wsServerCallback[cb](args[0], (reArgs) => {
callback(reArgs)
log("返回接口 [" + cb + "] 到 " + client.handshake.address + ",参数为 " + JSON.stringify(reArgs))
if (reArgs.code != 0)
logw(`返回接口 [${cb}] 到 ${client.handshake.address},参数为 ${JSON.stringify(reArgs)}`)
else
log(`返回接口 [${cb}] 到 ${client.handshake.address},参数为 ${JSON.stringify(reArgs)}`)
}, client, cachedClients)
} catch (e) {
log(color.yellow + "调用接口或返回数据时出错: " + e + color.none)
loge(`调用接口或返回数据时出错: ${e}`)
callback({ code: -1, msg: e })
}
})
@@ -69,7 +70,7 @@ wsServer.on("connect", (client) => {
client.on("disconnect", () => {
if (!client.handshake.auth.passCheck)
return log("未验证的客户端 " + client.handshake.address + " 已断开, 未验证的用户名: " + client.handshake.auth.name)
return logw(`未验证的客户端 ${client.handshake.address} 已断开, 未验证的用户名: ${client.handshake.auth.name}`)
// 为了支持多客户端登录 我豁出去了
if (cachedClients[client.handshake.auth.name].length === 1)
@@ -80,14 +81,14 @@ wsServer.on("connect", (client) => {
arr.splice(index, 1)
}
})
log("客户端 " + client.handshake.address + " 已断开, 用户名: " + client.handshake.auth.name)
log(`客户端 ${client.handshake.address} 已断开, 用户名: ${client.handshake.auth.name}`)
})
})
httpServer.listen(vals.LINGCHAIR_SERVER_CONFIG.port)
console.log(color.red + "=== 铃之椅 - Server ===" + color.none + "\n\r")
console.log(color.yellow + "Github: MoonLeeeaf" + color.none)
log(color.green + "运行服务于端口 " + vals.LINGCHAIR_SERVER_CONFIG.port + " 上," + (vals.LINGCHAIR_SERVER_CONFIG.useHttps == true ? "已" : "未") + "使用 HTTPS" + color.none)
log(color.green + "服务已启动..." + color.none)
log(`${color.green}HTTP 和 WebSocket 服务已在端口 ${vals.LINGCHAIR_SERVER_CONFIG.port} 上启动,${vals.LINGCHAIR_SERVER_CONFIG.useHttps == true ? "已" : "未"}使用 HTTPS${color.none}`)
log(`${color.green}感谢使用!${color.none}`)
log(`${color.green}GitHub @MoonLeeeaf${color.none}`)
log(`${color.green}服务已启动...${color.none}`)

View File

@@ -48,7 +48,8 @@ if (!io.exists(vals.LINGCHAIR_SERVER_CONFIG_FILE)) io.open(vals.LINGCHAIR_SERVER
cert: "",
},
})).close()
if (!io.exists(vals.LINGCHAIR_USERS_COUNT_FILE)) io.open(vals.LINGCHAIR_USERS_COUNT_FILE, "w").write("10000").close()
if (!io.exists(vals.LINGCHAIR_USERS_COUNT_FILE))
io.open(vals.LINGCHAIR_USERS_COUNT_FILE, "w").write("10000").close()
// 加载服务端配置文件
vals.LINGCHAIR_SERVER_CONFIG = JSON.parse(io.open(vals.LINGCHAIR_SERVER_CONFIG_FILE, "r").read("*a"))

View File

@@ -4,9 +4,7 @@
* 铃之椅 Node 服务端
*/
const log = (t) => {
console.log("[" + new Date().toLocaleTimeString('en-US', { hour12: false }) + "] " + t)
}
const { log, loge, logw } = require("./log")
const msgs = require("./api-msgs")
const users = require("./api-users")
@@ -48,7 +46,7 @@ let api = {
if (!users.checkRefreshToken(a.name, a.refreshToken))
return cb({ code: -1, msg: "刷新令牌错误", invalid: true })
log(color.yellow + "客户端 " + client.handshake.address + " 完成了用户 " + a.name + " 的验证" + color.none)
logw(`客户端 ${client.handshake.address} 完成了用户 ${a.name} 的验证`)
// 更新映射
client.handshake.auth.passCheck = true
@@ -149,18 +147,17 @@ let api = {
},
// 添加好友
// {name: 账号, accessToken: 访问令牌} 返回 {friends: []}
// WIP
// {name: 账号, accessToken: 访问令牌}
"user.addFriend": (a, cb) => {
if (checkEmpty([a.name, a.accessToken]))
if (checkEmpty([a.name, a.target, a.accessToken]))
return cb({ msg: "参数缺失", code: -1 })
let { msg, code, friends } = users.getFriends(a.name, a.accessToken)
let { msg, code } = users.addFriend(a.name, a.target, a.accessToken)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { friends: friends } })
cb({ msg: msg, code: 0 })
},
"user.getNick": (a, cb) => {