Update.
This commit is contained in:
parent
78f9679f30
commit
e7cd397e0f
@ -22,7 +22,8 @@ class ChannelMessageService(BaseService):
|
|||||||
context.update(dict(
|
context.update(dict(
|
||||||
user_uid=user['uid'],
|
user_uid=user['uid'],
|
||||||
username=user['username'],
|
username=user['username'],
|
||||||
user_nick=user['nick']
|
user_nick=user['nick'],
|
||||||
|
color=user['color']
|
||||||
))
|
))
|
||||||
try:
|
try:
|
||||||
template = self.app.jinja2_env.get_template("message.html")
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
@ -34,4 +35,27 @@ class ChannelMessageService(BaseService):
|
|||||||
return model
|
return model
|
||||||
raise Exception(f"Failed to create channel message: {model.errors}.")
|
raise Exception(f"Failed to create channel message: {model.errors}.")
|
||||||
|
|
||||||
|
async def to_extended_dict(self, message):
|
||||||
|
user = await self.services.user.get(uid=message["user_uid"])
|
||||||
|
if not user:
|
||||||
|
print("User not found!", flush=True)
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"uid": message["uid"],
|
||||||
|
"color": user['color'],
|
||||||
|
"user_uid": message["user_uid"],
|
||||||
|
"channel_uid": message["channel_uid"],
|
||||||
|
"user_nick": user['nick'],
|
||||||
|
"message": message["message"],
|
||||||
|
"created_at": message["created_at"],
|
||||||
|
"html": message['html'],
|
||||||
|
"username": user['username']
|
||||||
|
}
|
||||||
|
|
||||||
|
async def offset(self, channel_uid, offset=0):
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async for model in self.query("SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 60 OFFSET :offset",dict(channel_uid=channel_uid, offset=offset)):
|
||||||
|
results.append(model)
|
||||||
|
results.sort(key=lambda x: x['created_at'])
|
||||||
|
return results
|
||||||
|
@ -289,7 +289,7 @@ class App extends EventHandler {
|
|||||||
this.audio = new NotificationAudio(500);
|
this.audio = new NotificationAudio(500);
|
||||||
const me = this
|
const me = this
|
||||||
this.ws.addEventListener("channel-message", (data) => {
|
this.ws.addEventListener("channel-message", (data) => {
|
||||||
me.emit(data.channel_uid, data);
|
me.emit("channel-message", data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rpc.getUser(null).then(user => {
|
this.rpc.getUser(null).then(user => {
|
||||||
@ -300,7 +300,31 @@ class App extends EventHandler {
|
|||||||
playSound(index) {
|
playSound(index) {
|
||||||
this.audio.play(index);
|
this.audio.play(index);
|
||||||
}
|
}
|
||||||
|
timeDescription(isoDate) {
|
||||||
|
const date = new Date(isoDate);
|
||||||
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
timeAgo(date1, date2) {
|
||||||
|
const diffMs = Math.abs(date2 - date1);
|
||||||
|
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
if (days) {
|
||||||
|
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
||||||
|
}
|
||||||
|
if (hours) {
|
||||||
|
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
||||||
|
}
|
||||||
|
if (minutes) {
|
||||||
|
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
||||||
|
}
|
||||||
|
return 'just now';
|
||||||
|
}
|
||||||
async benchMark(times = 100, message = "Benchmark Message") {
|
async benchMark(times = 100, message = "Benchmark Message") {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
const me = this;
|
const me = this;
|
||||||
|
@ -161,7 +161,7 @@ message-list {
|
|||||||
}
|
}
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
@ -211,9 +211,20 @@ message-list {
|
|||||||
color: #e6e6e6;
|
color: #e6e6e6;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
display: block;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.message-content {
|
||||||
|
width: 100%;
|
||||||
|
display:block;
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.message-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
.chat-messages .message .message-content .time {
|
.chat-messages .message .message-content .time {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
@ -73,7 +73,8 @@ class ChatWindowElement extends HTMLElement {
|
|||||||
const me = this;
|
const me = this;
|
||||||
channelElement.addEventListener("message", (message) => {
|
channelElement.addEventListener("message", (message) => {
|
||||||
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
||||||
message.detail.element.scrollIntoView();
|
|
||||||
|
message.detail.element.scrollIntoView({"block": "end"});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ this.onpush = (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register("service-worker.js")
|
.register("/service-worker.js")
|
||||||
.then((serviceWorkerRegistration) => {
|
.then((serviceWorkerRegistration) => {
|
||||||
serviceWorkerRegistration.pushManager.subscribe().then(
|
serviceWorkerRegistration.pushManager.subscribe().then(
|
||||||
(pushSubscription) => {
|
(pushSubscription) => {
|
||||||
|
@ -21,7 +21,7 @@ class UploadButtonElement extends HTMLElement {
|
|||||||
|
|
||||||
const files = fileInput.files;
|
const files = fileInput.files;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('channel_uid', this.chatInput.channelUid);
|
formData.append('channel_uid', this.channelUid);
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
formData.append('files[]', files[i]);
|
formData.append('files[]', files[i]);
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ class UploadButtonElement extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
|
this.channelUid = this.getAttribute('channel');
|
||||||
this.uploadButton = this.container.querySelector('.upload-button');
|
this.uploadButton = this.container.querySelector('.upload-button');
|
||||||
this.fileInput = this.container.querySelector('.hidden-input');
|
this.fileInput = this.container.querySelector('.hidden-input');
|
||||||
this.uploadButton.addEventListener('click', () => {
|
this.uploadButton.addEventListener('click', () => {
|
||||||
|
@ -16,6 +16,7 @@ def set_link_target_blank(text):
|
|||||||
element.attrs['target'] = '_blank'
|
element.attrs['target'] = '_blank'
|
||||||
element.attrs['rel'] = 'noopener noreferrer'
|
element.attrs['rel'] = 'noopener noreferrer'
|
||||||
element.attrs['referrerpolicy'] = 'no-referrer'
|
element.attrs['referrerpolicy'] = 'no-referrer'
|
||||||
|
element.attrs['href'] = element.attrs['href'].strip(".")
|
||||||
|
|
||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ def set_link_target_blank(text):
|
|||||||
def linkify_https(text):
|
def linkify_https(text):
|
||||||
if not "https://" in text:
|
if not "https://" in text:
|
||||||
return text
|
return text
|
||||||
url_pattern = r'(?<!["\'])\bhttps://[^\s<>()]+'
|
url_pattern = r'(?<!["\'])\bhttps://[^\s<>()]+(?<!\.)'
|
||||||
|
|
||||||
soup = BeautifulSoup(text, 'html.parser')
|
soup = BeautifulSoup(text, 'html.parser')
|
||||||
|
|
||||||
|
@ -12,11 +12,6 @@
|
|||||||
<script src="/html-frame.js"></script>
|
<script src="/html-frame.js"></script>
|
||||||
<script src="/schedule.js"></script>
|
<script src="/schedule.js"></script>
|
||||||
<script src="/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
<script src="/models.js"></script>
|
|
||||||
<script src="/message-list.js"></script>
|
|
||||||
<script src="/message-list-manager.js"></script>
|
|
||||||
<script src="/chat-input.js"></script>
|
|
||||||
<script src="/chat-window.js"></script>
|
|
||||||
<link rel="stylesheet" href="/base.css">
|
<link rel="stylesheet" href="/base.css">
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="icon" type="image/png" href="/image/snek1.png" sizes="32x32">
|
<link rel="icon" type="image/png" href="/image/snek1.png" sizes="32x32">
|
||||||
|
@ -1 +1 @@
|
|||||||
<link rel="stylesheet" href="/highlight.css">{#<div style="max-width:100%;" data-uid="{{uid}}" data-color="{{color}}" data-channel_uid="{{channel_uid}}" data-user_nick="{{user_nick}}" data-created_at="{{created_at}}" data-user_uid="{{user_uid}}" data-message="{{message}}" class="message"><div class="avatar" style="background-color: {{color}}; color: black;">{{user_nick[0]}}</div><div class="message-content"><div class="author" style="color: {{color}};">{{user_nick}}</div><div class="text">#}{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}{#</div><div class="time">{{created_at}}</div></div></div>#}
|
<div style="max-width:100%;" data-uid="{{uid}}" data-color="{{color}}" data-channel_uid="{{channel_uid}}" data-user_nick="{{user_nick}}" data-created_at="{{created_at}}" data-user_uid="{{user_uid}}" data-message="{{message}}" class="message"><div class="avatar" style="background-color: {{color}}; color: black;">{{user_nick[0]}}</div><div class="message-content"><div class="author" style="color: {{color}};">{{user_nick}}</div><div class="text">{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}</div><div class="time no-select" data-created_at="{{created_at}}"></div></div></div>
|
||||||
|
@ -1,4 +1,99 @@
|
|||||||
{% extends "app.html" %}
|
{% extends "app.html" %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<chat-window class="chat-area"></chat-window>
|
<section class="chat-area" id="chat">
|
||||||
|
<div class="chat-header"><h2>{{ channel.label.value }}</h2></div>
|
||||||
|
<div class="chat-messages">
|
||||||
|
|
||||||
|
{% for message in messages %}
|
||||||
|
{% autoescape false %}
|
||||||
|
{{message.html}}
|
||||||
|
{% endautoescape %}
|
||||||
|
<div style="display:none" class="message {% if loop.first or message.username != messages[loop.index0 - 1].username %}switch-user{% endif %}" data-uid="{{message.uid}}" data-color="{{message.color}}" data-user_nick="{{message.user_nick}}" data-created_at="{{message.created_at}}">
|
||||||
|
<div class="avatar no-select" style="background-color: {{message.color}}; color: black;">{{message.user_nick[0]}}</div>
|
||||||
|
<div class="message-content">
|
||||||
|
{% autoescape false %}
|
||||||
|
{{ message.html }}
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
<div class="time no-select" data-created_at="{{message.created_at}}">{{message.created_at}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<chat-window style="display:none" class="chat-area"></chat-window>
|
||||||
|
<div class="chat-input">
|
||||||
|
<textarea placeholder="Type a message..." rows="2"></textarea>
|
||||||
|
<upload-button channel="{{channel.channel_uid.value}}"></upload-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
const channelUid = "{{channel.channel_uid.value}}"
|
||||||
|
|
||||||
|
function initInputField(textBox){
|
||||||
|
|
||||||
|
textBox.addEventListener('change', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
textBox.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
const message = e.target.value.trim();
|
||||||
|
if (!message) return;
|
||||||
|
app.rpc.sendMessage(channelUid, e.target.value)
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initInputField(document.querySelector("textarea"))
|
||||||
|
function updateTimes(){
|
||||||
|
document.querySelectorAll(".time").forEach((time) => {
|
||||||
|
time.innerText = app.timeDescription(time.dataset.created_at)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function updateLayout() {
|
||||||
|
|
||||||
|
document.querySelectorAll(".chat-messages").forEach((messages) => messages.scrollTop = messages.scrollHeight + 1000)
|
||||||
|
//document.querySelectorAll(".chat-messages .message:last-child").forEach((message) => message.scrollIntoView({block: "end"}))
|
||||||
|
updateTimes()
|
||||||
|
let previousUser = null;
|
||||||
|
document.querySelectorAll(".message").forEach((message) => {
|
||||||
|
if(previousUser != message.dataset.user_uid) {
|
||||||
|
message.classList.add("switch-user")
|
||||||
|
previousUser = message.dataset.user_uid
|
||||||
|
}else{
|
||||||
|
message.classList.remove("switch-user")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setInterval(() => {
|
||||||
|
|
||||||
|
|
||||||
|
updateTimes()
|
||||||
|
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
app.addEventListener("channel-message", (data) => {
|
||||||
|
if(data.channel_uid != channelUid) return
|
||||||
|
if(data.username != "{{user.username.value}}"){
|
||||||
|
app.playSound(0)
|
||||||
|
}
|
||||||
|
const message = document.createElement("div")
|
||||||
|
//message.classList.add("message")
|
||||||
|
message.dataset.color = data.color
|
||||||
|
message.dataset.created_at = data.created_at
|
||||||
|
message.dataset.user_nick = data.user_nick
|
||||||
|
message.dataset.uid = data.uid
|
||||||
|
message.innerHTML = data.html
|
||||||
|
document.querySelector(".chat-messages").appendChild(message.firstChild)
|
||||||
|
setTimeout(()=>{
|
||||||
|
updateLayout()
|
||||||
|
},50)
|
||||||
|
})
|
||||||
|
updateLayout()
|
||||||
|
//.scrollTop = document.querySelector(".chat-messages").scrollHeight + 5000
|
||||||
|
// })
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -73,22 +73,11 @@ class RPCView(BaseView):
|
|||||||
async def get_messages(self, channel_uid, offset=0):
|
async def get_messages(self, channel_uid, offset=0):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
messages = []
|
messages = []
|
||||||
async for message in self.services.channel_message.query("SELECT * FROM channel_message ORDER BY created_at DESC LIMIT 60"):
|
print("Channel uid:", channel_uid, flush=True)
|
||||||
user = await self.services.user.get(uid=message["user_uid"])
|
for message in await self.services.channel_message.offset(channel_uid, offset):
|
||||||
if not user:
|
print(message, flush=True)
|
||||||
print("User not found!", flush=True)
|
extended_dict = await self.services.channel_message.to_extended_dict(message)
|
||||||
continue
|
messages.append(extended_dict)
|
||||||
messages.insert(0, {
|
|
||||||
"uid": message["uid"],
|
|
||||||
"color": user['color'],
|
|
||||||
"user_uid": message["user_uid"],
|
|
||||||
"channel_uid": message["channel_uid"],
|
|
||||||
"user_nick": user['nick'],
|
|
||||||
"message": message["message"],
|
|
||||||
"created_at": message["created_at"],
|
|
||||||
"html": message['html'],
|
|
||||||
"username": user['username']
|
|
||||||
})
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
async def get_channels(self):
|
async def get_channels(self):
|
||||||
|
@ -70,4 +70,12 @@ class WebView(BaseView):
|
|||||||
if not user:
|
if not user:
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
|
|
||||||
return await self.render_template("web.html", {"channel": channel_member,"user": user})
|
if self.request.path.endswith(".json"):
|
||||||
|
return await super().get()
|
||||||
|
|
||||||
|
messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(
|
||||||
|
channel["uid"]
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
return await self.render_template("web.html", {"channel": channel_member,"user": user,"messages": messages})
|
||||||
|
Loading…
Reference in New Issue
Block a user