/* * Copyright (c) 2010-2018 Tim Düsterhus. * * Use of this software is governed by the Business Source License * included in the LICENSE file. * * Change Date: 2024-11-01 * * On the date above, in accordance with the Business Source * License, use of this software will be governed by version 2 * or later of the General Public License. */ define([ './console' , 'Bastelstu.be/bottle' , 'WoltLabSuite/Core/Core' , './Message' , './Messenger' , './ProfileStore' , './Room' , './Template' , './Ui/Log' , './Ui/MessageStream' , './Ui/MessageActions/Delete' ], function (console, Bottle, Core, Message, Messenger, ProfileStore, Room, Template, Ui, UiMessageStream, UiMessageActionDelete) { "use strict"; const loader = Symbol('loader') class Log { constructor(params, config) { console.debug('ChatLog.constructor', 'Constructing …') this.config = config this.sessionID = Core.getUuid() this.bottle = new Bottle() this.bottle.value('bottle', this.bottle) this.bottle.value('config', config) this.bottle.constant('sessionID', this.sessionID) this.bottle.constant('roomID', params.roomID) // Register chat components this.service('Messenger', Messenger) this.service('ProfileStore', ProfileStore) this.service('Room', Room) // Register UI components this.service('Ui', Ui) this.service('UiMessageActionDelete', UiMessageActionDelete) this.service('UiMessageStream', UiMessageStream) // Register Models this.bottle.instanceFactory('Message', (container, m) => { return new Message(container.MessageType, m) }) // Register Templates const selector = [ '[type="x-text/template"]' , '[data-application="be.bastelstu.chat"]' , '[data-template-name]' ].join('') const templates = elBySelAll(selector) templates.forEach((function (template) { this.bottle.factory(`Template.${elData(template, 'template-name')}`, function (container) { const includeNames = (elData(template, 'template-includes') || '').split(/ /).filter(item => item !== "") const includes = { } includeNames.forEach(item => includes[item] = container[item]) return new Template(template.textContent, includes) }) }).bind(this)) // Register MessageTypes const messageTypes = Object.entries(this.config.messageTypes) messageTypes.forEach(([ objectType, messageType ]) => { const MessageType = require(messageType.module) this.bottle.factory(`MessageType.${objectType.replace(/\./g, '-')}`, _ => { const deps = this.bottle.digest(MessageType.DEPENDENCIES || []) return new MessageType(...deps, objectType) }) }) this.knows = { from: undefined , to: undefined } this.messageSinks = new Set() this.params = params this.pulling = false } service(name, _constructor, args = [ ]) { this.bottle.factory(name, function (container) { const deps = (_constructor.DEPENDENCIES || [ ]).map(dep => container[dep]) return new _constructor(...deps, ...args) }) } async bootstrap() { console.debug('ChatLog.bootstrap', 'Initializing …') this.ui = this.bottle.container.Ui this.ui.bootstrap() this.registerMessageSink(this.bottle.container.UiMessageStream) if (this.params.messageID > 0) { await Promise.all([ this.pull(undefined, this.params.messageID) , this.pull(this.params.messageID + 1) ]) } else { await this.pull() } this.bottle.container.UiMessageStream.on('nearTop', this.pullOlder.bind(this)) this.bottle.container.UiMessageStream.on('reachedTop', this.pullOlder.bind(this)) this.bottle.container.UiMessageStream.on('nearBottom', this.pullNewer.bind(this)) this.bottle.container.UiMessageStream.on('reachedBottom', this.pullNewer.bind(this)) const element = document.querySelector(`#message-${this.params.messageID}`) // Force changing the hash to trigger a new lookup of the element. // At least Chrome won’t target an element if it is not in the DOM // on the initial page load with an hash set. window.location.hash = '' window.location.hash = `message-${this.params.messageID}` if (element && element.scrollIntoView) element.scrollIntoView() return this } registerMessageSink(sink) { if (typeof sink.ingest !== 'function') { throw new Error('The given sink does not provide a .ingest function.') } this.messageSinks.add(sink) } unregisterMessageSink(sink) { this.messageSinks.delete(sink) } async pull(from, to) { try { await this.handlePull(await this.performPull(from, to)) } catch (e) { this.handleError(e) } } async pullOlder() { if (this.pulling) return if (this.knows.from <= 1) return this.pulling = true await this.pull(undefined, this.knows.from - 1) this.pulling = false } async pullNewer() { if (this.pulling) return this.pulling = true await this.pull(this.knows.to + 1) this.pulling = false } async performPull(from = undefined, to = undefined) { console.debug('ChatLog.performPull', `Pulling new messages; from: ${from !== undefined ? from : 'undefined'}, to: ${to !== undefined ? to : 'undefined'}`) return this.bottle.container.Messenger.pull(from, to, true) } handleError(error) { console.debug('ChatLog.handleError', `Request failed`) console.debugException(error) } async handlePull(payload) { console.debug('ChatLog.handlePull', payload) // Null range: No messages satisfy the constraints if (payload.from > payload.to) return let messages = payload.messages if (this.knows.from !== undefined && this.knows.to !== undefined) { messages = messages.filter((function (message) { return !(this.knows.from <= message.messageID && message.messageID <= this.knows.to) }).bind(this)) } if (this.knows.from === undefined || payload.from < this.knows.from) this.knows.from = payload.from if (this.knows.to === undefined || payload.to > this.knows.to) this.knows.to = payload.to await Promise.all(messages.map((message) => { return message.getMessageType().preProcess(message) })) const userIDs = messages.map(message => message.userID) .filter(userID => userID !== null) await this.bottle.container.ProfileStore.ensureUsersByIDs(userIDs) this.messageSinks.forEach(sink => sink.ingest(messages)) } } return Log });