16 Commits

Author SHA1 Message Date
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
40 changed files with 1732 additions and 2951 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,2 @@
node_modules/
ling_chair_data/
ling_chair_config/
ling_chair_http/

View File

@@ -1 +0,0 @@
node_modules/

View File

@@ -1,3 +0,0 @@
## Babel
请从本仓库根目录的 .github 文件夹内找到对应的zip文件并把 node_modules 解压在此,方可使用

View File

@@ -1,85 +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.
*/
.chat-message-right {
display: flex;
justify-content: flex-end;
align-items: flex-start;
margin: 13px;
}
.chat-message-left {
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin: 13px;
}
.message-content {
margin-top: 13px;
margin-bottom: 7px;
margin-left: 5px;
margin-right: 5px;
max-width: 100%;
white-space: normal;
word-break: break-all;
font-size: medium;
/* 使用了 CardView 就不需要边框了 */
/* border: 1.3px solid; */
padding: 15px;
border-radius: 15px;
/* 添加圆角样式 */
/* 设置外边距为 7px */
}
.message-content-with-nickname-right {
display: flex;
align-items: center;
margin: 7px;
flex-direction: column;
/* 垂直排列元素 */
align-items: flex-end;
/* 左对齐元素 */
}
.message-content-with-nickname-left {
display: flex;
align-items: center;
margin: 7px;
flex-direction: column;
/* 垂直排列元素 */
align-items: flex-start;
/* 左对齐元素 */
}
.chat-message-left .message-content-with-nickname-left .nickname,
.chat-message-right .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;
border-radius: 50%;
}

View File

@@ -1,54 +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.
*/
html, body {
max-height: 100%;
margin: 0;
padding: 0;
/* overflow: hidden; */
/*font: initial;*/
}
body {
margin: 0;
padding: 0;
}
.container {
display: flex;
flex-direction: column;
overflow: auto;
}
.content {
flex: 1;
}
.menu-on-message {
margin-top: 60px;
z-index: 100;
}
[n-id=pageChatSeesion]::after {
content: "";
position: sticky;
bottom: 0;
display: block;
height: var(--pseudo-height); /* 设置伪元素的高度 */
z-index: -1; /* 防止遮挡实际内容 */
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,54 +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.
*/
html, body {
max-height: 100%;
margin: 0;
padding: 0;
/* overflow: hidden; */
/*font: initial;*/
}
body {
margin: 0;
padding: 0;
}
.container {
display: flex;
flex-direction: column;
overflow: auto;
}
.content {
flex: 1;
}
.menu-on-message {
margin-top: 60px;
z-index: 100;
}
[n-id=pageChatSeesion]::after {
content: "";
position: sticky;
bottom: 0;
display: block;
height: var(--pseudo-height); /* 设置伪元素的高度 */
z-index: -1; /* 防止遮挡实际内容 */
}

View File

@@ -1,339 +0,0 @@
<!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.
-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no" />
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<!-- 给老旧的设备提供支持 支持不了, 照样没法运行 不 试一下红米2可不可以-->
<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> -->
<!-- Styles -->
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" />
<link rel="stylesheet" href="index.css" />
<link rel="stylesheet" href="chat-message.css" />
<link rel="stylesheet" href="mdui-prettier.css" />
<!-- 代替私人 fixed 并提供更好的兼容性 -->
<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/clipboard@2.0.11/dist/clipboard.min.js"></script>
<link rel="icon" href="res/icon.ico" />
<title>铃之椅</title>
</head>
<body
class="mdui-theme-primary-teal mdui-theme-accent-teal mdui-drawer-body-left mdui-appbar-with-toolbar mdui-theme-layout-auto"
id="app">
<input n-id="textCopierBtn" class="mdui-hidden" />
<div id="lingchair-app" style="height: 100%;">
<!-- 侧滑栏 -->
<div class="mdui-drawer" id="main-drawer">
<ul class="mdui-list" mdui-collapse="{accordion: true}">
<li class="mdui-list-item mdui-ripple">
<div class="mdui-list-item-avatar">
<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>
</li>
<li class="mdui-list-item mdui-ripple" onclick="new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()">
<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="drawerSignOut">
<i class="mdui-list-item-icon mdui-icon material-icons">exit_to_app</i>
<div class="mdui-list-item-content">登出</div>
</li>
<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>
</ul>
<ul class="mdui-list mdui-hidden">
<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>
</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>
</ul>
</div>
<!-- 应用栏 -->
<header class="mdui-appbar mdui-appbar-fixed">
<!-- Toolbar -->
<div class="mdui-toolbar mdui-color-theme">
<a mdui-drawer="{target: '#main-drawer'}" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">menu</i>
</a>
<a class="mdui-typo-title" n-id="appTitle"></a>
<div class="mdui-toolbar-spacer"></div>
<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">
<i class="mdui-icon material-icons">add</i>
</a>
<a class="mdui-btn mdui-btn-icon mdui-ripple" n-id="switchNotifications">
<i class="mdui-icon material-icons" n-id="switchNotificationsIcon">notifications_off</i>
</a>
<a mdui-menu="{target: '#appbar-menu'}" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">more_vert</i>
</a>
<ul class="mdui-menu" id="appbar-menu">
<li class="mdui-menu-item">
<a class="mdui-ripple" n-id="menuAbout">关于</a>
</li>
</ul>
</div>
</header>
<!-- Tab 栏 -->
<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-seesion" n-id="tabChatSeesion" class="mdui-ripple" style="text-transform: none;"></a>
</div>
<!-- 滚动到底部咋这么难写... -->
<div style="display: flex;flex-direction: column;">
<!-- 写时间居中写到吐了 这样式表不能要了 -->
<div
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="ChatMsgAdapter.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>
<!-- 妈的黑化了 私人玩意这么难整 早知道 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">
<ul class="mdui-menu" id="msg-input-more">
<li class="mdui-menu-item">
<a class="mdui-ripple">插入图片</a>
</li>
</ul>
<div class="mdui-textfield" style="width: 100%;max-width: 100%;">
<textarea class="mdui-textfield-input" type="text" placeholder="(。・ω・。)" n-id="inputMsg"></textarea>
</div>
<div class="mdui-toolbar-spacer"></div>
<a n-id="sendMsg" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">send</i>
</a>
<a mdui-menu="{target: '#msg-input-more', position: 'top'}" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">more_vert</i>
</a>
</div>
</div>
<!-- 登录对话框 -->
<div class="mdui-dialog" n-id="dialogSignIn">
<div class="mdui-dialog-title">
登录到 铃之椅
</div>
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<div class="mdui-textfield" n-id="dialogSignInServerLabel">
<i class="mdui-icon material-icons">cloud_circle</i>
<label class="mdui-textfield-label">服务器地址</label>
<input n-id="dialogSignInServer" class="mdui-textfield-input" type="text" placeholder="留空以使用网页所在地址"
n-input-ls="server" n-id="dialogSignServer" onblur="setUpClient(viewBinding.dialogSignServer.val())" />
</div>
<div class="mdui-textfield">
<i class="mdui-icon material-icons">account_circle</i>
<label class="mdui-textfield-label">账号</label>
<input n-id="dialogSignInName" class="mdui-textfield-input" maxlength="25" type="text"
n-input-ls="userName" />
</div>
<div class="mdui-textfield">
<i class="mdui-icon material-icons">lock</i>
<label class="mdui-textfield-label">密码</label>
<input n-id="dialogSignInPasswd" class="mdui-textfield-input" maxlength="30" type="password" />
</div>
<span>注:使用非已知的服务提供商提供的服务器时, 请注意个人信息保护哦 o(。・ω・。)o</span>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple"
onclick="User.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>
</div>
</div>
</div>
<!-- 资料卡对话框 -->
<div class="mdui-dialog" n-id="dialogProfile">
<div class="mdui-dialog-content" style="margin-left: 15px; margin-right: 15px; height: 101px;">
<div style="display: flex;justify-content: flex-start">
<img style="width: 60px; height: 60px;" class="mdui-img-circle" n-id="dialogProfileHead">
<div n-id="dialogProfileNick" style="font-size: 22px;align-self: center;margin-left: 20px;"
class="mdui-text-color-white"></div>
</div>
<div style="margin-left: 80px;"></div>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" mdui-dialog-close>关闭</button>
</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">
</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">
修改昵称
</div>
<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" />
</div>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" n-id="dialogEditNickClose" mdui-dialog-close
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>
</div>
</div>
<!-- 懂得都懂 -->
<div class="mdui-dialog" n-id="dialogNewFriendRequest">
<div class="mdui-dialog-title">
新的好友请求
</div>
<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="" class="mdui-textfield-input" maxlength="30" type="text" />
</div>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" mdui-dialog-close>关闭</button>
</div>
</div>
<!-- 懂得都懂 -->
<div class="mdui-dialog" n-id="dialogNewContact">
<div class="mdui-dialog-title">
添加好友/群
</div>
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<div class="mdui-textfield">
<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">
<option value="single">好友</option>
<option value="group">群聊</option>
</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>取消</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="User.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>
</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)"
accept="image/png, image/jpeg" />
</div>
<!-- Scripts -->
<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>
</body>
</html>

View File

@@ -1,810 +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
// https://www.ruanyifeng.com/blog/2021/09/detecting-mobile-browser.html
function isMobile() {
return ('ontouchstart' in document.documentElement);
}
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: "res/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('这是一个开源项目<br/>作者: MoonLeeeaf<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='res/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()
}
}
// 第一次写前端的消息加载, 代码很乱, 还请原谅~
// v0.7.0 大改UI 畏惧了 太庞大了
class ChatPage {
static cached = {}
constructor(name, type) {
}
static switchTo(name, type) {
if (!this.cached[name])
this.cached[name] = new ChatPage(name, type)
}
}
class ChatMsgAdapter {
static type
static target
// static msgList
static minMsgId
static time
static bbn
static resizeDick
// 切换聊天对象
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='res/default_head.png'" />
</div>`
else
temp = `<div class="chat-message-left">
<img class="avatar" src="` + User.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" 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去整啊
viewBinding.chatPager.get(0).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() {
// 实验表面移动端切出输入法时会触发1-2次resize事件
// 可以利用这个特性来实现自动滚动文本
let resize = () => {
viewBinding.pageChatSeesion.height(window.innerHeight - viewBinding.inputToolbar.height() - $("header.mdui-appbar").height() - viewBinding.chatTab.height() - 50)
let ledi = this.resizeDick - window.innerHeight
if (isMobile()) viewBinding.chatPager.get(0).scrollBy({
// 5.19晚1056分调配出来的秘方
// < 0 为窗口变大
// cnm的调试十万次就你tm检测不到底是吧就你语法天天错误是吧
// 欺负我现在用不了电脑
top: -(ledi) * ( (ledi < 0 && this.isAtBottom()) ? 6 : -1 ), // (ledi < 0 ? 6 : 6),
behavior: 'smooth'
})
this.resizeDick = window.innerHeight
}
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()
function refreshAll() {
ContactsList.reloadList()
delete NickCache.data
NickCache.data = {}
}

View File

@@ -1,53 +0,0 @@
/*
* ©2024 满月叶
* GitHub: MoonLeeeaf
* 是 UI 美化,好耶!
*/
/* 美化UI */
body {
font-family: -apple-system, system-ui, -webkit-system-font;
}
.mdui-dialog {
border-radius: 23px;
}
.mdui-menu {
border-radius: 10px;
}
.mdui-menu-item > a {
padding-right: 3px;
}
.mdui-btn:not(.mdui-btn-icon, .mdui-dialog-actions button, .mdui-dialog-actions a) {
padding-left: 20px;
padding-right: 20px;
height: 40px;
border-radius: 10px;
}
.mdui-dialog-actions a,
.mdui-dialog-actions button {
padding-left: 20px;
padding-right: 20px;
height: 40px;
border-radius: 40px;
}
.mdui-select-open {
border-radius: 10px;
}
@media not screen and (min-width: 768px) {
.mdui-snackbar {
border-radius: 10px;
}
}
/* 配色方案 */
.mdui-theme-color-auto {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
.mdui-theme-color-auto {
background-color: #303030;
}
}

View File

@@ -1,4 +0,0 @@
{
"appTitle": "",
"canChangeServer": true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,13 +0,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.

View File

@@ -79,7 +79,13 @@
.chat-message-left > .avatar,
.chat-message-right > .avatar {
width: 45px;
height: 45px;
width: 50px;
height: 50px;
border-radius: 50%;
}
}
.message-image {
max-width: 40%;
max-height: 40%;
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()
const showLinkDialog = (link) => mdui.alert(decodeURI(link) + "<br/>如果你确认此链接是安全的, 那么请<a class=\"mdui-text-color-theme-accent\" href=\"" + link + "\">点我</a>", '链接', () => { }, { confirmText: "关闭" })
const showImageDialog = (link, id, alt) => mdui.alert(`此图片链接来源未知: ${decodeURI(link)}<br/>如果你希望加载, 请<a class="mdui-text-color-theme-accent" 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-accent" 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-accent" onclick="showImageDialog('${encodeURI(href)}', '${h}', '${encodeURI(text)}')">[外部图片] ${text}</a></div>`
},
code(src) {
return `<a class="mdui-text-color-theme-accent" onclick="showCodeDialog(\`${encodeURI(src)}\`)">[代码块]</a>`
},
}
marked.use({
gfm: true,
renderer: renderer,
async: true,
})

573
ling_chair_http/handler.js Normal file
View File

@@ -0,0 +1,573 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 业务逻辑
*/
// ================================
// 当前用户
// ================================
class CurrentUser {
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
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()
}
if (ChatMsgAdapter.target !== 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 = 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)
})
}
}
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")
})
}
})
}
/**
* 添加联系人/群峦
* @param {String} nameOrId
*/
static add(name, type) {
if (type == "single") {
}
}
/**
* 打开添加联系人的对话框
*/
static openAddDialog() {
new mdui.Dialog(viewBinding.dialogNewContact.get(0)).open()
}
}
// 消息核心
class ChatPage {
static cached = {}
constructor(name, type) {
}
/**
* 切换到某一个聊天对象
* @param {String} name
* @param {String} type
*/
static switchTo(name, type) {
if (!this.cached[name])
this.cached[name] = new ChatPage(name, type)
}
}
class ChatMsgAdapter {
static type
static target
static minMsgId
static time
static minutesCache
static resizeDick
/**
* 切换到某一个聊天对象
* @param {String} name
* @param {String} type
*/
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.minMsgId = null
viewBinding.pageChatSeesion.empty()
await this.loadMore()
this.scrollToBottom()
}
/**
* 发送消息
* @param {String} msg
*/
static async send(msg) {
client.emit("user.sendSingleMsg", {
name: localStorage.userName,
target: this.target,
msg: msg,
accessToken: await CurrentUser.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()
}
})
}
/**
* 获取聊天消息记录
* @param {int} 起始点
* @param {int} 获取数量
*/
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 CurrentUser.getAccessToken(),
startId: start,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
res(re.data.histroy)
})
})
}
/**
* 加载更多聊天记录
* @param {int}} 加载数量
*/
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'
})
}
/**
* 添加系统消息
* @param {String} 消息
* @param {String} 是否加到顶部
* @returns {jQuery} 消息元素
*/
static addSystemMsg(m, re) {
let e
if (re)
// 加到头部
e = $($.parseHTML(m)).prependTo(viewBinding.pageChatSeesion)
else
// 加到尾部
e = $($.parseHTML(m)).appendTo(viewBinding.pageChatSeesion)
return e
}
/**
* 是否在底部
* @returns {Boolean} 是否在底部
*/
static isAtBottom() {
let elementRect = viewBinding.pageChatSeesion.get(0).getBoundingClientRect()
return (elementRect.bottom <= window.innerHeight)
}
// 添加消息 返回消息的JQ对象
// name: 用户id m: 消息 t: 时间戳 re: 默认加到尾部 msgid: 消息id
/**
* 添加聊天记录
* @param {String} name
* @param {String} msg
* @param {String} type
* @param {String} 是否加到头部
* @param {String || int} 消息id
* @returns {jQuery} 消息元素
*/
static 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
}
/**
* 从服务器加载一些聊天记录
* @param {int} 数量
*/
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 scrollToBottom() {
// 吐了啊 原来这样就行了 我何必在子element去整啊
viewBinding.chatPager.get(0).scrollBy({
top: 1145141919810,
behavior: 'smooth'
})
}
/**
* 初始化输入框位置调整器
*/
static initInputResizer() {
// 实验表面移动端切出输入法时会触发1-2次resize事件
// 可以利用这个特性来实现自动滚动文本
let resize = () => {
// CSS 牵一发而动全身 因此这个减少的数值是每天都要更改的
viewBinding.pageChatSeesion.height(window.innerHeight - viewBinding.inputToolbar.height() - $("header.mdui-appbar").height() - viewBinding.chatTab.height() - 65)
let ledi = this.resizeDick - window.innerHeight
if (isMobile()) viewBinding.chatPager.get(0).scrollBy({
// 5.19晚1056分调配出来的秘方
// < 0 为窗口变大
// cnm的调试十万次就你tm检测不到底是吧就你语法天天错误是吧
// 欺负我现在用不了电脑
top: -(ledi) * ((ledi < 0 && this.isAtBottom()) ? 6 : -1), // (ledi < 0 ? 6 : 6),
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 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(\`${ e.find("#raw-msg-content").text() }\`, '消息原文', () => { }, { 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

57
ling_chair_http/index.css Normal file
View File

@@ -0,0 +1,57 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 铃之椅 网页端
*/
html, body {
max-height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
/*font: initial;*/
}
body {
margin: 0;
padding: 0;
}
.container {
display: flex;
flex-direction: column;
overflow: auto;
}
.content {
flex: 1;
}
.menu-on-message {
margin-top: 60px;
z-index: 100;
}
[n-id=pageChatSeesion]::after {
content: "";
position: sticky;
bottom: 0;
display: block;
}
.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,9 +12,8 @@
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<!-- 给老旧的设备提供支持 支持不了, 照样没法运行 不 试一下红米2可不可以-->
<!-- 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>
<!-- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> -->
<!-- Styles -->
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" />
@@ -38,10 +21,11 @@
<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/clipboard@2.0.11/dist/clipboard.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="icon" href="res/icon.ico" />
<title>铃之椅</title>
</head>
@@ -148,13 +132,14 @@
<div class="mdui-center" style="margin: 15px;"><a href="javascript:;" onclick="ChatMsgAdapter.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 n-id="pageChatSeesion" class="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>
@@ -200,9 +185,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>
@@ -222,21 +207,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">
</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">
@@ -252,7 +222,7 @@
<button class="mdui-btn mdui-ripple" n-id="dialogEditNickClose" mdui-dialog-close
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>
@@ -279,7 +249,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">
@@ -307,7 +277,7 @@
<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()">
<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>
@@ -320,7 +290,7 @@
</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>
@@ -328,12 +298,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

@@ -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

@@ -6,9 +6,12 @@
/* 美化UI */
/* 恢复系统字体 */
body {
font-family: -apple-system, system-ui, -webkit-system-font;
}
/* 圆角化 */
.mdui-dialog {
border-radius: 23px;
}
@@ -34,7 +37,7 @@ body {
.mdui-select-open {
border-radius: 10px;
}
@media not screen and (min-width: 768px) {
@media screen and (min-width: 768px) {
.mdui-snackbar {
border-radius: 10px;
}
@@ -42,10 +45,14 @@ body {
/* 配色方案 */
.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

71
ling_chair_http/ui.js Normal file
View File

@@ -0,0 +1,71 @@
/*
* ©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/>作者: MoonLeeeaf<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")
viewBinding.inputMsg.blur(() => {
window.initInputResizerResize()
})

208
ling_chair_http/utils.js Normal file
View File

@@ -0,0 +1,208 @@
/*
* ©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)
}
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)
}
}
window.copyText = copyText
window.NData = NData
window.escapeHTML = escapeHTML
window.isMobile = isMobile
window.checkEmpty = checkEmpty
window.sleep = sleep
window.Hash = Hash
window.通知 = 通知

797
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,11 +0,0 @@
# 复制资源文件,这些文件不需要编译
mkdir -p ling_chair_http
cp -r client_src/* build_cache/
# 向前兼容脚本
cd babel_lib
node node_modules/@babel/cli/bin/babel ../client_src --out-dir ../build_cache --presets=@babel/env
# 打包以减小负载
cd ../webpack_lib
node node_modules/webpack-cli/bin/cli.js --config webpack_config.js

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

@@ -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

View File

@@ -19,10 +19,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)

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

@@ -1 +0,0 @@
node_modules/

View File

@@ -1,3 +0,0 @@
## Webpack
请从本仓库根目录的 .github 文件夹内找到对应的zip文件并把 node_modules 解压在此,方可使用

View File

@@ -1,20 +0,0 @@
const path = require('path')
const { BannerPlugin } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: '../build_cache/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, '../ling_chair_http'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
}