Files
LingChair-V0/ling_chair_http/handler.js
2024-06-29 17:25:59 +08:00

715 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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