Compare commits
No commits in common. "abce2e03d1e47992d234779cd38d476e0bfc766a" and "f9f1179db5e72492e22b59cc0415577b599fa119" have entirely different histories.
abce2e03d1
...
f9f1179db5
@ -12,10 +12,6 @@ class ChannelModel(BaseModel):
|
|||||||
index = ModelField(name="index", required=True, kind=int, value=1000)
|
index = ModelField(name="index", required=True, kind=int, value=1000)
|
||||||
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
|
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
|
||||||
history_start = ModelField(name="history_start", required=False, kind=str)
|
history_start = ModelField(name="history_start", required=False, kind=str)
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dm(self):
|
|
||||||
return 'dm' in self['tag'].lower()
|
|
||||||
|
|
||||||
async def get_last_message(self) -> ChannelMessageModel:
|
async def get_last_message(self) -> ChannelMessageModel:
|
||||||
history_start_filter = ""
|
history_start_filter = ""
|
||||||
|
|||||||
@ -69,10 +69,10 @@ class ChannelMessageService(BaseService):
|
|||||||
"color": user["color"],
|
"color": user["color"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
context['message'] = whitelist_attributes(context['message'])
|
|
||||||
try:
|
try:
|
||||||
template = self.app.jinja2_env.get_template("message.html")
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
model["html"] = template.render(**context)
|
model["html"] = template.render(**context)
|
||||||
|
model["html"] = whitelist_attributes(model["html"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex, flush=True)
|
print(ex, flush=True)
|
||||||
|
|
||||||
@ -118,7 +118,6 @@ class ChannelMessageService(BaseService):
|
|||||||
async def save(self, model):
|
async def save(self, model):
|
||||||
context = {}
|
context = {}
|
||||||
context.update(model.record)
|
context.update(model.record)
|
||||||
context['message'] = whitelist_attributes(context['message'])
|
|
||||||
user = await self.app.services.user.get(model["user_uid"])
|
user = await self.app.services.user.get(model["user_uid"])
|
||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
@ -130,6 +129,7 @@ class ChannelMessageService(BaseService):
|
|||||||
)
|
)
|
||||||
template = self.app.jinja2_env.get_template("message.html")
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
model["html"] = template.render(**context)
|
model["html"] = template.render(**context)
|
||||||
|
model["html"] = whitelist_attributes(model["html"])
|
||||||
return await super().save(model)
|
return await super().save(model)
|
||||||
|
|
||||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||||
|
|||||||
@ -368,7 +368,7 @@ input[type="text"], .chat-input textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.switch-user + .message, .message.long-time + .message, .message-list-bottom + .message{
|
.message.switch-user + .message, .message.long-time + .message, .message:first-child {
|
||||||
.time {
|
.time {
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@ -9,53 +9,8 @@ import {app} from "./app.js";
|
|||||||
|
|
||||||
const LONG_TIME = 1000 * 60 * 20
|
const LONG_TIME = 1000 * 60 * 20
|
||||||
|
|
||||||
export class ReplyEvent extends Event {
|
|
||||||
constructor(messageTextTarget) {
|
|
||||||
super('reply', { bubbles: true, composed: true });
|
|
||||||
this.messageTextTarget = messageTextTarget;
|
|
||||||
|
|
||||||
const newMessage = messageTextTarget.cloneNode(true);
|
|
||||||
newMessage.style.maxHeight = "0"
|
|
||||||
messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);
|
|
||||||
|
|
||||||
newMessage.querySelectorAll('.embed-url-link').forEach(link => {
|
|
||||||
link.remove()
|
|
||||||
})
|
|
||||||
|
|
||||||
newMessage.querySelectorAll('picture').forEach(picture => {
|
|
||||||
const img = picture.querySelector('img');
|
|
||||||
if (img) {
|
|
||||||
picture.replaceWith(img);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
newMessage.querySelectorAll('img').forEach(img => {
|
|
||||||
const src = img.src || img.currentSrc;
|
|
||||||
img.replaceWith(document.createTextNode(src));
|
|
||||||
})
|
|
||||||
|
|
||||||
newMessage.querySelectorAll('iframe').forEach(iframe => {
|
|
||||||
const src = iframe.src || iframe.currentSrc;
|
|
||||||
iframe.replaceWith(document.createTextNode(src));
|
|
||||||
})
|
|
||||||
|
|
||||||
newMessage.querySelectorAll('a').forEach(a => {
|
|
||||||
const href = a.getAttribute('href');
|
|
||||||
const text = a.innerText || a.textContent;
|
|
||||||
if (text === href || text === '') {
|
|
||||||
a.replaceWith(document.createTextNode(href));
|
|
||||||
} else {
|
|
||||||
a.replaceWith(document.createTextNode(`[${text}](${href})`));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
|
|
||||||
newMessage.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MessageElement extends HTMLElement {
|
class MessageElement extends HTMLElement {
|
||||||
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
||||||
|
|
||||||
isVisible() {
|
isVisible() {
|
||||||
if (!this) return false;
|
if (!this) return false;
|
||||||
@ -96,12 +51,6 @@ class MessageElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.timeDiv = this.querySelector('.time span');
|
this.timeDiv = this.querySelector('.time span');
|
||||||
this.replyDiv = this.querySelector('.time a');
|
|
||||||
|
|
||||||
this.replyDiv.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.dispatchEvent(new ReplyEvent(this.messageDiv));
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.siblingGenerated && this.nextElementSibling) {
|
if (!this.siblingGenerated && this.nextElementSibling) {
|
||||||
@ -150,9 +99,7 @@ class MessageList extends HTMLElement {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
app.ws.addEventListener("update_message_text", (data) => {
|
app.ws.addEventListener("update_message_text", (data) => {
|
||||||
if (this.messageMap.has(data.uid)) {
|
this.upsertMessage(data);
|
||||||
this.upsertMessage(data);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
app.ws.addEventListener("set_typing", (data) => {
|
app.ws.addEventListener("set_typing", (data) => {
|
||||||
this.triggerGlow(data.user_uid,data.color);
|
this.triggerGlow(data.user_uid,data.color);
|
||||||
@ -161,33 +108,29 @@ class MessageList extends HTMLElement {
|
|||||||
this.messageMap = new Map();
|
this.messageMap = new Map();
|
||||||
this.visibleSet = new Set();
|
this.visibleSet = new Set();
|
||||||
this._observer = new IntersectionObserver((entries) => {
|
this._observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.visibleSet.add(entry.target);
|
this.visibleSet.add(entry.target);
|
||||||
const messageElement = entry.target;
|
const messageElement = entry.target;
|
||||||
if (messageElement instanceof MessageElement) {
|
if (messageElement instanceof MessageElement) {
|
||||||
messageElement.updateUI();
|
messageElement.updateUI();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.visibleSet.delete(entry.target);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
this.visibleSet.delete(entry.target);
|
console.log(this.visibleSet);
|
||||||
}
|
|
||||||
});
|
|
||||||
}, {
|
}, {
|
||||||
root: this,
|
root: this,
|
||||||
threshold: 0,
|
threshold: 0.1
|
||||||
})
|
})
|
||||||
|
|
||||||
for(const c of this.children) {
|
for(const c of this.children) {
|
||||||
this._observer.observe(c);
|
this._observer.observe(c);
|
||||||
if (c instanceof MessageElement) {
|
if (c instanceof MessageElement) {
|
||||||
this.messageMap.set(c.dataset.uid, c);
|
this.messageMap.set(c.dataset.uid, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.endOfMessages = document.createElement('div');
|
|
||||||
this.endOfMessages.classList.add('message-list-bottom');
|
|
||||||
this.prepend(this.endOfMessages);
|
|
||||||
|
|
||||||
this.scrollToBottom(true);
|
this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,6 +174,7 @@ class MessageList extends HTMLElement {
|
|||||||
};
|
};
|
||||||
document.addEventListener('keydown', escListener);
|
document.addEventListener('keydown', escListener);
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
isElementVisible(element) {
|
isElementVisible(element) {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
@ -243,13 +187,13 @@ class MessageList extends HTMLElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
isScrolledToBottom() {
|
isScrolledToBottom() {
|
||||||
return this.isElementVisible(this.endOfMessages);
|
return this.isElementVisible(this.firstElementChild);
|
||||||
}
|
}
|
||||||
scrollToBottom(force = false, behavior= 'instant') {
|
scrollToBottom(force = false, behavior= 'smooth') {
|
||||||
if (force || !this.isScrolledToBottom()) {
|
if (force || this.isScrolledToBottom()) {
|
||||||
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
this.firstElementChild.scrollIntoView({ behavior, block: 'start' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
this.firstElementChild.scrollIntoView({ behavior, block: 'start' });
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,8 +227,9 @@ class MessageList extends HTMLElement {
|
|||||||
|
|
||||||
upsertMessage(data) {
|
upsertMessage(data) {
|
||||||
let message = this.messageMap.get(data.uid);
|
let message = this.messageMap.get(data.uid);
|
||||||
|
const newMessage = !!message;
|
||||||
if (message) {
|
if (message) {
|
||||||
message.parentElement?.removeChild(message);
|
message.parentElement.removeChild(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.message) return
|
if (!data.message) return
|
||||||
@ -294,16 +239,16 @@ class MessageList extends HTMLElement {
|
|||||||
wrapper.innerHTML = data.html;
|
wrapper.innerHTML = data.html;
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
|
message.updateMessage(...wrapper.firstElementChild._originalChildren);
|
||||||
} else {
|
} else {
|
||||||
message = wrapper.firstElementChild;
|
message = wrapper.firstElementChild;
|
||||||
this.messageMap.set(data.uid, message);
|
this.messageMap.set(data.uid, message);
|
||||||
this._observer.observe(message);
|
this._observer.observe(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrolledToBottom = this.isScrolledToBottom();
|
const scrolledToBottom = this.isScrolledToBottom();
|
||||||
this.prepend(message);
|
this.prepend(message);
|
||||||
if (scrolledToBottom) this.scrollToBottom(true);
|
if (scrolledToBottom) this.scrollToBottom(true, !newMessage ? 'smooth' : 'auto');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
class RestClient {
|
class RestClient {
|
||||||
constructor({ baseURL = '', headers = {} } = {}) {
|
constructor({ baseURL = '', headers = {} } = {}) {
|
||||||
this.baseURL = baseURL;
|
this.baseURL = baseURL;
|
||||||
@ -208,52 +210,27 @@ class Njet extends HTMLElement {
|
|||||||
customElements.define(name, component);
|
customElements.define(name, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(config) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// Store the config for use in render and other methods
|
|
||||||
this.config = config || {};
|
|
||||||
|
|
||||||
if (!Njet._root) {
|
if (!Njet._root) {
|
||||||
Njet._root = this
|
Njet._root = this
|
||||||
Njet._rest = new RestClient({ baseURL: '/' || null })
|
Njet._rest = new RestClient({ baseURL: '/' || null })
|
||||||
}
|
}
|
||||||
this.root._elements.push(this)
|
this.root._elements.push(this)
|
||||||
this.classList.add('njet');
|
this.classList.add('njet');
|
||||||
|
|
||||||
// Initialize properties from config before rendering
|
|
||||||
this.initProps(this.config);
|
|
||||||
|
|
||||||
// Call render after properties are initialized
|
|
||||||
this.render.call(this);
|
this.render.call(this);
|
||||||
|
//this.initProps(config);
|
||||||
// Call construct if defined
|
//if (typeof this.config.construct === 'function')
|
||||||
if (typeof this.config.construct === 'function') {
|
// this.config.construct.call(this)
|
||||||
this.config.construct.call(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initProps(config) {
|
initProps(config) {
|
||||||
const props = Object.keys(config)
|
const props = Object.keys(config)
|
||||||
props.forEach(prop => {
|
props.forEach(prop => {
|
||||||
// Skip special properties that are handled separately
|
if (config[prop] !== undefined) {
|
||||||
if (['construct', 'items', 'classes'].includes(prop)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there's a setter for this property
|
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), prop);
|
|
||||||
if (descriptor && descriptor.set) {
|
|
||||||
// Use the setter
|
|
||||||
this[prop] = config[prop];
|
this[prop] = config[prop];
|
||||||
} else if (prop in this) {
|
|
||||||
// Property exists, set it directly
|
|
||||||
this[prop] = config[prop];
|
|
||||||
} else {
|
|
||||||
// Set as attribute for unknown properties
|
|
||||||
this.setAttribute(prop, config[prop]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.classes) {
|
if (config.classes) {
|
||||||
this.classList.add(...config.classes);
|
this.classList.add(...config.classes);
|
||||||
}
|
}
|
||||||
@ -365,7 +342,7 @@ class NjetDialog extends Component {
|
|||||||
const buttonContainer = document.createElement('div');
|
const buttonContainer = document.createElement('div');
|
||||||
buttonContainer.style.marginTop = '20px';
|
buttonContainer.style.marginTop = '20px';
|
||||||
buttonContainer.style.display = 'flex';
|
buttonContainer.style.display = 'flex';
|
||||||
buttonContainer.style.justifyContent = 'flex-end';
|
buttonContainer.style.justifyContent = 'flenjet-end';
|
||||||
buttonContainer.style.gap = '10px';
|
buttonContainer.style.gap = '10px';
|
||||||
if (secondaryButton) {
|
if (secondaryButton) {
|
||||||
const secondary = new NjetButton(secondaryButton);
|
const secondary = new NjetButton(secondaryButton);
|
||||||
@ -395,9 +372,8 @@ class NjetWindow extends Component {
|
|||||||
header.textContent = title;
|
header.textContent = title;
|
||||||
this.appendChild(header);
|
this.appendChild(header);
|
||||||
}
|
}
|
||||||
if (this.config.items) {
|
this.config.items.forEach(item => this.appendChild(item));
|
||||||
this.config.items.forEach(item => this.appendChild(item));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show(){
|
show(){
|
||||||
@ -432,8 +408,7 @@ class NjetGrid extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Njet.registerComponent('njet-grid', NjetGrid);
|
Njet.registerComponent('njet-grid', NjetGrid);
|
||||||
|
/*
|
||||||
/* Example usage:
|
|
||||||
const button = new NjetButton({
|
const button = new NjetButton({
|
||||||
classes: ['my-button'],
|
classes: ['my-button'],
|
||||||
text: 'Shared',
|
text: 'Shared',
|
||||||
@ -518,7 +493,7 @@ document.body.appendChild(dialog);
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class NjetComponent extends Component {}
|
class NjetComponent extends Component {}
|
||||||
const njet = Njet
|
const njet = Njet
|
||||||
njet.showDialog = function(args){
|
njet.showDialog = function(args){
|
||||||
const dialog = new NjetDialog(args)
|
const dialog = new NjetDialog(args)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
@ -570,16 +545,15 @@ njet.showWindow = function(args) {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
njet.publish = function(event, data) {
|
njet.publish = function(event, data) {
|
||||||
if (this.root && this.root._subscriptions && this.root._subscriptions[event]) {
|
if (this.root._subscriptions[event]) {
|
||||||
this.root._subscriptions[event].forEach(callback => callback(data))
|
this.root._subscriptions[event].forEach(callback => callback(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
njet.subscribe = function(event, callback) {
|
njet.subscribe = function(event, callback) {
|
||||||
if (!this.root) return;
|
|
||||||
if (!this.root._subscriptions[event]) {
|
if (!this.root._subscriptions[event]) {
|
||||||
this.root._subscriptions[event] = []
|
this.root._subscriptions[event] = []
|
||||||
}
|
}
|
||||||
this.root._subscriptions[event].push(callback)
|
this.root._subscriptions[event].push(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow, eventBus };
|
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow,eventBus };
|
||||||
|
|||||||
@ -79,12 +79,44 @@ emoji.EMOJI_DATA[
|
|||||||
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
|
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["picture"]
|
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
|
||||||
|
"img",
|
||||||
|
"video",
|
||||||
|
"audio",
|
||||||
|
"source",
|
||||||
|
"iframe",
|
||||||
|
"picture",
|
||||||
|
"span",
|
||||||
|
]
|
||||||
|
ALLOWED_ATTRIBUTES = {
|
||||||
|
**bleach.sanitizer.ALLOWED_ATTRIBUTES,
|
||||||
|
"img": ["src", "alt", "title", "width", "height"],
|
||||||
|
"a": ["href", "title", "target", "rel", "referrerpolicy", "class"],
|
||||||
|
"iframe": [
|
||||||
|
"src",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"frameborder",
|
||||||
|
"allow",
|
||||||
|
"allowfullscreen",
|
||||||
|
"title",
|
||||||
|
"referrerpolicy",
|
||||||
|
"style",
|
||||||
|
],
|
||||||
|
"video": ["src", "controls", "width", "height"],
|
||||||
|
"audio": ["src", "controls"],
|
||||||
|
"source": ["src", "type"],
|
||||||
|
"span": ["class"],
|
||||||
|
"picture": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def sanitize_html(value):
|
def sanitize_html(value):
|
||||||
return bleach.clean(
|
return bleach.clean(
|
||||||
value,
|
value,
|
||||||
protocols=list(bleach.sanitizer.ALLOWED_PROTOCOLS) + ["data"],
|
tags=ALLOWED_TAGS,
|
||||||
|
attributes=ALLOWED_ATTRIBUTES,
|
||||||
|
protocols=bleach.sanitizer.ALLOWED_PROTOCOLS + ["data"],
|
||||||
strip=True,
|
strip=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,8 +132,50 @@ def set_link_target_blank(text):
|
|||||||
|
|
||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
|
SAFE_ATTRIBUTES = {
|
||||||
|
"href",
|
||||||
|
"src",
|
||||||
|
"alt",
|
||||||
|
"title",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"style",
|
||||||
|
"id",
|
||||||
|
"class",
|
||||||
|
"rel",
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"value",
|
||||||
|
"placeholder",
|
||||||
|
"aria-hidden",
|
||||||
|
"aria-label",
|
||||||
|
"srcset",
|
||||||
|
"target",
|
||||||
|
"rel",
|
||||||
|
"referrerpolicy",
|
||||||
|
"controls",
|
||||||
|
"frameborder",
|
||||||
|
"allow",
|
||||||
|
"allowfullscreen",
|
||||||
|
"referrerpolicy",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def whitelist_attributes(html):
|
def whitelist_attributes(html):
|
||||||
return sanitize_html(html)
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
|
for tag in soup.find_all():
|
||||||
|
if hasattr(tag, "attrs"):
|
||||||
|
if tag.name in ["script", "form", "input"]:
|
||||||
|
tag.replace_with("")
|
||||||
|
continue
|
||||||
|
attrs = dict(tag.attrs)
|
||||||
|
for attr in list(attrs):
|
||||||
|
# Check if attribute is in the safe list or is a data-* attribute
|
||||||
|
if not (attr in SAFE_ATTRIBUTES or attr.startswith("data-")):
|
||||||
|
del tag.attrs[attr]
|
||||||
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
def embed_youtube(text):
|
def embed_youtube(text):
|
||||||
|
|||||||
@ -12,7 +12,7 @@ function showTerm(options){
|
|||||||
|
|
||||||
|
|
||||||
class StarField {
|
class StarField {
|
||||||
constructor({ count = 100, container = document.body } = {}) {
|
constructor({ count = 200, container = document.body } = {}) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.starCount = count;
|
this.starCount = count;
|
||||||
this.stars = [];
|
this.stars = [];
|
||||||
@ -567,7 +567,7 @@ const count = Array.from(messages).filter(el => el.textContent.trim() === text).
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const starField = new StarField({starCount: 100});
|
const starField = new StarField({starCount: 200});
|
||||||
app.starField = starField;
|
app.starField = starField;
|
||||||
|
|
||||||
class DemoSequence {
|
class DemoSequence {
|
||||||
|
|||||||
@ -72,13 +72,12 @@ function throttle(fn, wait) {
|
|||||||
// --- Scroll: load extra messages, throttled ---
|
// --- Scroll: load extra messages, throttled ---
|
||||||
let isLoadingExtra = false;
|
let isLoadingExtra = false;
|
||||||
async function loadExtra() {
|
async function loadExtra() {
|
||||||
const firstMessage = messagesContainer.lastElementChild;
|
const firstMessage = messagesContainer.children[messagesContainer.children.length - 1];
|
||||||
if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return;
|
if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return;
|
||||||
isLoadingExtra = true;
|
isLoadingExtra = true;
|
||||||
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
||||||
if (messages.length) {
|
if (messages.length) {
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
messages.reverse();
|
|
||||||
messages.forEach(msg => {
|
messages.forEach(msg => {
|
||||||
const temp = document.createElement("div");
|
const temp = document.createElement("div");
|
||||||
temp.innerHTML = msg.html;
|
temp.innerHTML = msg.html;
|
||||||
@ -139,16 +138,10 @@ chatInputField.textarea.focus();
|
|||||||
|
|
||||||
// --- Reply helper ---
|
// --- Reply helper ---
|
||||||
function replyMessage(message) {
|
function replyMessage(message) {
|
||||||
chatInputField.value = "```markdown\n> " + (message || '').trim().split("\n").join("\n> ") + "\n```\n";
|
chatInputField.value = "```markdown\n> " + (message || '').split("\n").join("\n> ") + "\n```\n";
|
||||||
chatInputField.textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
chatInputField.focus();
|
chatInputField.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesContainer.addEventListener("reply", (e) => {
|
|
||||||
const messageText = e.replyText || e.messageTextTarget.textContent.trim();
|
|
||||||
replyMessage(messageText);
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- Mention helpers ---
|
// --- Mention helpers ---
|
||||||
function extractMentions(message) {
|
function extractMentions(message) {
|
||||||
return [...new Set(message.match(/@\w+/g) || [])];
|
return [...new Set(message.match(/@\w+/g) || [])];
|
||||||
@ -222,7 +215,7 @@ document.addEventListener('keydown', function(event) {
|
|||||||
keyTimeout = setTimeout(() => { gPressCount = 0; }, 300);
|
keyTimeout = setTimeout(() => { gPressCount = 0; }, 300);
|
||||||
if (gPressCount === 2) {
|
if (gPressCount === 2) {
|
||||||
gPressCount = 0;
|
gPressCount = 0;
|
||||||
messagesContainer.lastElementChild?.scrollIntoView({ block: "end", inline: "nearest" });
|
messagesContainer.querySelector(".message:first-child")?.scrollIntoView({ block: "end", inline: "nearest" });
|
||||||
loadExtra();
|
loadExtra();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,7 +254,7 @@ function updateLayout(doScrollDown) {
|
|||||||
function isScrolledPastHalf() {
|
function isScrolledPastHalf() {
|
||||||
let scrollTop = messagesContainer.scrollTop;
|
let scrollTop = messagesContainer.scrollTop;
|
||||||
let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;
|
let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;
|
||||||
return Math.abs(scrollTop) > scrollableHeight / 2;
|
return scrollTop < scrollableHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initial layout update ---
|
// --- Initial layout update ---
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class WebView(BaseView):
|
|||||||
user_uid=self.session.get("uid"), channel_uid=channel["uid"]
|
user_uid=self.session.get("uid"), channel_uid=channel["uid"]
|
||||||
)
|
)
|
||||||
if not channel_member:
|
if not channel_member:
|
||||||
if not channel["is_private"] and not channel.is_dm:
|
if not channel["is_private"]:
|
||||||
channel_member = await self.app.services.channel_member.create(
|
channel_member = await self.app.services.channel_member.create(
|
||||||
channel_uid=channel["uid"],
|
channel_uid=channel["uid"],
|
||||||
user_uid=self.session.get("uid"),
|
user_uid=self.session.get("uid"),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user