Compare commits
	
		
			No commits in common. "abce2e03d1e47992d234779cd38d476e0bfc766a" and "f9f1179db5e72492e22b59cc0415577b599fa119" have entirely different histories.
		
	
	
		
			abce2e03d1
			...
			f9f1179db5
		
	
		
@ -13,10 +13,6 @@ class ChannelModel(BaseModel):
 | 
				
			|||||||
    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 = ""
 | 
				
			||||||
        if self["history_start"]:
 | 
					        if self["history_start"]:
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
@ -172,9 +119,10 @@ class MessageList extends HTMLElement {
 | 
				
			|||||||
            this.visibleSet.delete(entry.target);
 | 
					            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) {
 | 
				
			||||||
@ -183,11 +131,6 @@ class MessageList extends HTMLElement {
 | 
				
			|||||||
            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,7 +239,7 @@ 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);
 | 
				
			||||||
@ -303,7 +248,7 @@ class MessageList extends HTMLElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    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