Compare commits
	
		
			9 Commits
		
	
	
		
			f9f1179db5
			...
			abce2e03d1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| abce2e03d1 | |||
| 54d7d5b74e | |||
| 17bb88050a | |||
|   | 8c2e20dfe8 | ||
|   | 3e2dd7ea04 | ||
|   | 70eebefac7 | ||
|   | ac47d201d8 | ||
|   | 11e19f48e8 | ||
|   | 5ac49522d9 | 
| @ -13,6 +13,10 @@ 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,6 +118,7 @@ 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( | ||||||
|             { |             { | ||||||
| @ -129,7 +130,6 @@ 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:first-child { | .message.switch-user + .message, .message.long-time + .message, .message-list-bottom + .message{ | ||||||
|   .time { |   .time { | ||||||
|     display: block; |     display: block; | ||||||
|     opacity: 1; |     opacity: 1; | ||||||
|  | |||||||
| @ -9,8 +9,53 @@ 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; | ||||||
| @ -51,6 +96,12 @@ 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) { | ||||||
| @ -99,7 +150,9 @@ 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); | ||||||
| @ -119,10 +172,9 @@ class MessageList extends HTMLElement { | |||||||
|           this.visibleSet.delete(entry.target); |           this.visibleSet.delete(entry.target); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|             console.log(this.visibleSet); |  | ||||||
|     }, { |     }, { | ||||||
|       root: this, |       root: this, | ||||||
|         threshold: 0.1 |       threshold: 0, | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     for(const c of this.children) { |     for(const c of this.children) { | ||||||
| @ -131,6 +183,11 @@ 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); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -174,7 +231,6 @@ class MessageList extends HTMLElement { | |||||||
|       }; |       }; | ||||||
|       document.addEventListener('keydown', escListener); |       document.addEventListener('keydown', escListener); | ||||||
|     }) |     }) | ||||||
| 
 |  | ||||||
|   } |   } | ||||||
|   isElementVisible(element) { |   isElementVisible(element) { | ||||||
|     if (!element) return false; |     if (!element) return false; | ||||||
| @ -187,13 +243,13 @@ class MessageList extends HTMLElement { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   isScrolledToBottom() { |   isScrolledToBottom() { | ||||||
|     return this.isElementVisible(this.firstElementChild); |     return this.isElementVisible(this.endOfMessages); | ||||||
|   } |   } | ||||||
|   scrollToBottom(force = false, behavior= 'smooth') { |   scrollToBottom(force = false, behavior= 'instant') { | ||||||
|     if (force || this.isScrolledToBottom()) { |     if (force || !this.isScrolledToBottom()) { | ||||||
|       this.firstElementChild.scrollIntoView({ behavior, block: 'start' }); |       this.firstElementChild.scrollIntoView({ behavior, block: 'end' }); | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
|         this.firstElementChild.scrollIntoView({ behavior, block: 'start' }); |         this.firstElementChild.scrollIntoView({ behavior, block: 'end' }); | ||||||
|       }, 200); |       }, 200); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -227,9 +283,8 @@ 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 | ||||||
| @ -239,7 +294,7 @@ class MessageList extends HTMLElement { | |||||||
|     wrapper.innerHTML = data.html; |     wrapper.innerHTML = data.html; | ||||||
| 
 | 
 | ||||||
|     if (message) { |     if (message) { | ||||||
|       message.updateMessage(...wrapper.firstElementChild._originalChildren); |       message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children)); | ||||||
|     } else { |     } else { | ||||||
|       message = wrapper.firstElementChild; |       message = wrapper.firstElementChild; | ||||||
|       this.messageMap.set(data.uid, message); |       this.messageMap.set(data.uid, message); | ||||||
| @ -248,7 +303,7 @@ class MessageList extends HTMLElement { | |||||||
| 
 | 
 | ||||||
|     const scrolledToBottom = this.isScrolledToBottom(); |     const scrolledToBottom = this.isScrolledToBottom(); | ||||||
|     this.prepend(message); |     this.prepend(message); | ||||||
|     if (scrolledToBottom) this.scrollToBottom(true, !newMessage ? 'smooth' : 'auto'); |     if (scrolledToBottom) this.scrollToBottom(true); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,3 @@ | |||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class RestClient { | class RestClient { | ||||||
|   constructor({ baseURL = '', headers = {} } = {}) { |   constructor({ baseURL = '', headers = {} } = {}) { | ||||||
|     this.baseURL = baseURL; |     this.baseURL = baseURL; | ||||||
| @ -210,27 +208,52 @@ class Njet extends HTMLElement { | |||||||
|         customElements.define(name, component); |         customElements.define(name, component); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor(config) { | ||||||
|         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);
 |          | ||||||
|         //if (typeof this.config.construct === 'function')
 |         // Call construct if defined
 | ||||||
|         //    this.config.construct.call(this)
 |         if (typeof this.config.construct === 'function') { | ||||||
|  |             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 => { | ||||||
|             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]; |                 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); | ||||||
|         } |         } | ||||||
| @ -342,7 +365,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 = 'flenjet-end'; |         buttonContainer.style.justifyContent = 'flex-end'; | ||||||
|         buttonContainer.style.gap = '10px'; |         buttonContainer.style.gap = '10px'; | ||||||
|         if (secondaryButton) { |         if (secondaryButton) { | ||||||
|             const secondary = new NjetButton(secondaryButton); |             const secondary = new NjetButton(secondaryButton); | ||||||
| @ -372,8 +395,9 @@ 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(){ | ||||||
| @ -408,7 +432,8 @@ 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', | ||||||
| @ -545,11 +570,12 @@ njet.showWindow = function(args) { | |||||||
|     return w |     return w | ||||||
| } | } | ||||||
| njet.publish = function(event, data) { | 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)) |         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] = [] | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -79,44 +79,12 @@ 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) + [ | ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["picture"] | ||||||
|     "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, | ||||||
|         tags=ALLOWED_TAGS, |         protocols=list(bleach.sanitizer.ALLOWED_PROTOCOLS) + ["data"], | ||||||
|         attributes=ALLOWED_ATTRIBUTES, |  | ||||||
|         protocols=bleach.sanitizer.ALLOWED_PROTOCOLS + ["data"], |  | ||||||
|         strip=True, |         strip=True, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| @ -132,50 +100,8 @@ 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): | ||||||
|     soup = BeautifulSoup(html, "html.parser") |     return sanitize_html(html) | ||||||
| 
 |  | ||||||
|     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 = 200, container = document.body } = {}) { |   constructor({ count = 100, 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: 200}); | const starField = new StarField({starCount: 100}); | ||||||
| app.starField = starField; | app.starField = starField; | ||||||
| 
 | 
 | ||||||
| class DemoSequence { | class DemoSequence { | ||||||
|  | |||||||
| @ -72,12 +72,13 @@ 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.children[messagesContainer.children.length - 1]; |     const firstMessage = messagesContainer.lastElementChild; | ||||||
|     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; | ||||||
| @ -138,10 +139,16 @@ chatInputField.textarea.focus(); | |||||||
| 
 | 
 | ||||||
| // --- Reply helper --- | // --- Reply helper --- | ||||||
| function replyMessage(message) { | function replyMessage(message) { | ||||||
|     chatInputField.value = "```markdown\n> " + (message || '').split("\n").join("\n> ") + "\n```\n"; |     chatInputField.value = "```markdown\n> " + (message || '').trim().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) || [])]; | ||||||
| @ -215,7 +222,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.querySelector(".message:first-child")?.scrollIntoView({ block: "end", inline: "nearest" }); |             messagesContainer.lastElementChild?.scrollIntoView({ block: "end", inline: "nearest" }); | ||||||
|             loadExtra(); |             loadExtra(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -254,7 +261,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 scrollTop < scrollableHeight / 2; |     return Math.abs(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"]: |             if not channel["is_private"] and not channel.is_dm: | ||||||
|                 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