1
0
mirror of https://github.com/wbbaddons/Tims-Chat.git synced 2024-12-20 21:20:08 +00:00

Run prettier on all files

This commit is contained in:
Tim Düsterhus 2020-11-01 17:41:19 +01:00
parent cfe91be22d
commit 3a88e73ee1
Signed by: TimWolla
GPG Key ID: 8FF75566094168AF
87 changed files with 2002 additions and 1463 deletions

View File

@ -11,54 +11,85 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Chat/console' define([
, 'Bastelstu.be/bottle' './Chat/console',
, 'Bastelstu.be/_Push' 'Bastelstu.be/bottle',
, 'WoltLabSuite/Core/Core' 'Bastelstu.be/_Push',
, 'WoltLabSuite/Core/Language' 'WoltLabSuite/Core/Core',
, 'WoltLabSuite/Core/Timer/Repeating' 'WoltLabSuite/Core/Language',
, 'WoltLabSuite/Core/User' 'WoltLabSuite/Core/Timer/Repeating',
, './Chat/Autocompleter' 'WoltLabSuite/Core/User',
, './Chat/CommandHandler' './Chat/Autocompleter',
, './Chat/DataStructure/Throttle' './Chat/CommandHandler',
, './Chat/Message' './Chat/DataStructure/Throttle',
, './Chat/Messenger' './Chat/Message',
, './Chat/ParseError' './Chat/Messenger',
, './Chat/ProfileStore' './Chat/ParseError',
, './Chat/Room' './Chat/ProfileStore',
, './Chat/Template' './Chat/Room',
, './Chat/Ui/Attachment/Upload' './Chat/Template',
, './Chat/Ui/AutoAway' './Chat/Ui/Attachment/Upload',
, './Chat/Ui/Chat' './Chat/Ui/AutoAway',
, './Chat/Ui/ConnectionWarning' './Chat/Ui/Chat',
, './Chat/Ui/ErrorDialog' './Chat/Ui/ConnectionWarning',
, './Chat/Ui/Input' './Chat/Ui/ErrorDialog',
, './Chat/Ui/Input/Autocompleter' './Chat/Ui/Input',
, './Chat/Ui/MessageStream' './Chat/Ui/Input/Autocompleter',
, './Chat/Ui/MessageActions/Delete' './Chat/Ui/MessageStream',
, './Chat/Ui/Mobile' './Chat/Ui/MessageActions/Delete',
, './Chat/Ui/Notification' './Chat/Ui/Mobile',
, './Chat/Ui/ReadMarker' './Chat/Ui/Notification',
, './Chat/Ui/Settings' './Chat/Ui/ReadMarker',
, './Chat/Ui/Topic' './Chat/Ui/Settings',
, './Chat/Ui/UserActionDropdownHandler' './Chat/Ui/Topic',
, './Chat/Ui/UserList' './Chat/Ui/UserActionDropdownHandler',
], function (console, Bottle, Push, Core, Language, RepeatingTimer, CoreUser, Autocompleter, './Chat/Ui/UserList',
CommandHandler, Throttle, Message, Messenger, ParseError, ProfileStore, Room, Template, UiAttachmentUpload, UiAutoAway, Ui, ], function (
UiConnectionWarning, ErrorDialog, UiInput, UiInputAutocompleter, UiMessageStream, UiMessageActionDelete, UiMobile, UiNotification, console,
UiReadMarker, UiSettings, UiTopic, UiUserActionDropdownHandler, UiUserList) { Bottle,
"use strict"; Push,
Core,
Language,
RepeatingTimer,
CoreUser,
Autocompleter,
CommandHandler,
Throttle,
Message,
Messenger,
ParseError,
ProfileStore,
Room,
Template,
UiAttachmentUpload,
UiAutoAway,
Ui,
UiConnectionWarning,
ErrorDialog,
UiInput,
UiInputAutocompleter,
UiMessageStream,
UiMessageActionDelete,
UiMobile,
UiNotification,
UiReadMarker,
UiSettings,
UiTopic,
UiUserActionDropdownHandler,
UiUserList
) {
'use strict'
class Chat { class Chat {
constructor(roomID, config) { constructor(roomID, config) {
console.debug('Chat.constructor', 'Constructing …') console.debug('Chat.constructor', 'Constructing …')
this.config = config this.config = config
this.sessionID = Core.getUuid() this.sessionID = Core.getUuid()
// Setup Bottle containers // Setup Bottle containers
this.bottle = new Bottle() this.bottle = new Bottle()
this.bottle.value('bottle', this.bottle) this.bottle.value('bottle', this.bottle)
this.bottle.value('config', config) this.bottle.value('config', config)
this.bottle.constant('sessionID', this.sessionID) this.bottle.constant('sessionID', this.sessionID)
@ -94,79 +125,105 @@ define([ './Chat/console'
}) })
// Register Templates // Register Templates
const selector = [ '[type="x-text/template"]' const selector = [
, '[data-application="be.bastelstu.chat"]' '[type="x-text/template"]',
, '[data-template-name]' '[data-application="be.bastelstu.chat"]',
].join('') '[data-template-name]',
].join('')
const templates = elBySelAll(selector) const templates = elBySelAll(selector)
Array.prototype.forEach.call(templates, (function (template) { Array.prototype.forEach.call(
this.bottle.factory(`Template.${template.dataset.templateName}`, function (container) { templates,
const includeNames = (template.dataset.templateIncludes || '').split(/ /).filter(item => item !== "") function (template) {
const includes = { } this.bottle.factory(
includeNames.forEach(item => includes[item] = container[item]) `Template.${template.dataset.templateName}`,
function (container) {
const includeNames = (template.dataset.templateIncludes || '')
.split(/ /)
.filter((item) => item !== '')
const includes = {}
includeNames.forEach((item) => (includes[item] = container[item]))
return new Template(template.textContent, includes) return new Template(template.textContent, includes)
}) }
}).bind(this)) )
}.bind(this)
)
// Register MessageTypes // Register MessageTypes
Object.entries(this.config.messageTypes) Object.entries(this.config.messageTypes).forEach(
.forEach(([ objectType, messageType ]) => { ([objectType, messageType]) => {
const MessageType = require(messageType.module) const MessageType = require(messageType.module)
this.bottle.factory(`MessageType.${objectType.replace(/\./g, '-')}`, _ => { this.bottle.factory(
const deps = this.bottle.digest(MessageType.DEPENDENCIES || []) `MessageType.${objectType.replace(/\./g, '-')}`,
(_) => {
const deps = this.bottle.digest(MessageType.DEPENDENCIES || [])
return new MessageType(...deps, objectType) return new MessageType(...deps, objectType)
}) }
}) )
}
)
// Register Commands // Register Commands
Object.values(this.config.commands).forEach(command => { Object.values(this.config.commands).forEach((command) => {
const Command = require(command.module) const Command = require(command.module)
this.bottle.factory(`Command.${command.package.replace(/\./g, '-')}:${command.identifier}`, _ => { this.bottle.factory(
const deps = this.bottle.digest(Command.DEPENDENCIES || []) `Command.${command.package.replace(/\./g, '-')}:${
command.identifier
}`,
(_) => {
const deps = this.bottle.digest(Command.DEPENDENCIES || [])
return new Command(...deps, command) return new Command(...deps, command)
}) }
)
}) })
this.bottle.constant('Trigger', new Map(Object.entries(this.config.triggers).map(([ trigger, commandID ]) => { this.bottle.constant(
const command = this.config.commands[commandID] 'Trigger',
const key = [ command.package, command.identifier ] new Map(
return [ trigger, key ] Object.entries(this.config.triggers).map(([trigger, commandID]) => {
}))) const command = this.config.commands[commandID]
const key = [command.package, command.identifier]
return [trigger, key]
})
)
)
// Register Settings // Register Settings
Array.from(elBySelAll('#chatQuickSettingsNavigation .button[data-module]')).forEach(item => { Array.from(
elBySelAll('#chatQuickSettingsNavigation .button[data-module]')
).forEach((item) => {
const Button = require(item.dataset.module) const Button = require(item.dataset.module)
this.bottle.instanceFactory(`UiSettingsButton.${item.dataset.module.replace(/\./g, '-')}`, (_, element) => { this.bottle.instanceFactory(
const deps = this.bottle.digest(Button.DEPENDENCIES || []) `UiSettingsButton.${item.dataset.module.replace(/\./g, '-')}`,
return new Button(element, ...deps) (_, element) => {
}) const deps = this.bottle.digest(Button.DEPENDENCIES || [])
return new Button(element, ...deps)
}
)
}) })
this.knows = { from: undefined this.knows = { from: undefined, to: undefined }
, to: undefined
}
this.processMessagesThrottled = Throttle(this.processMessages.bind(this)) this.processMessagesThrottled = Throttle(this.processMessages.bind(this))
this.queuedMessages = [ ] this.queuedMessages = []
this.messageSinks = new Set() this.messageSinks = new Set()
this.pullTimer = undefined this.pullTimer = undefined
this.pullUserListTimer = undefined this.pullUserListTimer = undefined
this.pushConnected = false this.pushConnected = false
this.firstFailure = null this.firstFailure = null
} }
service(name, _constructor, args = [ ]) { service(name, _constructor, args = []) {
this.bottle.factory(name, _ => { this.bottle.factory(name, (_) => {
const deps = this.bottle.digest(_constructor.DEPENDENCIES || [ ]) const deps = this.bottle.digest(_constructor.DEPENDENCIES || [])
return new _constructor(...deps, ...args) return new _constructor(...deps, ...args)
}) })
@ -187,42 +244,58 @@ define([ './Chat/console'
await this.bottle.container.Room.join() await this.bottle.container.Room.join()
// Bind unload event to leave the Chat // Bind unload event to leave the Chat
window.addEventListener('unload', this.bottle.container.Room.leave.bind(this.bottle.container.Room, true)) window.addEventListener(
document.addEventListener('visibilitychange', _ => { 'unload',
this.bottle.container.Room.leave.bind(this.bottle.container.Room, true)
)
document.addEventListener('visibilitychange', (_) => {
this.processMessagesThrottled.setDelay(document.hidden ? 10000 : 125) this.processMessagesThrottled.setDelay(document.hidden ? 10000 : 125)
}) })
this.pullTimer = new RepeatingTimer(Throttle(this.pullMessages.bind(this)), this.config.reloadTime * 1e3) this.pullTimer = new RepeatingTimer(
Throttle(this.pullMessages.bind(this)),
this.config.reloadTime * 1e3
)
Push.onConnect(_ => { Push.onConnect((_) => {
console.debug('Chat.bootstrap', 'Push connected') console.debug('Chat.bootstrap', 'Push connected')
this.pushConnected = true this.pushConnected = true
this.pullTimer.setDelta(30e3) this.pullTimer.setDelta(30e3)
}).catch((error) => {
console.debug(error)
}) })
.catch(error => { console.debug(error) })
Push.onDisconnect(_ => { Push.onDisconnect((_) => {
console.debug('Chat.bootstrap', 'Push disconnected') console.debug('Chat.bootstrap', 'Push disconnected')
this.pushConnected = false this.pushConnected = false
this.pullTimer.setDelta(this.config.reloadTime * 1e3) this.pullTimer.setDelta(this.config.reloadTime * 1e3)
}).catch((error) => {
console.debug(error)
}) })
.catch(error => { console.debug(error) })
Push.onMessage('be.bastelstu.chat.message', this.pullMessages.bind(this)) Push.onMessage(
.catch(error => { console.debug(error) }) 'be.bastelstu.chat.message',
this.pullMessages.bind(this)
).catch((error) => {
console.debug(error)
})
// Fetch user list every 60 seconds // Fetch user list every 60 seconds
// This acts as a safety net: It should be kept current by messages whenever possible. // This acts as a safety net: It should be kept current by messages whenever possible.
this.pullUserListTimer = new RepeatingTimer(this.updateUsers.bind(this), 60e3) this.pullUserListTimer = new RepeatingTimer(
this.updateUsers.bind(this),
60e3
)
this.registerMessageSink(this.bottle.container.UiMessageStream) this.registerMessageSink(this.bottle.container.UiMessageStream)
this.registerMessageSink(this.bottle.container.UiNotification) this.registerMessageSink(this.bottle.container.UiNotification)
this.registerMessageSink(this.bottle.container.UiAutoAway) this.registerMessageSink(this.bottle.container.UiAutoAway)
await Promise.all([ this.pullMessages() await Promise.all([
, this.updateUsers() this.pullMessages(),
, this.bottle.container.ProfileStore.ensureUsersByIDs([ CoreUser.userId ]) this.updateUsers(),
]) this.bottle.container.ProfileStore.ensureUsersByIDs([CoreUser.userId]),
])
return this return this
} }
@ -240,7 +313,11 @@ define([ './Chat/console'
} }
hcf(err = undefined) { hcf(err = undefined) {
console.debug('Chat.hcf', 'Gotcha! FIRE was caught! FIREs data was newly added to the POKéDEX.', err) console.debug(
'Chat.hcf',
'Gotcha! FIRE was caught! FIREs data was newly added to the POKéDEX.',
err
)
this.pullTimer.stop() this.pullTimer.stop()
this.pullUserListTimer.stop() this.pullUserListTimer.stop()
@ -259,47 +336,64 @@ define([ './Chat/console'
this.markAsBack() this.markAsBack()
let [ trigger, parameterString ] = this.bottle.container.CommandHandler.splitCommand(value) let [
trigger,
parameterString,
] = this.bottle.container.CommandHandler.splitCommand(value)
let command = null let command = null
if (trigger === null) { if (trigger === null) {
command = this.bottle.container.CommandHandler.getCommandByIdentifier('be.bastelstu.chat', 'plain') command = this.bottle.container.CommandHandler.getCommandByIdentifier(
} 'be.bastelstu.chat',
else { 'plain'
command = this.bottle.container.CommandHandler.getCommandByTrigger(trigger) )
} else {
command = this.bottle.container.CommandHandler.getCommandByTrigger(
trigger
)
} }
if (command === null) { if (command === null) {
this.ui.input.inputError(Language.get('chat.error.triggerNotFound', { trigger })) this.ui.input.inputError(
Language.get('chat.error.triggerNotFound', { trigger })
)
return return
} }
try { try {
let parameters let parameters
try { try {
parameters = this.bottle.container.CommandHandler.applyCommand(command, parameterString) parameters = this.bottle.container.CommandHandler.applyCommand(
} command,
catch (e) { parameterString
)
} catch (e) {
if (e instanceof ParseError) { if (e instanceof ParseError) {
e = new Error(Language.get('chat.error.invalidParameters', { data: e.data })) e = new Error(
Language.get('chat.error.invalidParameters', { data: e.data })
)
} }
throw e throw e
} }
const payload = { commandID: command.id const payload = { commandID: command.id, parameters }
, parameters
}
try { try {
await this.bottle.container.Messenger.push(payload) await this.bottle.container.Messenger.push(payload)
this.ui.input.hideInputError() this.ui.input.hideInputError()
} } catch (error) {
catch (error) {
let seriousError = true let seriousError = true
if (error.returnValues && error.returnValues.fieldName === 'message' && (error.returnValues.realErrorMessage || error.returnValues.errorType)) { if (
this.ui.input.inputError(error.returnValues.realErrorMessage || error.returnValues.errorType) error.returnValues &&
error.returnValues.fieldName === 'message' &&
(error.returnValues.realErrorMessage ||
error.returnValues.errorType)
) {
this.ui.input.inputError(
error.returnValues.realErrorMessage ||
error.returnValues.errorType
)
seriousError = false seriousError = false
} } else {
else {
this.ui.input.inputError(error.message) this.ui.input.inputError(error.message)
} }
@ -314,8 +408,7 @@ define([ './Chat/console'
} }
console.debug('Chat.onSubmit', `Done`) console.debug('Chat.onSubmit', `Done`)
} } catch (e) {
catch (e) {
this.ui.input.inputError(e.message) this.ui.input.inputError(e.message)
} }
} }
@ -325,16 +418,23 @@ define([ './Chat/console'
if (this.bottle.container.ProfileStore.getSelf().away == null) return if (this.bottle.container.ProfileStore.getSelf().away == null) return
console.debug('Chat.markAsBack', `Marking as back`) console.debug('Chat.markAsBack', `Marking as back`)
const command = this.bottle.container.CommandHandler.getCommandByIdentifier('be.bastelstu.chat', 'back') const command = this.bottle.container.CommandHandler.getCommandByIdentifier(
return this.bottle.container.Messenger.push({ commandID: command.id, parameters: { } }) 'be.bastelstu.chat',
} 'back'
catch (err) { )
return this.bottle.container.Messenger.push({
commandID: command.id,
parameters: {},
})
} catch (err) {
console.error('Chat.markAsBack', err) console.error('Chat.markAsBack', err)
} }
} }
async onSendAttachment(event) { async onSendAttachment(event) {
return this.bottle.container.Messenger.pushAttachment(event.detail.tmpHash) return this.bottle.container.Messenger.pushAttachment(
event.detail.tmpHash
)
} }
onAutocomplete(event) { onAutocomplete(event) {
@ -354,18 +454,23 @@ define([ './Chat/console'
} }
async pullMessages() { async pullMessages() {
console.debug('Chat.pullMessages', `Pulling new messages, starting at ${this.knows.to ? this.knows.to + 1 : ''}`) console.debug(
'Chat.pullMessages',
`Pulling new messages, starting at ${
this.knows.to ? this.knows.to + 1 : ''
}`
)
let payload let payload
try { try {
if (this.knows.to === undefined) { if (this.knows.to === undefined) {
payload = await this.bottle.container.Messenger.pull() payload = await this.bottle.container.Messenger.pull()
} else {
payload = await this.bottle.container.Messenger.pull(
this.knows.to + 1
)
} }
else { } catch (e) {
payload = await this.bottle.container.Messenger.pull(this.knows.to + 1)
}
}
catch (e) {
this.handleError(e) this.handleError(e)
return return
} }
@ -386,12 +491,17 @@ define([ './Chat/console'
if (this.knows.from !== undefined && this.knows.to !== undefined) { if (this.knows.from !== undefined && this.knows.to !== undefined) {
messages = messages.filter((message) => { messages = messages.filter((message) => {
return !(this.knows.from <= message.messageID && message.messageID <= this.knows.to) return !(
this.knows.from <= message.messageID &&
message.messageID <= this.knows.to
)
}) })
} }
if (this.knows.from === undefined || payload.from < this.knows.from) this.knows.from = payload.from if (this.knows.from === undefined || payload.from < this.knows.from)
if (this.knows.to === undefined || payload.to > this.knows.to) this.knows.to = payload.to this.knows.from = payload.from
if (this.knows.to === undefined || payload.to > this.knows.to)
this.knows.to = payload.to
this.queuedMessages.push(messages) this.queuedMessages.push(messages)
const end = (performance ? performance : Date).now() const end = (performance ? performance : Date).now()
@ -402,14 +512,17 @@ define([ './Chat/console'
handleError(error) { handleError(error) {
if (this.firstFailure === null) { if (this.firstFailure === null) {
console.error('Chat.handleError', `Request failed, 30 seconds until shutdown`) console.error(
'Chat.handleError',
`Request failed, 30 seconds until shutdown`
)
this.firstFailure = Date.now() this.firstFailure = Date.now()
this.ui.connectionWarning.show() this.ui.connectionWarning.show()
} }
console.debugException(error) console.debugException(error)
if ((Date.now() - this.firstFailure) >= 30e3) { if (Date.now() - this.firstFailure >= 30e3) {
console.error('Chat.handleError', ' Failures for 30 seconds, aborting') console.error('Chat.handleError', ' Failures for 30 seconds, aborting')
this.hcf(error) this.hcf(error)
@ -419,16 +532,18 @@ define([ './Chat/console'
async processMessages() { async processMessages() {
console.debug('Chat.processMessages', 'Processing messages') console.debug('Chat.processMessages', 'Processing messages')
const start = (performance ? performance : Date).now() const start = (performance ? performance : Date).now()
const messages = [ ].concat(...this.queuedMessages) const messages = [].concat(...this.queuedMessages)
this.queuedMessages = [] this.queuedMessages = []
if (messages.length === 0) return if (messages.length === 0) return
await Promise.all(messages.map(async (message) => { await Promise.all(
this.bottle.container.ProfileStore.pushLastActivity(message.userID) messages.map(async (message) => {
this.bottle.container.ProfileStore.pushLastActivity(message.userID)
return message.getMessageType().preProcess(message) return message.getMessageType().preProcess(message)
})) })
)
const updateUserList = messages.some((message) => { const updateUserList = messages.some((message) => {
return message.getMessageType().shouldUpdateUserList(message) return message.getMessageType().shouldUpdateUserList(message)
@ -438,13 +553,19 @@ define([ './Chat/console'
this.updateUsers() this.updateUsers()
} }
await this.bottle.container.ProfileStore.ensureUsersByIDs([ ].concat(...messages.map(message => message.getMessageType().getReferencedUsers(message)))) await this.bottle.container.ProfileStore.ensureUsersByIDs(
[].concat(
...messages.map((message) =>
message.getMessageType().getReferencedUsers(message)
)
)
)
messages.forEach((message) => { messages.forEach((message) => {
message.getMessageType().preRender(message) message.getMessageType().preRender(message)
}) })
this.messageSinks.forEach(sink => sink.ingest(messages)) this.messageSinks.forEach((sink) => sink.ingest(messages))
const end = (performance ? performance : Date).now() const end = (performance ? performance : Date).now()
console.debug('Chat.processMessages', `took ${(end - start) / 1000}s`) console.debug('Chat.processMessages', `took ${(end - start) / 1000}s`)
} }
@ -453,10 +574,12 @@ define([ './Chat/console'
console.debug('Chat.updateUsers') console.debug('Chat.updateUsers')
const users = await this.bottle.container.Room.getUsers() const users = await this.bottle.container.Room.getUsers()
await this.bottle.container.ProfileStore.ensureUsersByIDs(users.map(user => user.userID)) await this.bottle.container.ProfileStore.ensureUsersByIDs(
users.map((user) => user.userID)
)
this.ui.userList.render(users) this.ui.userList.render(users)
} }
} }
return Chat return Chat
}); })

View File

@ -11,35 +11,42 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './CommandHandler' define(['./CommandHandler', './Parser'], function (CommandHandler, Parser) {
, './Parser' 'use strict'
], function (CommandHandler, Parser) {
"use strict";
const DEPENDENCIES = [ 'CommandHandler' ] const DEPENDENCIES = ['CommandHandler']
class Autocompleter { class Autocompleter {
constructor(commandHandler) { constructor(commandHandler) {
if (!(commandHandler instanceof CommandHandler)) throw new TypeError('You must pass a CommandHandler to the Autocompleter') if (!(commandHandler instanceof CommandHandler))
throw new TypeError(
'You must pass a CommandHandler to the Autocompleter'
)
this.commandHandler = commandHandler this.commandHandler = commandHandler
} }
* autocomplete(text) { *autocomplete(text) {
if (text === '/') { if (text === '/') {
yield * this.autocompleteCommandTrigger(text, '') yield* this.autocompleteCommandTrigger(text, '')
return return
} }
const [ trigger, parameterString ] = this.commandHandler.splitCommand(text) const [trigger, parameterString] = this.commandHandler.splitCommand(text)
let command let command
if (trigger === null) { if (trigger === null) {
command = this.commandHandler.getCommandByIdentifier('be.bastelstu.chat', 'plain') command = this.commandHandler.getCommandByIdentifier(
} 'be.bastelstu.chat',
else { 'plain'
const triggerDone = Parser.Slash.thenRight(Parser.AlnumTrigger.or(Parser.SymbolicTrigger).thenLeft(Parser.Whitespace)).parse(Parser.Streams.ofString(text)) )
} else {
const triggerDone = Parser.Slash.thenRight(
Parser.AlnumTrigger.or(Parser.SymbolicTrigger).thenLeft(
Parser.Whitespace
)
).parse(Parser.Streams.ofString(text))
if (!triggerDone.isAccepted()) { if (!triggerDone.isAccepted()) {
yield * this.autocompleteCommandTrigger(text, trigger) yield* this.autocompleteCommandTrigger(text, trigger)
return return
} }
@ -56,13 +63,12 @@ define([ './CommandHandler'
for (const item of values) { for (const item of values) {
yield `/${trigger} ${item}` yield `/${trigger} ${item}`
} }
} } else {
else { yield* values
yield * values
} }
} }
* autocompleteCommandTrigger(text, prefix) { *autocompleteCommandTrigger(text, prefix) {
const triggers = Array.from(this.commandHandler.getTriggers()) const triggers = Array.from(this.commandHandler.getTriggers())
triggers.sort() triggers.sort()
@ -70,7 +76,8 @@ define([ './CommandHandler'
for (const trigger of triggers) { for (const trigger of triggers) {
if (trigger === '') continue if (trigger === '') continue
if (!trigger.startsWith(prefix)) continue if (!trigger.startsWith(prefix)) continue
if (!this.commandHandler.getCommandByTrigger(trigger).isAvailable) continue if (!this.commandHandler.getCommandByTrigger(trigger).isAvailable)
continue
yield `/${trigger} ` yield `/${trigger} `
} }
@ -79,4 +86,4 @@ define([ './CommandHandler'
Autocompleter.DEPENDENCIES = DEPENDENCIES Autocompleter.DEPENDENCIES = DEPENDENCIES
return Autocompleter return Autocompleter
}); })

View File

@ -11,13 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './console' define([
, 'Bastelstu.be/_Push' './console',
, 'WoltLabSuite/Core/Dom/Util' 'Bastelstu.be/_Push',
, 'WoltLabSuite/Core/Timer/Repeating' 'WoltLabSuite/Core/Dom/Util',
, 'Bastelstu.be/PromiseWrap/Ajax' 'WoltLabSuite/Core/Timer/Repeating',
], function (console, Push, DomUtil, RepeatingTimer, Ajax) { 'Bastelstu.be/PromiseWrap/Ajax',
"use strict"; ], function (console, Push, DomUtil, RepeatingTimer, Ajax) {
'use strict'
let timer = undefined let timer = undefined
const mapping = new Map() const mapping = new Map()
@ -29,26 +30,44 @@ define([ './console'
mapping.set(container, this) mapping.set(container, this)
if (timer == null) { if (timer == null) {
timer = new RepeatingTimer(BoxRoomList.updateBoxes.bind(BoxRoomList), 60e3) timer = new RepeatingTimer(
BoxRoomList.updateBoxes.bind(BoxRoomList),
60e3
)
} }
Push.onConnect(timer.setDelta.bind(timer, 300e3)).catch(error => { console.debug(error) }) Push.onConnect(timer.setDelta.bind(timer, 300e3)).catch((error) => {
Push.onDisconnect(timer.setDelta.bind(timer, 60e3)).catch(error => { console.debug(error) }) console.debug(error)
Push.onMessage('be.bastelstu.chat.join', BoxRoomList.updateBoxes.bind(BoxRoomList)).catch(error => { console.debug(error) }) })
Push.onMessage('be.bastelstu.chat.leave', BoxRoomList.updateBoxes.bind(BoxRoomList)).catch(error => { console.debug(error) }) Push.onDisconnect(timer.setDelta.bind(timer, 60e3)).catch((error) => {
console.debug(error)
})
Push.onMessage(
'be.bastelstu.chat.join',
BoxRoomList.updateBoxes.bind(BoxRoomList)
).catch((error) => {
console.debug(error)
})
Push.onMessage(
'be.bastelstu.chat.leave',
BoxRoomList.updateBoxes.bind(BoxRoomList)
).catch((error) => {
console.debug(error)
})
} }
static updateBoxes() { static updateBoxes() {
mapping.forEach(object => { mapping.forEach((object) => {
object.update() object.update()
}) })
} }
async update() { async update() {
const payload = { className: 'chat\\data\\room\\RoomAction' const payload = {
, actionName: 'getBoxRoomList' className: 'chat\\data\\room\\RoomAction',
, parameters: { } actionName: 'getBoxRoomList',
} parameters: {},
}
payload.parameters.activeRoomID = this.container.dataset.activeRoomId payload.parameters.activeRoomID = this.container.dataset.activeRoomId
payload.parameters.boxID = this.container.dataset.boxId payload.parameters.boxID = this.container.dataset.boxId
@ -59,9 +78,12 @@ define([ './console'
} }
replace(data) { replace(data) {
if (data.returnValues.template == null) throw new Error('template could not be found in returnValues') if (data.returnValues.template == null)
throw new Error('template could not be found in returnValues')
const fragment = DomUtil.createFragmentFromHtml(data.returnValues.template) const fragment = DomUtil.createFragmentFromHtml(
data.returnValues.template
)
const oldRoomList = this.container.querySelector('.chatBoxRoomList') const oldRoomList = this.container.querySelector('.chatBoxRoomList')
const newRoomList = fragment.querySelector('.chatBoxRoomList') const newRoomList = fragment.querySelector('.chatBoxRoomList')
@ -69,7 +91,9 @@ define([ './console'
throw new Error('.chatBoxRoomList could not be found in container') throw new Error('.chatBoxRoomList could not be found in container')
} }
if (newRoomList == null) { if (newRoomList == null) {
throw new Error('.chatBoxRoomList could not be found in returned template') throw new Error(
'.chatBoxRoomList could not be found in returned template'
)
} }
if (oldRoomList.dataset.hash !== newRoomList.dataset.hash) { if (oldRoomList.dataset.hash !== newRoomList.dataset.hash) {
@ -78,11 +102,9 @@ define([ './console'
} }
_ajaxSetup() { _ajaxSetup() {
return { silent: true return { silent: true, ignoreError: true }
, ignoreError: true
}
} }
} }
return BoxRoomList return BoxRoomList
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Parser' ], function (Parser) { define(['./Parser'], function (Parser) {
"use strict"; 'use strict'
const data = Symbol('data') const data = Symbol('data')
@ -28,9 +28,7 @@ define([ './Parser' ], function (Parser) {
return Parser.Rest return Parser.Rest
} }
* autocomplete(parameterString) { *autocomplete(parameterString) {}
}
get id() { get id() {
return this[data].commandID return this[data].commandID
@ -54,4 +52,4 @@ define([ './Parser' ], function (Parser) {
} }
return Command return Command
}); })

View File

@ -11,16 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
class Away extends Command { class Away extends Command {
getParameterParser() { getParameterParser() {
return Parser.Rest.map(reason => ({ reason })) return Parser.Rest.map((reason) => ({ reason }))
} }
} }
return Away return Away
}); })

View File

@ -11,10 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
class Back extends Command { class Back extends Command {
getParameterParser() { getParameterParser() {
@ -23,4 +21,4 @@ define([ '../Command'
} }
return Back return Back
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './_Suspension' ], function (Suspension) { define(['./_Suspension'], function (Suspension) {
"use strict"; 'use strict'
class Ban extends Suspension { class Ban extends Suspension {}
}
return Ban return Ban
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Plain' ], function (Plain) { define(['./Plain'], function (Plain) {
"use strict"; 'use strict'
class Broadcast extends Plain { class Broadcast extends Plain {}
}
return Broadcast return Broadcast
}); })

View File

@ -11,21 +11,22 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
class Color extends Command { class Color extends Command {
getParameterParser() { getParameterParser() {
// Either match a color in hexadecimal RGB notation or a color name (just letters) // Either match a color in hexadecimal RGB notation or a color name (just letters)
const color = Parser.F.try(Parser.RGBHex.map(color => ({ type: 'hex', value: color }))) const color = Parser.F.try(
.or(new Parser.X().word().map(word => ({ type: 'word', value: word }))) Parser.RGBHex.map((color) => ({ type: 'hex', value: color }))
).or(new Parser.X().word().map((word) => ({ type: 'word', value: word })))
// Either match a single color or two colors separated by a space // Either match a single color or two colors separated by a space
return Parser.F.try(color.then(Parser.C.char(' ').thenRight(color))).or(color.map(item => [ item ])) return Parser.F.try(color.then(Parser.C.char(' ').thenRight(color))).or(
color.map((item) => [item])
)
} }
} }
return Color return Color
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
const DEPENDENCIES = [ 'ProfileStore' ] const DEPENDENCIES = ['ProfileStore']
class Info extends Command { class Info extends Command {
constructor(profileStore, id) { constructor(profileStore, id) {
super(id) super(id)
@ -24,10 +22,10 @@ define([ '../Command'
} }
getParameterParser() { getParameterParser() {
return Parser.Username.map(username => ({ username })) return Parser.Username.map((username) => ({ username }))
} }
* autocomplete(parameterString) { *autocomplete(parameterString) {
for (const userID of this.profileStore.getLastActivity()) { for (const userID of this.profileStore.getLastActivity()) {
const user = this.profileStore.get(userID) const user = this.profileStore.get(userID)
if (!user.username.startsWith(parameterString)) continue if (!user.username.startsWith(parameterString)) continue
@ -39,4 +37,4 @@ define([ '../Command'
Info.DEPENDENCIES = DEPENDENCIES Info.DEPENDENCIES = DEPENDENCIES
return Info return Info
}); })

View File

@ -11,14 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Plain', '../Parser' ], function (Plain, Parser) { define(['./Plain', '../Parser'], function (Plain, Parser) {
"use strict"; 'use strict'
class Me extends Plain { class Me extends Plain {
getParameterParser() { getParameterParser() {
return Parser.Rest1.map(text => ({ text })) return Parser.Rest1.map((text) => ({ text }))
} }
} }
return Me return Me
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './_Suspension' ], function (Suspension) { define(['./_Suspension'], function (Suspension) {
"use strict"; 'use strict'
class Mute extends Suspension { class Mute extends Suspension {}
}
return Mute return Mute
}); })

View File

@ -11,13 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser', 'WoltLabSuite/Core/StringUtil'], function (
, '../Parser' Command,
, 'WoltLabSuite/Core/StringUtil' Parser,
], function (Command, Parser, StringUtil) { StringUtil
"use strict"; ) {
'use strict'
const DEPENDENCIES = [ 'ProfileStore' ] const DEPENDENCIES = ['ProfileStore']
class Plain extends Command { class Plain extends Command {
constructor(profileStore, id) { constructor(profileStore, id) {
super(id) super(id)
@ -25,12 +26,12 @@ define([ '../Command'
} }
getParameterParser() { getParameterParser() {
return Parser.Rest1 return Parser.Rest1.map(
.map(StringUtil.escapeHTML.bind(StringUtil)) StringUtil.escapeHTML.bind(StringUtil)
.map(text => ({ text })) ).map((text) => ({ text }))
} }
* autocomplete(parameterString) { *autocomplete(parameterString) {
const parts = parameterString.split(/ /) const parts = parameterString.split(/ /)
const lastWord = parts.pop().toLowerCase() const lastWord = parts.pop().toLowerCase()
@ -41,13 +42,21 @@ define([ '../Command'
for (const userID of this.profileStore.getLastActivity()) { for (const userID of this.profileStore.getLastActivity()) {
const user = this.profileStore.get(userID) const user = this.profileStore.get(userID)
const username = user.username.toLowerCase() const username = user.username.toLowerCase()
if (!username.startsWith(parameterString) && !username.startsWith(lastWord.replace(/^@/, ''))) continue if (
!username.startsWith(parameterString) &&
!username.startsWith(lastWord.replace(/^@/, ''))
)
continue
yield `${parts.concat([ lastWord.startsWith('@') ? `@${user.username}` : user.username ]).join(' ')} ` yield `${parts
.concat([
lastWord.startsWith('@') ? `@${user.username}` : user.username,
])
.join(' ')} `
} }
} }
} }
Plain.DEPENDENCIES = DEPENDENCIES Plain.DEPENDENCIES = DEPENDENCIES
return Plain return Plain
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Plain' ], function (Plain) { define(['./Plain'], function (Plain) {
"use strict"; 'use strict'
class Team extends Plain { class Team extends Plain {}
}
return Team return Team
}); })

View File

@ -11,39 +11,44 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
class Temproom extends Command { class Temproom extends Command {
getParameterParser() { getParameterParser() {
const Create = Parser.C.string('create').thenReturns({ type: 'create' }) const Create = Parser.C.string('create').thenReturns({ type: 'create' })
const Invite = Parser.C.string('invite').thenLeft(Parser.Whitespace.rep()).thenRight(Parser.Username).map((username) => { const Invite = Parser.C.string('invite')
return { type: 'invite' .thenLeft(Parser.Whitespace.rep())
, username .thenRight(Parser.Username)
} .map((username) => {
}) return { type: 'invite', username }
})
const Delete = Parser.C.string('delete').thenReturns({ type: 'delete' }) const Delete = Parser.C.string('delete').thenReturns({ type: 'delete' })
return Create.or(Invite).or(Delete) return Create.or(Invite).or(Delete)
} }
* autocomplete(parameterString) { *autocomplete(parameterString) {
const Create = Parser.C.string('create') const Create = Parser.C.string('create')
const Invite = Parser.C.string('invite') const Invite = Parser.C.string('invite')
const Delete = Parser.C.string('delete') const Delete = Parser.C.string('delete')
const subcommandDone = Create.or(Invite).or(Delete).thenLeft(Parser.Whitespace) const subcommandDone = Create.or(Invite)
.or(Delete)
.thenLeft(Parser.Whitespace)
const subcommandCheck = subcommandDone.parse(Parser.Streams.ofString(parameterString)) const subcommandCheck = subcommandDone.parse(
Parser.Streams.ofString(parameterString)
)
if (subcommandCheck.isAccepted()) { if (subcommandCheck.isAccepted()) {
return return
} }
yield * [ 'create', 'invite ', 'delete' ].filter(item => item.startsWith(parameterString)) yield* ['create', 'invite ', 'delete'].filter((item) =>
item.startsWith(parameterString)
)
} }
} }
return Temproom return Temproom
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './_Unsuspension' ], function (Unsuspension) { define(['./_Unsuspension'], function (Unsuspension) {
"use strict"; 'use strict'
class Unban extends Unsuspension { class Unban extends Unsuspension {}
}
return Unban return Unban
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './_Unsuspension' ], function (Unsuspension) { define(['./_Unsuspension'], function (Unsuspension) {
"use strict"; 'use strict'
class Unmute extends Unsuspension { class Unmute extends Unsuspension {}
}
return Unmute return Unmute
}); })

View File

@ -11,10 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
class Where extends Command { class Where extends Command {
getParameterParser() { getParameterParser() {
@ -23,4 +21,4 @@ define([ '../Command'
} }
return Where return Where
}); })

View File

@ -11,25 +11,27 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Parser' define(['../Parser', './Plain'], function (Parser, Plain) {
, './Plain' 'use strict'
], function (Parser, Plain) {
"use strict";
class Whisper extends Plain { class Whisper extends Plain {
getParameterParser() { getParameterParser() {
return Parser.Username.thenLeft(Parser.Whitespace.rep()).then(super.getParameterParser()).map(([ username, object ]) => { return Parser.Username.thenLeft(Parser.Whitespace.rep())
object.username = username .then(super.getParameterParser())
.map(([username, object]) => {
object.username = username
return object return object
}) })
} }
* autocomplete(parameterString) { *autocomplete(parameterString) {
const usernameDone = Parser.Username.thenLeft(Parser.Whitespace).parse(Parser.Streams.ofString(parameterString)) const usernameDone = Parser.Username.thenLeft(Parser.Whitespace).parse(
Parser.Streams.ofString(parameterString)
)
if (usernameDone.isAccepted()) { if (usernameDone.isAccepted()) {
yield * super.autocomplete(parameterString) yield* super.autocomplete(parameterString)
return return
} }
@ -43,4 +45,4 @@ define([ '../Parser'
} }
return Whisper return Whisper
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
const DEPENDENCIES = [ 'ProfileStore' ] const DEPENDENCIES = ['ProfileStore']
class Suspension extends Command { class Suspension extends Command {
constructor(profileStore, id) { constructor(profileStore, id) {
super(id) super(id)
@ -24,51 +22,68 @@ define([ '../Command'
} }
getParameterParser() { getParameterParser() {
const Globally = Parser.C.string('global').thenLeft(Parser.C.string('ly').opt()) const Globally = Parser.C.string('global').thenLeft(
Parser.C.string('ly').opt()
)
const Forever = Parser.C.string('forever').thenReturns(null) const Forever = Parser.C.string('forever').thenReturns(null)
const Timespan = Parser.N.digits.then(Parser.C.charIn('dhm')).map(function ([ span, unit ]) { const Timespan = Parser.N.digits
switch (unit) { .then(Parser.C.charIn('dhm'))
case 'd': .map(function ([span, unit]) {
return span * 86400; switch (unit) {
case 'h': case 'd':
return span * 3600; return span * 86400
case 'm': case 'h':
return span * 60; return span * 3600
} case 'm':
throw new Error('Unreachable') return span * 60
}) }
.rep() throw new Error('Unreachable')
.map(parts => parts.array().reduce((carry, item) => carry + item, 0)) })
.map(offset => Math.floor(Date.now() / 1000) + offset) .rep()
.map((parts) => parts.array().reduce((carry, item) => carry + item, 0))
.map((offset) => Math.floor(Date.now() / 1000) + offset)
const Duration = Forever.or(Timespan).or(Parser.ISODate.map(item => Math.floor(item.valueOf() / 1000))) const Duration = Forever.or(Timespan).or(
Parser.ISODate.map((item) => Math.floor(item.valueOf() / 1000))
)
return Parser.Username.thenLeft(Parser.Whitespace.rep()) return Parser.Username.thenLeft(Parser.Whitespace.rep())
.then(Globally.thenLeft(Parser.Whitespace.rep()).thenReturns(true).or(Parser.F.returns(false))) .then(
.then(Duration) Globally.thenLeft(Parser.Whitespace.rep())
.then(Parser.Whitespace.rep().thenRight(Parser.Rest1).or(Parser.F.eos.thenReturns(null))) .thenReturns(true)
.map(([ username, globally, duration, reason ]) => { .or(Parser.F.returns(false))
return { username )
, globally .then(Duration)
, duration .then(
, reason Parser.Whitespace.rep()
} .thenRight(Parser.Rest1)
}) .or(Parser.F.eos.thenReturns(null))
)
.map(([username, globally, duration, reason]) => {
return { username, globally, duration, reason }
})
} }
* autocomplete(parameterString) { *autocomplete(parameterString) {
const usernameDone = Parser.Username.thenLeft(Parser.Whitespace.rep()).map(username => `"${username.replace(/"/g, '""')}"`) const usernameDone = Parser.Username.thenLeft(
const globallyDone = usernameDone.then(Parser.C.string('global').thenLeft(Parser.C.string('ly').opt())).thenLeft(Parser.Whitespace.rep()) Parser.Whitespace.rep()
).map((username) => `"${username.replace(/"/g, '""')}"`)
const globallyDone = usernameDone
.then(Parser.C.string('global').thenLeft(Parser.C.string('ly').opt()))
.thenLeft(Parser.Whitespace.rep())
const usernameCheck = usernameDone.parse(Parser.Streams.ofString(parameterString)) const usernameCheck = usernameDone.parse(
Parser.Streams.ofString(parameterString)
)
if (usernameCheck.isAccepted()) { if (usernameCheck.isAccepted()) {
const globallyCheck = globallyDone.parse(Parser.Streams.ofString(parameterString)) const globallyCheck = globallyDone.parse(
Parser.Streams.ofString(parameterString)
)
let prefix, rest let prefix, rest
if (globallyCheck.isAccepted()) { if (globallyCheck.isAccepted()) {
prefix = parameterString.substring(0, globallyCheck.offset) prefix = parameterString.substring(0, globallyCheck.offset)
rest = parameterString.substring(globallyCheck.offset) rest = parameterString.substring(globallyCheck.offset)
} } else {
else {
prefix = parameterString.substring(0, usernameCheck.offset) prefix = parameterString.substring(0, usernameCheck.offset)
rest = parameterString.substring(usernameCheck.offset) rest = parameterString.substring(usernameCheck.offset)
} }
@ -101,4 +116,4 @@ define([ '../Command'
Suspension.DEPENDENCIES = DEPENDENCIES Suspension.DEPENDENCIES = DEPENDENCIES
return Suspension return Suspension
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Command' define(['../Command', '../Parser'], function (Command, Parser) {
, '../Parser' 'use strict'
], function (Command, Parser) {
"use strict";
const DEPENDENCIES = [ 'ProfileStore' ] const DEPENDENCIES = ['ProfileStore']
class Unsuspension extends Command { class Unsuspension extends Command {
constructor(profileStore, id) { constructor(profileStore, id) {
super(id) super(id)
@ -24,30 +22,39 @@ define([ '../Command'
} }
getParameterParser() { getParameterParser() {
const Globally = Parser.C.string('global').thenLeft(Parser.C.string('ly').opt()) const Globally = Parser.C.string('global').thenLeft(
Parser.C.string('ly').opt()
)
return Parser.Username return Parser.Username.then(
.then(Parser.Whitespace.rep().thenRight(Globally.thenReturns(true)).or(Parser.F.returns(false))) Parser.Whitespace.rep()
.map(([ username, globally ]) => { .thenRight(Globally.thenReturns(true))
return { username .or(Parser.F.returns(false))
, globally ).map(([username, globally]) => {
} return { username, globally }
}) })
} }
* autocomplete(parameterString) { *autocomplete(parameterString) {
const usernameDone = Parser.Username.thenLeft(Parser.Whitespace.rep()).map(username => `"${username.replace(/"/g, '""')}"`) const usernameDone = Parser.Username.thenLeft(
const globallyDone = usernameDone.then(Parser.C.string('global').thenLeft(Parser.C.string('ly').opt())).thenLeft(Parser.Whitespace.rep()) Parser.Whitespace.rep()
).map((username) => `"${username.replace(/"/g, '""')}"`)
const globallyDone = usernameDone
.then(Parser.C.string('global').thenLeft(Parser.C.string('ly').opt()))
.thenLeft(Parser.Whitespace.rep())
const usernameCheck = usernameDone.parse(Parser.Streams.ofString(parameterString)) const usernameCheck = usernameDone.parse(
Parser.Streams.ofString(parameterString)
)
if (usernameCheck.isAccepted()) { if (usernameCheck.isAccepted()) {
const globallyCheck = globallyDone.parse(Parser.Streams.ofString(parameterString)) const globallyCheck = globallyDone.parse(
Parser.Streams.ofString(parameterString)
)
let prefix, rest let prefix, rest
if (globallyCheck.isAccepted()) { if (globallyCheck.isAccepted()) {
prefix = parameterString.substring(0, globallyCheck.offset) prefix = parameterString.substring(0, globallyCheck.offset)
rest = parameterString.substring(globallyCheck.offset) rest = parameterString.substring(globallyCheck.offset)
} } else {
else {
prefix = parameterString.substring(0, usernameCheck.offset) prefix = parameterString.substring(0, usernameCheck.offset)
rest = parameterString.substring(usernameCheck.offset) rest = parameterString.substring(usernameCheck.offset)
} }
@ -68,4 +75,4 @@ define([ '../Command'
Unsuspension.DEPENDENCIES = DEPENDENCIES Unsuspension.DEPENDENCIES = DEPENDENCIES
return Unsuspension return Unsuspension
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Parser' define(['./Parser', './ParseError'], function (Parser, ParseError) {
, './ParseError' 'use strict'
], function (Parser, ParseError) {
"use strict";
const DEPENDENCIES = [ 'Trigger', 'Command' ] const DEPENDENCIES = ['Trigger', 'Command']
class CommandHandler { class CommandHandler {
constructor(triggers, commands) { constructor(triggers, commands) {
this.triggers = triggers this.triggers = triggers
@ -28,19 +26,19 @@ define([ './Parser'
if (result.isAccepted()) { if (result.isAccepted()) {
return result.value return result.value
} } else {
else {
throw new ParseError('Empty trigger') throw new ParseError('Empty trigger')
} }
} }
applyCommand(command, parameterString) { applyCommand(command, parameterString) {
const result = command.getParameterParser().parse(Parser.Streams.ofString(parameterString)) const result = command
.getParameterParser()
.parse(Parser.Streams.ofString(parameterString))
if (result.isAccepted()) { if (result.isAccepted()) {
return result.value return result.value
} } else {
else {
throw new ParseError('Could not parse', { result, parameterString }) throw new ParseError('Could not parse', { result, parameterString })
} }
} }
@ -64,4 +62,4 @@ define([ './Parser'
CommandHandler.DEPENDENCIES = DEPENDENCIES CommandHandler.DEPENDENCIES = DEPENDENCIES
return CommandHandler return CommandHandler
}); })

View File

@ -11,13 +11,13 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
const listeners = new WeakMap() const listeners = new WeakMap()
const EventEmitter = function (target) { const EventEmitter = function (target) {
Object.assign(target, { Object.assign(target, {
on(type, listener, options = { }) { on(type, listener, options = {}) {
if (!listeners.has(this)) { if (!listeners.has(this)) {
listeners.set(this, new Map()) listeners.set(this, new Map())
} }
@ -33,22 +33,24 @@ define([ ], function () {
listeners.get(this).get(type).delete(listener) listeners.get(this).get(type).delete(listener)
}, },
emit(type, detail = { }) { emit(type, detail = {}) {
if (!listeners.has(this)) return if (!listeners.has(this)) return
if (!listeners.get(this).has(type)) return if (!listeners.get(this).has(type)) return
const set = listeners.get(this).get(type) const set = listeners.get(this).get(type)
set.forEach((function ({ listener, options }) { set.forEach(
if (options.once) { function ({ listener, options }) {
set.delete(listener) if (options.once) {
} set.delete(listener)
}
listener({ target: this, detail }) listener({ target: this, detail })
}).bind(this)) }.bind(this)
} )
},
}) })
} }
return EventEmitter return EventEmitter
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
const s = Symbol('s') const s = Symbol('s')
const start = Symbol('start') const start = Symbol('start')
@ -24,7 +24,7 @@ define([ ], function () {
} }
add(value) { add(value) {
if (this[start] && this[start].value === value) { if (this[start] && this[start].value === value) {
return return
} }
@ -45,14 +45,13 @@ define([ ], function () {
this[s].set(value, obj) this[s].set(value, obj)
} }
* [Symbol.iterator]() { *[Symbol.iterator]() {
let current = this[start] let current = this[start]
do { do {
yield current.value yield current.value
} } while ((current = current.next))
while ((current = current.next))
} }
} }
return LRU return LRU
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
class Node { class Node {
constructor(value) { constructor(value) {
@ -93,39 +93,52 @@ define([ ], function () {
} }
search(value) { search(value) {
if (value === this.value) return [ 'IS', this ] if (value === this.value) return ['IS', this]
if (value < this.value) { if (value < this.value) {
if (this.left !== undefined) return this.left.search(value) if (this.left !== undefined) return this.left.search(value)
return [ 'LEFT', this ] return ['LEFT', this]
} }
if (value > this.value) { if (value > this.value) {
if (this.right !== undefined) return this.right.search(value) if (this.right !== undefined) return this.right.search(value)
return [ 'RIGHT', this ] return ['RIGHT', this]
} }
throw new Error('Unreachable') throw new Error('Unreachable')
} }
print(depth = 0) { print(depth = 0) {
console.log(" ".repeat(depth) + `${this.value}: ${this.color} (Parent: ${this.parent ? this.parent.value : '-'})`) console.log(
' '.repeat(depth) +
`${this.value}: ${this.color} (Parent: ${
this.parent ? this.parent.value : '-'
})`
)
if (this.left) this.left.print(depth + 1) if (this.left) this.left.print(depth + 1)
else console.log(" ".repeat(depth + 1) + '-') else console.log(' '.repeat(depth + 1) + '-')
if (this.right) this.right.print(depth + 1) if (this.right) this.right.print(depth + 1)
else console.log(" ".repeat(depth + 1) + '-') else console.log(' '.repeat(depth + 1) + '-')
} }
check() { check() {
if (this.left && this.left.value >= this.value) throw new Error('Invalid' + this.value); if (this.left && this.left.value >= this.value)
if (this.right && this.right.value <= this.value) throw new Error('Invalid' + this.value); throw new Error('Invalid' + this.value)
if (this.color === 'RED' && ((this.left && this.left.color !== 'BLACK') || (this.right && this.right.color !== 'BLACK'))) throw new Error('Invalid' + this.value); if (this.right && this.right.value <= this.value)
throw new Error('Invalid' + this.value)
if (
this.color === 'RED' &&
((this.left && this.left.color !== 'BLACK') ||
(this.right && this.right.color !== 'BLACK'))
)
throw new Error('Invalid' + this.value)
let leftBlacks = 1, rightBlacks = 1 let leftBlacks = 1,
rightBlacks = 1
if (this.left) { if (this.left) {
leftBlacks = this.left.check() leftBlacks = this.left.check()
} }
if (this.right) { if (this.right) {
rightBlacks = this.right.check() rightBlacks = this.right.check()
} }
if (leftBlacks !== rightBlacks) throw new Error('Invalid' + this.value); if (leftBlacks !== rightBlacks) throw new Error('Invalid' + this.value)
if (this.color === 'BLACK') return leftBlacks + 1 if (this.color === 'BLACK') return leftBlacks + 1
return leftBlacks return leftBlacks
@ -133,4 +146,4 @@ define([ ], function () {
} }
return Node return Node
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Node' ], function (Node) { define(['./Node'], function (Node) {
"use strict"; 'use strict'
class Tree { class Tree {
constructor() { constructor() {
@ -29,22 +29,22 @@ define([ './Node' ], function (Node) {
if (this.root === undefined) { if (this.root === undefined) {
this.root = node this.root = node
this.fix(node) this.fix(node)
return [ 'RIGHT', undefined ] return ['RIGHT', undefined]
} }
const search = this.search(value) const search = this.search(value)
const [ side, parent ] = search const [side, parent] = search
if (side === 'IS') return [ side, parent.value ] if (side === 'IS') return [side, parent.value]
if (side === 'LEFT') { if (side === 'LEFT') {
parent.left = node parent.left = node
this.fix(node) this.fix(node)
return [ side, parent.value ] return [side, parent.value]
} }
if (side === 'RIGHT') { if (side === 'RIGHT') {
parent.right = node parent.right = node
this.fix(node) this.fix(node)
return [ side, parent.value ] return [side, parent.value]
} }
throw new Error('Unreachable') throw new Error('Unreachable')
} }
@ -74,8 +74,7 @@ define([ './Node' ], function (Node) {
if (N.isRightChild && N.parent.isLeftChild) { if (N.isRightChild && N.parent.isLeftChild) {
this.rotateLeft(N.parent) this.rotateLeft(N.parent)
N = N.left N = N.left
} } else if (N.isLeftChild && N.parent.isRightChild) {
else if (N.isLeftChild && N.parent.isRightChild) {
this.rotateRight(N.parent) this.rotateRight(N.parent)
N = N.right N = N.right
} }
@ -86,8 +85,7 @@ define([ './Node' ], function (Node) {
G.color = 'RED' G.color = 'RED'
if (N.isLeftChild) { if (N.isLeftChild) {
this.rotateRight(G) this.rotateRight(G)
} } else {
else {
this.rotateLeft(G) this.rotateLeft(G)
} }
} }
@ -99,11 +97,9 @@ define([ './Node' ], function (Node) {
N.right = right.left N.right = right.left
if (N.parent === undefined) { if (N.parent === undefined) {
this.root = right this.root = right
} } else if (N.isLeftChild) {
else if (N.isLeftChild) {
N.parent.left = right N.parent.left = right
} } else if (N.isRightChild) {
else if (N.isRightChild) {
N.parent.right = right N.parent.right = right
} }
@ -117,11 +113,9 @@ define([ './Node' ], function (Node) {
N.left = left.right N.left = left.right
if (N.parent === undefined) { if (N.parent === undefined) {
this.root = left this.root = left
} } else if (N.isLeftChild) {
else if (N.isLeftChild) {
N.parent.left = left N.parent.left = left
} } else if (N.isRightChild) {
else if (N.isRightChild) {
N.parent.right = left N.parent.right = left
} }
left.right = N left.right = N
@ -129,4 +123,4 @@ define([ './Node' ], function (Node) {
} }
return Tree return Tree
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
class Throttler { class Throttler {
constructor(callback, delay = 125) { constructor(callback, delay = 125) {
@ -37,7 +37,7 @@ define([ ], function () {
clearTimeout(this.timer) clearTimeout(this.timer)
} }
this.timer = setTimeout(_ => { this.timer = setTimeout((_) => {
this.timer = null this.timer = null
this.hot = false this.hot = false
@ -60,8 +60,7 @@ define([ ], function () {
guardedExecute() { guardedExecute() {
if (this.hot) { if (this.hot) {
this.awaiting = true this.awaiting = true
} } else {
else {
this.execute() this.execute()
} }
} }
@ -71,11 +70,10 @@ define([ ], function () {
} }
set delay(newDelay) { set delay(newDelay) {
if (this.awaiting && (Date.now() - this.last) > newDelay) { if (this.awaiting && Date.now() - this.last > newDelay) {
this._delay = 0 this._delay = 0
this.setTimer() this.setTimer()
} } else if (this.timer) {
else if (this.timer) {
this._delay = Math.max(0, newDelay - (Date.now() - this.last)) this._delay = Math.max(0, newDelay - (Date.now() - this.last))
this.setTimer() this.setTimer()
} }
@ -98,4 +96,4 @@ define([ ], function () {
} }
return throttle return throttle
}); })

View File

@ -11,10 +11,11 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Date/Util' define(['WoltLabSuite/Core/Date/Util', 'WoltLabSuite/Core/Language'], function (
, 'WoltLabSuite/Core/Language' DateUtil,
], function (DateUtil, Language) { Language
"use strict"; ) {
'use strict'
class Helper { class Helper {
static deepFreeze(obj) { static deepFreeze(obj) {
@ -38,11 +39,13 @@ define([ 'WoltLabSuite/Core/Date/Util'
*/ */
static isInput(element) { static isInput(element) {
if (element.tagName === 'INPUT') { if (element.tagName === 'INPUT') {
if (element.getAttribute('type') !== 'text' && element.getAttribute('type') !== 'password') { if (
element.getAttribute('type') !== 'text' &&
element.getAttribute('type') !== 'password'
) {
return false return false
} }
} } else if (element.tagName !== 'TEXTAREA') {
else if (element.tagName !== 'TEXTAREA') {
return false return false
} }
@ -53,21 +56,20 @@ define([ 'WoltLabSuite/Core/Date/Util'
let last = 0 let last = 0
let deferTimer = null let deferTimer = null
return function() { return function () {
const now = new Date().getTime() const now = new Date().getTime()
const args = arguments const args = arguments
const context = scope || this const context = scope || this
if (last && (now < (last + threshold))) { if (last && now < last + threshold) {
clearTimeout(deferTimer) clearTimeout(deferTimer)
return deferTimer = setTimeout(function() { return (deferTimer = setTimeout(function () {
last = now last = now
return fn.apply(context, args) return fn.apply(context, args)
}, threshold) }, threshold))
} } else {
else {
last = now last = now
return fn.apply(context, args) return fn.apply(context, args)
@ -108,8 +110,7 @@ define([ 'WoltLabSuite/Core/Date/Util'
if (element.nextSibling) { if (element.nextSibling) {
element.parentNode.insertBefore(wrapper, element.nextSibling) element.parentNode.insertBefore(wrapper, element.nextSibling)
} } else {
else {
element.parentNode.appendChild(wrapper) element.parentNode.appendChild(wrapper)
} }
@ -122,7 +123,7 @@ define([ 'WoltLabSuite/Core/Date/Util'
throw new Error(`Unsupported element type: ${textarea.tagName}`) throw new Error(`Unsupported element type: ${textarea.tagName}`)
} }
const pre = document.createElement('pre') const pre = document.createElement('pre')
const span = document.createElement('span') const span = document.createElement('span')
const mirror = function () { const mirror = function () {
@ -141,7 +142,7 @@ define([ 'WoltLabSuite/Core/Date/Util'
pre.appendChild(document.createElement('br')) pre.appendChild(document.createElement('br'))
textarea.parentNode.insertBefore(pre, textarea) textarea.parentNode.insertBefore(pre, textarea)
textarea.addEventListener('input', mirror) textarea.addEventListener('input', mirror)
mirror() mirror()
} }
@ -150,11 +151,12 @@ define([ 'WoltLabSuite/Core/Date/Util'
constructor(size) { constructor(size) {
super() super()
Object.defineProperty(this, 'size', { enumerable: false Object.defineProperty(this, 'size', {
, value: size enumerable: false,
, writable: false value: size,
, configurable: false writable: false,
}); configurable: false,
})
} }
push() { push() {
@ -164,7 +166,7 @@ define([ 'WoltLabSuite/Core/Date/Util'
super.shift() super.shift()
} }
return this.length; return this.length
} }
unshift() { unshift() {
@ -174,7 +176,7 @@ define([ 'WoltLabSuite/Core/Date/Util'
super.pop() super.pop()
} }
return this.length; return this.length
} }
first() { first() {
@ -190,9 +192,9 @@ define([ 'WoltLabSuite/Core/Date/Util'
} }
static intToRGBHex(integer) { static intToRGBHex(integer) {
const r = ((integer >> 16) & 0xFF).toString(16) const r = ((integer >> 16) & 0xff).toString(16)
const g = ((integer >> 8) & 0xFF).toString(16) const g = ((integer >> 8) & 0xff).toString(16)
const b = ((integer >> 0) & 0xFF).toString(16) const b = ((integer >> 0) & 0xff).toString(16)
const rr = r.length == 1 ? `0${r}` : r const rr = r.length == 1 ? `0${r}` : r
const gg = g.length == 1 ? `0${g}` : g const gg = g.length == 1 ? `0${g}` : g
@ -263,8 +265,7 @@ define([ 'WoltLabSuite/Core/Date/Util'
if (firstTextNode) { if (firstTextNode) {
nodeRange.setStart(firstTextNode, 0) nodeRange.setStart(firstTextNode, 0)
nodeRange.setEnd(lastTextNode, lastTextNode.length) nodeRange.setEnd(lastTextNode, lastTextNode.length)
} } else {
else {
nodeRange.selectNodeContents(node) nodeRange.selectNodeContents(node)
} }
@ -282,9 +283,10 @@ define([ 'WoltLabSuite/Core/Date/Util'
* @return {String} * @return {String}
*/ */
static getTextContent(node) { static getTextContent(node) {
const acceptNode = node => { const acceptNode = (node) => {
if (node instanceof Element) { if (node instanceof Element) {
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') return NodeFilter.FILTER_REJECT if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE')
return NodeFilter.FILTER_REJECT
} }
return NodeFilter.FILTER_ACCEPT return NodeFilter.FILTER_ACCEPT
@ -295,33 +297,34 @@ define([ 'WoltLabSuite/Core/Date/Util'
const flags = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT const flags = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT
const treeWalker = document.createTreeWalker(node, flags, { acceptNode }) const treeWalker = document.createTreeWalker(node, flags, { acceptNode })
const ignoredLinks = [ ] const ignoredLinks = []
while (treeWalker.nextNode()) { while (treeWalker.nextNode()) {
const node = treeWalker.currentNode const node = treeWalker.currentNode
if (node instanceof Text) { if (node instanceof Text) {
if (node.parentNode.tagName === 'A' && ignoredLinks.indexOf(node.parentNode) >= 0) { if (
node.parentNode.tagName === 'A' &&
ignoredLinks.indexOf(node.parentNode) >= 0
) {
continue continue
} }
out += node.nodeValue.replace(/\n/g, '') out += node.nodeValue.replace(/\n/g, '')
} } else {
else {
switch (node.tagName) { switch (node.tagName) {
case 'IMG': { case 'IMG': {
const alt = node.getAttribute('alt') const alt = node.getAttribute('alt')
if (node.classList.contains('smiley')) { if (node.classList.contains('smiley')) {
out += ` ${alt} ` out += ` ${alt} `
} } else if (alt && alt !== '') {
else if (alt && alt !== '') {
out += ` ${alt} [Image ${node.src}] ` out += ` ${alt} [Image ${node.src}] `
} } else {
else {
out += ` [Image ${node.src}] ` out += ` [Image ${node.src}] `
} }
break } break
}
case 'BR': case 'BR':
case 'LI': case 'LI':
@ -329,16 +332,16 @@ define([ 'WoltLabSuite/Core/Date/Util'
case 'DIV': case 'DIV':
case 'TR': case 'TR':
out += '\n' out += '\n'
break break
case 'TH': case 'TH':
case 'TD': case 'TD':
out += '\t' out += '\t'
break break
case 'P': case 'P':
out += '\n\n' out += '\n\n'
break break
case 'A': { case 'A': {
let link = node.href let link = node.href
@ -354,7 +357,9 @@ define([ 'WoltLabSuite/Core/Date/Util'
const parts = text.split(/\u2026/) const parts = text.split(/\u2026/)
if (parts.length === 2) { if (parts.length === 2) {
truncated = node.href.startsWith(parts[0]) && node.href.endsWith(parts[1]) truncated =
node.href.startsWith(parts[0]) &&
node.href.endsWith(parts[1])
} }
} }
@ -364,7 +369,8 @@ define([ 'WoltLabSuite/Core/Date/Util'
} }
out += link out += link
break } break
}
} }
} }
} }
@ -374,5 +380,4 @@ define([ 'WoltLabSuite/Core/Date/Util'
} }
return Helper return Helper
}); })

View File

@ -11,10 +11,13 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, LocalStorageEmulator) { define(['WoltLabSuite/Core/Core', './LocalStorageEmulator'], function (
'use strict'; Core,
LocalStorageEmulator
) {
'use strict'
const DEPENDENCIES = [ ] const DEPENDENCIES = []
class LocalStorage { class LocalStorage {
constructor(subprefix) { constructor(subprefix) {
this.subprefix = subprefix this.subprefix = subprefix
@ -23,15 +26,17 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
} }
static isQuotaExceeded(error) { static isQuotaExceeded(error) {
return error instanceof DOMException && ( return (
error instanceof DOMException &&
// everything except Firefox // everything except Firefox
error.code === 22 || (error.code === 22 ||
// Firefox // Firefox
error.code === 1014 || error.code === 1014 ||
// everything except Firefox // everything except Firefox
error.name === 'QuotaExceededError' || error.name === 'QuotaExceededError' ||
// Firefox // Firefox
error.name === 'NS_ERROR_DOM_QUOTA_REACHED') error.name === 'NS_ERROR_DOM_QUOTA_REACHED')
)
} }
static isAvailable() { static isAvailable() {
@ -40,8 +45,7 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
window.localStorage.setItem(x, x) window.localStorage.setItem(x, x)
window.localStorage.removeItem(x) window.localStorage.removeItem(x)
return true return true
} } catch (error) {
catch (error) {
return LocalStorage.isQuotaExceeded(error) return LocalStorage.isQuotaExceeded(error)
} }
} }
@ -50,8 +54,7 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
if (LocalStorage.isAvailable()) { if (LocalStorage.isAvailable()) {
this.storage = window.localStorage this.storage = window.localStorage
this.hasLocalStorage = true this.hasLocalStorage = true
} } else {
else {
console.info('Falling back to in-memory local storage emulation') console.info('Falling back to in-memory local storage emulation')
this.storage = new LocalStorageEmulator() this.storage = new LocalStorageEmulator()
} }
@ -97,13 +100,19 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
*/ */
set(key, value) { set(key, value) {
try { try {
this.storage.setItem(`${this.storagePrefix}${key}`, JSON.stringify(value)) this.storage.setItem(
} `${this.storagePrefix}${key}`,
catch (error) { JSON.stringify(value)
)
} catch (error) {
if (!LocalStorage.isQuotaExceeded(error)) throw error if (!LocalStorage.isQuotaExceeded(error)) throw error
console.warn(`Your localStorage has exceeded the size quota for this domain`) console.warn(
console.warn(`We are falling back to an in-memory storage, this does not persist data!`) `Your localStorage has exceeded the size quota for this domain`
)
console.warn(
`We are falling back to an in-memory storage, this does not persist data!`
)
console.error(error) console.error(error)
const storage = new LocalStorageEmulator() const storage = new LocalStorageEmulator()
@ -152,7 +161,7 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
* @returns {string} The last value of the provided setting * @returns {string} The last value of the provided setting
*/ */
remove(key) { remove(key) {
const value = this.get(key) const value = this.get(key)
const storageKey = `${this.storagePrefix}${key}` const storageKey = `${this.storagePrefix}${key}`
this.storage.removeItem(storageKey) this.storage.removeItem(storageKey)
@ -167,13 +176,20 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
clear() { clear() {
const _clear = (target) => { const _clear = (target) => {
for (let key in target) { for (let key in target) {
if (!key.startsWith(this.storagePrefix) || !target.hasOwnProperty(key)) continue if (
!key.startsWith(this.storagePrefix) ||
!target.hasOwnProperty(key)
)
continue
target.removeItem(key) target.removeItem(key)
} }
} }
if (this.hasLocalStorage && this.storage instanceof LocalStorageEmulator) { if (
this.hasLocalStorage &&
this.storage instanceof LocalStorageEmulator
) {
try { try {
// Try to clear the real localStorage // Try to clear the real localStorage
_clear(localStorage) _clear(localStorage)
@ -188,8 +204,9 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
this.storage = localStorage this.storage = localStorage
console.log('Switched back to using the localStorage') console.log('Switched back to using the localStorage')
} catch (error) {
/* no we cant */
} }
catch (error) { /* no we cant */ }
} }
_clear(this.storage) _clear(this.storage)
@ -198,4 +215,4 @@ define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, L
LocalStorage.DEPENDENCIES = DEPENDENCIES LocalStorage.DEPENDENCIES = DEPENDENCIES
return LocalStorage return LocalStorage
}); })

View File

@ -11,16 +11,19 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
'use strict'; 'use strict'
class LocalStorageEmulator { class LocalStorageEmulator {
constructor () { constructor() {
this._data = new Map() this._data = new Map()
return new Proxy(this, { return new Proxy(this, {
get(target, property) { get(target, property) {
// Check if the property exists on the object or its prototype // Check if the property exists on the object or its prototype
if (target.hasOwnProperty(property) || Object.getPrototypeOf(target)[property]) { if (
target.hasOwnProperty(property) ||
Object.getPrototypeOf(target)[property]
) {
return target[property] return target[property]
} }
@ -29,18 +32,22 @@ define([ ], function () {
}, },
set(target, property, value) { set(target, property, value) {
// Check if the property exists on the object or its prototype // Check if the property exists on the object or its prototype
if (target.hasOwnProperty(property) || Object.getPrototypeOf(target)[property]) { if (
target.hasOwnProperty(property) ||
Object.getPrototypeOf(target)[property]
) {
target[property] = value target[property] = value
} } else {
else {
// Proxy to the underlying map // Proxy to the underlying map
target.setItem(property, value) target.setItem(property, value)
} }
}, },
has(target, property) { has(target, property) {
return target.hasOwnProperty(property) // check the properties of the object return (
|| Object.getPrototypeOf(target)[property] // check its prototype target.hasOwnProperty(property) || // check the properties of the object
|| target._data.has(property) // check the underlying map Object.getPrototypeOf(target)[property] || // check its prototype
target._data.has(property)
) // check the underlying map
}, },
ownKeys(target) { ownKeys(target) {
// Proxy to the underlying map // Proxy to the underlying map
@ -50,9 +57,9 @@ define([ ], function () {
// Make the properties of the map visible // Make the properties of the map visible
return { return {
enumerable: true, enumerable: true,
configurable: true configurable: true,
} }
} },
}) })
} }
@ -80,10 +87,10 @@ define([ ], function () {
this._data.clear() this._data.clear()
} }
* [Symbol.iterator]() { *[Symbol.iterator]() {
yield * this._data.values() yield* this._data.values()
} }
} }
return LocalStorageEmulator return LocalStorageEmulator
}); })

View File

@ -11,19 +11,32 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './console' define([
, 'Bastelstu.be/bottle' './console',
, 'WoltLabSuite/Core/Core' 'Bastelstu.be/bottle',
, './Message' 'WoltLabSuite/Core/Core',
, './Messenger' './Message',
, './ProfileStore' './Messenger',
, './Room' './ProfileStore',
, './Template' './Room',
, './Ui/Log' './Template',
, './Ui/MessageStream' './Ui/Log',
, './Ui/MessageActions/Delete' './Ui/MessageStream',
], function (console, Bottle, Core, Message, Messenger, ProfileStore, Room, Template, Ui, UiMessageStream, UiMessageActionDelete) { './Ui/MessageActions/Delete',
"use strict"; ], function (
console,
Bottle,
Core,
Message,
Messenger,
ProfileStore,
Room,
Template,
Ui,
UiMessageStream,
UiMessageActionDelete
) {
'use strict'
const loader = Symbol('loader') const loader = Symbol('loader')
@ -31,10 +44,10 @@ define([ './console'
constructor(params, config) { constructor(params, config) {
console.debug('ChatLog.constructor', 'Constructing …') console.debug('ChatLog.constructor', 'Constructing …')
this.config = config this.config = config
this.sessionID = Core.getUuid() this.sessionID = Core.getUuid()
this.bottle = new Bottle() this.bottle = new Bottle()
this.bottle.value('bottle', this.bottle) this.bottle.value('bottle', this.bottle)
this.bottle.value('config', config) this.bottle.value('config', config)
this.bottle.constant('sessionID', this.sessionID) this.bottle.constant('sessionID', this.sessionID)
@ -56,36 +69,45 @@ define([ './console'
}) })
// Register Templates // Register Templates
const selector = [ '[type="x-text/template"]' const selector = [
, '[data-application="be.bastelstu.chat"]' '[type="x-text/template"]',
, '[data-template-name]' '[data-application="be.bastelstu.chat"]',
].join('') '[data-template-name]',
].join('')
const templates = elBySelAll(selector) const templates = elBySelAll(selector)
templates.forEach((function (template) { templates.forEach(
this.bottle.factory(`Template.${elData(template, 'template-name')}`, function (container) { function (template) {
const includeNames = (elData(template, 'template-includes') || '').split(/ /).filter(item => item !== "") this.bottle.factory(
const includes = { } `Template.${elData(template, 'template-name')}`,
includeNames.forEach(item => includes[item] = container[item]) 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) return new Template(template.textContent, includes)
}) }
}).bind(this)) )
}.bind(this)
)
// Register MessageTypes // Register MessageTypes
const messageTypes = Object.entries(this.config.messageTypes) const messageTypes = Object.entries(this.config.messageTypes)
messageTypes.forEach(([ objectType, messageType ]) => { messageTypes.forEach(([objectType, messageType]) => {
const MessageType = require(messageType.module) const MessageType = require(messageType.module)
this.bottle.factory(`MessageType.${objectType.replace(/\./g, '-')}`, _ => { this.bottle.factory(
const deps = this.bottle.digest(MessageType.DEPENDENCIES || []) `MessageType.${objectType.replace(/\./g, '-')}`,
(_) => {
const deps = this.bottle.digest(MessageType.DEPENDENCIES || [])
return new MessageType(...deps, objectType) return new MessageType(...deps, objectType)
}) }
)
}) })
this.knows = { from: undefined this.knows = { from: undefined, to: undefined }
, to: undefined
}
this.messageSinks = new Set() this.messageSinks = new Set()
@ -94,9 +116,11 @@ define([ './console'
this.pulling = false this.pulling = false
} }
service(name, _constructor, args = [ ]) { service(name, _constructor, args = []) {
this.bottle.factory(name, function (container) { this.bottle.factory(name, function (container) {
const deps = (_constructor.DEPENDENCIES || [ ]).map(dep => container[dep]) const deps = (_constructor.DEPENDENCIES || []).map(
(dep) => container[dep]
)
return new _constructor(...deps, ...args) return new _constructor(...deps, ...args)
}) })
@ -111,20 +135,34 @@ define([ './console'
this.registerMessageSink(this.bottle.container.UiMessageStream) this.registerMessageSink(this.bottle.container.UiMessageStream)
if (this.params.messageID > 0) { if (this.params.messageID > 0) {
await Promise.all([ this.pull(undefined, this.params.messageID) await Promise.all([
, this.pull(this.params.messageID + 1) this.pull(undefined, this.params.messageID),
]) this.pull(this.params.messageID + 1),
} ])
else { } else {
await this.pull() await this.pull()
} }
this.bottle.container.UiMessageStream.on('nearTop', this.pullOlder.bind(this)) this.bottle.container.UiMessageStream.on(
this.bottle.container.UiMessageStream.on('reachedTop', this.pullOlder.bind(this)) 'nearTop',
this.bottle.container.UiMessageStream.on('nearBottom', this.pullNewer.bind(this)) this.pullOlder.bind(this)
this.bottle.container.UiMessageStream.on('reachedBottom', this.pullNewer.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}`) const element = document.querySelector(
`#message-${this.params.messageID}`
)
// Force changing the hash to trigger a new lookup of the element. // Force changing the hash to trigger a new lookup of the element.
// At least Chrome wont target an element if it is not in the DOM // At least Chrome wont target an element if it is not in the DOM
@ -152,8 +190,7 @@ define([ './console'
async pull(from, to) { async pull(from, to) {
try { try {
await this.handlePull(await this.performPull(from, to)) await this.handlePull(await this.performPull(from, to))
} } catch (e) {
catch (e) {
this.handleError(e) this.handleError(e)
} }
} }
@ -180,7 +217,12 @@ define([ './console'
} }
async performPull(from = undefined, to = undefined) { async performPull(from = undefined, to = undefined) {
console.debug('ChatLog.performPull', `Pulling new messages; from: ${from !== undefined ? from : 'undefined'}, to: ${to !== 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) return this.bottle.container.Messenger.pull(from, to, true)
} }
@ -199,25 +241,35 @@ define([ './console'
let messages = payload.messages let messages = payload.messages
if (this.knows.from !== undefined && this.knows.to !== undefined) { if (this.knows.from !== undefined && this.knows.to !== undefined) {
messages = messages.filter((function (message) { messages = messages.filter(
return !(this.knows.from <= message.messageID && message.messageID <= this.knows.to) function (message) {
}).bind(this)) 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.from === undefined || payload.from < this.knows.from)
if (this.knows.to === undefined || payload.to > this.knows.to) this.knows.to = payload.to 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) => { await Promise.all(
return message.getMessageType().preProcess(message) messages.map((message) => {
})) return message.getMessageType().preProcess(message)
})
)
const userIDs = messages.map(message => message.userID) const userIDs = messages
.filter(userID => userID !== null) .map((message) => message.userID)
.filter((userID) => userID !== null)
await this.bottle.container.ProfileStore.ensureUsersByIDs(userIDs) await this.bottle.container.ProfileStore.ensureUsersByIDs(userIDs)
this.messageSinks.forEach(sink => sink.ingest(messages)) this.messageSinks.forEach((sink) => sink.ingest(messages))
} }
} }
return Log return Log
}); })

View File

@ -11,13 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Helper' define([
, 'WoltLabSuite/Core/Date/Util' './Helper',
, 'WoltLabSuite/Core/User' 'WoltLabSuite/Core/Date/Util',
], function (Helper, DateUtil, User) { 'WoltLabSuite/Core/User',
"use strict"; ], function (Helper, DateUtil, User) {
'use strict'
const m = Symbol('message') const m = Symbol('message')
class Message { class Message {
constructor(MessageType, message) { constructor(MessageType, message) {
@ -87,4 +88,4 @@ define([ './Helper'
} }
return Message return Message
}); })

View File

@ -11,14 +11,15 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Date/Util' define([
, 'WoltLabSuite/Core/Language' 'WoltLabSuite/Core/Date/Util',
, 'WoltLabSuite/Core/Dom/Util' 'WoltLabSuite/Core/Language',
, 'Bastelstu.be/Chat/User' 'WoltLabSuite/Core/Dom/Util',
], function (DateUtil, Language, DomUtil, User) { 'Bastelstu.be/Chat/User',
"use strict"; ], function (DateUtil, Language, DomUtil, User) {
'use strict'
const DEPENDENCIES = [ 'ProfileStore', 'Template' ] const DEPENDENCIES = ['ProfileStore', 'Template']
class MessageType { class MessageType {
constructor(profileStore, templates, objectType) { constructor(profileStore, templates, objectType) {
this.profileStore = profileStore this.profileStore = profileStore
@ -32,32 +33,31 @@ define([ 'WoltLabSuite/Core/Date/Util'
} }
getReferencedUsers(message) { getReferencedUsers(message) {
if (message.userID === null) return [ ] if (message.userID === null) return []
return [ message.userID ] return [message.userID]
} }
preProcess(message) { preProcess(message) {}
} preRender(message) {}
preRender(message) {
}
render(message) { render(message) {
const variables = { message const variables = {
, users: this.profileStore message,
, author: this.profileStore.get(message.userID) users: this.profileStore,
, DateUtil author: this.profileStore.get(message.userID),
, Language DateUtil,
} Language,
}
if (variables.author == null) { if (variables.author == null) {
variables.author = User.getGuest(message.username) variables.author = User.getGuest(message.username)
} }
return DomUtil.createFragmentFromHtml(this.templates[message.objectType.replace(/\./g, '-')].fetch(variables)) return DomUtil.createFragmentFromHtml(
this.templates[message.objectType.replace(/\./g, '-')].fetch(variables)
)
} }
renderPlainText(message) { renderPlainText(message) {
@ -71,4 +71,4 @@ define([ 'WoltLabSuite/Core/Date/Util'
MessageType.DEPENDENCIES = DEPENDENCIES MessageType.DEPENDENCIES = DEPENDENCIES
return MessageType return MessageType
}); })

View File

@ -11,10 +11,12 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'ProfileStore', 'roomID' ].concat(MessageType.DEPENDENCIES || [ ]) const DEPENDENCIES = ['ProfileStore', 'roomID'].concat(
MessageType.DEPENDENCIES || []
)
class Away extends MessageType { class Away extends MessageType {
constructor(profileStore, roomID, ...superDeps) { constructor(profileStore, roomID, ...superDeps) {
super(...superDeps) super(...superDeps)
@ -24,12 +26,13 @@ define([ '../MessageType' ], function (MessageType) {
} }
render(message) { render(message) {
const isSilent = message.payload.rooms.find(room => room.roomID === this.roomID).isSilent const isSilent = message.payload.rooms.find(
(room) => room.roomID === this.roomID
).isSilent
if (!isSilent) { if (!isSilent) {
return super.render(message) return super.render(message)
} } else {
else {
return false return false
} }
} }
@ -45,4 +48,4 @@ define([ '../MessageType' ], function (MessageType) {
Away.DEPENDENCIES = DEPENDENCIES Away.DEPENDENCIES = DEPENDENCIES
return Away return Away
}); })

View File

@ -11,10 +11,12 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'ProfileStore', 'roomID' ].concat(MessageType.DEPENDENCIES || [ ]) const DEPENDENCIES = ['ProfileStore', 'roomID'].concat(
MessageType.DEPENDENCIES || []
)
class Back extends MessageType { class Back extends MessageType {
constructor(profileStore, roomID, ...superDeps) { constructor(profileStore, roomID, ...superDeps) {
super(...superDeps) super(...superDeps)
@ -24,12 +26,13 @@ define([ '../MessageType' ], function (MessageType) {
} }
render(message) { render(message) {
const isSilent = message.payload.rooms.find(room => room.roomID === this.roomID).isSilent const isSilent = message.payload.rooms.find(
(room) => room.roomID === this.roomID
).isSilent
if (!isSilent) { if (!isSilent) {
return super.render(message) return super.render(message)
} } else {
else {
return false return false
} }
} }
@ -45,4 +48,4 @@ define([ '../MessageType' ], function (MessageType) {
Back.DEPENDENCIES = DEPENDENCIES Back.DEPENDENCIES = DEPENDENCIES
return Back return Back
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Plain' ], function (Plain) { define(['./Plain'], function (Plain) {
"use strict"; 'use strict'
class Broadcast extends Plain { class Broadcast extends Plain {
renderPlainText(message) { renderPlainText(message) {
@ -21,4 +21,4 @@ define([ './Plain' ], function (Plain) {
} }
return Broadcast return Broadcast
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
class ChatUpdate extends MessageType { class ChatUpdate extends MessageType {
preRender(message) { preRender(message) {
@ -25,4 +25,4 @@ define([ '../MessageType' ], function (MessageType) {
} }
return ChatUpdate return ChatUpdate
}); })

View File

@ -11,15 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
class Color extends MessageType { class Color extends MessageType {
render(message) { render(message) {
if (message.isOwnMessage()) { if (message.isOwnMessage()) {
return super.render(message) return super.render(message)
} } else {
else {
return false return false
} }
} }
@ -34,4 +33,4 @@ define([ '../MessageType' ], function (MessageType) {
} }
return Color return Color
}); })

View File

@ -11,12 +11,13 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Dom/Traverse' define([
, 'WoltLabSuite/Core/Language' 'WoltLabSuite/Core/Dom/Traverse',
, '../Helper' 'WoltLabSuite/Core/Language',
, '../MessageType' '../Helper',
], function (DomTraverse, Language, Helper, MessageType) { '../MessageType',
"use strict"; ], function (DomTraverse, Language, Helper, MessageType) {
'use strict'
const decorators = Symbol('decorators') const decorators = Symbol('decorators')
@ -36,16 +37,19 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
} }
getReferencedUsers(message) { getReferencedUsers(message) {
return super.getReferencedUsers(message).concat([ message.payload.user.userID ]) return super
.getReferencedUsers(message)
.concat([message.payload.user.userID])
} }
render(message) { render(message) {
const rooms = message.payload.rooms.map(function (item) { const rooms = message.payload.rooms.map(function (item) {
const aug = { lastPull: null const aug = {
, lastPullHTML: null lastPull: null,
, lastPush: null lastPullHTML: null,
, lastPushHTML: null lastPush: null,
} lastPushHTML: null,
}
if (item.lastPull) { if (item.lastPull) {
aug.lastPull = new Date(item.lastPull * 1000) aug.lastPull = new Date(item.lastPull * 1000)
@ -57,28 +61,35 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
aug.lastPushHTML = Helper.getTimeElementHTML(aug.lastPush) aug.lastPushHTML = Helper.getTimeElementHTML(aug.lastPush)
} }
return Object.assign({ }, item, aug) return Object.assign({}, item, aug)
}) })
const payload = Helper.deepFreeze( const payload = Helper.deepFreeze(
Array.from(this[decorators]).reduce( (payload, decorator) => decorator(payload) Array.from(this[decorators]).reduce(
, Object.assign({ }, message.payload, { rooms }) (payload, decorator) => decorator(payload),
) Object.assign({}, message.payload, { rooms })
)
) )
const fragment = super.render(new Proxy(message, { const fragment = super.render(
get: function (target, property) { new Proxy(message, {
if (property === 'payload') return payload get: function (target, property) {
return target[property] if (property === 'payload') return payload
} return target[property]
})) },
})
)
const icon = elCreate('span') const icon = elCreate('span')
icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon') icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon')
icon.setAttribute('title', Language.get('wcf.global.button.hide')) icon.setAttribute('title', Language.get('wcf.global.button.hide'))
icon.addEventListener('click', () => elHide(DomTraverse.parentBySel(icon, '.chatMessageBoundary'))) icon.addEventListener('click', () =>
elHide(DomTraverse.parentBySel(icon, '.chatMessageBoundary'))
)
const elem = fragment.querySelector('.chatMessage .containerList > li:first-child .containerHeadline') const elem = fragment.querySelector(
'.chatMessage .containerList > li:first-child .containerHeadline'
)
elem.insertBefore(icon, elem.firstChild) elem.insertBefore(icon, elem.firstChild)
return fragment return fragment
@ -86,4 +97,4 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
} }
return Info return Info
}); })

View File

@ -11,8 +11,11 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType, Language) { define(['../MessageType', 'WoltLabSuite/Core/Language'], function (
"use strict"; MessageType,
Language
) {
'use strict'
class Join extends MessageType { class Join extends MessageType {
shouldUpdateUserList(message) { shouldUpdateUserList(message) {
@ -20,9 +23,15 @@ define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType
} }
renderPlainText(message) { renderPlainText(message) {
return '[➡️] ' + Language.get('chat.messageType.be.bastelstu.chat.messageType.join.plain', { author: { username: message.username } }) return (
'[➡️] ' +
Language.get(
'chat.messageType.be.bastelstu.chat.messageType.join.plain',
{ author: { username: message.username } }
)
)
} }
} }
return Join return Join
}); })

View File

@ -11,8 +11,11 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType, Language) { define(['../MessageType', 'WoltLabSuite/Core/Language'], function (
"use strict"; MessageType,
Language
) {
'use strict'
class Leave extends MessageType { class Leave extends MessageType {
shouldUpdateUserList(message) { shouldUpdateUserList(message) {
@ -20,9 +23,15 @@ define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType
} }
renderPlainText(message) { renderPlainText(message) {
return '[⬅️️] ' + Language.get('chat.messageType.be.bastelstu.chat.messageType.leave.plain', { author: { username: message.username } }) return (
'[⬅️️] ' +
Language.get(
'chat.messageType.be.bastelstu.chat.messageType.leave.plain',
{ author: { username: message.username } }
)
)
} }
} }
return Leave return Leave
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
class Me extends MessageType { class Me extends MessageType {}
}
return Me return Me
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
class Plain extends MessageType { class Plain extends MessageType {
joinable(a, b) { joinable(a, b) {
@ -25,4 +25,4 @@ define([ '../MessageType' ], function (MessageType) {
} }
return Plain return Plain
}); })

View File

@ -11,28 +11,35 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Helper' define([
, 'WoltLabSuite/Core/Date/Util' '../Helper',
, '../MessageType' 'WoltLabSuite/Core/Date/Util',
], function (Helper, DateUtil, MessageType) { '../MessageType',
"use strict"; ], function (Helper, DateUtil, MessageType) {
'use strict'
class Suspend extends MessageType { class Suspend extends MessageType {
render(message) { render(message) {
const expires = message.payload.suspension.expires !== null ? new Date(message.payload.suspension.expires * 1000) : null const expires =
const formattedExpires = expires !== null ? DateUtil.formatDateTime(expires) : null message.payload.suspension.expires !== null
const aug = { expires ? new Date(message.payload.suspension.expires * 1000)
, formattedExpires : null
} const formattedExpires =
const suspension = Object.assign({ }, message.payload.suspension, aug) expires !== null ? DateUtil.formatDateTime(expires) : null
const payload = Helper.deepFreeze(Object.assign({ }, message.payload, { suspension })) const aug = { expires, formattedExpires }
const suspension = Object.assign({}, message.payload.suspension, aug)
const payload = Helper.deepFreeze(
Object.assign({}, message.payload, { suspension })
)
return super.render(new Proxy(message, { return super.render(
get: function (target, property) { new Proxy(message, {
if (property === 'payload') return payload get: function (target, property) {
return target[property] if (property === 'payload') return payload
} return target[property]
})) },
})
)
} }
shouldUpdateUserList(message) { shouldUpdateUserList(message) {
@ -41,4 +48,4 @@ define([ '../Helper'
} }
return Suspend return Suspend
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Plain' ], function (Plain) { define(['./Plain'], function (Plain) {
"use strict"; 'use strict'
class Team extends Plain { class Team extends Plain {
joinable(a, b) { joinable(a, b) {
@ -25,4 +25,4 @@ define([ './Plain' ], function (Plain) {
} }
return Team return Team
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
class TemproomCreated extends MessageType { class TemproomCreated extends MessageType {}
}
return TemproomCreated return TemproomCreated
}); })

View File

@ -11,12 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
class TemproomInvited extends MessageType { class TemproomInvited extends MessageType {}
}
return TemproomInvited return TemproomInvited
}); })

View File

@ -11,10 +11,12 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'UiMessageStream' ].concat(MessageType.DEPENDENCIES || [ ]) const DEPENDENCIES = ['UiMessageStream'].concat(
MessageType.DEPENDENCIES || []
)
class Tombstone extends MessageType { class Tombstone extends MessageType {
constructor(messageStream, ...superDeps) { constructor(messageStream, ...superDeps) {
super(...superDeps) super(...superDeps)
@ -37,17 +39,21 @@ define([ '../MessageType' ], function (MessageType) {
if (!chatMessage) return false if (!chatMessage) return false
const rendered = super.render(message) const rendered = super.render(message)
const oldIcon = node.querySelector('.chatMessageContent > .chatMessageIcon') const oldIcon = node.querySelector(
'.chatMessageContent > .chatMessageIcon'
)
const newIcon = rendered.querySelector('.chatMessageIcon') const newIcon = rendered.querySelector('.chatMessageIcon')
if (oldIcon) { if (oldIcon) {
oldIcon.parentNode.replaceChild(newIcon, oldIcon) oldIcon.parentNode.replaceChild(newIcon, oldIcon)
} } else {
else {
chatMessage.parentNode.insertBefore(newIcon, chatMessage) chatMessage.parentNode.insertBefore(newIcon, chatMessage)
} }
chatMessage.parentNode.replaceChild(rendered.querySelector('.chatMessage'), chatMessage) chatMessage.parentNode.replaceChild(
rendered.querySelector('.chatMessage'),
chatMessage
)
return false return false
} }
@ -55,4 +61,4 @@ define([ '../MessageType' ], function (MessageType) {
Tombstone.DEPENDENCIES = DEPENDENCIES Tombstone.DEPENDENCIES = DEPENDENCIES
return Tombstone return Tombstone
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../MessageType' ], function (MessageType) { define(['../MessageType'], function (MessageType) {
"use strict"; 'use strict'
class Unsuspend extends MessageType { class Unsuspend extends MessageType {
shouldUpdateUserList(message) { shouldUpdateUserList(message) {
@ -21,4 +21,4 @@ define([ '../MessageType' ], function (MessageType) {
} }
return Unsuspend return Unsuspend
}); })

View File

@ -11,11 +11,12 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Dom/Traverse' define([
, 'WoltLabSuite/Core/Language' 'WoltLabSuite/Core/Dom/Traverse',
, '../MessageType' 'WoltLabSuite/Core/Language',
], function (DomTraverse, Language, MessageType) { '../MessageType',
"use strict"; ], function (DomTraverse, Language, MessageType) {
'use strict'
class Where extends MessageType { class Where extends MessageType {
render(message) { render(message) {
@ -24,7 +25,9 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
const icon = elCreate('span') const icon = elCreate('span')
icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon') icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon')
icon.setAttribute('title', Language.get('wcf.global.button.hide')) icon.setAttribute('title', Language.get('wcf.global.button.hide'))
icon.addEventListener('click', () => elHide(DomTraverse.parentBySel(icon, '.chatMessageBoundary'))) icon.addEventListener('click', () =>
elHide(DomTraverse.parentBySel(icon, '.chatMessageBoundary'))
)
const elem = fragment.querySelector('.jsRoomInfo > .containerHeadline') const elem = fragment.querySelector('.jsRoomInfo > .containerHeadline')
elem.insertBefore(icon, elem.firstChild) elem.insertBefore(icon, elem.firstChild)
@ -34,4 +37,4 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
} }
return Where return Where
}); })

View File

@ -11,10 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Plain' ], function (Plain) { define(['./Plain'], function (Plain) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'UiInput' ].concat(Plain.DEPENDENCIES || [ ]) const DEPENDENCIES = ['UiInput'].concat(Plain.DEPENDENCIES || [])
class Whisper extends Plain { class Whisper extends Plain {
constructor(input, ...superDeps) { constructor(input, ...superDeps) {
super(...superDeps) super(...superDeps)
@ -26,28 +26,39 @@ define([ './Plain' ], function (Plain) {
const fragment = super.render(message) const fragment = super.render(message)
if (this.input != null) { if (this.input != null) {
Array.prototype.forEach.call(fragment.querySelectorAll('[data-insert-whisper]'), (function (el) { Array.prototype.forEach.call(
el.addEventListener('click', (function () { fragment.querySelectorAll('[data-insert-whisper]'),
const username = el.dataset.insertWhisper function (el) {
const sanitizedUsername = username.replace(/"/g, '""') el.addEventListener(
const command = `/whisper "${sanitizedUsername}"` 'click',
function () {
const username = el.dataset.insertWhisper
const sanitizedUsername = username.replace(/"/g, '""')
const command = `/whisper "${sanitizedUsername}"`
if (this.input.getText().indexOf(command) !== 0) { if (this.input.getText().indexOf(command) !== 0) {
this.input.insertText(`${command} `, { prepend: true, append: false }) this.input.insertText(`${command} `, {
this.input.focus() prepend: true,
} append: false,
}).bind(this)) })
}).bind(this)) this.input.focus()
}
}.bind(this)
)
}.bind(this)
)
} }
return fragment return fragment
} }
joinable(a, b) { joinable(a, b) {
return a.userID === b.userID && a.payload.recipient === b.payload.recipient return (
a.userID === b.userID && a.payload.recipient === b.payload.recipient
)
} }
} }
Whisper.DEPENDENCIES = DEPENDENCIES Whisper.DEPENDENCIES = DEPENDENCIES
return Whisper return Whisper
}); })

View File

@ -11,16 +11,18 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './console' define(['./console', 'Bastelstu.be/PromiseWrap/Ajax', './Room'], function (
, 'Bastelstu.be/PromiseWrap/Ajax' console,
, './Room' Ajax,
], function (console, Ajax, Room) { Room
"use strict"; ) {
'use strict'
const DEPENDENCIES = [ 'sessionID', 'Room', 'Message' ] const DEPENDENCIES = ['sessionID', 'Room', 'Message']
class Messenger { class Messenger {
constructor(sessionID, room, Message) { constructor(sessionID, room, Message) {
if (!(room instanceof Room)) throw new TypeError('You must pass a Room to the Messenger') if (!(room instanceof Room))
throw new TypeError('You must pass a Room to the Messenger')
this.sessionID = sessionID this.sessionID = sessionID
this.room = room this.room = room
@ -30,9 +32,7 @@ define([ './console'
async pull(from = 0, to = 0, inLog = false) { async pull(from = 0, to = 0, inLog = false) {
console.debug(`Messenger.pull`, 'from', from, 'to', to, 'inLog', inLog) console.debug(`Messenger.pull`, 'from', from, 'to', to, 'inLog', inLog)
const payload = { actionName: 'pull' const payload = { actionName: 'pull', parameters: { inLog } }
, parameters: { inLog }
}
if (from !== 0 && to !== 0) { if (from !== 0 && to !== 0) {
throw new Error('You must not set both from and to') throw new Error('You must not set both from and to')
} }
@ -40,42 +40,41 @@ define([ './console'
if (to !== 0) payload.parameters.to = to if (to !== 0) payload.parameters.to = to
const data = await Ajax.api(this, payload) const data = await Ajax.api(this, payload)
const messages = Object.values(data.returnValues.messages).map((item) => this.Message.instance(item)) const messages = Object.values(data.returnValues.messages).map((item) =>
this.Message.instance(item)
)
const { from: newFrom, to: newTo } = data.returnValues const { from: newFrom, to: newTo } = data.returnValues
return { messages, from: newFrom, to: newTo } return { messages, from: newFrom, to: newTo }
} }
async push({ commandID, parameters }) { async push({ commandID, parameters }) {
const payload = { actionName: 'push' const payload = {
, parameters: { commandID actionName: 'push',
, parameters: JSON.stringify(parameters) parameters: { commandID, parameters: JSON.stringify(parameters) },
} }
}
return Ajax.api(this, payload) return Ajax.api(this, payload)
} }
async pushAttachment(tmpHash) { async pushAttachment(tmpHash) {
const payload = { actionName: 'pushAttachment' const payload = { actionName: 'pushAttachment', parameters: { tmpHash } }
, parameters: { tmpHash }
}
return Ajax.api(this, payload) return Ajax.api(this, payload)
} }
_ajaxSetup() { _ajaxSetup() {
return { silent: true return {
, ignoreError: true silent: true,
, data: { className: 'chat\\data\\message\\MessageAction' ignoreError: true,
, parameters: { roomID: this.room.roomID data: {
, sessionID: this.sessionID className: 'chat\\data\\message\\MessageAction',
} parameters: { roomID: this.room.roomID, sessionID: this.sessionID },
} },
} }
} }
} }
Messenger.DEPENDENCIES = DEPENDENCIES Messenger.DEPENDENCIES = DEPENDENCIES
return Messenger return Messenger
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
class ParseError extends Error { class ParseError extends Error {
constructor(message, data) { constructor(message, data) {
@ -23,4 +23,4 @@ define([ ], function () {
} }
return ParseError return ParseError
}); })

View File

@ -11,88 +11,112 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'Bastelstu.be/parser-combinator' define(['Bastelstu.be/parser-combinator'], function (parsec) {
], function (parsec) { 'use strict'
"use strict";
const { C, F, N, X, parser, Streams } = parsec const { C, F, N, X, parser, Streams } = parsec
const response = parsec.parsec.response const response = parsec.parsec.response
const peek = function (p) { const peek = function (p) {
return new parser((input, index = 0) => return new parser((input, index = 0) =>
p p.parse(input, index).fold(
.parse(input, index) (accept) => response.accept(accept.value, accept.input, index, false),
.fold( (reject) => response.reject(input.location(reject.offset), false)
accept => response.accept(accept.value, accept.input, index, false),
reject => response.reject(input.location(reject.offset), false)
) )
); )
} }
const Whitespace = F.satisfy(item => /\s/.test(item)) const Whitespace = F.satisfy((item) => /\s/.test(item))
const Rest = F.any.optrep().map(item => item.join('')) const Rest = F.any.optrep().map((item) => item.join(''))
const Rest1 = F.any.rep().map(item => item.join('')) const Rest1 = F.any.rep().map((item) => item.join(''))
const AlnumTrigger = C.letter.or(N.digit).rep().map(item => item.join('')) const AlnumTrigger = C.letter
const SymbolicTrigger = F.not(C.letter.or(N.digit).or(Whitespace)).rep().map(item => item.join('')) .or(N.digit)
.rep()
.map((item) => item.join(''))
const SymbolicTrigger = F.not(C.letter.or(N.digit).or(Whitespace))
.rep()
.map((item) => item.join(''))
const Slash = C.char('/') const Slash = C.char('/')
const Trigger = Slash.thenRight( const Trigger = Slash.thenRight(
peek(Slash.map(item => null)).or(AlnumTrigger.thenLeft(Whitespace.rep().or(F.eos))).or(SymbolicTrigger.thenLeft(Whitespace.optrep())) peek(Slash.map((item) => null))
.or(AlnumTrigger.thenLeft(Whitespace.rep().or(F.eos)))
.or(SymbolicTrigger.thenLeft(Whitespace.optrep()))
).or(F.returns(null)) ).or(F.returns(null))
const Command = Trigger.then(Rest) const Command = Trigger.then(Rest)
const Quote = C.char('"') const Quote = C.char('"')
const QuotedUsername = Quote.thenRight( const QuotedUsername = Quote.thenRight(
((Quote.thenRight(Quote)).or(F.not(Quote))).rep() Quote.thenRight(Quote).or(F.not(Quote)).rep()
).thenLeft(Quote).map(item => item.join('')) )
.thenLeft(Quote)
.map((item) => item.join(''))
const Comma = C.char(',') const Comma = C.char(',')
const UnquotedUsername = F.not(Comma.or(Quote).or(Whitespace)).then(F.not(Comma.or(Whitespace)).optrep().map(item => item.join(''))).map(item => item.join('')) const UnquotedUsername = F.not(Comma.or(Quote).or(Whitespace))
.then(
F.not(Comma.or(Whitespace))
.optrep()
.map((item) => item.join(''))
)
.map((item) => item.join(''))
const Username = QuotedUsername.or(UnquotedUsername) const Username = QuotedUsername.or(UnquotedUsername)
const Decimal = (length) => N.digit.occurrence(length).map(item => parseInt(item.join(''), 10)) const Decimal = (length) =>
N.digit.occurrence(length).map((item) => parseInt(item.join(''), 10))
const Hexadecimal = N.digit const Hexadecimal = N.digit
.or(C.charIn('abcdefABCDEF')) .or(C.charIn('abcdefABCDEF'))
.rep() .rep()
.map(x => x.join('')) .map((x) => x.join(''))
const RGBHex = (C.char('#').opt()) const RGBHex = C.char('#')
.opt()
.thenRight( .thenRight(
Hexadecimal.filter(x => x.length === 3 || x.length === 6) Hexadecimal.filter((x) => x.length === 3 || x.length === 6).map(
.map(item => { (item) => {
if (item.length === 3) { if (item.length === 3) {
item = `${item[0]}${item[0]}${item[1]}${item[1]}${item[2]}${item[2]}` item = `${item[0]}${item[0]}${item[1]}${item[1]}${item[2]}${item[2]}`
} }
return item return item
}) }
).map(item => `#${item}`) )
)
.map((item) => `#${item}`)
const Dash = C.char('-') const Dash = C.char('-')
const Datestring = Decimal(4).filter(item => 2000 <= item && item <= 2030) const Datestring = Decimal(4)
.thenLeft(Dash).then(Decimal(2).filter(item => 1 <= item && item <= 12)) .filter((item) => 2000 <= item && item <= 2030)
.thenLeft(Dash).then(Decimal(2).filter(item => 1 <= item)) .thenLeft(Dash)
.then(Decimal(2).filter((item) => 1 <= item && item <= 12))
.thenLeft(Dash)
.then(Decimal(2).filter((item) => 1 <= item))
const Colon = C.char(':') const Colon = C.char(':')
const Timestring = Decimal(2).filter(item => 0 <= item && item <= 23) const Timestring = Decimal(2)
.thenLeft(Colon).then(Decimal(2).filter(item => 0 <= item && item <= 59)) .filter((item) => 0 <= item && item <= 23)
.thenLeft(Colon).then(Decimal(2).filter(item => 0 <= item && item <= 59)) .thenLeft(Colon)
.then(Decimal(2).filter((item) => 0 <= item && item <= 59))
.thenLeft(Colon)
.then(Decimal(2).filter((item) => 0 <= item && item <= 59))
const ISODate = Datestring.then(C.char('T').thenRight(Timestring).opt()).map(function ([ year, month, day, time ]) { const ISODate = Datestring.then(C.char('T').thenRight(Timestring).opt()).map(
const date = new Date() function ([year, month, day, time]) {
date.setFullYear(year) const date = new Date()
date.setMonth(month - 1) date.setFullYear(year)
date.setDate(day) date.setMonth(month - 1)
date.setDate(day)
time.map(function ([ hour, minute, second ]) { time.map(function ([hour, minute, second]) {
date.setHours(hour) date.setHours(hour)
date.setMinutes(minute) date.setMinutes(minute)
date.setSeconds(second) date.setSeconds(second)
}) })
return date return date
}) }
)
return { return {
Streams, Streams,
@ -122,4 +146,4 @@ define([ 'Bastelstu.be/parser-combinator'
N, N,
X, X,
} }
}); })

View File

@ -11,14 +11,15 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'Bastelstu.be/PromiseWrap/Ajax' define([
, './DataStructure/LRU' 'Bastelstu.be/PromiseWrap/Ajax',
, './User' './DataStructure/LRU',
, 'WoltLabSuite/Core/User' './User',
], function (Ajax, LRU, User, CoreUser) { 'WoltLabSuite/Core/User',
"use strict"; ], function (Ajax, LRU, User, CoreUser) {
'use strict'
const DEPENDENCIES = [ ] const DEPENDENCIES = []
/** /**
* ProfileStore stores information about users. * ProfileStore stores information about users.
*/ */
@ -40,44 +41,47 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
*/ */
async ensureUsersByIDs(userIDs) { async ensureUsersByIDs(userIDs) {
// Dedup // Dedup
userIDs = userIDs.filter((value, index, self) => self.indexOf(value) === index) userIDs = userIDs
.map(userID => parseInt(userID, 10)) .filter((value, index, self) => self.indexOf(value) === index)
.map((userID) => parseInt(userID, 10))
const missing = [ ] const missing = []
const promises = [ ] const promises = []
userIDs.forEach((function (userID) { userIDs.forEach(
if (this.isRecent(userID)) return function (userID) {
if (this.processing.has(userID)) { if (this.isRecent(userID)) return
promises.push(this.processing.get(userID)) if (this.processing.has(userID)) {
return promises.push(this.processing.get(userID))
} return
missing.push(userID) }
}).bind(this)) missing.push(userID)
}.bind(this)
)
if (missing.length > 0) { if (missing.length > 0) {
const payload = { actionName: 'getUsersByID' const payload = {
, parameters: { userIDs: missing } actionName: 'getUsersByID',
} parameters: { userIDs: missing },
const request = (async _ => { }
const request = (async (_) => {
try { try {
const response = await Ajax.api(this, payload) const response = await Ajax.api(this, payload)
return Object.entries(response.returnValues).forEach(([ userID, user ]) => { return Object.entries(response.returnValues).forEach(
userID = parseInt(userID, 10) ([userID, user]) => {
const data = { user: new User(user) userID = parseInt(userID, 10)
, date: Date.now() const data = { user: new User(user), date: Date.now() }
} this.users.set(userID, data)
this.users.set(userID, data) this.processing.delete(userID)
this.processing.delete(userID) }
}) )
} } catch (err) {
catch (err) { missing.forEach((userID) => this.processing.delete(userID))
missing.forEach(userID => this.processing.delete(userID))
throw err throw err
} }
})() })()
missing.forEach(userID => this.processing.set(userID, request)) missing.forEach((userID) => this.processing.set(userID, request))
promises.push(request) promises.push(request)
} }
@ -93,7 +97,7 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
async getUsersByIDs(userIDs) { async getUsersByIDs(userIDs) {
await this.ensureUsersByIDs(userIDs) await this.ensureUsersByIDs(userIDs)
return new Map(userIDs.map(userID => [ userID, this.get(userID) ])) return new Map(userIDs.map((userID) => [userID, this.get(userID)]))
} }
/** /**
@ -157,7 +161,7 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
const user = this.users.get(userID) const user = this.users.get(userID)
if (user != null) { if (user != null) {
return user.date > (Date.now() - (5 * 60e3)) return user.date > Date.now() - 5 * 60e3
} }
return false return false
@ -169,7 +173,7 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
* @returns {User[]} * @returns {User[]}
*/ */
values() { values() {
return Array.from(this.users.values()).map(item => item.user) return Array.from(this.users.values()).map((item) => item.user)
} }
pushLastActivity(userID) { pushLastActivity(userID) {
@ -178,18 +182,19 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
this.lastActivity.add(userID) this.lastActivity.add(userID)
} }
* getLastActivity() { *getLastActivity() {
yield * this.lastActivity yield* this.lastActivity
} }
_ajaxSetup() { _ajaxSetup() {
return { silent: true return {
, ignoreError: true silent: true,
, data: { className: 'chat\\data\\user\\UserAction' } ignoreError: true,
} data: { className: 'chat\\data\\user\\UserAction' },
}
} }
} }
ProfileStore.DEPENDENCIES = DEPENDENCIES ProfileStore.DEPENDENCIES = DEPENDENCIES
return ProfileStore return ProfileStore
}); })

View File

@ -11,20 +11,21 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'Bastelstu.be/PromiseWrap/Ajax' define([
, 'WoltLabSuite/Core/Core' 'Bastelstu.be/PromiseWrap/Ajax',
, './User' 'WoltLabSuite/Core/Core',
], function (Ajax, Core, User) { './User',
"use strict"; ], function (Ajax, Core, User) {
'use strict'
const DEPENDENCIES = [ 'sessionID', 'roomID' ] const DEPENDENCIES = ['sessionID', 'roomID']
/** /**
* Represents a chat room. * Represents a chat room.
*/ */
class Room { class Room {
constructor(sessionID, roomID) { constructor(sessionID, roomID) {
this.sessionID = sessionID this.sessionID = sessionID
this.roomID = roomID this.roomID = roomID
} }
/** /**
@ -33,12 +34,11 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
* @returns {Promise} * @returns {Promise}
*/ */
async join() { async join() {
const payload = { className: 'chat\\data\\room\\RoomAction' const payload = {
, actionName: 'join' className: 'chat\\data\\room\\RoomAction',
, parameters: { roomID: this.roomID actionName: 'join',
, sessionID: this.sessionID parameters: { roomID: this.roomID, sessionID: this.sessionID },
} }
}
return Ajax.api(this, payload) return Ajax.api(this, payload)
} }
@ -49,12 +49,11 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
* @param {boolean} unload Send a beacon if true'ish and a regular AJAX request otherwise. * @param {boolean} unload Send a beacon if true'ish and a regular AJAX request otherwise.
*/ */
leave(unload = false) { leave(unload = false) {
const payload = { className: 'chat\\data\\room\\RoomAction' const payload = {
, actionName: 'leave' className: 'chat\\data\\room\\RoomAction',
, parameters: { roomID: this.roomID actionName: 'leave',
, sessionID: this.sessionID parameters: { roomID: this.roomID, sessionID: this.sessionID },
} }
}
if (unload && FormData && (navigator.sendBeacon || window.fetch)) { if (unload && FormData && (navigator.sendBeacon || window.fetch)) {
// Ordinary AJAX requests are unreliable during unload: // Ordinary AJAX requests are unreliable during unload:
@ -65,22 +64,26 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
const formData = new FormData() const formData = new FormData()
Core.serialize(payload) Core.serialize(payload)
.split('&') .split('&')
.map((item) => item.split('=')) .map((item) => item.split('='))
.map((item) => item.map(decodeURIComponent)) .map((item) => item.map(decodeURIComponent))
.forEach((item) => formData.append(item[0], item[1])) .forEach((item) => formData.append(item[0], item[1]))
if (navigator.sendBeacon) { if (navigator.sendBeacon) {
navigator.sendBeacon(url, formData) navigator.sendBeacon(url, formData)
} }
if (window.fetch) { if (window.fetch) {
fetch(url, { method: 'POST', keepalive: true, redirect: 'follow', body: formData }) fetch(url, {
method: 'POST',
keepalive: true,
redirect: 'follow',
body: formData,
})
} }
return Promise.resolve() return Promise.resolve()
} } else {
else {
return Ajax.api(this, payload) return Ajax.api(this, payload)
} }
} }
@ -91,23 +94,22 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
* @returns {Promise} * @returns {Promise}
*/ */
async getUsers() { async getUsers() {
const payload = { className: 'chat\\data\\room\\RoomAction' const payload = {
, actionName: 'getUsers' className: 'chat\\data\\room\\RoomAction',
, objectIDs: [ this.roomID ] actionName: 'getUsers',
} objectIDs: [this.roomID],
}
const result = await Ajax.api(this, payload) const result = await Ajax.api(this, payload)
return Object.values(result.returnValues).map(user => new User(user)) return Object.values(result.returnValues).map((user) => new User(user))
} }
_ajaxSetup() { _ajaxSetup() {
return { silent: true return { silent: true, ignoreError: true }
, ignoreError: true
}
} }
} }
Room.DEPENDENCIES = DEPENDENCIES Room.DEPENDENCIES = DEPENDENCIES
return Room return Room
}); })

View File

@ -11,30 +11,30 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Template' ], function (_Template) { define(['WoltLabSuite/Core/Template'], function (_Template) {
"use strict"; 'use strict'
/** /**
* Template extends WoltLab Suite's Templates by passing in a list of * Template extends WoltLab Suite's Templates by passing in a list of
* re-usable sub-templates. * re-usable sub-templates.
*/ */
class Template extends _Template { class Template extends _Template {
constructor(string, templates = { }) { constructor(string, templates = {}) {
super(string) super(string)
this.templates = templates this.templates = templates
const oldFetch = this.fetch const oldFetch = this.fetch
this.fetch = (function (variables) { this.fetch = function (variables) {
variables = Object.assign({ }, variables) variables = Object.assign({}, variables)
const templates = Object.assign({ }, this.templates, variables.t || { }) const templates = Object.assign({}, this.templates, variables.t || {})
variables.t = templates variables.t = templates
return oldFetch(variables) return oldFetch(variables)
}).bind(this) }.bind(this)
} }
} }
return Template return Template
}); })

View File

@ -11,13 +11,12 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
class Ui { class Ui {
constructor() { constructor() {}
}
} }
return Ui return Ui
}); })

View File

@ -11,30 +11,42 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Language' define([
, 'WoltLabSuite/Core/Upload' 'WoltLabSuite/Core/Language',
, 'WoltLabSuite/Core/Dom/Change/Listener' 'WoltLabSuite/Core/Upload',
, 'WoltLabSuite/Core/Dom/Util' 'WoltLabSuite/Core/Dom/Change/Listener',
, 'WoltLabSuite/Core/Ui/Dialog' 'WoltLabSuite/Core/Dom/Util',
, '../../DataStructure/EventEmitter' 'WoltLabSuite/Core/Ui/Dialog',
], function(Language, Upload, DomChangeListener, DomUtil, Dialog, EventEmitter) { '../../DataStructure/EventEmitter',
"use strict"; ], function (
Language,
Upload,
DomChangeListener,
DomUtil,
Dialog,
EventEmitter
) {
'use strict'
const DIALOG_BUTTON_ID = 'chatAttachmentUploadButton' const DIALOG_BUTTON_ID = 'chatAttachmentUploadButton'
const DIALOG_CONTAINER_ID = 'chatAttachmentUploadDialog' const DIALOG_CONTAINER_ID = 'chatAttachmentUploadDialog'
const DEPENDENCIES = [ 'UiInput', 'Room' ]; const DEPENDENCIES = ['UiInput', 'Room']
class UiAttachmentUpload extends Upload { class UiAttachmentUpload extends Upload {
constructor(input, room) { constructor(input, room) {
const buttonContainer = document.querySelector(`#${DIALOG_CONTAINER_ID} > .upload`) const buttonContainer = document.querySelector(
`#${DIALOG_CONTAINER_ID} > .upload`
)
const buttonContainerId = DomUtil.identify(buttonContainer) const buttonContainerId = DomUtil.identify(buttonContainer)
const previewContainer = document.querySelector(`#${DIALOG_CONTAINER_ID} > .attachmentPreview`) const previewContainer = document.querySelector(
`#${DIALOG_CONTAINER_ID} > .attachmentPreview`
)
const previewContainerId = DomUtil.identify(previewContainer) const previewContainerId = DomUtil.identify(previewContainer)
super(buttonContainerId, previewContainerId, { super(buttonContainerId, previewContainerId, {
className: 'wcf\\data\\attachment\\AttachmentAction', className: 'wcf\\data\\attachment\\AttachmentAction',
acceptableFiles: [ '.png', '.gif', '.jpg', '.jpeg' ] acceptableFiles: ['.png', '.gif', '.jpg', '.jpeg'],
}) })
this.input = input this.input = input
@ -44,7 +56,9 @@ define([ 'WoltLabSuite/Core/Language'
} }
bootstrap() { bootstrap() {
this.uploadDescription = document.querySelector(`#${DIALOG_CONTAINER_ID} > small`) this.uploadDescription = document.querySelector(
`#${DIALOG_CONTAINER_ID} > small`
)
const button = document.getElementById(DIALOG_BUTTON_ID) const button = document.getElementById(DIALOG_BUTTON_ID)
const container = document.getElementById(DIALOG_CONTAINER_ID) const container = document.getElementById(DIALOG_CONTAINER_ID)
@ -59,18 +73,20 @@ define([ 'WoltLabSuite/Core/Language'
Dialog.openStatic(container.id, null, { Dialog.openStatic(container.id, null, {
title: elData(container, 'title'), title: elData(container, 'title'),
onShow: () => this.showDialog() onShow: () => this.showDialog(),
}) })
}) })
const deleteAction = new WCF.Action.Delete('wcf\\data\\attachment\\AttachmentAction', `#${this.previewContainer.id} > p`) const deleteAction = new WCF.Action.Delete(
'wcf\\data\\attachment\\AttachmentAction',
`#${this.previewContainer.id} > p`
)
deleteAction.setCallback(() => this.closeDialog()) deleteAction.setCallback(() => this.closeDialog())
this.input.on('input', (event) => { this.input.on('input', (event) => {
if (event.target.input.value.length == 0) { if (event.target.input.value.length == 0) {
button.classList.remove('disabled') button.classList.remove('disabled')
} } else {
else {
button.classList.add('disabled') button.classList.add('disabled')
} }
}) })
@ -95,16 +111,13 @@ define([ 'WoltLabSuite/Core/Language'
async send(tmpHash, event) { async send(tmpHash, event) {
event.preventDefault() event.preventDefault()
const parameters = { promise: Promise.resolve() const parameters = { promise: Promise.resolve(), tmpHash }
, tmpHash
}
this.emit('send', parameters) this.emit('send', parameters)
try { try {
await parameters.promise await parameters.promise
this.closeDialog() this.closeDialog()
} } catch (error) {
catch (error) {
// TODO: Error handling // TODO: Error handling
console.error(error) console.error(error)
} }
@ -141,14 +154,15 @@ define([ 'WoltLabSuite/Core/Language'
* @see WoltLabSuite/Core/Upload#_getParameters * @see WoltLabSuite/Core/Upload#_getParameters
*/ */
_getParameters() { _getParameters() {
this.tmpHash = [ ...crypto.getRandomValues(new Uint8Array(20)) ] this.tmpHash = [...crypto.getRandomValues(new Uint8Array(20))]
.map(m => ('0' + m.toString(16)).slice(-2)) .map((m) => ('0' + m.toString(16)).slice(-2))
.join('') .join('')
return { objectType: "be.bastelstu.chat.message" return {
, parentObjectID: this.room.roomID objectType: 'be.bastelstu.chat.message',
, tmpHash: this.tmpHash parentObjectID: this.room.roomID,
} tmpHash: this.tmpHash,
}
} }
/** /**
@ -158,22 +172,28 @@ define([ 'WoltLabSuite/Core/Language'
if (data.returnValues.errors && data.returnValues.errors[0]) { if (data.returnValues.errors && data.returnValues.errors[0]) {
const error = data.returnValues.errors[0] const error = data.returnValues.errors[0]
elInnerError(this._button, Language.get(`wcf.attachment.upload.error.${error.errorType}`, { elInnerError(
filename: error.filename this._button,
})) Language.get(`wcf.attachment.upload.error.${error.errorType}`, {
filename: error.filename,
})
)
return return
} } else {
else {
elInnerError(this._button, '') elInnerError(this._button, '')
} }
if (data.returnValues.attachments && data.returnValues.attachments[uploadId]) { if (
data.returnValues.attachments &&
data.returnValues.attachments[uploadId]
) {
this._removeButton() this._removeButton()
elHide(this.uploadDescription) elHide(this.uploadDescription)
const attachment = data.returnValues.attachments[uploadId] const attachment = data.returnValues.attachments[uploadId]
const url = attachment.thumbnailURL || attachment.tinyURL || attachment.url const url =
attachment.thumbnailURL || attachment.tinyURL || attachment.url
if (!url) { if (!url) {
throw new Error('Missing image URL') throw new Error('Missing image URL')
@ -188,8 +208,7 @@ define([ 'WoltLabSuite/Core/Language'
if (url === attachment.thumbnailURL) { if (url === attachment.thumbnailURL) {
img.classList.add('attachmentThumbnail') img.classList.add('attachmentThumbnail')
} } else if (url === attachment.tinyURL) {
else if (url === attachment.tinyURL) {
img.classList.add('attachmentTinyThumbnail') img.classList.add('attachmentTinyThumbnail')
} }
@ -199,9 +218,8 @@ define([ 'WoltLabSuite/Core/Language'
DomUtil.replaceElement(progress, img) DomUtil.replaceElement(progress, img)
this.createButtonGroup(uploadId, attachment.attachmentID, this.tmpHash) this.createButtonGroup(uploadId, attachment.attachmentID, this.tmpHash)
} } else {
else { console.error('Received neither an error nor an attachment response')
console.error("Received neither an error nor an attachment response")
console.error(data.returnValues) console.error(data.returnValues)
} }
} }

View File

@ -11,25 +11,46 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../console' define([
, '../CommandHandler' '../console',
, '../LocalStorage' '../CommandHandler',
, '../Messenger' '../LocalStorage',
, '../ProfileStore' '../Messenger',
, 'WoltLabSuite/Core/Language' '../ProfileStore',
, 'WoltLabSuite/Core/Timer/Repeating' 'WoltLabSuite/Core/Language',
], function (console, CommandHandler, LocalStorage, Messenger, ProfileStore, Language, RepeatingTimer) { 'WoltLabSuite/Core/Timer/Repeating',
"use strict"; ], function (
console,
CommandHandler,
LocalStorage,
Messenger,
ProfileStore,
Language,
RepeatingTimer
) {
'use strict'
const DEPENDENCIES = [ 'config', 'CommandHandler', 'Messenger', 'ProfileStore', 'UiInput' ] const DEPENDENCIES = [
'config',
'CommandHandler',
'Messenger',
'ProfileStore',
'UiInput',
]
class AutoAway { class AutoAway {
constructor(config, commandHandler, messenger, profileStore, input) { constructor(config, commandHandler, messenger, profileStore, input) {
if (!(commandHandler instanceof CommandHandler)) throw new TypeError('You must pass a CommandHandler to the AutoAway') if (!(commandHandler instanceof CommandHandler))
if (!(messenger instanceof Messenger)) throw new TypeError('You must pass a Messenger to the AutoAway') throw new TypeError('You must pass a CommandHandler to the AutoAway')
if (!(profileStore instanceof ProfileStore)) throw new TypeError('You must pass a ProfileStore to the AutoAway') if (!(messenger instanceof Messenger))
throw new TypeError('You must pass a Messenger to the AutoAway')
if (!(profileStore instanceof ProfileStore))
throw new TypeError('You must pass a ProfileStore to the AutoAway')
this.storage = new LocalStorage('AutoAway.') this.storage = new LocalStorage('AutoAway.')
this.awayCommand = commandHandler.getCommandByIdentifier('be.bastelstu.chat', 'away') this.awayCommand = commandHandler.getCommandByIdentifier(
'be.bastelstu.chat',
'away'
)
if (this.awayCommand == null) { if (this.awayCommand == null) {
throw new Error('Unreachable') throw new Error('Unreachable')
} }
@ -47,16 +68,22 @@ define([ '../console'
return return
} }
this.timer = new RepeatingTimer(this.setAway.bind(this), this.config.autoAwayTime * 60e3) this.timer = new RepeatingTimer(
this.input.on('input', this.inputListener = (event) => { this.setAway.bind(this),
this.storage.set('channel', Date.now()) this.config.autoAwayTime * 60e3
this.reset() )
}) this.input.on(
'input',
(this.inputListener = (event) => {
this.storage.set('channel', Date.now())
this.reset()
})
)
this.storage.observe('channel', this.reset.bind(this)) this.storage.observe('channel', this.reset.bind(this))
} }
ingest(messages) { ingest(messages) {
if (messages.some(message => message.isOwnMessage())) this.reset() if (messages.some((message) => message.isOwnMessage())) this.reset()
} }
reset() { reset() {
@ -70,8 +97,11 @@ define([ '../console'
async setAway() { async setAway() {
console.debug('AutoAway.setAway', `Attempting to set as away`) console.debug('AutoAway.setAway', `Attempting to set as away`)
if (this.storage.get('setAway') >= (Date.now() - 10e3)) { if (this.storage.get('setAway') >= Date.now() - 10e3) {
console.debug('AutoAway.setAway', `setAway called within the last 10 seconds in another Tab`) console.debug(
'AutoAway.setAway',
`setAway called within the last 10 seconds in another Tab`
)
return return
} }
this.storage.set('setAway', Date.now()) this.storage.set('setAway', Date.now())
@ -81,10 +111,13 @@ define([ '../console'
return return
} }
this.messenger.push({ commandID: this.awayCommand.id, parameters: { reason: Language.get('chat.user.autoAway') } }) this.messenger.push({
commandID: this.awayCommand.id,
parameters: { reason: Language.get('chat.user.autoAway') },
})
} }
} }
AutoAway.DEPENDENCIES = DEPENDENCIES AutoAway.DEPENDENCIES = DEPENDENCIES
return AutoAway return AutoAway
}); })

View File

@ -11,42 +11,58 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Ui' ], function (Ui) { define(['../Ui'], function (Ui) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'UiAttachmentUpload' const DEPENDENCIES = [
, 'UiAutoAway' 'UiAttachmentUpload',
, 'UiConnectionWarning' 'UiAutoAway',
, 'UiInput' 'UiConnectionWarning',
, 'UiInputAutocompleter' 'UiInput',
, 'UiMessageActionDelete' 'UiInputAutocompleter',
, 'UiMessageStream' 'UiMessageActionDelete',
, 'UiMobile' 'UiMessageStream',
, 'UiNotification' 'UiMobile',
, 'UiReadMarker' 'UiNotification',
, 'UiSettings' 'UiReadMarker',
, 'UiTopic' 'UiSettings',
, 'UiUserActionDropdownHandler' 'UiTopic',
, 'UiUserList' 'UiUserActionDropdownHandler',
] 'UiUserList',
]
class Chat extends Ui { class Chat extends Ui {
constructor(attachmentUpload, autoAway, connectionWarning, input, autocompleter, messageActionDelete, messageStream, mobile, notification, readMarker, settings, topic, userActionDropdownHandler, userList) { constructor(
attachmentUpload,
autoAway,
connectionWarning,
input,
autocompleter,
messageActionDelete,
messageStream,
mobile,
notification,
readMarker,
settings,
topic,
userActionDropdownHandler,
userList
) {
super() super()
this.actionDropdownHandler = userActionDropdownHandler this.actionDropdownHandler = userActionDropdownHandler
this.attachmentUpload = attachmentUpload this.attachmentUpload = attachmentUpload
this.autoAway = autoAway this.autoAway = autoAway
this.autocompleter = autocompleter this.autocompleter = autocompleter
this.connectionWarning = connectionWarning this.connectionWarning = connectionWarning
this.input = input this.input = input
this.messageActionDelete = messageActionDelete this.messageActionDelete = messageActionDelete
this.messageStream = messageStream this.messageStream = messageStream
this.mobile = mobile this.mobile = mobile
this.notification = notification this.notification = notification
this.readMarker = readMarker this.readMarker = readMarker
this.settings = settings this.settings = settings
this.topic = topic this.topic = topic
this.userList = userList this.userList = userList
} }
bootstrap() { bootstrap() {
@ -69,4 +85,4 @@ define([ '../Ui' ], function (Ui) {
Chat.DEPENDENCIES = DEPENDENCIES Chat.DEPENDENCIES = DEPENDENCIES
return Chat return Chat
}); })

View File

@ -11,25 +11,22 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../console' define(['../console'], function (console) {
], function (console) { 'use strict'
"use strict";
class ConnectionWarning { class ConnectionWarning {
constructor() { constructor() {
this.warning = elById('chatConnectionWarning') this.warning = elById('chatConnectionWarning')
} }
bootstrap() { bootstrap() {}
}
show() { show() {
elShow(this.warning) elShow(this.warning)
if (this.timeout) return if (this.timeout) return
console.debug('ConnectionWarning.show', 'Setting timeout') console.debug('ConnectionWarning.show', 'Setting timeout')
this.timeout = setTimeout(_ => { this.timeout = setTimeout((_) => {
console.debug('ConnectionWarning.show', 'Timeout has passed') console.debug('ConnectionWarning.show', 'Timeout has passed')
this.timeout = undefined this.timeout = undefined
@ -44,9 +41,11 @@ define([ '../console'
if (!this.timeout || force) { if (!this.timeout || force) {
elHide(this.warning) elHide(this.warning)
window.clearTimeout(this.timeout) window.clearTimeout(this.timeout)
} } else {
else { console.debug(
console.debug('ConnectionWarning.hide', 'Automatically hiding after timeout has passed') 'ConnectionWarning.hide',
'Automatically hiding after timeout has passed'
)
this.autoHide = true this.autoHide = true
} }
} }
@ -57,4 +56,4 @@ define([ '../console'
} }
return ConnectionWarning return ConnectionWarning
}); })

View File

@ -11,28 +11,31 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Language' define([
, 'WoltLabSuite/Core/Template' 'WoltLabSuite/Core/Language',
, 'WoltLabSuite/Core/Ui/Dialog' 'WoltLabSuite/Core/Template',
], function (Language, Template, UiDialog) { 'WoltLabSuite/Core/Ui/Dialog',
"use strict"; ], function (Language, Template, UiDialog) {
'use strict'
const html = [ '[type="x-text/template"]' const html = [
, '[data-application="be.bastelstu.chat"]' '[type="x-text/template"]',
, '[data-template-name="be-bastelstu-chat-errorDialog"]' '[data-application="be.bastelstu.chat"]',
].join('') '[data-template-name="be-bastelstu-chat-errorDialog"]',
].join('')
const wrapper = new Template(elBySel(html).textContent) const wrapper = new Template(elBySel(html).textContent)
class ErrorDialog { class ErrorDialog {
constructor(message) { constructor(message) {
const options = { title: Language.get('wcf.global.error.title') const options = {
, closable: false title: Language.get('wcf.global.error.title'),
} closable: false,
}
UiDialog.openStatic('chatError', wrapper.fetch({ message }), options) UiDialog.openStatic('chatError', wrapper.fetch({ message }), options)
} }
} }
return ErrorDialog return ErrorDialog
}); })

View File

@ -11,21 +11,22 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../console' define([
, '../Helper' '../console',
, 'WoltLabSuite/Core/Core' '../Helper',
, 'WoltLabSuite/Core/Event/Key' 'WoltLabSuite/Core/Core',
, '../DataStructure/EventEmitter' 'WoltLabSuite/Core/Event/Key',
, '../DataStructure/Throttle' '../DataStructure/EventEmitter',
], function (console, Helper, Core, EventKey, EventEmitter, Throttle) { '../DataStructure/Throttle',
"use strict"; ], function (console, Helper, Core, EventKey, EventEmitter, Throttle) {
'use strict'
class Input { class Input {
constructor() { constructor() {
this.inputContainer = elById('chatInputContainer') this.inputContainer = elById('chatInputContainer')
this.input = elBySel('textarea', this.inputContainer) this.input = elBySel('textarea', this.inputContainer)
this.charCounter = elBySel('.charCounter', this.inputContainer) this.charCounter = elBySel('.charCounter', this.inputContainer)
this.errorElement = elBySel('.innerError', this.inputContainer) this.errorElement = elBySel('.innerError', this.inputContainer)
} }
bootstrap() { bootstrap() {
@ -34,21 +35,29 @@ define([ '../console'
} }
this.input.addEventListener('keydown', this.handleInputKeyDown.bind(this)) this.input.addEventListener('keydown', this.handleInputKeyDown.bind(this))
this.input.addEventListener('input', Throttle(this.handleInput.bind(this))) this.input.addEventListener(
'input',
Throttle(this.handleInput.bind(this))
)
Helper.makeFlexible(this.input) Helper.makeFlexible(this.input)
this.handleInput() this.handleInput()
} }
handleInput(event) { handleInput(event) {
this.charCounter.textContent = `${this.input.value.length} / ${this.input.getAttribute('maxlength')}` this.charCounter.textContent = `${
this.input.value.length
} / ${this.input.getAttribute('maxlength')}`
this.emit('input') this.emit('input')
} }
handleInputKeyDown(event) { handleInputKeyDown(event) {
if (EventKey.Enter(event) && !event.shiftKey) { if (EventKey.Enter(event) && !event.shiftKey) {
if (event.isComposing) { if (event.isComposing) {
console.debug('Ui/Input.handleInputKeyDown', 'Ignored Enter key while composing characters.') console.debug(
'Ui/Input.handleInputKeyDown',
'Ignored Enter key while composing characters.'
)
return return
} }
@ -62,8 +71,7 @@ define([ '../console'
if (!parameters.cancel) { if (!parameters.cancel) {
this.emit('submit') this.emit('submit')
} }
} } else if (EventKey.Tab(event)) {
else if (EventKey.Tab(event)) {
// prevent leaving the input // prevent leaving the input
event.preventDefault() event.preventDefault()
@ -92,9 +100,7 @@ define([ '../console'
insertText(text, options) { insertText(text, options) {
this.focus() this.focus()
options = Object.assign({ append: true options = Object.assign({ append: true, prepend: false }, options)
, prepend: false
}, options)
if (!(options.append || options.prepend)) { if (!(options.append || options.prepend)) {
// replace // replace
@ -102,11 +108,11 @@ define([ '../console'
} }
if (options.append) { if (options.append) {
this.input.value += text; this.input.value += text
} }
if (options.prepend) { if (options.prepend) {
this.input.value = text + this.input.value; this.input.value = text + this.input.value
} }
// always position caret at the end // always position caret at the end
@ -119,8 +125,7 @@ define([ '../console'
inputError(message) { inputError(message) {
if (typeof window.elInnerError === 'function') { if (typeof window.elInnerError === 'function') {
elInnerError(this.inputContainer.firstElementChild, message) elInnerError(this.inputContainer.firstElementChild, message)
} } else {
else {
this.inputContainer.classList.add('formError') this.inputContainer.classList.add('formError')
this.errorElement.textContent = message this.errorElement.textContent = message
elShow(this.errorElement) elShow(this.errorElement)
@ -130,8 +135,7 @@ define([ '../console'
hideInputError() { hideInputError() {
if (typeof window.elInnerError === 'function') { if (typeof window.elInnerError === 'function') {
elInnerError(this.inputContainer.firstElementChild, false) elInnerError(this.inputContainer.firstElementChild, false)
} } else {
else {
this.inputContainer.classList.remove('formError') this.inputContainer.classList.remove('formError')
this.errorElement.textContent = '' this.errorElement.textContent = ''
elHide(this.errorElement) elHide(this.errorElement)
@ -141,4 +145,4 @@ define([ '../console'
EventEmitter(Input.prototype) EventEmitter(Input.prototype)
return Input return Input
}); })

View File

@ -11,17 +11,21 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Dom/Util' define([
, 'WoltLabSuite/Core/Event/Key' 'WoltLabSuite/Core/Dom/Util',
, 'WoltLabSuite/Core/Ui/Suggestion' 'WoltLabSuite/Core/Event/Key',
], function (DomUtil, EventKey, Suggestion) { 'WoltLabSuite/Core/Ui/Suggestion',
"use strict"; ], function (DomUtil, EventKey, Suggestion) {
'use strict'
const DEPENDENCIES = [ 'UiInput' ] const DEPENDENCIES = ['UiInput']
class Autocompleter extends Suggestion { class Autocompleter extends Suggestion {
constructor(input) { constructor(input) {
const elementId = DomUtil.identify(input.input) const elementId = DomUtil.identify(input.input)
const options = { callbackSelect: (_elementId, selection) => this.insertSelection(selection) } const options = {
callbackSelect: (_elementId, selection) =>
this.insertSelection(selection),
}
super(elementId, options) super(elementId, options)
@ -76,11 +80,9 @@ define([ 'WoltLabSuite/Core/Dom/Util'
sendCompletions(completions) { sendCompletions(completions) {
this.completions = new Map() this.completions = new Map()
const returnValues = completions.map(completion => { const returnValues = completions.map((completion) => {
this.completions.set(++this.completionId, completion) this.completions.set(++this.completionId, completion)
return { label: completion return { label: completion, objectID: this.completionId }
, objectID: this.completionId
}
}) })
this._ajaxSuccess({ returnValues }) this._ajaxSuccess({ returnValues })
@ -89,4 +91,4 @@ define([ 'WoltLabSuite/Core/Dom/Util'
Autocompleter.DEPENDENCIES = DEPENDENCIES Autocompleter.DEPENDENCIES = DEPENDENCIES
return Autocompleter return Autocompleter
}); })

View File

@ -11,10 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Ui' ], function (Ui) { define(['../Ui'], function (Ui) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'UiMessageStream', 'UiMessageActionDelete' ] const DEPENDENCIES = ['UiMessageStream', 'UiMessageActionDelete']
class Log extends Ui { class Log extends Ui {
constructor(messageStream, messageActionDelete) { constructor(messageStream, messageActionDelete) {
super() super()
@ -32,4 +32,4 @@ define([ '../Ui' ], function (Ui) {
Log.DEPENDENCIES = DEPENDENCIES Log.DEPENDENCIES = DEPENDENCIES
return Log return Log
}); })

View File

@ -11,10 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'Bastelstu.be/Chat/Message', 'Bastelstu.be/PromiseWrap/Ajax', 'Bastelstu.be/PromiseWrap/Ui/Confirmation' ], function (Message, Ajax, Confirmation) { define([
"use strict"; 'Bastelstu.be/Chat/Message',
'Bastelstu.be/PromiseWrap/Ajax',
'Bastelstu.be/PromiseWrap/Ui/Confirmation',
], function (Message, Ajax, Confirmation) {
'use strict'
const DEPENDENCIES = [ 'UiMessageStream', 'Message' ] const DEPENDENCIES = ['UiMessageStream', 'Message']
class Delete { class Delete {
constructor(messageStream, message) { constructor(messageStream, message) {
this.messageStream = messageStream this.messageStream = messageStream
@ -26,19 +30,19 @@ define([ 'Bastelstu.be/Chat/Message', 'Bastelstu.be/PromiseWrap/Ajax', 'Bastelst
} }
bindListener({ detail }) { bindListener({ detail }) {
detail.forEach(item => { detail.forEach((item) => {
if (!item) return if (!item) return
const { node, message } = item const { node, message } = item
const button = node.querySelector('.jsDeleteButton') const button = node.querySelector('.jsDeleteButton')
if (!button) return if (!button) return
button.addEventListener('click', async event => { button.addEventListener('click', async (event) => {
event.preventDefault() event.preventDefault()
await Confirmation.show({ await Confirmation.show({
message: button.dataset.confirmMessageHtml, message: button.dataset.confirmMessageHtml,
messageIsHtml: true messageIsHtml: true,
}) })
await this.delete(message.messageID) await this.delete(message.messageID)
@ -50,31 +54,31 @@ define([ 'Bastelstu.be/Chat/Message', 'Bastelstu.be/PromiseWrap/Ajax', 'Bastelst
async delete(messageID) { async delete(messageID) {
{ {
const payload = { objectIDs: [ messageID ] } const payload = { objectIDs: [messageID] }
await Ajax.api(this, payload) await Ajax.api(this, payload)
} }
{ {
const objectType = 'be.bastelstu.chat.messageType.tombstone' const objectType = 'be.bastelstu.chat.messageType.tombstone'
const payload = { messageID const payload = { messageID, userID: null }
, userID: null
}
const message = this.Message.instance({ objectType, payload }) const message = this.Message.instance({ objectType, payload })
message.getMessageType().render(message) message.getMessageType().render(message)
} }
} }
_ajaxSetup() { _ajaxSetup() {
return { silent: true return {
, ignoreError: true silent: true,
, data: { className: 'chat\\data\\message\\MessageAction' ignoreError: true,
, actionName: 'trash' data: {
} className: 'chat\\data\\message\\MessageAction',
} actionName: 'trash',
},
}
} }
} }
Delete.DEPENDENCIES = DEPENDENCIES Delete.DEPENDENCIES = DEPENDENCIES
return Delete return Delete
}); })

View File

@ -11,29 +11,39 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../Helper' define([
, 'WoltLabSuite/Core/Date/Util' '../Helper',
, 'WoltLabSuite/Core/Dom/Change/Listener' 'WoltLabSuite/Core/Date/Util',
, 'WoltLabSuite/Core/Language' 'WoltLabSuite/Core/Dom/Change/Listener',
, 'WoltLabSuite/Core/User' 'WoltLabSuite/Core/Language',
, 'WoltLabSuite/Core/Dom/Traverse' 'WoltLabSuite/Core/User',
, '../DataStructure/EventEmitter' 'WoltLabSuite/Core/Dom/Traverse',
, '../DataStructure/RedBlackTree/Tree' '../DataStructure/EventEmitter',
], function (Helper, DateUtil, DomChangeListener, Language, User, DOMTraverse, EventEmitter, Tree) { '../DataStructure/RedBlackTree/Tree',
"use strict"; ], function (
Helper,
DateUtil,
DomChangeListener,
Language,
User,
DOMTraverse,
EventEmitter,
Tree
) {
'use strict'
const enableAutoscroll = Symbol('enableAutoscroll') const enableAutoscroll = Symbol('enableAutoscroll')
const DEPENDENCIES = [ ] const DEPENDENCIES = []
class MessageStream { class MessageStream {
constructor() { constructor() {
this.stream = elById('chatMessageStream') this.stream = elById('chatMessageStream')
this.scrollContainer = elBySel('.scrollContainer', this.stream) this.scrollContainer = elBySel('.scrollContainer', this.stream)
this[enableAutoscroll] = true this[enableAutoscroll] = true
this.lastScrollPosition = undefined this.lastScrollPosition = undefined
this.nodeMap = new WeakMap() this.nodeMap = new WeakMap()
this.positions = new Tree() this.positions = new Tree()
} }
get enableAutoscroll() { get enableAutoscroll() {
@ -50,7 +60,11 @@ define([ '../Helper'
bootstrap() { bootstrap() {
this.scrollContainer.addEventListener('copy', this.onCopy.bind(this)) this.scrollContainer.addEventListener('copy', this.onCopy.bind(this))
this.scrollContainer.addEventListener('scroll', Helper.throttle(this.onScroll, 100, this), { passive: true }) this.scrollContainer.addEventListener(
'scroll',
Helper.throttle(this.onScroll, 100, this),
{ passive: true }
)
} }
getDateMarker(date) { getDateMarker(date) {
@ -69,123 +83,131 @@ define([ '../Helper'
} }
ingest(messages) { ingest(messages) {
let scrollTopBefore = this.enableAutoscroll ? 0 : this.scrollContainer.scrollTop let scrollTopBefore = this.enableAutoscroll
? 0
: this.scrollContainer.scrollTop
let prependedHeight = 0 let prependedHeight = 0
const ul = elBySel('ul', this.scrollContainer) const ul = elBySel('ul', this.scrollContainer)
const first = ul.firstElementChild const first = ul.firstElementChild
const ingested = messages.map((function (item) { const ingested = messages.map(
let currentScrollHeight = 0 function (item) {
let currentScrollHeight = 0
const li = elCreate('li') const li = elCreate('li')
// Allow messages types to not render a messages // Allow messages types to not render a messages
// This can be used for status messages like ChatUpdate // This can be used for status messages like ChatUpdate
let fragment let fragment
if ((fragment = item.getMessageType().render(item)) === false) return if ((fragment = item.getMessageType().render(item)) === false) return
if (fragment.querySelector(`.userMention[data-user-id="${User.userId}"]`)) li.classList.add('mentioned') if (
fragment.querySelector(
`.userMention[data-user-id="${User.userId}"]`
)
)
li.classList.add('mentioned')
li.appendChild(fragment) li.appendChild(fragment)
li.classList.add('chatMessageBoundary') li.classList.add('chatMessageBoundary')
li.setAttribute('id', `message-${item.messageID}`) li.setAttribute('id', `message-${item.messageID}`)
li.dataset.objectType = item.objectType li.dataset.objectType = item.objectType
li.dataset.userId = item.userID li.dataset.userId = item.userID
if (item.isOwnMessage()) li.classList.add('own') if (item.isOwnMessage()) li.classList.add('own')
if (item.isDeleted) li.classList.add('tombstone') if (item.isDeleted) li.classList.add('tombstone')
const position = this.positions.insert(item.messageID) const position = this.positions.insert(item.messageID)
if (position[1] !== undefined) { if (position[1] !== undefined) {
const sibling = elById(`message-${position[1]}`) const sibling = elById(`message-${position[1]}`)
if (!sibling) throw new Error('Unreachable') if (!sibling) throw new Error('Unreachable')
let nodeBefore, nodeAfter let nodeBefore, nodeAfter
let dateMarkerBetween = false let dateMarkerBetween = false
if (position[0] === 'LEFT') { if (position[0] === 'LEFT') {
nodeAfter = sibling nodeAfter = sibling
nodeBefore = sibling.previousElementSibling
if (nodeBefore && nodeBefore.classList.contains('dateMarker')) {
elRemove(nodeBefore)
nodeBefore = sibling.previousElementSibling nodeBefore = sibling.previousElementSibling
}
}
else if (position[0] === 'RIGHT') {
nodeBefore = sibling
nodeAfter = sibling.nextElementSibling
if (nodeAfter && nodeAfter.classList.contains('dateMarker')) { if (nodeBefore && nodeBefore.classList.contains('dateMarker')) {
elRemove(nodeAfter) elRemove(nodeBefore)
nodeBefore = sibling.previousElementSibling
}
} else if (position[0] === 'RIGHT') {
nodeBefore = sibling
nodeAfter = sibling.nextElementSibling nodeAfter = sibling.nextElementSibling
if (nodeAfter && nodeAfter.classList.contains('dateMarker')) {
elRemove(nodeAfter)
nodeAfter = sibling.nextElementSibling
}
} else {
throw new Error('Unreachable')
} }
}
else {
throw new Error('Unreachable')
}
const messageBefore = this.nodeMap.get(nodeBefore) const messageBefore = this.nodeMap.get(nodeBefore)
if (nodeBefore && !messageBefore) throw new Error('Unreachable') if (nodeBefore && !messageBefore) throw new Error('Unreachable')
const messageAfter = this.nodeMap.get(nodeAfter) const messageAfter = this.nodeMap.get(nodeAfter)
if (nodeAfter && !messageAfter) throw new Error('Unreachable') if (nodeAfter && !messageAfter) throw new Error('Unreachable')
if (!this.enableAutoscroll && nodeAfter) currentScrollHeight = this.scrollContainer.scrollHeight if (!this.enableAutoscroll && nodeAfter)
currentScrollHeight = this.scrollContainer.scrollHeight
let context = nodeAfter let context = nodeAfter
if (nodeAfter) nodeAfter.classList.remove('first') if (nodeAfter) nodeAfter.classList.remove('first')
if (messageBefore) { if (messageBefore) {
if (this.onDifferentDays(messageBefore.date, item.date)) { if (this.onDifferentDays(messageBefore.date, item.date)) {
const dateMarker = this.getDateMarker(item.date) const dateMarker = this.getDateMarker(item.date)
ul.insertBefore(dateMarker, nodeAfter) ul.insertBefore(dateMarker, nodeAfter)
li.classList.add('first')
} else {
if (
messageBefore.objectType !== item.objectType ||
!item.getMessageType().joinable(messageBefore, item)
) {
li.classList.add('first')
}
}
} else {
li.classList.add('first') li.classList.add('first')
} }
else { if (messageAfter) {
if (messageBefore.objectType !== item.objectType || !item.getMessageType().joinable(messageBefore, item)) { if (this.onDifferentDays(messageAfter.date, item.date)) {
li.classList.add('first') const dateMarker = this.getDateMarker(messageAfter.date)
} ul.insertBefore(dateMarker, nodeAfter)
} context = dateMarker
}
else {
li.classList.add('first')
}
if (messageAfter) {
if (this.onDifferentDays(messageAfter.date, item.date)) {
const dateMarker = this.getDateMarker(messageAfter.date)
ul.insertBefore(dateMarker, nodeAfter)
context = dateMarker
nodeAfter.classList.add('first')
}
else {
if (messageAfter.objectType !== item.objectType || !item.getMessageType().joinable(item, messageAfter)) {
nodeAfter.classList.add('first') nodeAfter.classList.add('first')
} else {
if (
messageAfter.objectType !== item.objectType ||
!item.getMessageType().joinable(item, messageAfter)
) {
nodeAfter.classList.add('first')
}
} }
} }
ul.insertBefore(li, context)
if (!this.enableAutoscroll && nodeAfter) {
prependedHeight +=
this.scrollContainer.scrollHeight - currentScrollHeight
}
} else {
li.classList.add('first')
ul.insertBefore(li, null)
} }
ul.insertBefore(li, context); this.nodeMap.set(li, item)
if (!this.enableAutoscroll && nodeAfter) { return { node: li, message: item }
prependedHeight += this.scrollContainer.scrollHeight - currentScrollHeight }.bind(this)
} )
}
else {
li.classList.add('first')
ul.insertBefore(li, null)
}
this.nodeMap.set(li, item) if (ingested.some((item) => item != null)) {
return { node: li
, message: item
}
}).bind(this));
if (ingested.some(item => item != null)) {
if (this.enableAutoscroll) { if (this.enableAutoscroll) {
this.scrollToBottom() this.scrollToBottom()
} } else {
else {
this.stream.classList.add('activity') this.stream.classList.add('activity')
this.scrollContainer.scrollTop = scrollTopBefore + prependedHeight this.scrollContainer.scrollTop = scrollTopBefore + prependedHeight
} }
@ -208,8 +230,11 @@ define([ '../Helper'
let direction = 'down' let direction = 'down'
if (this.lastScrollPosition != null && scrollTop < this.lastScrollPosition) { if (
direction = 'up' this.lastScrollPosition != null &&
scrollTop < this.lastScrollPosition
) {
direction = 'up'
} }
if (direction === 'up') { if (direction === 'up') {
@ -219,12 +244,10 @@ define([ '../Helper'
if (distanceFromTop <= 7) { if (distanceFromTop <= 7) {
this.emit('reachedTop') this.emit('reachedTop')
} } else if (distanceFromTop <= 300) {
else if (distanceFromTop <= 300) {
this.emit('nearTop') this.emit('nearTop')
} }
} } else if (direction === 'down') {
else if (direction === 'down') {
if (distanceFromTop > 7) { if (distanceFromTop > 7) {
this.emit('scrollDown') this.emit('scrollDown')
} }
@ -232,8 +255,7 @@ define([ '../Helper'
if (distanceFromBottom <= 7) { if (distanceFromBottom <= 7) {
this.scrollToBottom() this.scrollToBottom()
this.emit('reachedBottom') this.emit('reachedBottom')
} } else if (distanceFromBottom <= 300) {
else if (distanceFromBottom <= 300) {
this.emit('nearBottom') this.emit('nearBottom')
} }
} }
@ -250,16 +272,19 @@ define([ '../Helper'
// Get the first and last node in the selection // Get the first and last node in the selection
let originalStart, start, end, originalEnd let originalStart, start, end, originalEnd
start = originalStart = selection.getRangeAt(0).startContainer start = originalStart = selection.getRangeAt(0).startContainer
end = originalEnd = selection.getRangeAt(selection.rangeCount - 1).endContainer end = originalEnd = selection.getRangeAt(selection.rangeCount - 1)
.endContainer
const startOffset = selection.getRangeAt(0).startOffset const startOffset = selection.getRangeAt(0).startOffset
const endOffset = selection.getRangeAt(selection.rangeCount - 1).endOffset const endOffset = selection.getRangeAt(selection.rangeCount - 1).endOffset
// The Traverse module needs nodes of the Element type, the selected elements could be of type Text // The Traverse module needs nodes of the Element type, the selected elements could be of type Text
while (!(start instanceof Element) && start.parentNode) start = start.parentNode while (!(start instanceof Element) && start.parentNode)
start = start.parentNode
while (!(end instanceof Element) && end.parentNode) end = end.parentNode while (!(end instanceof Element) && end.parentNode) end = end.parentNode
if (!start || !end) throw new Error('Unexpected error, no element nodes in selection') if (!start || !end)
throw new Error('Unexpected error, no element nodes in selection')
// Try to find the starting li element in the selection // Try to find the starting li element in the selection
if (!start.id || start.id.indexOf('message-') !== 0) { if (!start.id || start.id.indexOf('message-') !== 0) {
@ -272,7 +297,10 @@ define([ '../Helper'
} }
// Do not select a message if we selected only a new line // Do not select a message if we selected only a new line
if (originalStart instanceof Text && originalStart.textContent.substring(startOffset) === "") { if (
originalStart instanceof Text &&
originalStart.textContent.substring(startOffset) === ''
) {
start = DOMTraverse.next(start) start = DOMTraverse.next(start)
} }
@ -289,15 +317,14 @@ define([ '../Helper'
end = DOMTraverse.prev(end) end = DOMTraverse.prev(end)
} }
const elements = [ ] const elements = []
let next = start let next = start
do { do {
elements.push(next) elements.push(next)
if (next === end) break if (next === end) break
} } while ((next = DOMTraverse.next(next)))
while (next = DOMTraverse.next(next))
// Only apply our custom formatting when selecting multiple or whole messages // Only apply our custom formatting when selecting multiple or whole messages
if (elements.length === 1) { if (elements.length === 1) {
@ -305,37 +332,59 @@ define([ '../Helper'
range.setStart(originalStart, startOffset) range.setStart(originalStart, startOffset)
range.setEnd(originalEnd, endOffset) range.setEnd(originalEnd, endOffset)
if (!Helper.rangeSpansTextContent(range, start.querySelector('.chatMessage'))) return if (
!Helper.rangeSpansTextContent(
range,
start.querySelector('.chatMessage')
)
)
return
} }
try { try {
event.clipboardData.setData('text/plain', elements.map((el, index, arr) => { event.clipboardData.setData(
const message = this.nodeMap.get(el) 'text/plain',
elements
.map((el, index, arr) => {
const message = this.nodeMap.get(el)
if (el.classList.contains('dateMarker')) return `== ${el.textContent.trim()} ==` if (el.classList.contains('dateMarker'))
return `== ${el.textContent.trim()} ==`
if (!message) return if (!message) return
if (el.classList.contains('tombstone')) { if (el.classList.contains('tombstone')) {
return `[${message.formattedTime}] ${Language.get('chat.messageType.be.bastelstu.chat.messageType.tombstone.message')}` return `[${message.formattedTime}] ${Language.get(
} 'chat.messageType.be.bastelstu.chat.messageType.tombstone.message'
)}`
}
const elem = elBySel('.chatMessage', el) const elem = elBySel('.chatMessage', el)
let body let body
if (typeof (body = message.getMessageType().renderPlainText(message)) === 'undefined' || body === false) { if (
body = Helper.getTextContent(elem).replace(/\t+/g, '\t') // collapse multiple tabs typeof (body = message
.replace(/ +/g, ' ') // collapse multiple spaces .getMessageType()
.replace(/([\t ]*\n){2,}/g, '\n') // collapse line consisting of tabs, spaces and newlines .renderPlainText(message)) === 'undefined' ||
.replace(/^[\t ]+|[\t ]+$/gm, '') // remove leading and trailing whitespace per line body === false
} ) {
body = Helper.getTextContent(elem)
.replace(/\t+/g, '\t') // collapse multiple tabs
.replace(/ +/g, ' ') // collapse multiple spaces
.replace(/([\t ]*\n){2,}/g, '\n') // collapse line consisting of tabs, spaces and newlines
.replace(/^[\t ]+|[\t ]+$/gm, '') // remove leading and trailing whitespace per line
}
return `[${message.formattedTime}] <${message.username}> ${body.trim()}` return `[${message.formattedTime}] <${
}).filter(x => x).join('\n')) message.username
}> ${body.trim()}`
})
.filter((x) => x)
.join('\n')
)
event.preventDefault() event.preventDefault()
} } catch (e) {
catch (e) {
console.error('Unable to use the clipboard API') console.error('Unable to use the clipboard API')
console.error(e) console.error(e)
} }
@ -345,4 +394,4 @@ define([ '../Helper'
MessageStream.DEPENDENCIES = DEPENDENCIES MessageStream.DEPENDENCIES = DEPENDENCIES
return MessageStream return MessageStream
}); })

View File

@ -11,8 +11,8 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Ui/Screen' ], function (UiScreen) { define(['WoltLabSuite/Core/Ui/Screen'], function (UiScreen) {
"use strict"; 'use strict'
const initialized = Symbol('initialized') const initialized = Symbol('initialized')
@ -22,10 +22,11 @@ define([ 'WoltLabSuite/Core/Ui/Screen' ], function (UiScreen) {
} }
bootstrap() { bootstrap() {
UiScreen.on('screen-md-down', { match: this.enable.bind(this) UiScreen.on('screen-md-down', {
, unmatch: this.disable.bind(this) match: this.enable.bind(this),
, setup: this.init.bind(this) unmatch: this.disable.bind(this),
}) setup: this.init.bind(this),
})
} }
init() { init() {
@ -36,19 +37,15 @@ define([ 'WoltLabSuite/Core/Ui/Screen' ], function (UiScreen) {
this.initQuickSettings() this.initQuickSettings()
} }
enable() { enable() {}
} disable() {}
disable() {
}
initQuickSettings() { initQuickSettings() {
const navigation = elBySel('#chatQuickSettingsNavigation > ul') const navigation = elBySel('#chatQuickSettingsNavigation > ul')
const quickSettings = elById('chatQuickSettings') const quickSettings = elById('chatQuickSettings')
navigation.addEventListener(WCF_CLICK_EVENT, event => { navigation.addEventListener(WCF_CLICK_EVENT, (event) => {
event.stopPropagation() event.stopPropagation()
// mimic dropdown behavior // mimic dropdown behavior
@ -57,7 +54,7 @@ define([ 'WoltLabSuite/Core/Ui/Screen' ], function (UiScreen) {
}, 10) }, 10)
}) })
quickSettings.addEventListener(WCF_CLICK_EVENT, event => { quickSettings.addEventListener(WCF_CLICK_EVENT, (event) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@ -67,4 +64,4 @@ define([ 'WoltLabSuite/Core/Ui/Screen' ], function (UiScreen) {
} }
return Mobile return Mobile
}); })

View File

@ -11,10 +11,10 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Language' ], function (Language) { define(['WoltLabSuite/Core/Language'], function (Language) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'ProfileStore' ] const DEPENDENCIES = ['ProfileStore']
class Notification { class Notification {
constructor(profileStore) { constructor(profileStore) {
this.profileStore = profileStore this.profileStore = profileStore
@ -27,11 +27,14 @@ define([ 'WoltLabSuite/Core/Language' ], function (Language) {
} }
bootstrap() { bootstrap() {
document.addEventListener('visibilitychange', this.onVisibilitychange.bind(this)) document.addEventListener(
'visibilitychange',
this.onVisibilitychange.bind(this)
)
} }
get systemSupported() { get systemSupported() {
return "Notification" in window return 'Notification' in window
} }
get systemDenied() { get systemDenied() {
@ -40,7 +43,10 @@ define([ 'WoltLabSuite/Core/Language' ], function (Language) {
get systemGranted() { get systemGranted() {
if (this.systemDenied) { if (this.systemDenied) {
console.warn('[Notification]', 'System Notifications: permission denied') console.warn(
'[Notification]',
'System Notifications: permission denied'
)
} }
return window.Notification.permission === 'granted' return window.Notification.permission === 'granted'
@ -57,7 +63,7 @@ define([ 'WoltLabSuite/Core/Language' ], function (Language) {
ingest(messages) { ingest(messages) {
if (!this.active) { if (!this.active) {
messages.forEach(message => { messages.forEach((message) => {
const body = message.getMessageType().renderPlainText(message) const body = message.getMessageType().renderPlainText(message)
if (body === false) return if (body === false) return
@ -70,10 +76,7 @@ define([ 'WoltLabSuite/Core/Language' ], function (Language) {
// The user information is guaranteed to be cached at this point // The user information is guaranteed to be cached at this point
const user = this.profileStore.get(message.userID) const user = this.profileStore.get(message.userID)
const title = Language.get('chat.notification.title', { message }) const title = Language.get('chat.notification.title', { message })
const options = { body const options = { body, icon: user.imageUrl, badge: user.imageUrl }
, icon: user.imageUrl
, badge: user.imageUrl
}
const notification = new window.Notification(title, options) const notification = new window.Notification(title, options)
@ -88,14 +91,14 @@ define([ 'WoltLabSuite/Core/Language' ], function (Language) {
updateBrowserTitle() { updateBrowserTitle() {
if (this.unread > 0) { if (this.unread > 0) {
document.title = `(${this.unread}) ${this.browserTitle}` document.title = `(${this.unread}) ${this.browserTitle}`
} } else {
else {
document.title = this.browserTitle document.title = this.browserTitle
} }
} }
enableSystemNotifications() { enableSystemNotifications() {
if (!this.systemSupported) return Promise.reject(new Error('Notifications are not supported')) if (!this.systemSupported)
return Promise.reject(new Error('Notifications are not supported'))
if (this.systemGranted) { if (this.systemGranted) {
this.systemEnabled = true this.systemEnabled = true
@ -104,13 +107,12 @@ define([ 'WoltLabSuite/Core/Language' ], function (Language) {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
window.Notification.requestPermission(permission => { window.Notification.requestPermission((permission) => {
this.systemEnabled = permission === 'granted' this.systemEnabled = permission === 'granted'
if (this.systemEnabled) { if (this.systemEnabled) {
resolve() resolve()
} } else {
else {
reject(new Error(permission)) reject(new Error(permission))
} }
}) })
@ -124,4 +126,4 @@ define([ 'WoltLabSuite/Core/Language' ], function (Language) {
Notification.DEPENDENCIES = DEPENDENCIES Notification.DEPENDENCIES = DEPENDENCIES
return Notification return Notification
}); })

View File

@ -11,28 +11,34 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'UiMessageStream' ] const DEPENDENCIES = ['UiMessageStream']
class ReadMarker { class ReadMarker {
constructor(messageStream) { constructor(messageStream) {
this.messageStream = messageStream this.messageStream = messageStream
} }
bootstrap() { bootstrap() {
document.addEventListener('visibilitychange', this.onVisibilitychange.bind(this)) document.addEventListener(
'visibilitychange',
this.onVisibilitychange.bind(this)
)
} }
onVisibilitychange() { onVisibilitychange() {
if (document.hidden) { if (document.hidden) {
const ul = elBySel('ul', this.messageStream.stream) const ul = elBySel('ul', this.messageStream.stream)
let lc = ul.lastElementChild let lc = ul.lastElementChild
// delete previous markers // delete previous markers
Array.prototype.forEach.call(document.querySelectorAll('.readMarker'), marker => { Array.prototype.forEach.call(
marker.classList.remove('readMarker') document.querySelectorAll('.readMarker'),
}) (marker) => {
marker.classList.remove('readMarker')
}
)
if (lc) { if (lc) {
lc.classList.add('readMarker') lc.classList.add('readMarker')
@ -43,4 +49,4 @@ define([ ], function () {
ReadMarker.DEPENDENCIES = DEPENDENCIES ReadMarker.DEPENDENCIES = DEPENDENCIES
return ReadMarker return ReadMarker
}); })

View File

@ -11,23 +11,27 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
'use strict'; 'use strict'
const DEPENDENCIES = [ 'UiSettingsButton' ] const DEPENDENCIES = ['UiSettingsButton']
class Settings { class Settings {
constructor(modules) { constructor(modules) {
this.modules = modules this.modules = modules
this.buttons = Array.from(elBySelAll('#chatQuickSettingsNavigation .button[data-module]')) this.buttons = Array.from(
elBySelAll('#chatQuickSettingsNavigation .button[data-module]')
)
} }
bootstrap() { bootstrap() {
this.buttons.forEach(element => { this.buttons.forEach((element) => {
this.modules[element.dataset.module.replace(/\./g, '-')].instance(element).bootstrap() this.modules[element.dataset.module.replace(/\./g, '-')]
.instance(element)
.bootstrap()
}) })
} }
} }
Settings.DEPENDENCIES = DEPENDENCIES Settings.DEPENDENCIES = DEPENDENCIES
return Settings return Settings
}); })

View File

@ -11,10 +11,12 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './ToggleButton' ], function (ToggleButton) { define(['./ToggleButton'], function (ToggleButton) {
'use strict'; 'use strict'
const DEPENDENCIES = [ 'UiMessageStream' ].concat(ToggleButton.DEPENDENCIES || [ ]) const DEPENDENCIES = ['UiMessageStream'].concat(
ToggleButton.DEPENDENCIES || []
)
class AutoscrollButton extends ToggleButton { class AutoscrollButton extends ToggleButton {
constructor(element, messageStream, ...superDeps) { constructor(element, messageStream, ...superDeps) {
super(element, true, undefined, ...superDeps) super(element, true, undefined, ...superDeps)
@ -40,4 +42,4 @@ define([ './ToggleButton' ], function (ToggleButton) {
AutoscrollButton.DEPENDENCIES = DEPENDENCIES AutoscrollButton.DEPENDENCIES = DEPENDENCIES
return AutoscrollButton return AutoscrollButton
}); })

View File

@ -11,13 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
'use strict'; 'use strict'
const DEPENDENCIES = [ ] const DEPENDENCIES = []
class Button { class Button {
constructor(element) { constructor(element) {
if (!element || !element instanceof Element) throw new Error('No DOM element provided') if (!element || !element instanceof Element)
throw new Error('No DOM element provided')
this.element = element this.element = element
} }
@ -33,4 +34,4 @@ define([ ], function () {
Button.DEPENDENCIES = DEPENDENCIES Button.DEPENDENCIES = DEPENDENCIES
return Button return Button
}); })

View File

@ -11,12 +11,17 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './ToggleButton' ], function (ToggleButton) { define(['./ToggleButton'], function (ToggleButton) {
'use strict'; 'use strict'
class FullscreenButton extends ToggleButton { class FullscreenButton extends ToggleButton {
constructor(element, ...superDeps) { constructor(element, ...superDeps) {
super(element, false, 'Bastelstu.be/Chat/Ui/Settings/FullscreenButton', ...superDeps) super(
element,
false,
'Bastelstu.be/Chat/Ui/Settings/FullscreenButton',
...superDeps
)
} }
enable() { enable() {
@ -31,4 +36,4 @@ define([ './ToggleButton' ], function (ToggleButton) {
} }
return FullscreenButton return FullscreenButton
}); })

View File

@ -11,13 +11,20 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './ToggleButton' ], function (ToggleButton) { define(['./ToggleButton'], function (ToggleButton) {
'use strict'; 'use strict'
const DEPENDENCIES = [ 'UiNotification' ].concat(ToggleButton.DEPENDENCIES || [ ]) const DEPENDENCIES = ['UiNotification'].concat(
ToggleButton.DEPENDENCIES || []
)
class NotificationsButton extends ToggleButton { class NotificationsButton extends ToggleButton {
constructor(element, notification, ...superDeps) { constructor(element, notification, ...superDeps) {
super(element, false, 'Bastelstu.be/Chat/Ui/Settings/NotificationsButton', ...superDeps) super(
element,
false,
'Bastelstu.be/Chat/Ui/Settings/NotificationsButton',
...superDeps
)
this.notification = notification this.notification = notification
} }
@ -26,14 +33,17 @@ define([ './ToggleButton' ], function (ToggleButton) {
super.bootstrap() super.bootstrap()
// Hide the button if notifications are not supported or the permission has been denied // Hide the button if notifications are not supported or the permission has been denied
if (!this.notification.systemSupported || this.notification.systemDenied) { if (
!this.notification.systemSupported ||
this.notification.systemDenied
) {
elRemove(this.element.closest('li')) elRemove(this.element.closest('li'))
} }
} }
enable() { enable() {
super.enable() super.enable()
this.notification.enableSystemNotifications().catch(error => { this.notification.enableSystemNotifications().catch((error) => {
this.disable() this.disable()
if (this.notification.systemDenied) elRemove(this.element) if (this.notification.systemDenied) elRemove(this.element)
@ -48,4 +58,4 @@ define([ './ToggleButton' ], function (ToggleButton) {
NotificationsButton.DEPENDENCIES = DEPENDENCIES NotificationsButton.DEPENDENCIES = DEPENDENCIES
return NotificationsButton return NotificationsButton
}); })

View File

@ -11,12 +11,13 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './ToggleButton' define(['./ToggleButton', 'WoltLabSuite/Core/Ui/Screen'], function (
, 'WoltLabSuite/Core/Ui/Screen' ToggleButton,
], function (ToggleButton, UiScreen) { UiScreen
'use strict'; ) {
'use strict'
const DEPENDENCIES = [ 'UiInput' ].concat(ToggleButton.DEPENDENCIES || [ ]) const DEPENDENCIES = ['UiInput'].concat(ToggleButton.DEPENDENCIES || [])
class SmiliesButton extends ToggleButton { class SmiliesButton extends ToggleButton {
constructor(element, input, ...superDeps) { constructor(element, input, ...superDeps) {
super(element, false, undefined, ...superDeps) super(element, false, undefined, ...superDeps)
@ -25,7 +26,7 @@ define([ './ToggleButton'
} }
bootstrap() { bootstrap() {
this.container = elById('smileyPickerContainer') this.container = elById('smileyPickerContainer')
// Remove this button if smileys are disabled // Remove this button if smileys are disabled
if (!this.container) { if (!this.container) {
@ -37,7 +38,11 @@ define([ './ToggleButton'
// Initialize the smiley picker tab menu // Initialize the smiley picker tab menu
$('.messageTabMenu').messageTabMenu() $('.messageTabMenu').messageTabMenu()
$('#smilies-text').on('mousedown', '.jsSmiley', this.insertSmiley.bind(this)) $('#smilies-text').on(
'mousedown',
'.jsSmiley',
this.insertSmiley.bind(this)
)
this.closeButton.addEventListener('mousedown', this.disable.bind(this)) this.closeButton.addEventListener('mousedown', this.disable.bind(this))
// Start in desktop mode // Start in desktop mode
@ -48,9 +53,9 @@ define([ './ToggleButton'
// Setup media queries // Setup media queries
UiScreen.on('screen-md-down', { UiScreen.on('screen-md-down', {
match: this.enableMobile.bind(this), match: this.enableMobile.bind(this),
unmatch: this.disableMobile.bind(this), unmatch: this.disableMobile.bind(this),
setup: this.setupMobile.bind(this) setup: this.setupMobile.bind(this),
}) })
} }
@ -63,12 +68,23 @@ define([ './ToggleButton'
*/ */
setupMobile() { setupMobile() {
this.shadowToggleButton = document.createElement('span') this.shadowToggleButton = document.createElement('span')
this.shadowToggleButton.classList.add('smiliesToggleMobileButton', 'button', 'small') this.shadowToggleButton.classList.add(
this.shadowToggleButton.innerHTML = '<span class="icon icon24 fa-smile-o"></span>' 'smiliesToggleMobileButton',
this.shadowToggleButton.addEventListener('mousedown', this.onClick.bind(this)) 'button',
'small'
)
this.shadowToggleButton.innerHTML =
'<span class="icon icon24 fa-smile-o"></span>'
this.shadowToggleButton.addEventListener(
'mousedown',
this.onClick.bind(this)
)
const shadowContainer = elBySel('#chatInputContainer > div') const shadowContainer = elBySel('#chatInputContainer > div')
shadowContainer.insertBefore(this.shadowToggleButton, shadowContainer.firstChild) shadowContainer.insertBefore(
this.shadowToggleButton,
shadowContainer.firstChild
)
this.enableMobile() this.enableMobile()
} }
@ -156,4 +172,4 @@ define([ './ToggleButton'
SmiliesButton.DEPENDENCIES = DEPENDENCIES SmiliesButton.DEPENDENCIES = DEPENDENCIES
return SmiliesButton return SmiliesButton
}); })

View File

@ -11,13 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Button' define([
, '../../LocalStorage' './Button',
, '../../DataStructure/EventEmitter' '../../LocalStorage',
], function (Button, LocalStorage, EventEmitter) { '../../DataStructure/EventEmitter',
'use strict'; ], function (Button, LocalStorage, EventEmitter) {
'use strict'
const DEPENDENCIES = [ ].concat(Button.DEPENDENCIES || [ ]) const DEPENDENCIES = [].concat(Button.DEPENDENCIES || [])
class ToggleButton extends Button { class ToggleButton extends Button {
constructor(element, defaultState, storageKey, ...superDeps) { constructor(element, defaultState, storageKey, ...superDeps) {
super(element, ...superDeps) super(element, ...superDeps)
@ -38,8 +39,7 @@ define([ './Button'
if (this.defaultState) { if (this.defaultState) {
this.enable() this.enable()
} } else {
else {
this.disable() this.disable()
} }
} }
@ -69,8 +69,7 @@ define([ './Button'
if (this.enabled) { if (this.enabled) {
this.disable() this.disable()
} } else {
else {
this.enable() this.enable()
} }
} }
@ -79,4 +78,4 @@ define([ './Button'
ToggleButton.DEPENDENCIES = DEPENDENCIES ToggleButton.DEPENDENCIES = DEPENDENCIES
return ToggleButton return ToggleButton
}); })

View File

@ -11,18 +11,21 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Dom/Traverse' ], function (Traverse) { define(['WoltLabSuite/Core/Dom/Traverse'], function (Traverse) {
"use strict"; 'use strict'
class Topic { class Topic {
bootstrap() { bootstrap() {
elBySelAll('.chatRoomTopic', document, function (element) { elBySelAll('.chatRoomTopic', document, function (element) {
elBySel('.jsDismissRoomTopicButton', element).addEventListener('click', function (event) { elBySel('.jsDismissRoomTopicButton', element).addEventListener(
elRemove(element) 'click',
}) function (event) {
elRemove(element)
}
)
}) })
} }
} }
return Topic return Topic
}); })

View File

@ -11,18 +11,23 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Dom/Traverse' define([
, 'WoltLabSuite/Core/Dom/Util' 'WoltLabSuite/Core/Dom/Traverse',
, 'WoltLabSuite/Core/Ui/Dropdown/Simple' 'WoltLabSuite/Core/Dom/Util',
], function (DomTraverse, DomUtil, SimpleDropdown) { 'WoltLabSuite/Core/Ui/Dropdown/Simple',
"use strict"; ], function (DomTraverse, DomUtil, SimpleDropdown) {
'use strict'
const DEPENDENCIES = [ 'ProfileStore', 'Template.UserListDropdownMenuItems', 'bottle' ] const DEPENDENCIES = [
'ProfileStore',
'Template.UserListDropdownMenuItems',
'bottle',
]
class UserActionDropdownHandler { class UserActionDropdownHandler {
constructor(profiles, dropdownTemplate, bottle) { constructor(profiles, dropdownTemplate, bottle) {
this.profiles = profiles this.profiles = profiles
this.dropdownTemplate = dropdownTemplate this.dropdownTemplate = dropdownTemplate
this.bottle = bottle this.bottle = bottle
this.container = elById('main') this.container = elById('main')
} }
@ -32,7 +37,15 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
} }
onClick(event) { onClick(event) {
const userElement = event.target.classList.contains('jsUserActionDropdown') ? event.target : DomTraverse.parentByClass(event.target, 'jsUserActionDropdown', this.container) const userElement = event.target.classList.contains(
'jsUserActionDropdown'
)
? event.target
: DomTraverse.parentByClass(
event.target,
'jsUserActionDropdown',
this.container
)
if (!userElement) return if (!userElement) return
@ -46,23 +59,37 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
// Note: We would usually use firstElementChild here, but this // Note: We would usually use firstElementChild here, but this
// is not supported in Safari and Edge // is not supported in Safari and Edge
const dropdown = DomUtil.createFragmentFromHtml(this.dropdownTemplate.fetch({ user })).querySelector('*') const dropdown = DomUtil.createFragmentFromHtml(
this.dropdownTemplate.fetch({ user })
).querySelector('*')
Array.from(elBySelAll('[data-module]', dropdown)).forEach(element => { Array.from(elBySelAll('[data-module]', dropdown)).forEach((element) => {
const moduleName = element.dataset.module const moduleName = element.dataset.module
let userAction let userAction
if (!this.bottle.container.UserAction || (userAction = this.bottle.container.UserAction[`${moduleName.replace(/\./g, '-')}`]) == null) { if (
this.bottle.factory(`UserAction.${moduleName.replace(/\./g, '-')}`, _ => { !this.bottle.container.UserAction ||
const UserAction = require(moduleName) (userAction = this.bottle.container.UserAction[
const deps = this.bottle.digest(UserAction.DEPENDENCIES || []) `${moduleName.replace(/\./g, '-')}`
]) == null
) {
this.bottle.factory(
`UserAction.${moduleName.replace(/\./g, '-')}`,
(_) => {
const UserAction = require(moduleName)
const deps = this.bottle.digest(UserAction.DEPENDENCIES || [])
return new UserAction(...deps) return new UserAction(...deps)
}) }
)
userAction = this.bottle.container.UserAction[`${moduleName.replace(/\./g, '-')}`] userAction = this.bottle.container.UserAction[
`${moduleName.replace(/\./g, '-')}`
]
} }
element.addEventListener(WCF_CLICK_EVENT, (event) => userAction.onClick(user, event)) element.addEventListener(WCF_CLICK_EVENT, (event) =>
userAction.onClick(user, event)
)
}) })
SimpleDropdown.initFragment(userElement, dropdown) SimpleDropdown.initFragment(userElement, dropdown)
@ -77,4 +104,4 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
UserActionDropdownHandler.DEPENDENCIES = DEPENDENCIES UserActionDropdownHandler.DEPENDENCIES = DEPENDENCIES
return UserActionDropdownHandler return UserActionDropdownHandler
}); })

View File

@ -11,14 +11,14 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
class Action { class Action {
constructor() { } constructor() {}
onClick(userID, event) { } onClick(userID, event) {}
} }
return Action return Action
}); })

View File

@ -11,17 +11,15 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../../console' define(['../../console', './Action'], function (console, Action) {
, './Action' 'use strict'
], function (console, Action) {
"use strict";
const DEPENDENCIES = [ 'UiInput' ] const DEPENDENCIES = ['UiInput']
class BanAction extends Action { class BanAction extends Action {
constructor(input) { constructor(input) {
super() super()
this.input = input this.input = input
} }
onClick(user, event) { onClick(user, event) {
@ -35,7 +33,7 @@ define([ '../../console'
this.input.insertText(command, { append: false, prepend: true }) this.input.insertText(command, { append: false, prepend: true })
this.input.focus() this.input.focus()
setTimeout(_ => { setTimeout((_) => {
this.input.emit('autocomplete') this.input.emit('autocomplete')
}, 1) }, 1)
} }
@ -43,4 +41,4 @@ define([ '../../console'
BanAction.DEPENDENCIES = DEPENDENCIES BanAction.DEPENDENCIES = DEPENDENCIES
return BanAction return BanAction
}); })

View File

@ -11,17 +11,15 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ '../../console' define(['../../console', './Action'], function (console, Action) {
, './Action' 'use strict'
], function (console, Action) {
"use strict";
const DEPENDENCIES = [ 'UiInput' ] const DEPENDENCIES = ['UiInput']
class MuteAction extends Action { class MuteAction extends Action {
constructor(input) { constructor(input) {
super() super()
this.input = input this.input = input
} }
onClick(user, event) { onClick(user, event) {
@ -35,7 +33,7 @@ define([ '../../console'
this.input.insertText(command, { append: false, prepend: true }) this.input.insertText(command, { append: false, prepend: true })
this.input.focus() this.input.focus()
setTimeout(_ => { setTimeout((_) => {
this.input.emit('autocomplete') this.input.emit('autocomplete')
}, 1) }, 1)
} }
@ -43,4 +41,4 @@ define([ '../../console'
MuteAction.DEPENDENCIES = DEPENDENCIES MuteAction.DEPENDENCIES = DEPENDENCIES
return MuteAction return MuteAction
}); })

View File

@ -11,15 +11,15 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ './Action', '../../console' ], function (Action, console) { define(['./Action', '../../console'], function (Action, console) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'UiInput' ] const DEPENDENCIES = ['UiInput']
class WhisperAction extends Action { class WhisperAction extends Action {
constructor(input) { constructor(input) {
super() super()
this.input = input this.input = input
} }
onClick(user, event) { onClick(user, event) {
@ -38,4 +38,4 @@ define([ './Action', '../../console' ], function (Action, console) {
WhisperAction.DEPENDENCIES = DEPENDENCIES WhisperAction.DEPENDENCIES = DEPENDENCIES
return WhisperAction return WhisperAction
}); })

View File

@ -11,28 +11,26 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/Dom/Util' ], function (DomUtil) { define(['WoltLabSuite/Core/Dom/Util'], function (DomUtil) {
"use strict"; 'use strict'
const DEPENDENCIES = [ 'Template.UserList' ] const DEPENDENCIES = ['Template.UserList']
class UserList { class UserList {
constructor(userListTemplate) { constructor(userListTemplate) {
this.userListTemplate = userListTemplate this.userListTemplate = userListTemplate
this.chatUserList = elById('chatUserList') this.chatUserList = elById('chatUserList')
} }
bootstrap() { bootstrap() {}
}
render(users) { render(users) {
users.sort((a, b) => a.username.localeCompare(b.username)) users.sort((a, b) => a.username.localeCompare(b.username))
const html = this.userListTemplate.fetch({ users }) const html = this.userListTemplate.fetch({ users })
const fragment = DomUtil.createFragmentFromHtml(html) const fragment = DomUtil.createFragmentFromHtml(html)
// Replace the current user list with the new one // Replace the current user list with the new one
const currentList = elBySel('#chatUserList > .boxContent > ul') const currentList = elBySel('#chatUserList > .boxContent > ul')
const parentNode = currentList.parentNode const parentNode = currentList.parentNode
parentNode.removeChild(currentList) parentNode.removeChild(currentList)
parentNode.appendChild(fragment) parentNode.appendChild(fragment)
} }
@ -40,4 +38,4 @@ define([ 'WoltLabSuite/Core/Dom/Util' ], function (DomUtil) {
UserList.DEPENDENCIES = DEPENDENCIES UserList.DEPENDENCIES = DEPENDENCIES
return UserList return UserList
}); })

View File

@ -11,11 +11,12 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ 'WoltLabSuite/Core/User' define([
, 'WoltLabSuite/Core/StringUtil' 'WoltLabSuite/Core/User',
, './Helper' 'WoltLabSuite/Core/StringUtil',
], function (CoreUser, StringUtil, Helper) { './Helper',
"use strict"; ], function (CoreUser, StringUtil, Helper) {
'use strict'
const u = Symbol('user') const u = Symbol('user')
@ -25,15 +26,16 @@ define([ 'WoltLabSuite/Core/User'
class User { class User {
constructor(user) { constructor(user) {
this[u] = Helper.deepFreeze(user) this[u] = Helper.deepFreeze(user)
Object.getOwnPropertyNames(this[u]).forEach(key => { Object.getOwnPropertyNames(this[u]).forEach((key) => {
if (this[key]) { if (this[key]) {
throw new Error('Attempting to override existing property') throw new Error('Attempting to override existing property')
} }
Object.defineProperty(this, key, { value: this[u][key] Object.defineProperty(this, key, {
, enumerable: true value: this[u][key],
}) enumerable: true,
})
}) })
} }
@ -42,28 +44,36 @@ define([ 'WoltLabSuite/Core/User'
if (this.color1 === null && this.color2 === null) return this.username if (this.color1 === null && this.color2 === null) return this.username
// Single color // Single color
if (this.color1 === this.color2) return `<span style="color: ${Helper.intToRGBHex(this.color1)};">${StringUtil.escapeHTML(this.username)}</span>` if (this.color1 === this.color2)
return `<span style="color: ${Helper.intToRGBHex(
this.color1
)};">${StringUtil.escapeHTML(this.username)}</span>`
// Gradient // Gradient
const r1 = (this.color1 >> 16) & 0xFF const r1 = (this.color1 >> 16) & 0xff
const r2 = (this.color2 >> 16) & 0xFF const r2 = (this.color2 >> 16) & 0xff
const g1 = (this.color1 >> 8) & 0xFF const g1 = (this.color1 >> 8) & 0xff
const g2 = (this.color2 >> 8) & 0xFF const g2 = (this.color2 >> 8) & 0xff
const b1 = this.color1 & 0xFF const b1 = this.color1 & 0xff
const b2 = this.color2 & 0xFF const b2 = this.color2 & 0xff
const steps = this.username.length - 1 const steps = this.username.length - 1
const r = (r1 - r2) / steps const r = (r1 - r2) / steps
const g = (g1 - g2) / steps const g = (g1 - g2) / steps
const b = (b1 - b2) / steps const b = (b1 - b2) / steps
return this[u].username.split('').map((letter, index) => { return this[u].username
const R = Math.round(r1 - index * r) .split('')
const G = Math.round(g1 - index * g) .map((letter, index) => {
const B = Math.round(b1 - index * b) const R = Math.round(r1 - index * r)
const G = Math.round(g1 - index * g)
const B = Math.round(b1 - index * b)
return `<span style="color: rgb(${R}, ${G}, ${B})">${StringUtil.escapeHTML(letter)}</span>` return `<span style="color: rgb(${R}, ${G}, ${B})">${StringUtil.escapeHTML(
}).join('') letter
)}</span>`
})
.join('')
} }
get self() { get self() {
@ -71,11 +81,7 @@ define([ 'WoltLabSuite/Core/User'
} }
static getGuest(username) { static getGuest(username) {
const payload = { username const payload = { username, userID: null, color1: null, color2: null }
, userID: null
, color1: null
, color2: null
}
return new User(payload) return new User(payload)
} }
@ -90,4 +96,4 @@ define([ 'WoltLabSuite/Core/User'
} }
return User return User
}); })

View File

@ -11,11 +11,11 @@
* or later of the General Public License. * or later of the General Public License.
*/ */
define([ ], function () { define([], function () {
"use strict"; 'use strict'
const start = Date.now() const start = Date.now()
let last = start let last = start
const group = function () { const group = function () {
if (window.console.group) window.console.group() if (window.console.group) window.console.group()
@ -46,13 +46,12 @@ define([ ], function () {
} }
const debug = function (handler, ...args) { const debug = function (handler, ...args) {
const now = Date.now() const now = Date.now()
const time = [ (now - start), `\t+${(now - last)}ms\t` ] const time = [now - start, `\t+${now - last}ms\t`]
if (args.length) { if (args.length) {
println('debug', ...time, `[${handler}]\t`, ...args) println('debug', ...time, `[${handler}]\t`, ...args)
} } else {
else {
println('debug', ...time, handler) println('debug', ...time, handler)
} }
@ -61,7 +60,7 @@ define([ ], function () {
const debugException = function (error) { const debugException = function (error) {
if (error instanceof Error) { if (error instanceof Error) {
let message = `[${error.name}] „${error.message}“ in ${error.fileName} on line ${error.lineNumber}\n` let message = `[${error.name}] „${error.message}“ in ${error.fileName} on line ${error.lineNumber}\n`
if (error.stack) { if (error.stack) {
message += 'Stacktrace:\n' message += 'Stacktrace:\n'
@ -69,8 +68,7 @@ define([ ], function () {
} }
println('error', message) println('error', message)
} } else if (error.code && error.message) {
else if (error.code && error.message) {
debugAjaxException(error) debugAjaxException(error)
} }
} }
@ -79,20 +77,21 @@ define([ ], function () {
groupCollapsed() groupCollapsed()
let details = `[${error.code}] ${error.message}` let details = `[${error.code}] ${error.message}`
const br2nl = (string) => string.split('\n') const br2nl = (string) =>
.map(line => line.replace(/<br\s*\/?>$/i, '')) string
.join('\n') .split('\n')
.map((line) => line.replace(/<br\s*\/?>$/i, ''))
.join('\n')
if (error.stacktrace) { if (error.stacktrace) {
details += `\nStacktrace:\n${br2nl(error.stacktrace)}` details += `\nStacktrace:\n${br2nl(error.stacktrace)}`
} } else if (error.exceptionID) {
else if (error.exceptionID) {
details += `\nException ID: ${error.exceptionID}` details += `\nException ID: ${error.exceptionID}`
} }
println('debug', details) println('debug', details)
error.previous.forEach(previous => { error.previous.forEach((previous) => {
let details = '' let details = ''
group() group()
@ -103,17 +102,18 @@ define([ ], function () {
println('debug', details) println('debug', details)
}) })
error.previous.forEach(_ => groupEnd()) error.previous.forEach((_) => groupEnd())
groupEnd() groupEnd()
} }
return { log return {
, warn log,
, error warn,
, debug error,
, debugException debug,
, group debugException,
, groupCollapsed group,
, groupEnd groupCollapsed,
} groupEnd,
}); }
})