diff --git a/.gitignore b/.gitignore
index 3fdc3ca..1740540 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,2 @@
node_modules/
ling_chair_data/
-ling_chair_config/
-ling_chair_http/
-build_cache/
diff --git a/ling_chair_http/chat-message.css b/ling_chair_http/chat-message.css
new file mode 100644
index 0000000..46e0dcf
--- /dev/null
+++ b/ling_chair_http/chat-message.css
@@ -0,0 +1,85 @@
+/*
+ * 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
+ * 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%;
+}
\ No newline at end of file
diff --git a/ling_chair_http/index.css b/ling_chair_http/index.css
new file mode 100644
index 0000000..debc8f2
--- /dev/null
+++ b/ling_chair_http/index.css
@@ -0,0 +1,54 @@
+/*
+ * 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
+ * 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; /* 防止遮挡实际内容 */
+ }
diff --git a/ling_chair_http/index.html b/ling_chair_http/index.html
new file mode 100644
index 0000000..06324f4
--- /dev/null
+++ b/ling_chair_http/index.html
@@ -0,0 +1,342 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 铃之椅
+
+
+
+
+
+
+
+
+
+
+ -
+
+

+
+
+
+ -
+ settings
+
设置
+
+ -
+ exit_to_app
+
登出
+
+
+ -
+
+
+
+
+
+
+
+
+
+ -
+ account_circle
+
资料
+
+ -
+ person_add
+
新的好友
+
+
+ -
+ settings
+
设置
+
+ -
+ cloud_circle
+
更换服务器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录到 铃之椅
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改昵称
+
+
+
+
+
+
+
+
+
+
+
+ 新的好友请求
+
+
+
+
+
+
+
+
+
+
+ 添加好友/群
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设置
+
+
+
+
+ -
+ edit
+
修改昵称
+
+ -
+ account_circle
+
上传头像
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '))
+ }
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ling_chair_http/index.js b/ling_chair_http/index.js
new file mode 100644
index 0000000..cc94285
--- /dev/null
+++ b/ling_chair_http/index.js
@@ -0,0 +1,812 @@
+/*
+ * 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
+ * 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 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);
+}
+
+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 '<'
+ case '>':
+ return '>'
+ case '&':
+ return '&'
+ case '"':
+ return '"'
+ case "'":
+ return '''
+ 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('这是一个开源项目
作者: MoonLeeeaf
欢迎访问我们的项目主页', '关于 铃之椅', () => { }, { 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(`` + dick + `
`)).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 = `
+
+
` + nick + `
+
+ ` + msg + `
+
+
+
 + `)
+
`
+ else
+ temp = `
+
 + `)
+
+
` + nick + `
+
+ ` + msg + `
+
+
+
`
+
+ let bn = new Date(t).getMinutes()
+ let e
+ if (re) {
+ this.addSystemMsg(temp, re)
+ if (this.bbn != bn) {
+ e = this.addSystemMsg(`` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `
`, re)
+ this.time = bn
+ }
+ } else {
+ if (this.bbn != bn) {
+ e = this.addSystemMsg(`` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `
`, 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晚10:56分调配出来的秘方
+ // < 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(``)
+ 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 = {}
+}
diff --git a/ling_chair_http/mdui-prettier.css b/ling_chair_http/mdui-prettier.css
new file mode 100644
index 0000000..4b86b3b
--- /dev/null
+++ b/ling_chair_http/mdui-prettier.css
@@ -0,0 +1,53 @@
+/*
+ * ©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;
+ }
+}
diff --git a/ling_chair_http/res/config.json b/ling_chair_http/res/config.json
new file mode 100644
index 0000000..d8ada1e
--- /dev/null
+++ b/ling_chair_http/res/config.json
@@ -0,0 +1,4 @@
+{
+ "appTitle": "",
+ "canChangeServer": true
+}
\ No newline at end of file
diff --git a/ling_chair_http/res/default_head.png b/ling_chair_http/res/default_head.png
new file mode 100644
index 0000000..4f6f226
Binary files /dev/null and b/ling_chair_http/res/default_head.png differ
diff --git a/ling_chair_http/res/icon.ico b/ling_chair_http/res/icon.ico
new file mode 100644
index 0000000..8677e11
Binary files /dev/null and b/ling_chair_http/res/icon.ico differ
diff --git a/ling_chair_http/res/license.txt b/ling_chair_http/res/license.txt
new file mode 100644
index 0000000..5c226ee
--- /dev/null
+++ b/ling_chair_http/res/license.txt
@@ -0,0 +1,13 @@
+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.
\ No newline at end of file