7 changed files with 62 additions and 117 deletions
+4
View File
@@ -12,6 +12,10 @@ class ChannelModel(BaseModel):
index = ModelField(name="index", required=True, kind=int, value=1000)
last_message_on = ModelField(name="last_message_on", 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:
history_start_filter = ""
+2 -2
View File
@@ -69,10 +69,10 @@ class ChannelMessageService(BaseService):
"color": user["color"],
}
)
context['message'] = whitelist_attributes(context['message'])
try:
template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context)
model["html"] = whitelist_attributes(model["html"])
except Exception as ex:
print(ex, flush=True)
@@ -118,6 +118,7 @@ class ChannelMessageService(BaseService):
async def save(self, model):
context = {}
context.update(model.record)
context['message'] = whitelist_attributes(context['message'])
user = await self.app.services.user.get(model["user_uid"])
context.update(
{
@@ -129,7 +130,6 @@ class ChannelMessageService(BaseService):
)
template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context)
model["html"] = whitelist_attributes(model["html"])
return await super().save(model)
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
+10 -21
View File
@@ -57,17 +57,6 @@ export class ReplyEvent extends Event {
class MessageElement extends HTMLElement {
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
isVisible() {
if (!this) return false;
const rect = this.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
updateUI() {
if (this._originalChildren === undefined) {
const { color, user_nick, created_at, user_uid} = this.dataset;
@@ -104,8 +93,8 @@ class MessageElement extends HTMLElement {
})
}
if (!this.siblingGenerated && this.nextElementSibling) {
this.siblingGenerated = true;
if ((!this.siblingGenerated || this.siblingGenerated !== this.nextElementSibling) && this.nextElementSibling) {
this.siblingGenerated = this.nextElementSibling;
if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
this.classList.add('switch-user');
} else {
@@ -177,6 +166,10 @@ class MessageList extends HTMLElement {
threshold: 0,
})
this.endOfMessages = document.createElement('div');
this.endOfMessages.classList.add('message-list-bottom');
this.prepend(this.endOfMessages);
for(const c of this.children) {
this._observer.observe(c);
if (c instanceof MessageElement) {
@@ -184,10 +177,6 @@ class MessageList extends HTMLElement {
}
}
this.endOfMessages = document.createElement('div');
this.endOfMessages.classList.add('message-list-bottom');
this.prepend(this.endOfMessages);
this.scrollToBottom(true);
}
@@ -243,13 +232,13 @@ class MessageList extends HTMLElement {
);
}
isScrolledToBottom() {
return this.isElementVisible(this.endOfMessages);
return this.visibleSet.has(this.endOfMessages)
}
scrollToBottom(force = false, behavior= 'instant') {
if (force || !this.isScrolledToBottom()) {
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
setTimeout(() => {
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
}, 200);
}
}
@@ -302,7 +291,7 @@ class MessageList extends HTMLElement {
}
const scrolledToBottom = this.isScrolledToBottom();
this.prepend(message);
this.endOfMessages.after(message);
if (scrolledToBottom) this.scrollToBottom(true);
}
}
+40 -14
View File
@@ -1,5 +1,3 @@
class RestClient {
constructor({ baseURL = '', headers = {} } = {}) {
this.baseURL = baseURL;
@@ -210,27 +208,52 @@ class Njet extends HTMLElement {
customElements.define(name, component);
}
constructor() {
constructor(config) {
super();
// Store the config for use in render and other methods
this.config = config || {};
if (!Njet._root) {
Njet._root = this
Njet._rest = new RestClient({ baseURL: '/' || null })
}
this.root._elements.push(this)
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.initProps(config);
//if (typeof this.config.construct === 'function')
// this.config.construct.call(this)
// Call construct if defined
if (typeof this.config.construct === 'function') {
this.config.construct.call(this)
}
}
initProps(config) {
const props = Object.keys(config)
props.forEach(prop => {
if (config[prop] !== undefined) {
// Skip special properties that are handled separately
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];
} 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) {
this.classList.add(...config.classes);
}
@@ -342,7 +365,7 @@ class NjetDialog extends Component {
const buttonContainer = document.createElement('div');
buttonContainer.style.marginTop = '20px';
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flenjet-end';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.gap = '10px';
if (secondaryButton) {
const secondary = new NjetButton(secondaryButton);
@@ -372,8 +395,9 @@ class NjetWindow extends Component {
header.textContent = title;
this.appendChild(header);
}
this.config.items.forEach(item => this.appendChild(item));
if (this.config.items) {
this.config.items.forEach(item => this.appendChild(item));
}
}
show(){
@@ -408,7 +432,8 @@ class NjetGrid extends Component {
}
}
Njet.registerComponent('njet-grid', NjetGrid);
/*
/* Example usage:
const button = new NjetButton({
classes: ['my-button'],
text: 'Shared',
@@ -493,7 +518,7 @@ document.body.appendChild(dialog);
*/
class NjetComponent extends Component {}
const njet = Njet
const njet = Njet
njet.showDialog = function(args){
const dialog = new NjetDialog(args)
dialog.show()
@@ -545,15 +570,16 @@ njet.showWindow = function(args) {
return w
}
njet.publish = function(event, data) {
if (this.root._subscriptions[event]) {
if (this.root && this.root._subscriptions && this.root._subscriptions[event]) {
this.root._subscriptions[event].forEach(callback => callback(data))
}
}
njet.subscribe = function(event, callback) {
if (!this.root) return;
if (!this.root._subscriptions[event]) {
this.root._subscriptions[event] = []
}
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 };
+3 -77
View File
@@ -79,44 +79,12 @@ emoji.EMOJI_DATA[
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
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": [],
}
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["picture"]
def sanitize_html(value):
return bleach.clean(
value,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=bleach.sanitizer.ALLOWED_PROTOCOLS + ["data"],
protocols=list(bleach.sanitizer.ALLOWED_PROTOCOLS) + ["data"],
strip=True,
)
@@ -132,50 +100,8 @@ def set_link_target_blank(text):
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):
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)
return sanitize_html(html)
def embed_youtube(text):
+2 -2
View File
@@ -12,7 +12,7 @@ function showTerm(options){
class StarField {
constructor({ count = 200, container = document.body } = {}) {
constructor({ count = 100, container = document.body } = {}) {
this.container = container;
this.starCount = count;
this.stars = [];
@@ -567,7 +567,7 @@ const count = Array.from(messages).filter(el => el.textContent.trim() === text).
const starField = new StarField({starCount: 200});
const starField = new StarField({starCount: 100});
app.starField = starField;
class DemoSequence {
+1 -1
View File
@@ -55,7 +55,7 @@ class WebView(BaseView):
user_uid=self.session.get("uid"), channel_uid=channel["uid"]
)
if not channel_member:
if not channel["is_private"]:
if not channel["is_private"] and not channel.is_dm:
channel_member = await self.app.services.channel_member.create(
channel_uid=channel["uid"],
user_uid=self.session.get("uid"),