25 Commits

Author SHA1 Message Date
MoonLeeeaf
5d7d7e7209 chore: 删除无用的测试文件 2024-06-15 18:37:30 +08:00
MoonLeeeaf
8446ba09c2 test: Tab Menu 2024-06-15 18:35:00 +08:00
MoonLeeeaf
d450797895 Merge branch 'main' of github.com:LingChair/LingChair 2024-06-15 18:32:50 +08:00
MoonLeeeaf
a326c16f1c fix: 异常数量的Tab栏指示器 2024-06-15 18:05:38 +08:00
MoonLeeeaf
3df9df6d85 style: ~ 2024-06-15 17:53:31 +08:00
MoonLeeeaf
585ea41831 change: 切换标签页不会滚动到底部 2024-06-15 12:44:57 +08:00
满月叶
12de793b26 docs: 使用h2代替h1 2024-06-15 10:34:26 +08:00
满月叶
84a5e52fbf docs: 修正英文readme语法错误 2024-06-15 10:33:57 +08:00
满月叶
436358e7c1 docs: add non-Chinese tip 2024-06-15 10:28:59 +08:00
满月叶
25d61b3a78 docs: remove a duplication language switch 2024-06-15 10:23:12 +08:00
满月叶
f2c9e51fd3 docs: readme_en 2024-06-15 10:19:34 +08:00
满月叶
cc5fcc1b02 docs: readme 2024-06-15 10:18:18 +08:00
满月叶
bf9ba20ede docs: English readme 2024-06-15 10:13:48 +08:00
满月叶
dcc4e040a5 docs: English readme 2024-06-15 10:09:42 +08:00
满月叶
733d5f76c3 docs: readme 2024-06-15 10:05:07 +08:00
MoonLeeeaf
1cb0dd3885 feat: 多标签页聊天, v0.8.0 2024-06-15 00:05:33 +08:00
MoonLeeeaf
d42caea57a feat: 多标签页聊天(实验性) 2024-06-14 23:50:33 +08:00
MoonLeeeaf
1a5afc8ad0 chore: rename symbols, add "原文" menu 2024-06-14 19:31:42 +08:00
MoonLeeeaf
4bdfad340f fix: image 2024-06-12 22:07:50 +08:00
MoonLeeeaf
6ac1b460bb feat: Markdown Code Dialog 2024-06-12 21:48:39 +08:00
MoonLeeeaf
29e224f87a test 2024-06-12 21:05:24 +08:00
MoonLeeeaf
a39973bb5c feat: markdown support 2024-06-11 22:03:41 +08:00
MoonLeeeaf
47afacbba3 chore: make limit happy 2024-06-01 14:33:40 +08:00
MoonLeeeaf
3a4d733c13 chore: 修改哈希输出(Base64改为Hex) 2024-06-01 14:33:26 +08:00
MoonLeeeaf
89263e6e2a chore: 命名 2024-06-01 14:09:45 +08:00
15 changed files with 704 additions and 276 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -23,14 +23,14 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: flex-start; align-items: flex-start;
margin: 13px; margin: 13px 13px 13px 10%;
} }
.chat-message-left { .chat-message-left {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
margin: 13px; margin: 13px 10% 13px 13px;
} }
.message-content { .message-content {
@@ -39,9 +39,10 @@
margin-left: 5px; margin-left: 5px;
margin-right: 5px; margin-right: 5px;
max-width: 100%; max-width: 100%;
min-width: 0%;
white-space: normal; white-space: normal;
word-break: break-all; word-break: break-all;
font-size: medium; /* font-size: medium; */
/* 使用了 CardView 就不需要边框了 */ /* 使用了 CardView 就不需要边框了 */
/* border: 1.3px solid; */ /* border: 1.3px solid; */
padding: 15px; padding: 15px;
@@ -83,3 +84,9 @@
height: 50px; height: 50px;
border-radius: 50%; 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()
ChatTabManager.initTabElementEvents()
const showLinkDialog = (link) => mdui.alert(decodeURI(link) + "<br/>如果你确认此链接是安全的, 那么请<a class=\"mdui-text-color-theme\" href=\"" + link + "\">点我</a>", '链接', () => { }, { confirmText: "关闭" })
const showImageDialog = (link, id, alt) => mdui.alert(`此图片链接来源未知: ${decodeURI(link)}<br/>如果你希望加载, 请<a class="mdui-text-color-theme" mdui-dialog-close onclick="$('#${id}').html('<img src=\\'${link}\\' alt=\\'${decodeURI(alt)}\\' class=\\'message-image\\'></img>')">点我</a>`, '外部图片', () => { }, { confirmText: "关闭" })
const showCodeDialog = (code) => mdui.alert(`<pre><code>${decodeURI(code)}</code></pre>`, '代码块', () => { }, { confirmText: "关闭" })
const renderer = {
heading(text, level) {
return text
},
paragraph(text) {
return text
},
blockquote(text) {
return text
},
link(href, title, text) {
return `<a class="mdui-text-color-theme" onclick="showLinkDialog('${encodeURI(href)}')">[链接] ${text}</a>`
},
image(href, title, text) {
let h = Hash.sha256(href)
let out = true
try {
out = new URL(href).hostname === new URL(location.href)
} catch(e) {}
if (out)
return `<img src="${encodeURI(href)}" alt="${text}" class="message-image"></img>`
else
return `<div id="${h}"><a class="mdui-text-color-theme" onclick="showImageDialog('${encodeURI(href)}', '${h}', '${encodeURI(text)}')">[外部图片] ${text}</a></div>`
},
code(src) {
return `<a class="mdui-text-color-theme" onclick="showCodeDialog(\`${encodeURI(src)}\`)">[代码块]</a>`
},
}
marked.use({
gfm: true,
renderer: renderer,
async: true,
})

View File

@@ -3,9 +3,20 @@
* Github: MoonLeeeaf * Github: MoonLeeeaf
* 业务逻辑 * 业务逻辑
*/ */
class User {
// ================================
// 当前用户
// ================================
class CurrentUser {
/** @type { String } */
static myAccessToken static myAccessToken
// 登录账号 通过回调函数返回刷新令牌 /**
* 登录账号
* @param { String } name
* @param { String } passwd
* @param { Function } callback
*/
static signIn(name, passwd, cb) { static signIn(name, passwd, cb) {
client.emit("user.signIn", { client.emit("user.signIn", {
name: name, name: name,
@@ -17,6 +28,12 @@ class User {
cb(re) cb(re)
}) })
} }
/**
* 注册账号
* @param { String } name
* @param { String } passwd
* @param { Function } callback
*/
static signUp(name, passwd, cb) { static signUp(name, passwd, cb) {
client.emit("user.signUp", { client.emit("user.signUp", {
name: name, name: name,
@@ -28,7 +45,11 @@ class User {
cb(re) cb(re)
}) })
} }
// 为登录对话框编写的 /**
* 登录对话框中的登录逻辑
* @param { String } name
* @param { String } passwd
*/
static signInWithDialog(name, passwd) { static signInWithDialog(name, passwd) {
this.signIn(name, passwd, (re) => { this.signIn(name, passwd, (re) => {
localStorage.refreshToken = re.data.refreshToken localStorage.refreshToken = re.data.refreshToken
@@ -37,6 +58,11 @@ class User {
location.reload() location.reload()
}) })
} }
/**
* 设置昵称
* @param { String } nick
* @param { Function } callback
*/
static async setNick(nick, cb) { static async setNick(nick, cb) {
client.emit("user.setNick", { client.emit("user.setNick", {
name: localStorage.userName, name: localStorage.userName,
@@ -48,10 +74,19 @@ class User {
if (cb) cb() if (cb) cb()
}) })
} }
// 获取头像链接 /**
* 获取用户头像的链接
* @param { String } name
* @returns { String } headImageUrl
*/
static getUserHeadUrl(name) { static getUserHeadUrl(name) {
return client.io.uri + "/users_head/" + name + ".png" return client.io.uri + "/users_head/" + name + ".png"
} }
/**
* 获取访问密钥
* @param { String } name
* @returns { Promise<String> } accessToken
*/
static async getAccessToken(er) { static async getAccessToken(er) {
if (this.myAccessToken == null) if (this.myAccessToken == null)
this.myAccessToken = await new Promise((res) => { this.myAccessToken = await new Promise((res) => {
@@ -62,17 +97,27 @@ class User {
}) })
return this.myAccessToken return this.myAccessToken
} }
/**
* 请求上传头像
*/
static uploadHeadImage() { static uploadHeadImage() {
viewBinding.uploadHeadImage.click() viewBinding.uploadHeadImage.click()
} }
/**
* 上传头像回调事件
* @param { Element } element
*/
static async uploadHeadImageCallback(self) { static async uploadHeadImageCallback(self) {
let img = self.files[0] let img = self.files[0]
client.emit("user.setHeadImage", { client.emit("user.setHeadImage", {
name: localStorage.userName, name: localStorage.userName,
accessToken: await User.getAccessToken(), accessToken: await CurrentUser.getAccessToken(),
headImage: img, headImage: img,
}, (re) => mdui.snackbar(re.msg)) }, (re) => mdui.snackbar(re.msg))
} }
/**
* 验证用户
*/
static auth() { static auth() {
client.emit("user.auth", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (re) => { client.emit("user.auth", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (re) => {
if (re.code !== 0) { if (re.code !== 0) {
@@ -80,7 +125,7 @@ class User {
if (!re.invalid) if (!re.invalid)
return mdui.snackbar("验证用户失败!") return mdui.snackbar("验证用户失败!")
mdui.alert("账号刷新令牌已过期, 请重新登录哦", "提示", () => User.signOutAndReload(), { mdui.alert("账号刷新令牌已过期, 请重新登录哦", "提示", () => CurrentUser.signOutAndReload(), {
confirmText: "确定", confirmText: "确定",
closeOnConfirm: false, closeOnConfirm: false,
closeOnEsc: false, closeOnEsc: false,
@@ -89,25 +134,33 @@ class User {
} }
}) })
} }
/**
* 登出并重载页面
*/
static signOutAndReload() { static signOutAndReload() {
localStorage.refreshToken = "" localStorage.refreshToken = ""
localStorage.isSignIn = false localStorage.isSignIn = false
setTimeout(() => location.reload(), 300) setTimeout(() => location.reload(), 300)
} }
/**
* 注册客户端回调事件
*/
static registerCallback() { static registerCallback() {
client.on("msg.receive", async (a) => { client.on("msg.receive", async (a) => {
if (checkEmpty([a.target, a.msg, a.type])) if (checkEmpty([a.target, a.msg, a.type]))
return return
if ((ChatMsgAdapter.target === a.target) && (ChatMsgAdapter.type === a.type)) { let currentPage = ChatPage.getCurrentChatPage()
if ((currentPage.chatTarget === a.target) && (currentPage.chatType === a.type)) {
let i = ChatMsgAdapter.isAtBottom() let i = ChatMsgAdapter.isAtBottom()
await ChatMsgAdapter.addMsg(a.target, a.msg.msg, a.msg.time) await currentPage.addMsg(a.target, a.msg.msg, a.msg.time, false, a.msg.msgid)
if (i) ChatMsgAdapter.scrollToBottom() if (i) ChatMsgAdapter.scrollToBottom()
} }
if (ChatMsgAdapter.target !== localStorage.userName) { if (currentPage.chatTarget !== localStorage.userName) {
let n = new 通知().setTitle("" + await NickCache.getNick(a.target)).setMessage(a.msg.msg).setIcon(User.getUserHeadUrl(a.target)).show(async () => { 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) await ChatMsgAdapter.switchTo(a.target, a.type)
location.replace("#msgid_" + a.msg.msgid) location.replace("#msgid_" + a.msg.msgid)
n.close() n.close()
@@ -115,24 +168,37 @@ class User {
} }
}) })
} }
/**
* 打开资料卡
* @param { String } name
*/
static async openProfileDialog(name) { static async openProfileDialog(name) {
viewBinding.dialogProfileHead.attr("src", User.getUserHeadUrl(name)) viewBinding.dialogProfileHead.attr("src", CurrentUser.getUserHeadUrl(name))
viewBinding.dialogProfileNick.text(await NickCache.getNick(name)) viewBinding.dialogProfileNick.text(await NickCache.getNick(name))
new mdui.Dialog(viewBinding.dialogProfile).open() new mdui.Dialog(viewBinding.dialogProfile).open()
} }
} }
// ================================
// 昵称缓存
// ================================
class NickCache { class NickCache {
static data = {} static data = {}
/**
* 获取昵称
* @param { String } name
* @returns { String } nick
*/
static async getNick(name) { static async getNick(name) {
return await new Promise((res, rej) => { return await new Promise((res, _rej) => {
// 这个this别摆着不放啊 不然两下就会去世 // 这个this别摆着不放啊 不然两下就会去世
let nick = this.data[name] let nick = NickCache.data[name]
if (nick == null) if (nick == null)
client.emit("user.getNick", { name: localStorage.userName }, (re) => { client.emit("user.getNick", { name: name }, (re) => {
let nk = re.data != null ? re.data.nick : name let nk = re.data != null ? re.data.nick : name
if (nk == null) nk = name if (nk == null) nk = name
this.data[name] = nk NickCache.data[name] = nk
res(nk) res(nk)
}) })
else else
@@ -141,11 +207,18 @@ class NickCache {
} }
} }
// ================================
// 联系人
// ================================
class ContactsList { class ContactsList {
/**
* 重载联系人列表
*/
static async reloadList() { static async reloadList() {
client.emit("user.getFriends", { client.emit("user.getFriends", {
name: localStorage.userName, name: localStorage.userName,
accessToken: await User.getAccessToken(), accessToken: await CurrentUser.getAccessToken(),
}, async (re) => { }, async (re) => {
if (re.code !== 0) if (re.code !== 0)
return mdui.snackbar(re.msg) return mdui.snackbar(re.msg)
@@ -155,66 +228,255 @@ class ContactsList {
for (let index in ls) { for (let index in ls) {
let name = ls[index] let name = ls[index]
let dick = await NickCache.getNick(name) 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="` + User.getUserHeadUrl(name) + `" onerror="this.src='res/default_head.png'" /></div><div class="mdui-list-item-content">` + dick + `</div></li>`)).appendTo(viewBinding.contactsList).click(() => { $($.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") ChatMsgAdapter.switchTo(name, "single")
}) })
} }
}) })
} }
// 添加联系人,好友或者群聊 /**
static add(name, type) { * 添加联系人/群峦
* @param { String } nameOrId
*/
static async add(name, type) {
if (type == "single") { if (type == "single") {
client.emit("user.addFriend", {
name: localStorage.userName,
target: name,
accessToken: await CurrentUser.getAccessToken(),
}, async (re) => {
// if (re.code !== 0)
return mdui.snackbar(re.msg)
})
} }
} }
/**
* 打开添加联系人的对话框
*/
static openAddDialog() { static openAddDialog() {
new mdui.Dialog(viewBinding.dialogNewContact.get(0)).open() new mdui.Dialog(viewBinding.dialogNewContact.get(0)).open()
} }
} }
// ================================
// 消息核心 // 消息核心
// ================================
class ChatPage { // 自古框架BUG多, 各种麻烦遭不住
static cached = {}
constructor(name, type) {
class ChatTabManager {
static tabs = {}
/**
* 添加Tab
* @param { String } title
* @param { String } target
*/
static add(title, target) {
if (this.tabs[target]) return
let tabElement = $($.parseHTML(`<a onclick="ChatMsgAdapter.switchTo('${target}');" tag="chatTab" id="chatTab_${target}" class="mdui-ripple" style="text-transform: none;">${title}</a>`))
tabElement.appendTo(viewBinding.chatTab)
// 就你MDUI的B事最多 加Tab还多一个下划线 删掉就解决了
$(".mdui-tab-indicator").remove()
new mdui.Tab(viewBinding.chatTab).handleUpdate()
this.tabs[target] = tabElement
if (Object.keys(this.tabs).length == 1) tabElement.addClass("mdui-tab-active")
} }
static switchTo(name, type) { /**
if (!this.cached[name]) * 寻找Tab
this.cached[name] = new ChatPage(name, type) * @param { String } target
* @returns { jQuery } element
*/
static find(target) {
return this.tabs[target]
}
/**
* 点击Tab
* @param { String } target
*/
static click(target) {
this.find(target).get(0).click()
}
/**
* 删除Tab
* @param { String } target
*/
static remove(target) {
this.find(target).remove()
this.tabs[target] = null
}
static initTabElementEvents() {
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: "auto",
// 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
}
})
} }
} }
class ChatMsgAdapter { class ChatPage {
static type static cached = {}
static target constructor(name, title, type) {
static minMsgId this.chatTarget = name
static time this.chatType = type
static bbn ChatTabManager.add(title, this.chatTarget)
static resizeDick this.chatPageElement = $($.parseHTML(`<div class="chat-seesion" id="chatPageTargetIs${this.chatTarget}" target="${this.chatTarget}"></div>`))
// 切换聊天对象 this.chatPageElement.hide()
static async switchTo(name, type) { this.chatPageElement.appendTo(viewBinding.pageChatSeesion)
viewBinding.tabChatSeesion.show() ;(async () => await this.loadMore())()
viewBinding.tabChatSeesion.text(await NickCache.getNick(name)) }
viewBinding.tabChatSeesion.get(0).click() /**
* 获取当前的聊天栏
* @returns { jQuery }
*/
static getCurrentChatSeesion() {
return $(".chat-seesion[actived=true]")
}
/**
* 获取当前聊天页面
* @returns { ChatPage }
*/
static getCurrentChatPage() {
return ChatPage.cached[$(".chat-seesion[actived=true]").attr("target")]
}
/**
* 切换选择的聊天对象
*/
async show() {
ChatTabManager.click(this.chatTarget)
this.type = type
this.target = name
this.minMsgId = null this.minMsgId = null
viewBinding.pageChatSeesion.empty() for (let k of Object.keys(ChatPage.cached)) {
let cpe = ChatPage.cached[k].chatPageElement
await this.loadMore() cpe.attr("actived", null)
this.scrollToBottom() cpe.hide()
} }
// 发送消息
static async send(msg) { $(this.chatPageElement).attr("actived", "true")
ChatTabManager.click(this.chatTarget)
$(this.chatPageElement).show()
}
/**
* 连带Tab一起销毁
*/
remove() {
ChatTabManager.remove(this.chatTarget)
ChatPage.cached[this.chatTarget].chatPageElement.remove()
ChatPage.cached[this.chatTarget] = null
}
/**
* 加载更多聊天记录
* @param { int } 加载数量
*/
async loadMore(limit) {
let histroy = await this.getHistroy(this.minMsgId, limit == null ? 13 : limit)
let chatPager = viewBinding.chatPager.get(0)
if (histroy.length == 0)
return mdui.snackbar("已经加载完了~")
let doReverse = this.minMsgId != null
this.minMsgId = histroy[0].msgid - 1
// 英语水平不够(
let scroll幅度 = 0
if (doReverse) histroy = histroy.reverse()
for (let index in histroy) {
let i = histroy[index]
let msgElement = await this.addMsg(i.name, i.msg, i.time, doReverse, i.msgid)
// 因为某些因素直接DEBUG到吐血 断点继续都不报错 原因不明
scroll幅度 = scroll幅度 + (msgElement == null ? 35 : getOffsetTop(chatPager, msgElement.get(0)))
}
chatPager.scrollBy({
top: scroll幅度,
behavior: 'smooth'
})
}
/**
* 获取聊天消息记录
* @param { int } 起始点
* @param { int } 获取数量
*/
async getHistroy(start, limit) {
if (this.chatType == "single")
return new Promise(async (res, _rej) => {
client.emit("user.getSingleChatHistroy", {
name: localStorage.userName,
target: this.chatTarget,
limit: limit,
accessToken: await CurrentUser.getAccessToken(),
startId: start,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
res(re.data.histroy)
})
})
throw new TypeError("Unsupported chat type!")
}
/**
* 发送消息
* @param { String } msg
*/
async send(msg) {
if (this.chatType == "single")
client.emit("user.sendSingleMsg", { client.emit("user.sendSingleMsg", {
name: localStorage.userName, name: localStorage.userName,
target: this.target, target: this.chatTarget,
msg: msg, msg: msg,
accessToken: await User.getAccessToken(), accessToken: await CurrentUser.getAccessToken(),
}, async (re) => { }, async (re) => {
if (re.code !== 0) if (re.code !== 0)
return mdui.snackbar(re.msg) return mdui.snackbar(re.msg)
@@ -224,118 +486,124 @@ class ChatMsgAdapter {
// 微机课闲的没事干玩玩 发现私聊会多发一个(一个是本地的, 另一个是发送成功的) 选择一个关掉就好了 // 微机课闲的没事干玩玩 发现私聊会多发一个(一个是本地的, 另一个是发送成功的) 选择一个关掉就好了
// 这里我选择服务端不发送回调, 不然多设备同步会吵死 // 这里我选择服务端不发送回调, 不然多设备同步会吵死
// 错了 应该是客户端少发条才对 不然不能多设备同步 // 错了 应该是客户端少发条才对 不然不能多设备同步
if ((ChatMsgAdapter.target !== localStorage.userName) && ChatMsgAdapter.type === "single") { if (this.chatTarget !== localStorage.userName) {
let i = ChatMsgAdapter.isAtBottom() let i = ChatMsgAdapter.isAtBottom()
await ChatMsgAdapter.addMsg(localStorage.userName, msg, re.data.time, re.data.msgid) await this.addMsg(localStorage.userName, msg, re.data.time, false, re.data.msgid)
if (i) ChatMsgAdapter.scrollToBottom() if (i) ChatMsgAdapter.scrollToBottom()
} }
}) })
throw new TypeError("Unsupported chat type!")
} }
static async getHistroy(start, limit) { /**
return new Promise(async (res, rej) => { * 添加系统消息
client.emit("user.getSingleChatHistroy", { * @param { String } 消息
name: localStorage.userName, * @param { Boolean } 是否加到顶部
target: this.target, * @returns { jQuery } 消息元素
limit: limit, */
accessToken: await User.getAccessToken(), addSystemMsg(msg, addToTop) {
startId: start, let element
}, (re) => { if (addToTop)
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) element = $($.parseHTML(msg)).prependTo(this.chatPageElement)
else else
// 加到尾部 // 加到尾部
e = $($.parseHTML(m)).appendTo(viewBinding.pageChatSeesion) element = $($.parseHTML(msg)).appendTo(this.chatPageElement)
return e return element
} }
static isAtBottom() { /**
let elementRect = viewBinding.pageChatSeesion.get(0).getBoundingClientRect() * 添加聊天记录
return (elementRect.bottom <= window.innerHeight) * @param { String } name
} * @param { String } msg
// 添加消息 返回消息的JQ对象 * @param { String } type
// name: 用户id m: 消息 t: 时间戳 re: 默认加到尾部 msgid: 消息id * @param { Boolean } 是否加到头部
static async addMsg(name, m, t, re, msgid) { * @param { String || int } 消息id
* @returns { jQuery } 消息元素
*/
async addMsg(name, preMsg, time, addToTop, msgid) {
let nick = await NickCache.getNick(name) // re.data == null ? name : re.data.nick let nick = await NickCache.getNick(name) // re.data == null ? name : re.data.nick
let msg = escapeHTML(m) let msg
try {
msg = await marked.parse(preMsg)
} catch (error) {
console.log("解析消息失败: " + error)
msg = escapeHTML(preMsg)
}
let temp let temp
if (name === localStorage.userName) if (name === localStorage.userName)
temp = `<div class="chat-message-right"> temp = `<div class="chat-message-right">
<div class="message-content-with-nickname-right"> <div class="message-content-with-nickname-right">
<span class="nickname">` + nick + `</span> <span class="nickname">${nick}</span>
<div class="message-content mdui-card" id="msgid_` + msgid + `"> <div class="message-content mdui-card" tag="msg-card" id="msgid_${msgid}">
<span id="msg-content">` + msg + `</span> <span id="msg-content">${msg}</span>
<pre class="mdui-hidden" id="raw-msg-content">${preMsg}</pre>
</div> </div>
</div> </div>
<img class="avatar" src="` + User.getUserHeadUrl(name) + `" onerror="this.src='res/default_head.png'" /> <img class="avatar" src="${CurrentUser.getUserHeadUrl(name)}" onerror="this.src='res/default_head.png'" />
</div>` </div>`
else else
temp = `<div class="chat-message-left"> temp = `<div class="chat-message-left">
<img class="avatar" src="` + User.getUserHeadUrl(name) + `" onerror="this.src='res/default_head.png'" /> <img class="avatar" src="${CurrentUser.getUserHeadUrl(name)}" onerror="this.src='res/default_head.png'" />
<div class="message-content-with-nickname-left"> <div class="message-content-with-nickname-left">
<span class="nickname">` + nick + `</span> <span class="nickname">${nick}</span>
<div class="message-content mdui-card" id="msgid_` + msgid + `"> <div class="message-content mdui-card" tag="msg-card" id="msgid_${msgid}">
<span id="msg-content">` + msg + `</span> <span id="msg-content">${msg}</span>
<pre class="mdui-hidden" id="raw-msg-content">${preMsg}</pre>
</div> </div>
</div> </div>
</div>` </div>`
let bn = new Date(t).getMinutes() let nowMinutes = new Date(time).getMinutes()
let e let msgElement
if (re) { if (addToTop) {
this.addSystemMsg(temp, re) this.addSystemMsg(temp, addToTop)
if (this.bbn != bn) { if (this.minutesCache != nowMinutes) {
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) 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 = bn this.time = nowMinutes
} }
} else { } else {
if (this.bbn != bn) { if (this.minutesCache != nowMinutes) {
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) 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 = bn this.time = nowMinutes
} }
this.addSystemMsg(temp, re) this.addSystemMsg(temp, addToTop)
} }
this.bbn = new Date(t).getMinutes() this.minutesCache = new Date(time).getMinutes()
return e return msgElement
} }
// 从服务器加载一些聊天记录, 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) class ChatMsgAdapter {
this.msgList = histroy static type
static target
static resizeDick
/**
* 切换到某一个聊天对象
* @param { String } name
* @param { String } type
*/
static async switchTo(name, type) {
if (!ChatPage.cached[name])
ChatPage.cached[name] = new ChatPage(name, await NickCache.getNick(name), type)
ChatPage.cached[name].show()
} }
/**
* 是否在底部
* @returns { Boolean } 是否在底部
*/
static isAtBottom() {
let elementRect = viewBinding.pageChatSeesion.get(0).getBoundingClientRect()
return (elementRect.bottom <= window.innerHeight)
}
/**
* 滑到底部
*/
static scrollToBottom() { static scrollToBottom() {
// 吐了啊 原来这样就行了 我何必在子element去整啊 // 吐了啊 原来这样就行了 我何必在子element去整啊
viewBinding.chatPager.get(0).scrollBy({ viewBinding.chatPager.get(0).scrollBy({
@@ -343,13 +611,15 @@ class ChatMsgAdapter {
behavior: 'smooth' behavior: 'smooth'
}) })
} }
// 自动调整使输入框置底 CSS真tm靠不住啊 /**
* 初始化输入框位置调整器
*/
static initInputResizer() { static initInputResizer() {
// 实验表面移动端切出输入法时会触发1-2次resize事件 // 实验表面移动端切出输入法时会触发1-2次resize事件
// 可以利用这个特性来实现自动滚动文本 // 可以利用这个特性来实现自动滚动文本
let resize = () => { let resize = () => {
// CSS 牵一发而动全身 因此这个减少的数值是每天都要更改的 // CSS 牵一发而动全身 因此这个减少的数值是每天都要更改的
viewBinding.pageChatSeesion.height(window.innerHeight - viewBinding.inputToolbar.height() - $("header.mdui-appbar").height() - viewBinding.chatTab.height() - 65) viewBinding.chatPager.height(window.innerHeight - viewBinding.inputToolbar.height() - $("header.mdui-appbar").height() - viewBinding.chatTab.height() - 17)
let ledi = this.resizeDick - window.innerHeight let ledi = this.resizeDick - window.innerHeight
if (isMobile()) viewBinding.chatPager.get(0).scrollBy({ if (isMobile()) viewBinding.chatPager.get(0).scrollBy({
// 5.19晚1056分调配出来的秘方 // 5.19晚1056分调配出来的秘方
@@ -365,19 +635,25 @@ class ChatMsgAdapter {
window.addEventListener("resize", resize) window.addEventListener("resize", resize)
resize() resize()
} }
// 为消息设置长按/右键事件 /**
* 初始化消息框右击事件
*/
static initMsgElementEvents() { static initMsgElementEvents() {
let listeners = {} let listeners = {}
let menu let menu
let callback = (e) => { let callback = (e) => {
if (menu) menu.close() if (menu) menu.close()
// 从 span 切到 div // 切到 div.message-content
if (e.get(0).tagName.toLowerCase() != "div") e = $(e.get(0).parentNode) let ele = e.get(0)
// 从 消息框 切到 更上层 while ($(ele).attr("tag") != "msg-card")
e = $(e.get(0).parentNode) ele = ele.parentNode
e = $(ele)
let menuHtml = $.parseHTML(`<ul class="mdui-menu menu-on-message"> let menuHtml = $.parseHTML(`<ul class="mdui-menu menu-on-message">
<li class="mdui-menu-item"> <li class="mdui-menu-item">
<a onclick="copyText(\`` + e.find("#msg-content").text() + `\`)" class="mdui-ripple">复制</a> <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>
<li class="mdui-menu-item"> <li class="mdui-menu-item">
<a onclick="mdui.alert('未制作功能', '提示', () => { }, { confirmText: '关闭' })" class="mdui-ripple">转发</a> <a onclick="mdui.alert('未制作功能', '提示', () => { }, { confirmText: '关闭' })" class="mdui-ripple">转发</a>
@@ -421,13 +697,16 @@ class ChatMsgAdapter {
} }
} }
/**
* 刷新联系人列表以及昵称缓存
*/
function refreshAll() { function refreshAll() {
ContactsList.reloadList() ContactsList.reloadList()
delete NickCache.data delete NickCache.data
NickCache.data = {} NickCache.data = {}
} }
window.User = User window.User = CurrentUser
window.ContactsList = ContactsList window.ContactsList = ContactsList
window.NickCache = NickCache window.NickCache = NickCache
window.ChatPage = ChatPage window.ChatPage = ChatPage

View File

@@ -25,6 +25,7 @@
<script src="https://cdn.jsdelivr.net/gh/wilddeer/stickyfill@2.1.0/dist/stickyfill.min.js"></script> <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://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/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" /> <link rel="icon" href="res/icon.ico" />
<title>铃之椅</title> <title>铃之椅</title>
</head> </head>
@@ -120,7 +121,6 @@
<div class="mdui-tab mdui-accent-theme mdui-theme-color-auto" style="position: fixed; z-index: 114;width: 100%;" <div class="mdui-tab mdui-accent-theme mdui-theme-color-auto" style="position: fixed; z-index: 114;width: 100%;"
mdui-tab n-id="chatTab"> mdui-tab n-id="chatTab">
<!-- 侧滑栏的 z-index 是2000, 在移动端会直接覆盖 --> <!-- 侧滑栏的 z-index 是2000, 在移动端会直接覆盖 -->
<a href="#page-chat-seesion" n-id="tabChatSeesion" class="mdui-ripple" style="text-transform: none;"></a>
</div> </div>
<!-- 滚动到底部咋这么难写... --> <!-- 滚动到底部咋这么难写... -->
<div style="display: flex;flex-direction: column;"> <div style="display: flex;flex-direction: column;">
@@ -128,11 +128,10 @@
<div <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;" 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"> n-id="chatPager">
<div class="mdui-center" style="margin: 15px;"><a href="javascript:;" onclick="ChatMsgAdapter.loadMore()" <div class="mdui-center" style="margin: 15px;"><a href="javascript:;" onclick="ChatPage.getCurrentChatPage().loadMore()"
class="mdui-text-color-theme">加载更多</a> | <a href="javascript:;" class="mdui-text-color-theme">加载更多</a> | <a href="javascript:;"
onclick="ChatMsgAdapter.scrollToBottom()" class="mdui-text-color-theme">回到底部</a></div> onclick="ChatMsgAdapter.scrollToBottom()" class="mdui-text-color-theme">回到底部</a></div>
<div n-id="pageChatSeesion" class="chat-seesion"> <div n-id="pageChatSeesion" class="chat-seesion" id="page-chat-seesion"></div>
</div>
<!-- 输入框和聊天消息重叠的原因就是死人 scrollbar, 把自动调整的距离调小, margin调大就行了 --> <!-- 输入框和聊天消息重叠的原因就是死人 scrollbar, 把自动调整的距离调小, margin调大就行了 -->
</div> </div>
<!-- 妈的黑化了 私人玩意这么难整 早知道 z-index 弄死它得了 浪费我时间 我就没试过这么离谱的样式表 第三方库真难写CSS 就应该先写后端的 啊啊啊啊啊啊 --> <!-- 妈的黑化了 私人玩意这么难整 早知道 z-index 弄死它得了 浪费我时间 我就没试过这么离谱的样式表 第三方库真难写CSS 就应该先写后端的 啊啊啊啊啊啊 -->
@@ -184,9 +183,9 @@
</div> </div>
<div class="mdui-dialog-actions"> <div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" <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" <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> </div>
</div> </div>
@@ -221,7 +220,7 @@
<button class="mdui-btn mdui-ripple" n-id="dialogEditNickClose" mdui-dialog-close <button class="mdui-btn mdui-ripple" n-id="dialogEditNickClose" mdui-dialog-close
onclick="new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()">关闭</button> onclick="new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()">关闭</button>
<button class="mdui-btn mdui-ripple" <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>
</div> </div>
@@ -276,7 +275,7 @@
<i class="mdui-list-item-icon mdui-icon material-icons">edit</i> <i class="mdui-list-item-icon mdui-icon material-icons">edit</i>
<div class="mdui-list-item-content">修改昵称</div> <div class="mdui-list-item-content">修改昵称</div>
</li> </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> <i class="mdui-list-item-icon mdui-icon material-icons">account_circle</i>
<div class="mdui-list-item-content">上传头像</div> <div class="mdui-list-item-content">上传头像</div>
</li> </li>
@@ -289,7 +288,7 @@
</div> </div>
<div class="mdui-hidden"> <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" /> accept="image/png, image/jpeg" />
</div> </div>
@@ -302,7 +301,7 @@
<script src="manager.js"></script> <script src="manager.js"></script>
<script src="ui.js"></script> <script src="ui.js"></script>
<script src="handler.js"></script> <script src="handler.js"></script>
<script src="index.js"></script> <script src="finally.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,50 +0,0 @@
/*
* ©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", User.getUserHeadUrl(localStorage.userName))
ContactsList.reloadList()
User.registerCallback()
}
// 感谢AI的力量
Stickyfill.add($("*").filter((a, b) => $(b).css('position') === 'sticky'))
ChatMsgAdapter.initMsgElementEvents()
ChatMsgAdapter.initInputResizer()

View File

@@ -8,6 +8,10 @@ const viewBinding = NData.mount($("#app").get(0))
let client let client
/**
* 初始化客户端
* @param {String} 服务器地址
*/
function setUpClient(server) { function setUpClient(server) {
if (server && server !== "") if (server && server !== "")
client = new io(server, { client = new io(server, {

View File

@@ -41,7 +41,7 @@ viewBinding.drawerSignOut.click(() => {
viewBinding.sendMsg.click((a) => { viewBinding.sendMsg.click((a) => {
let text = viewBinding.inputMsg.val() let text = viewBinding.inputMsg.val()
if (text.trim() !== "") if (text.trim() !== "")
ChatMsgAdapter.send(text) ChatPage.getCurrentChatPage().send(text)
}) })
viewBinding.inputMsg.keydown((e) => { viewBinding.inputMsg.keydown((e) => {

View File

@@ -42,6 +42,16 @@ const checkEmpty = (i) => {
return (i == null) || ("" === i) || (0 === i) return (i == null) || ("" === i) || (0 === i)
} }
// AI的力量太强了
function getOffsetTop(parent, child) {
let top = 0
while (child && child !== parent) {
top += child.offsetTop
child = child.offsetParent
}
return top
}
function escapeHTML(str) { function escapeHTML(str) {
return str.replace(/[<>&"']/g, function (match) { return str.replace(/[<>&"']/g, function (match) {
switch (match) { switch (match) {
@@ -62,6 +72,10 @@ function escapeHTML(str) {
} }
class NData { class NData {
/**
* 获取 MD5sum
* @param {String} 数据
*/
static mount(node) { static mount(node) {
// 便捷获得指定组件 // 便捷获得指定组件
let es = node.querySelectorAll("[n-id]") let es = node.querySelectorAll("[n-id]")
@@ -80,6 +94,11 @@ class NData {
} }
// https://www.runoob.com/w3cnote/javascript-copy-clipboard.html // https://www.runoob.com/w3cnote/javascript-copy-clipboard.html
/**
* 复制文字
* @param {String} 欲复制的文本
*/
function copyText(t) { function copyText(t) {
let btn = $("[n-id=textCopierBtn]") let btn = $("[n-id=textCopierBtn]")
btn.attr("data-clipboard-text", t) btn.attr("data-clipboard-text", t)
@@ -90,6 +109,13 @@ function copyText(t) {
} }
// https://zhuanlan.zhihu.com/p/162910462 // https://zhuanlan.zhihu.com/p/162910462
/**
* 格式化日期
* @param {int} 时间戳
* @param {String} 欲格式化的文本
* @returns {String} 格式后的文本
*/
Date.prototype.format = function (tms, format) { Date.prototype.format = function (tms, format) {
let tmd = new Date(tms) let tmd = new Date(tms)
/* /*
@@ -164,11 +190,21 @@ class 通知 {
} }
class Hash { class Hash {
/**
* 获取 MD5sum
* @param {String} 数据
* @returns {String} Hex化的哈希值
*/
static md5(data) { static md5(data) {
return CryptoJS.MD5(data).toString(CryptoJS.enc.Base64) return CryptoJS.MD5(data).toString(CryptoJS.enc.Hex)
} }
/**
* 获取 SHA256sum
* @param {String} 数据
* @returns {String} Hex化的哈希值
*/
static sha256(data) { static sha256(data) {
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Base64) return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex)
} }
} }

View File

@@ -1,31 +1,29 @@
## 铃之椅 [ 中文 | [English](readme_en.md) ]
<div align="center">
<h2> 铃之椅 </h2>
</div>
欢迎来到铃之椅! 这是一个即时通讯项目, 为通讯提供更多的选择, 为人民服务 欢迎来到铃之椅! 这是一个即时通讯项目, 为通讯提供更多的选择, 为人民服务
> [!NOTE] > [!NOTE]
> 本项目仍在实验阶段, [点我](final.md)可查看进展 > 本项目仍在实验阶段, [点我](final.md)可查看进展
> >
> 如果有任何问题,欢迎你提出来,我会不定时查看 > 欢迎各位提出项目修改意见
>
> 另外 Android 客户端也在开发, 但进展缓慢
### 使用 ### 使用
服务端: 服务端:
0. 确保安装 Node.js 0. 安装 Node.js
1. 克隆本仓库源代码到本地,并运行 run_build.sh 构建网页 1. 克隆或下载本仓库源代码,执行`npm install`,再执行`npm run start` 或者运行 run.bat / run.sh
2. 运行 run.sh 客户端:
网页端: * 使用服务端提供的网页 (推荐)
* 直接使用和服务端集成的网页 (推荐) * GitHub Pages (可能导致跨域问题)
* 克隆本仓库到本地并运行本地 HTTP 服务端
* 静态网页 (不推荐)
### [Q&A](.github/QA.md) ### [Q&A](.github/QA.md)

36
readme_en.md Normal file
View File

@@ -0,0 +1,36 @@
[ [中文](readme.md) | English ]
<div align="center">
<h2> LingChair </h2>
</div>
Welcome to LingChair! This is an IM project that it provide a new communication way. And it serves people.
> [!NOTE]
I'm sorry that I have no time to translate this project to English. This project is so large for me to rewrite, so there're still a lot of texts in Chinese.
>
> This project is still in progress, [Click me](final.md) to view progress. (Chinese)
>
> Comments and BUG report welcome
### Usage
Server:
0. Install Node.js
1. Clone this repo or download the source code, then run `npm install`, after that, run `npm run start` or run.bat / run.sh
Client:
* Use the pages that the server provide (Recommend)
* GitHub Pages (It may cause CORS security error)
### [Q&A (Chinese)](.github/QA.md)
### Credits
WIP
### [Do you know? (Chinese)](.github/do_you_know.md)

View File

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

View File

@@ -48,7 +48,8 @@ if (!io.exists(vals.LINGCHAIR_SERVER_CONFIG_FILE)) io.open(vals.LINGCHAIR_SERVER
cert: "", cert: "",
}, },
})).close() })).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")) vals.LINGCHAIR_SERVER_CONFIG = JSON.parse(io.open(vals.LINGCHAIR_SERVER_CONFIG_FILE, "r").read("*a"))

View File

@@ -149,18 +149,17 @@ let api = {
}, },
// 添加好友 // 添加好友
// {name: 账号, accessToken: 访问令牌} 返回 {friends: []} // {name: 账号, accessToken: 访问令牌}
// WIP
"user.addFriend": (a, cb) => { "user.addFriend": (a, cb) => {
if (checkEmpty([a.name, a.accessToken])) if (checkEmpty([a.name, a.target, a.accessToken]))
return cb({ msg: "参数缺失", code: -1 }) return cb({ msg: "参数缺失", code: -1 })
let { msg, code, friends } = users.getFriends(a.name, a.accessToken) let { msg, code } = users.addFriend(a.name, a.target, a.accessToken)
if (code !== 0) if (code !== 0)
return cb({ msg: msg, code: code }) return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { friends: friends } }) cb({ msg: msg, code: 0 })
}, },
"user.getNick": (a, cb) => { "user.getNick": (a, cb) => {