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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 铃之椅 + + + + + + +
+ +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+
+
+ + + +
+ + +
+
+ 登录到 铃之椅 +
+
+
+ cloud_circle + + +
+
+ account_circle + + +
+
+ lock + + +
+ 注:使用非已知的服务提供商提供的服务器时, 请注意个人信息保护哦 o(。・ω・。)o +
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+
+
+ +
+
+ + + + +
+
+ 修改昵称 +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ 新的好友请求 +
+
+
+ + +
+
+
+ +
+
+ + +
+
+ 添加好友/群 +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ 设置 +
+
+ +
+
+ +
+
+ +
+ +
+ + + + + + + ')) + } + + + + + + + + \ 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