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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,21 +11,22 @@
* or later of the General Public License.
*/
define([ '../Command'
, '../Parser'
], function (Command, Parser) {
"use strict";
define(['../Command', '../Parser'], function (Command, Parser) {
'use strict'
class Color extends Command {
getParameterParser() {
// 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 })))
.or(new Parser.X().word().map(word => ({ type: 'word', value: word })))
const color = Parser.F.try(
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
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
});
})

View File

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

View File

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

View File

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

View File

@ -11,13 +11,14 @@
* or later of the General Public License.
*/
define([ '../Command'
, '../Parser'
, 'WoltLabSuite/Core/StringUtil'
], function (Command, Parser, StringUtil) {
"use strict";
define(['../Command', '../Parser', 'WoltLabSuite/Core/StringUtil'], function (
Command,
Parser,
StringUtil
) {
'use strict'
const DEPENDENCIES = [ 'ProfileStore' ]
const DEPENDENCIES = ['ProfileStore']
class Plain extends Command {
constructor(profileStore, id) {
super(id)
@ -25,12 +26,12 @@ define([ '../Command'
}
getParameterParser() {
return Parser.Rest1
.map(StringUtil.escapeHTML.bind(StringUtil))
.map(text => ({ text }))
return Parser.Rest1.map(
StringUtil.escapeHTML.bind(StringUtil)
).map((text) => ({ text }))
}
* autocomplete(parameterString) {
*autocomplete(parameterString) {
const parts = parameterString.split(/ /)
const lastWord = parts.pop().toLowerCase()
@ -41,13 +42,21 @@ define([ '../Command'
for (const userID of this.profileStore.getLastActivity()) {
const user = this.profileStore.get(userID)
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
return Plain
});
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,14 +11,15 @@
* or later of the General Public License.
*/
define([ 'WoltLabSuite/Core/Date/Util'
, 'WoltLabSuite/Core/Language'
, 'WoltLabSuite/Core/Dom/Util'
, 'Bastelstu.be/Chat/User'
], function (DateUtil, Language, DomUtil, User) {
"use strict";
define([
'WoltLabSuite/Core/Date/Util',
'WoltLabSuite/Core/Language',
'WoltLabSuite/Core/Dom/Util',
'Bastelstu.be/Chat/User',
], function (DateUtil, Language, DomUtil, User) {
'use strict'
const DEPENDENCIES = [ 'ProfileStore', 'Template' ]
const DEPENDENCIES = ['ProfileStore', 'Template']
class MessageType {
constructor(profileStore, templates, objectType) {
this.profileStore = profileStore
@ -32,32 +33,31 @@ define([ 'WoltLabSuite/Core/Date/Util'
}
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) {
const variables = { message
, users: this.profileStore
, author: this.profileStore.get(message.userID)
, DateUtil
, Language
}
const variables = {
message,
users: this.profileStore,
author: this.profileStore.get(message.userID),
DateUtil,
Language,
}
if (variables.author == null) {
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) {
@ -71,4 +71,4 @@ define([ 'WoltLabSuite/Core/Date/Util'
MessageType.DEPENDENCIES = DEPENDENCIES
return MessageType
});
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,12 +11,13 @@
* or later of the General Public License.
*/
define([ 'WoltLabSuite/Core/Dom/Traverse'
, 'WoltLabSuite/Core/Language'
, '../Helper'
, '../MessageType'
], function (DomTraverse, Language, Helper, MessageType) {
"use strict";
define([
'WoltLabSuite/Core/Dom/Traverse',
'WoltLabSuite/Core/Language',
'../Helper',
'../MessageType',
], function (DomTraverse, Language, Helper, MessageType) {
'use strict'
const decorators = Symbol('decorators')
@ -36,16 +37,19 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
}
getReferencedUsers(message) {
return super.getReferencedUsers(message).concat([ message.payload.user.userID ])
return super
.getReferencedUsers(message)
.concat([message.payload.user.userID])
}
render(message) {
const rooms = message.payload.rooms.map(function (item) {
const aug = { lastPull: null
, lastPullHTML: null
, lastPush: null
, lastPushHTML: null
}
const aug = {
lastPull: null,
lastPullHTML: null,
lastPush: null,
lastPushHTML: null,
}
if (item.lastPull) {
aug.lastPull = new Date(item.lastPull * 1000)
@ -57,28 +61,35 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
aug.lastPushHTML = Helper.getTimeElementHTML(aug.lastPush)
}
return Object.assign({ }, item, aug)
return Object.assign({}, item, aug)
})
const payload = Helper.deepFreeze(
Array.from(this[decorators]).reduce( (payload, decorator) => decorator(payload)
, Object.assign({ }, message.payload, { rooms })
)
Array.from(this[decorators]).reduce(
(payload, decorator) => decorator(payload),
Object.assign({}, message.payload, { rooms })
)
)
const fragment = super.render(new Proxy(message, {
get: function (target, property) {
if (property === 'payload') return payload
return target[property]
}
}))
const fragment = super.render(
new Proxy(message, {
get: function (target, property) {
if (property === 'payload') return payload
return target[property]
},
})
)
const icon = elCreate('span')
icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon')
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)
return fragment
@ -86,4 +97,4 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
}
return Info
});
})

View File

@ -11,8 +11,11 @@
* or later of the General Public License.
*/
define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType, Language) {
"use strict";
define(['../MessageType', 'WoltLabSuite/Core/Language'], function (
MessageType,
Language
) {
'use strict'
class Join extends MessageType {
shouldUpdateUserList(message) {
@ -20,9 +23,15 @@ define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType
}
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
});
})

View File

@ -11,8 +11,11 @@
* or later of the General Public License.
*/
define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType, Language) {
"use strict";
define(['../MessageType', 'WoltLabSuite/Core/Language'], function (
MessageType,
Language
) {
'use strict'
class Leave extends MessageType {
shouldUpdateUserList(message) {
@ -20,9 +23,15 @@ define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType
}
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
});
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,11 +11,12 @@
* or later of the General Public License.
*/
define([ 'WoltLabSuite/Core/Dom/Traverse'
, 'WoltLabSuite/Core/Language'
, '../MessageType'
], function (DomTraverse, Language, MessageType) {
"use strict";
define([
'WoltLabSuite/Core/Dom/Traverse',
'WoltLabSuite/Core/Language',
'../MessageType',
], function (DomTraverse, Language, MessageType) {
'use strict'
class Where extends MessageType {
render(message) {
@ -24,7 +25,9 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
const icon = elCreate('span')
icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon')
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')
elem.insertBefore(icon, elem.firstChild)
@ -34,4 +37,4 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
}
return Where
});
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,20 +11,21 @@
* or later of the General Public License.
*/
define([ 'Bastelstu.be/PromiseWrap/Ajax'
, 'WoltLabSuite/Core/Core'
, './User'
], function (Ajax, Core, User) {
"use strict";
define([
'Bastelstu.be/PromiseWrap/Ajax',
'WoltLabSuite/Core/Core',
'./User',
], function (Ajax, Core, User) {
'use strict'
const DEPENDENCIES = [ 'sessionID', 'roomID' ]
const DEPENDENCIES = ['sessionID', 'roomID']
/**
* Represents a chat room.
*/
class Room {
constructor(sessionID, roomID) {
this.sessionID = sessionID
this.roomID = roomID
this.roomID = roomID
}
/**
@ -33,12 +34,11 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
* @returns {Promise}
*/
async join() {
const payload = { className: 'chat\\data\\room\\RoomAction'
, actionName: 'join'
, parameters: { roomID: this.roomID
, sessionID: this.sessionID
}
}
const payload = {
className: 'chat\\data\\room\\RoomAction',
actionName: 'join',
parameters: { roomID: this.roomID, sessionID: this.sessionID },
}
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.
*/
leave(unload = false) {
const payload = { className: 'chat\\data\\room\\RoomAction'
, actionName: 'leave'
, parameters: { roomID: this.roomID
, sessionID: this.sessionID
}
}
const payload = {
className: 'chat\\data\\room\\RoomAction',
actionName: 'leave',
parameters: { roomID: this.roomID, sessionID: this.sessionID },
}
if (unload && FormData && (navigator.sendBeacon || window.fetch)) {
// Ordinary AJAX requests are unreliable during unload:
@ -65,22 +64,26 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
const formData = new FormData()
Core.serialize(payload)
.split('&')
.map((item) => item.split('='))
.map((item) => item.map(decodeURIComponent))
.forEach((item) => formData.append(item[0], item[1]))
.split('&')
.map((item) => item.split('='))
.map((item) => item.map(decodeURIComponent))
.forEach((item) => formData.append(item[0], item[1]))
if (navigator.sendBeacon) {
navigator.sendBeacon(url, formData)
}
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()
}
else {
} else {
return Ajax.api(this, payload)
}
}
@ -91,23 +94,22 @@ define([ 'Bastelstu.be/PromiseWrap/Ajax'
* @returns {Promise}
*/
async getUsers() {
const payload = { className: 'chat\\data\\room\\RoomAction'
, actionName: 'getUsers'
, objectIDs: [ this.roomID ]
}
const payload = {
className: 'chat\\data\\room\\RoomAction',
actionName: 'getUsers',
objectIDs: [this.roomID],
}
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() {
return { silent: true
, ignoreError: true
}
return { silent: true, ignoreError: true }
}
}
Room.DEPENDENCIES = DEPENDENCIES
return Room
});
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,29 +11,39 @@
* or later of the General Public License.
*/
define([ '../Helper'
, 'WoltLabSuite/Core/Date/Util'
, 'WoltLabSuite/Core/Dom/Change/Listener'
, 'WoltLabSuite/Core/Language'
, 'WoltLabSuite/Core/User'
, 'WoltLabSuite/Core/Dom/Traverse'
, '../DataStructure/EventEmitter'
, '../DataStructure/RedBlackTree/Tree'
], function (Helper, DateUtil, DomChangeListener, Language, User, DOMTraverse, EventEmitter, Tree) {
"use strict";
define([
'../Helper',
'WoltLabSuite/Core/Date/Util',
'WoltLabSuite/Core/Dom/Change/Listener',
'WoltLabSuite/Core/Language',
'WoltLabSuite/Core/User',
'WoltLabSuite/Core/Dom/Traverse',
'../DataStructure/EventEmitter',
'../DataStructure/RedBlackTree/Tree',
], function (
Helper,
DateUtil,
DomChangeListener,
Language,
User,
DOMTraverse,
EventEmitter,
Tree
) {
'use strict'
const enableAutoscroll = Symbol('enableAutoscroll')
const DEPENDENCIES = [ ]
const DEPENDENCIES = []
class MessageStream {
constructor() {
this.stream = elById('chatMessageStream')
this.scrollContainer = elBySel('.scrollContainer', this.stream)
this[enableAutoscroll] = true
this[enableAutoscroll] = true
this.lastScrollPosition = undefined
this.nodeMap = new WeakMap()
this.positions = new Tree()
this.nodeMap = new WeakMap()
this.positions = new Tree()
}
get enableAutoscroll() {
@ -50,7 +60,11 @@ define([ '../Helper'
bootstrap() {
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) {
@ -69,123 +83,131 @@ define([ '../Helper'
}
ingest(messages) {
let scrollTopBefore = this.enableAutoscroll ? 0 : this.scrollContainer.scrollTop
let scrollTopBefore = this.enableAutoscroll
? 0
: this.scrollContainer.scrollTop
let prependedHeight = 0
const ul = elBySel('ul', this.scrollContainer)
const ul = elBySel('ul', this.scrollContainer)
const first = ul.firstElementChild
const ingested = messages.map((function (item) {
let currentScrollHeight = 0
const ingested = messages.map(
function (item) {
let currentScrollHeight = 0
const li = elCreate('li')
const li = elCreate('li')
// Allow messages types to not render a messages
// This can be used for status messages like ChatUpdate
let fragment
if ((fragment = item.getMessageType().render(item)) === false) return
// Allow messages types to not render a messages
// This can be used for status messages like ChatUpdate
let fragment
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.setAttribute('id', `message-${item.messageID}`)
li.dataset.objectType = item.objectType
li.dataset.userId = item.userID
if (item.isOwnMessage()) li.classList.add('own')
if (item.isDeleted) li.classList.add('tombstone')
li.classList.add('chatMessageBoundary')
li.setAttribute('id', `message-${item.messageID}`)
li.dataset.objectType = item.objectType
li.dataset.userId = item.userID
if (item.isOwnMessage()) li.classList.add('own')
if (item.isDeleted) li.classList.add('tombstone')
const position = this.positions.insert(item.messageID)
if (position[1] !== undefined) {
const sibling = elById(`message-${position[1]}`)
if (!sibling) throw new Error('Unreachable')
const position = this.positions.insert(item.messageID)
if (position[1] !== undefined) {
const sibling = elById(`message-${position[1]}`)
if (!sibling) throw new Error('Unreachable')
let nodeBefore, nodeAfter
let dateMarkerBetween = false
if (position[0] === 'LEFT') {
nodeAfter = sibling
nodeBefore = sibling.previousElementSibling
if (nodeBefore && nodeBefore.classList.contains('dateMarker')) {
elRemove(nodeBefore)
let nodeBefore, nodeAfter
let dateMarkerBetween = false
if (position[0] === 'LEFT') {
nodeAfter = sibling
nodeBefore = sibling.previousElementSibling
}
}
else if (position[0] === 'RIGHT') {
nodeBefore = sibling
nodeAfter = sibling.nextElementSibling
if (nodeAfter && nodeAfter.classList.contains('dateMarker')) {
elRemove(nodeAfter)
if (nodeBefore && nodeBefore.classList.contains('dateMarker')) {
elRemove(nodeBefore)
nodeBefore = sibling.previousElementSibling
}
} else if (position[0] === 'RIGHT') {
nodeBefore = sibling
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)
if (nodeBefore && !messageBefore) throw new Error('Unreachable')
const messageAfter = this.nodeMap.get(nodeAfter)
if (nodeAfter && !messageAfter) throw new Error('Unreachable')
const messageBefore = this.nodeMap.get(nodeBefore)
if (nodeBefore && !messageBefore) throw new Error('Unreachable')
const messageAfter = this.nodeMap.get(nodeAfter)
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
if (nodeAfter) nodeAfter.classList.remove('first')
if (messageBefore) {
if (this.onDifferentDays(messageBefore.date, item.date)) {
const dateMarker = this.getDateMarker(item.date)
ul.insertBefore(dateMarker, nodeAfter)
let context = nodeAfter
if (nodeAfter) nodeAfter.classList.remove('first')
if (messageBefore) {
if (this.onDifferentDays(messageBefore.date, item.date)) {
const dateMarker = this.getDateMarker(item.date)
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')
}
else {
if (messageBefore.objectType !== item.objectType || !item.getMessageType().joinable(messageBefore, item)) {
li.classList.add('first')
}
}
}
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)) {
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')
}
}
}
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) {
prependedHeight += this.scrollContainer.scrollHeight - currentScrollHeight
}
}
else {
li.classList.add('first')
ul.insertBefore(li, null)
}
return { node: li, message: item }
}.bind(this)
)
this.nodeMap.set(li, item)
return { node: li
, message: item
}
}).bind(this));
if (ingested.some(item => item != null)) {
if (ingested.some((item) => item != null)) {
if (this.enableAutoscroll) {
this.scrollToBottom()
}
else {
} else {
this.stream.classList.add('activity')
this.scrollContainer.scrollTop = scrollTopBefore + prependedHeight
}
@ -208,8 +230,11 @@ define([ '../Helper'
let direction = 'down'
if (this.lastScrollPosition != null && scrollTop < this.lastScrollPosition) {
direction = 'up'
if (
this.lastScrollPosition != null &&
scrollTop < this.lastScrollPosition
) {
direction = 'up'
}
if (direction === 'up') {
@ -219,12 +244,10 @@ define([ '../Helper'
if (distanceFromTop <= 7) {
this.emit('reachedTop')
}
else if (distanceFromTop <= 300) {
} else if (distanceFromTop <= 300) {
this.emit('nearTop')
}
}
else if (direction === 'down') {
} else if (direction === 'down') {
if (distanceFromTop > 7) {
this.emit('scrollDown')
}
@ -232,8 +255,7 @@ define([ '../Helper'
if (distanceFromBottom <= 7) {
this.scrollToBottom()
this.emit('reachedBottom')
}
else if (distanceFromBottom <= 300) {
} else if (distanceFromBottom <= 300) {
this.emit('nearBottom')
}
}
@ -250,16 +272,19 @@ define([ '../Helper'
// Get the first and last node in the selection
let originalStart, start, end, originalEnd
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 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
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
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
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
if (originalStart instanceof Text && originalStart.textContent.substring(startOffset) === "") {
if (
originalStart instanceof Text &&
originalStart.textContent.substring(startOffset) === ''
) {
start = DOMTraverse.next(start)
}
@ -289,15 +317,14 @@ define([ '../Helper'
end = DOMTraverse.prev(end)
}
const elements = [ ]
let next = start
const elements = []
let next = start
do {
elements.push(next)
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
if (elements.length === 1) {
@ -305,37 +332,59 @@ define([ '../Helper'
range.setStart(originalStart, startOffset)
range.setEnd(originalEnd, endOffset)
if (!Helper.rangeSpansTextContent(range, start.querySelector('.chatMessage'))) return
if (
!Helper.rangeSpansTextContent(
range,
start.querySelector('.chatMessage')
)
)
return
}
try {
event.clipboardData.setData('text/plain', elements.map((el, index, arr) => {
const message = this.nodeMap.get(el)
event.clipboardData.setData(
'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')) {
return `[${message.formattedTime}] ${Language.get('chat.messageType.be.bastelstu.chat.messageType.tombstone.message')}`
}
if (el.classList.contains('tombstone')) {
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
if (typeof (body = message.getMessageType().renderPlainText(message)) === 'undefined' || 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
}
let body
if (
typeof (body = message
.getMessageType()
.renderPlainText(message)) === 'undefined' ||
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()}`
}).filter(x => x).join('\n'))
return `[${message.formattedTime}] <${
message.username
}> ${body.trim()}`
})
.filter((x) => x)
.join('\n')
)
event.preventDefault()
}
catch (e) {
} catch (e) {
console.error('Unable to use the clipboard API')
console.error(e)
}
@ -345,4 +394,4 @@ define([ '../Helper'
MessageStream.DEPENDENCIES = DEPENDENCIES
return MessageStream
});
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,13 +11,14 @@
* or later of the General Public License.
*/
define([ ], function () {
'use strict';
define([], function () {
'use strict'
const DEPENDENCIES = [ ]
const DEPENDENCIES = []
class Button {
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
}
@ -33,4 +34,4 @@ define([ ], function () {
Button.DEPENDENCIES = DEPENDENCIES
return Button
});
})

View File

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

View File

@ -11,13 +11,20 @@
* or later of the General Public License.
*/
define([ './ToggleButton' ], function (ToggleButton) {
'use strict';
define(['./ToggleButton'], function (ToggleButton) {
'use strict'
const DEPENDENCIES = [ 'UiNotification' ].concat(ToggleButton.DEPENDENCIES || [ ])
const DEPENDENCIES = ['UiNotification'].concat(
ToggleButton.DEPENDENCIES || []
)
class NotificationsButton extends ToggleButton {
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
}
@ -26,14 +33,17 @@ define([ './ToggleButton' ], function (ToggleButton) {
super.bootstrap()
// 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'))
}
}
enable() {
super.enable()
this.notification.enableSystemNotifications().catch(error => {
this.notification.enableSystemNotifications().catch((error) => {
this.disable()
if (this.notification.systemDenied) elRemove(this.element)
@ -48,4 +58,4 @@ define([ './ToggleButton' ], function (ToggleButton) {
NotificationsButton.DEPENDENCIES = DEPENDENCIES
return NotificationsButton
});
})

View File

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

View File

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

View File

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

View File

@ -11,18 +11,23 @@
* or later of the General Public License.
*/
define([ 'WoltLabSuite/Core/Dom/Traverse'
, 'WoltLabSuite/Core/Dom/Util'
, 'WoltLabSuite/Core/Ui/Dropdown/Simple'
], function (DomTraverse, DomUtil, SimpleDropdown) {
"use strict";
define([
'WoltLabSuite/Core/Dom/Traverse',
'WoltLabSuite/Core/Dom/Util',
'WoltLabSuite/Core/Ui/Dropdown/Simple',
], function (DomTraverse, DomUtil, SimpleDropdown) {
'use strict'
const DEPENDENCIES = [ 'ProfileStore', 'Template.UserListDropdownMenuItems', 'bottle' ]
const DEPENDENCIES = [
'ProfileStore',
'Template.UserListDropdownMenuItems',
'bottle',
]
class UserActionDropdownHandler {
constructor(profiles, dropdownTemplate, bottle) {
this.profiles = profiles
this.dropdownTemplate = dropdownTemplate
this.bottle = bottle
this.profiles = profiles
this.dropdownTemplate = dropdownTemplate
this.bottle = bottle
this.container = elById('main')
}
@ -32,7 +37,15 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
}
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
@ -46,23 +59,37 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
// Note: We would usually use firstElementChild here, but this
// 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
let userAction
if (!this.bottle.container.UserAction || (userAction = this.bottle.container.UserAction[`${moduleName.replace(/\./g, '-')}`]) == null) {
this.bottle.factory(`UserAction.${moduleName.replace(/\./g, '-')}`, _ => {
const UserAction = require(moduleName)
const deps = this.bottle.digest(UserAction.DEPENDENCIES || [])
if (
!this.bottle.container.UserAction ||
(userAction = this.bottle.container.UserAction[
`${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)
@ -77,4 +104,4 @@ define([ 'WoltLabSuite/Core/Dom/Traverse'
UserActionDropdownHandler.DEPENDENCIES = DEPENDENCIES
return UserActionDropdownHandler
});
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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