mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 00:11:45 +03:00
Help chat (#7151)
* wip * wip 2 * wip * Copy types from bot * wip * Send and display messages * wip: shadow * Copy styles from design screenshot * Fix "create new thread" button * Fix switching threads * wip * Scrollback * Reactions * Fix issues with chat input sizing * Update changelog * Fix bugs; add scroll to thread list * Use types from backend definitions * Update git commit of chat backend; remove stray file path * hotfix: fix "edit thread" shortcut on macos * Address issues * Extract chat header to separate component * Begin matching appearance with Figma * Show reaction bar on hover * fix small scrollbar appear next to the message input * Disallow sending empty messages * Add chat URL to config - production URL will be added soon * add production chat url * fix linters --------- Co-authored-by: Paweł Buchowski <pawel.buchowski@enso.org>
This commit is contained in:
parent
67f7d33801
commit
457d0986b6
@ -197,6 +197,9 @@
|
||||
visualization size by dragging its right and bottom borders. Visualization
|
||||
width also follows the node's width, and visualizations are aligned to the
|
||||
left side of the node.
|
||||
- [Help chat][7151]. The link to the Discord server is replaced with a chat
|
||||
bridge to the Discord server. This is intended to have the chat visible at the
|
||||
same time as the IDE, so that help can be much more interactive.
|
||||
|
||||
[5910]: https://github.com/enso-org/enso/pull/5910
|
||||
[6279]: https://github.com/enso-org/enso/pull/6279
|
||||
@ -215,6 +218,7 @@
|
||||
[7028]: https://github.com/enso-org/enso/pull/7028
|
||||
[7014]: https://github.com/enso-org/enso/pull/7014
|
||||
[7146]: https://github.com/enso-org/enso/pull/7146
|
||||
[7151]: https://github.com/enso-org/enso/pull/7151
|
||||
[7164]: https://github.com/enso-org/enso/pull/7164
|
||||
|
||||
#### EnsoGL (rendering engine)
|
||||
|
@ -40,7 +40,7 @@ const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
|
||||
const JSX = ':matches(JSXElement, JSXFragment)'
|
||||
const NOT_PASCAL_CASE = '/^(?!_?([A-Z][a-z0-9]*)+$)/'
|
||||
const NOT_CAMEL_CASE = '/^(?!_?[a-z][a-z0-9*]*([A-Z0-9][a-z0-9]*)*$)(?!React$)/'
|
||||
const WHITELISTED_CONSTANTS = 'logger|.+Context'
|
||||
const WHITELISTED_CONSTANTS = 'logger|.+Context|interpolationFunction.+'
|
||||
const NOT_CONSTANT_CASE = `/^(?!${WHITELISTED_CONSTANTS}$|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$)/`
|
||||
|
||||
// =======================================
|
||||
|
7
app/ide-desktop/lib/assets/close_large.svg
Normal file
7
app/ide-desktop/lib/assets/close_large.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="black" fill-opacity="0.22" />
|
||||
<g opacity="0.66">
|
||||
<rect x="16.2426" y="17.6569" width="14" height="2" transform="rotate(-135 16.2426 17.6569)" fill="white" />
|
||||
<rect x="6.34302" y="16.2426" width="14" height="2" transform="rotate(-45 6.34302 16.2426)" fill="white" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 438 B |
5
app/ide-desktop/lib/assets/triangle_down.svg
Normal file
5
app/ide-desktop/lib/assets/triangle_down.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.9635 5.5547L8.83205 11.7519C8.43623 12.3457 7.56377 12.3457 7.16795 11.7519L3.03647 5.5547C2.59343 4.89015 3.06982 4 3.86852 4L12.1315 4C12.9302 4 13.4066 4.89015 12.9635 5.5547Z"
|
||||
fill="#484848" />
|
||||
</svg>
|
After Width: | Height: | Size: 333 B |
@ -38,10 +38,16 @@
|
||||
<link rel="stylesheet" href="/docsStyle.css" />
|
||||
<script type="module" src="/index.js" defer></script>
|
||||
<script type="module" src="/run.js" defer></script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.2/dist/twemoji.min.js"
|
||||
integrity="sha384-D6GSzpW7fMH86ilu73eB95ipkfeXcMPoOGVst/L04yqSSe+RTUY0jXcuEIZk0wrT"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="enso-dashboard" class="enso-dashboard"></div>
|
||||
<div id="root"></div>
|
||||
<div id="enso-chat" class="enso-chat"></div>
|
||||
<noscript>
|
||||
This page requires JavaScript to run. Please enable it in your browser.
|
||||
</noscript>
|
||||
|
@ -26,6 +26,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"enso-chat": "git://github.com/enso-org/enso-bot#wip/sb/initial-implementation",
|
||||
"enso-content": "^1.0.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.8",
|
||||
|
@ -0,0 +1,171 @@
|
||||
/** @file Functions to manually animate values over time.
|
||||
* This is useful if the values need to be known before paint.
|
||||
*
|
||||
* See MDN for information on the easing functions defined in this module:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function */
|
||||
import * as react from 'react'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
/** The number of times the segment from 0 to 1 will be bisected to find the x-value for
|
||||
* a cubic bezier curve. */
|
||||
const CUBIC_BEZIER_BISECTIONS = 10
|
||||
|
||||
/** Accepts a parameter containing the actual progress as a fraction between 0 and 1 inclusive,
|
||||
* and returns the fraction. */
|
||||
export type InterpolationFunction = (progress: number) => number
|
||||
|
||||
/** Interpolates between two values over time */
|
||||
export function useInterpolateOverTime(
|
||||
interpolationFunction: InterpolationFunction,
|
||||
durationMs: number,
|
||||
initialValue = 0
|
||||
): [value: number, setTargetValue: react.Dispatch<react.SetStateAction<number>>] {
|
||||
const [value, setValue] = react.useState(initialValue)
|
||||
const [startValue, setStartValue] = react.useState(initialValue)
|
||||
const [endValue, setEndValue] = react.useState(initialValue)
|
||||
|
||||
react.useEffect(() => {
|
||||
setStartValue(value)
|
||||
const startTime = Number(new Date())
|
||||
let isRunning = true
|
||||
const onTick = () => {
|
||||
const fraction = Math.min((Number(new Date()) - startTime) / durationMs, 1)
|
||||
if (isRunning && fraction < 1) {
|
||||
setValue(startValue + (endValue - startValue) * interpolationFunction(fraction))
|
||||
requestAnimationFrame(onTick)
|
||||
} else {
|
||||
setValue(endValue)
|
||||
setStartValue(endValue)
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(onTick)
|
||||
return () => {
|
||||
isRunning = false
|
||||
}
|
||||
}, [endValue])
|
||||
|
||||
return [value, setEndValue]
|
||||
}
|
||||
|
||||
/** Equivalent to the CSS easing function `linear(a, b, c, ...)`.
|
||||
*
|
||||
* `interpolationFunctionLinear()` is equivalent to `interpolationFunctionLinear(0, 1)`.
|
||||
*
|
||||
* Does not support percentages to control time spent on a specific line segment, unlike the CSS
|
||||
* `linear(0, 0.25 75%, 1)` */
|
||||
export function interpolationFunctionLinear(...points: number[]): InterpolationFunction {
|
||||
if (points.length === 0) {
|
||||
return progress => progress
|
||||
} else {
|
||||
const length = points.length
|
||||
return progress => {
|
||||
const effectiveIndex = progress * length
|
||||
// The following are guaranteed to be non-null, as `progress` is guaranteed
|
||||
// to be between 0 and 1.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const start = points[Math.floor(effectiveIndex)]!
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const end = points[Math.ceil(effectiveIndex)]!
|
||||
const progressThroughEffectiveIndex = effectiveIndex % 1
|
||||
return (end - start) * progressThroughEffectiveIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines a cubic Bézier curve with control points `(0, 0)`, `(x1, y1)`, `(x2, y2)`,
|
||||
* and `(1, 1)`.
|
||||
*
|
||||
* Equivalent to the CSS easing function `cubic-bezier(x1, y1, x2, y2)` */
|
||||
export function interpolationFunctionCubicBezier(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number
|
||||
): InterpolationFunction {
|
||||
return progress => {
|
||||
let minimum = 0
|
||||
let maximum = 1
|
||||
for (let i = 0; i < CUBIC_BEZIER_BISECTIONS; ++i) {
|
||||
const t = (minimum + maximum) / 2
|
||||
const u = 1 - t
|
||||
// See here for the source of the explicit form:
|
||||
// https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
|
||||
// `x0 = 0` and `x3 = 1` have been substituted in.
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const estimatedProgress = 3 * u * t * (u * x1 + t * x2) + t * t * t
|
||||
if (estimatedProgress > progress) {
|
||||
maximum = t
|
||||
} else {
|
||||
minimum = t
|
||||
}
|
||||
}
|
||||
const t = (minimum + maximum) / 2
|
||||
const u = 1 - t
|
||||
// Uses the same formula as above, but calculating `y` instead of `x`.
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
return 3 * u * t * (u * y1 + t * y2) + t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
// Magic numbers are allowable as these definitions are taken straight from the spec.
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
|
||||
/** Equivalent to the CSS easing function `ease`, which is itself equivalent to
|
||||
* `cubic-bezier(0.25, 0.1, 0.25, 1.0)`. */
|
||||
export const interpolationFunctionEase = interpolationFunctionCubicBezier(0.25, 0.1, 0.25, 1.0)
|
||||
|
||||
/** Equivalent to the CSS easing function `ease-in`, which is itself equivalent to
|
||||
* `cubic-bezier(0.42, 0.0, 1.0, 1.0)`. */
|
||||
export const interpolationFunctionEaseIn = interpolationFunctionCubicBezier(0.42, 0.0, 1.0, 1.0)
|
||||
|
||||
/** Equivalent to the CSS easing function `ease-in-out`, which is itself equivalent to
|
||||
* `cubic-bezier(0.42, 0.0, 0.58, 1.0)`. */
|
||||
export const interpolationFunctionEaseInOut = interpolationFunctionCubicBezier(0.42, 0.0, 0.58, 1.0)
|
||||
|
||||
/** Equivalent to the CSS easing function `ease-out`, which is itself equivalent to
|
||||
* `cubic-bezier(0.0, 0.0, 0.58, 1.0)`. */
|
||||
export const interpolationFunctionEaseOut = interpolationFunctionCubicBezier(0.0, 0.0, 0.58, 1.0)
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-magic-numbers */
|
||||
|
||||
/** Determines which sides should have a "jump" - a step that lasts for zero time, effectively
|
||||
* making the anmiation skip that end point. */
|
||||
export enum StepJumpSides {
|
||||
start = 'jump-start',
|
||||
end = 'jump-end',
|
||||
both = 'jump-both',
|
||||
none = 'jump-none',
|
||||
}
|
||||
|
||||
/** Equivalent to the CSS easing function `steps(stepCount, jumpSides)`. */
|
||||
export function interpolationFunctionSteps(
|
||||
stepCount: number,
|
||||
jumpSides: StepJumpSides
|
||||
): InterpolationFunction {
|
||||
switch (jumpSides) {
|
||||
case StepJumpSides.start: {
|
||||
return progress => Math.ceil(progress * stepCount) / stepCount
|
||||
}
|
||||
case StepJumpSides.end: {
|
||||
return progress => Math.floor(progress * stepCount) / stepCount
|
||||
}
|
||||
case StepJumpSides.both: {
|
||||
const stepCountPlusOne = stepCount + 1
|
||||
return progress => Math.ceil(progress * stepCount) / stepCountPlusOne
|
||||
}
|
||||
case StepJumpSides.none: {
|
||||
const stepCountMinusOne = stepCount - 1
|
||||
return progress => Math.min(1, Math.floor(progress * stepCount) / stepCountMinusOne)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Equivalent to the CSS easing function `step-start`, which is itself equivalent to
|
||||
* `steps(1, jump-start)`. */
|
||||
export const interpolationFunctionStepStart = interpolationFunctionSteps(1, StepJumpSides.start)
|
||||
|
||||
/** Equivalent to the CSS easing function `step-end`, which is itself equivalent to
|
||||
* `steps(1, jump-end)`. */
|
||||
export const interpolationFunctionStepEnd = interpolationFunctionSteps(1, StepJumpSides.end)
|
@ -30,19 +30,33 @@ const API_URLS = {
|
||||
production: newtype.asNewtype<ApiUrl>('https://7aqkn3tnbc.execute-api.eu-west-1.amazonaws.com'),
|
||||
}
|
||||
|
||||
/**
|
||||
* All possible Help Chat endpoint URLs, sorted by environment.
|
||||
*
|
||||
* In development mode, the chat bot will need to be run locally:
|
||||
* https://github.com/enso-org/enso-bot */
|
||||
const CHAT_URLS = {
|
||||
development: newtype.asNewtype<ChatUrl>('ws://localhost:8082'),
|
||||
// TODO[sb]: Insert the actual URL of the production chat bot here.
|
||||
production: newtype.asNewtype<ChatUrl>('wss://chat.cloud.enso.org'),
|
||||
}
|
||||
|
||||
/** All possible configuration options, sorted by environment. */
|
||||
const CONFIGS = {
|
||||
npekin: {
|
||||
cloudRedirect: CLOUD_REDIRECTS.development,
|
||||
apiUrl: API_URLS.npekin,
|
||||
chatUrl: CHAT_URLS.development,
|
||||
} satisfies Config,
|
||||
pbuchu: {
|
||||
cloudRedirect: CLOUD_REDIRECTS.development,
|
||||
apiUrl: API_URLS.pbuchu,
|
||||
chatUrl: CHAT_URLS.development,
|
||||
} satisfies Config,
|
||||
production: {
|
||||
cloudRedirect: CLOUD_REDIRECTS.production,
|
||||
apiUrl: API_URLS.production,
|
||||
chatUrl: CHAT_URLS.production,
|
||||
} satisfies Config,
|
||||
}
|
||||
/** Export the configuration that is currently in use. */
|
||||
@ -54,10 +68,14 @@ export const ACTIVE_CONFIG: Config = CONFIGS[ENVIRONMENT]
|
||||
|
||||
/** Interface defining the configuration options that we expect to provide for the Dashboard. */
|
||||
export interface Config {
|
||||
/** URL used as the OAuth redirect when running in the cloud app. */
|
||||
/** URL of the OAuth redirect when running in the cloud app.
|
||||
*
|
||||
* The desktop app redirects to a static deep link, so it does not have to be configured. */
|
||||
cloudRedirect: auth.OAuthRedirect
|
||||
/** URL used as the base URL for requests to our Cloud API backend. */
|
||||
/** Base URL for requests to our Cloud API backend. */
|
||||
apiUrl: ApiUrl
|
||||
/** URL to the websocket endpoint of the Help Chat. */
|
||||
chatUrl: ChatUrl
|
||||
}
|
||||
|
||||
// ===================
|
||||
@ -73,4 +91,7 @@ export type Environment = 'npekin' | 'pbuchu' | 'production'
|
||||
// ===========
|
||||
|
||||
/** Base URL for requests to our Cloud API backend. */
|
||||
type ApiUrl = newtype.Newtype<string, 'ApiUrl'>
|
||||
type ApiUrl = newtype.Newtype<`http://${string}` | `https://${string}`, 'ApiUrl'>
|
||||
|
||||
/** URL to the websocket endpoint of the Help Chat. */
|
||||
type ChatUrl = newtype.Newtype<`ws://${string}` | `wss://${string}`, 'ChatUrl'>
|
||||
|
@ -0,0 +1,853 @@
|
||||
/** @file A WebSocket-based chat directly to official support on the official Discord server. */
|
||||
import * as React from 'react'
|
||||
import * as reactDom from 'react-dom'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import CloseLargeIcon from 'enso-assets/close_large.svg'
|
||||
import DefaultUserIcon from 'enso-assets/default_user.svg'
|
||||
import TriangleDownIcon from 'enso-assets/triangle_down.svg'
|
||||
|
||||
import * as chat from 'enso-chat/chat'
|
||||
|
||||
import * as animations from '../../animations'
|
||||
import * as authProvider from '../../authentication/providers/auth'
|
||||
import * as config from '../../config'
|
||||
import * as dateTime from '../dateTime'
|
||||
import * as loggerProvider from '../../providers/logger'
|
||||
import * as newtype from '../../newtype'
|
||||
|
||||
import Twemoji from './twemoji'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
// TODO[sb]: Consider associating a project with a thread
|
||||
// (and providing a button to jump to the relevant project).
|
||||
// The project shouldn't be jumped to automatically, since it may take a long time
|
||||
// to switch projects, and undo history may be lost.
|
||||
|
||||
const HELP_CHAT_ID = 'enso-chat'
|
||||
export const ANIMATION_DURATION_MS = 200
|
||||
const WIDTH_PX = 336
|
||||
/** The size (both width and height) of each reaction button. */
|
||||
const REACTION_BUTTON_SIZE = 20
|
||||
/** The size (both width and height) of each reaction on a message. */
|
||||
const REACTION_SIZE = 16
|
||||
/** The list of reaction emojis, in order. */
|
||||
const REACTION_EMOJIS: chat.ReactionSymbol[] = ['❤️', '👍', '👎', '😀', '🙁', '👀', '🎉']
|
||||
/** The initial title of the thread. */
|
||||
const DEFAULT_THREAD_TITLE = 'New chat thread'
|
||||
/** A {@link RegExp} matching any non-whitespace character. */
|
||||
const NON_WHITESPACE_CHARACTER_REGEX = /\S/
|
||||
/** A {@link RegExp} matching auto-generated thread names. */
|
||||
const AUTOGENERATED_THREAD_TITLE_REGEX = /^New chat thread (\d+)$/
|
||||
/** The maximum number of lines to show in the message input, past which a scrollbar is shown. */
|
||||
const MAX_MESSAGE_INPUT_LINES = 10
|
||||
/** The maximum number of messages to fetch when opening a new thread.
|
||||
* This SHOULD be the same limit as the chat backend (the maximum number of messages sent in
|
||||
* `serverThread` events). */
|
||||
const MAX_MESSAGE_HISTORY = 25
|
||||
|
||||
// ==========================
|
||||
// === ChatDisplayMessage ===
|
||||
// ==========================
|
||||
|
||||
/** Information needed to display a chat message. */
|
||||
interface ChatDisplayMessage {
|
||||
id: chat.MessageId
|
||||
/** If `true`, this is a message from the staff to the user.
|
||||
* If `false`, this is a message from the user to the staff. */
|
||||
isStaffMessage: boolean
|
||||
avatar: string | null
|
||||
/** Name of the author of the message. */
|
||||
name: string
|
||||
content: string
|
||||
reactions: chat.ReactionSymbol[]
|
||||
/** Given in milliseconds since the unix epoch. */
|
||||
timestamp: number
|
||||
/** Given in milliseconds since the unix epoch. */
|
||||
editedTimestamp: number | null
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// === makeNewThreadTitle ===
|
||||
// ==========================
|
||||
|
||||
/** Returns an auto-generated thread title. */
|
||||
function makeNewThreadTitle(threads: chat.ThreadData[]) {
|
||||
const threadTitleNumbers = threads
|
||||
.map(thread => thread.title.match(AUTOGENERATED_THREAD_TITLE_REGEX))
|
||||
.flatMap(match => (match != null ? parseInt(match[1] ?? '0', 10) : []))
|
||||
return `${DEFAULT_THREAD_TITLE} ${Math.max(0, ...threadTitleNumbers) + 1}`
|
||||
}
|
||||
|
||||
// ===================
|
||||
// === ReactionBar ===
|
||||
// ===================
|
||||
|
||||
/** Props for a {@link ReactionBar}. */
|
||||
export interface ReactionBarProps {
|
||||
selectedReactions: Set<chat.ReactionSymbol>
|
||||
doReact: (reaction: chat.ReactionSymbol) => void
|
||||
doRemoveReaction: (reaction: chat.ReactionSymbol) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** A list of emoji reactions to choose from. */
|
||||
function ReactionBar(props: ReactionBarProps) {
|
||||
const { selectedReactions, doReact, doRemoveReaction, className } = props
|
||||
|
||||
return (
|
||||
<div className={`inline-block bg-white rounded-full m-1 ${className ?? ''}`}>
|
||||
{REACTION_EMOJIS.map(emoji => (
|
||||
<button
|
||||
key={emoji}
|
||||
onClick={() => {
|
||||
if (selectedReactions.has(emoji)) {
|
||||
doRemoveReaction(emoji)
|
||||
} else {
|
||||
doReact(emoji)
|
||||
}
|
||||
}}
|
||||
// FIXME: Grayscale has the wrong lightness
|
||||
className={`rounded-full hover:bg-gray-200 m-1 p-1 ${
|
||||
selectedReactions.has(emoji) ? '' : 'opacity-70 grayscale hover:grayscale-0'
|
||||
}`}
|
||||
>
|
||||
<Twemoji key={emoji} emoji={emoji} size={REACTION_BUTTON_SIZE} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =================
|
||||
// === Reactions ===
|
||||
// =================
|
||||
|
||||
/** Props for a {@link Reactions}. */
|
||||
export interface ReactionsProps {
|
||||
reactions: chat.ReactionSymbol[]
|
||||
}
|
||||
|
||||
/** A list of emoji reactions that have been on a message. */
|
||||
function Reactions(props: ReactionsProps) {
|
||||
const { reactions } = props
|
||||
|
||||
if (reactions.length === 0) {
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
{reactions.map(reaction => (
|
||||
<Twemoji key={reaction} emoji={reaction} size={REACTION_SIZE} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================
|
||||
// === ChatMessage ===
|
||||
// ===================
|
||||
|
||||
/** Props for a {@link ChatMessage}. */
|
||||
export interface ChatMessageProps {
|
||||
message: ChatDisplayMessage
|
||||
reactions: chat.ReactionSymbol[]
|
||||
shouldShowReactionBar: boolean
|
||||
doReact: (reaction: chat.ReactionSymbol) => void
|
||||
doRemoveReaction: (reaction: chat.ReactionSymbol) => void
|
||||
}
|
||||
|
||||
/** A chat message, including user info, sent date, and reactions (if any). */
|
||||
function ChatMessage(props: ChatMessageProps) {
|
||||
const { message, reactions, shouldShowReactionBar, doReact, doRemoveReaction } = props
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
return (
|
||||
<div
|
||||
className="mx-4 my-2"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
<div className="flex">
|
||||
<img
|
||||
crossOrigin="anonymous"
|
||||
src={message.avatar ?? DefaultUserIcon}
|
||||
className="rounded-full h-8 w-8 my-1"
|
||||
/>
|
||||
<div className="mx-2 leading-5">
|
||||
<div className="font-bold">{message.name}</div>
|
||||
<div className="text-opacity-50 text-primary">
|
||||
{dateTime.formatDateTimeChatFriendly(new Date(message.timestamp))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap">
|
||||
{message.content}
|
||||
<Reactions reactions={reactions} />
|
||||
</div>
|
||||
{shouldShowReactionBar && (
|
||||
<ReactionBar
|
||||
doReact={doReact}
|
||||
doRemoveReaction={doRemoveReaction}
|
||||
selectedReactions={new Set(message.reactions)}
|
||||
/>
|
||||
)}
|
||||
{message.isStaffMessage && !shouldShowReactionBar && isHovered && (
|
||||
<div className="relative h-0 py-1 -my-1">
|
||||
<ReactionBar
|
||||
doReact={doReact}
|
||||
doRemoveReaction={doRemoveReaction}
|
||||
selectedReactions={new Set(message.reactions)}
|
||||
className="absolute shadow-soft"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ==================
|
||||
// === ChatHeader ===
|
||||
// ==================
|
||||
|
||||
/** Props for a {@Link ChatHeader}. */
|
||||
interface InternalChatHeaderProps {
|
||||
threads: chat.ThreadData[]
|
||||
setThreads: React.Dispatch<React.SetStateAction<chat.ThreadData[]>>
|
||||
threadId: chat.ThreadId | null
|
||||
threadTitle: string
|
||||
setThreadTitle: (threadTitle: string) => void
|
||||
switchThread: (threadId: chat.ThreadId) => void
|
||||
sendMessage: (message: chat.ChatClientMessageData) => void
|
||||
doClose: () => void
|
||||
}
|
||||
|
||||
/** The header bar for a {@link Chat}. Includes the title, close button, and threads list. */
|
||||
function ChatHeader(props: InternalChatHeaderProps) {
|
||||
const {
|
||||
threads,
|
||||
setThreads,
|
||||
threadId,
|
||||
threadTitle,
|
||||
setThreadTitle,
|
||||
switchThread,
|
||||
sendMessage,
|
||||
doClose,
|
||||
} = props
|
||||
const [isThreadListVisible, setIsThreadListVisible] = React.useState(false)
|
||||
|
||||
// These will never be `null` as their values are set immediately.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const titleInputRef = React.useRef<HTMLInputElement>(null!)
|
||||
|
||||
React.useEffect(() => {
|
||||
titleInputRef.current.value = threadTitle
|
||||
}, [threadTitle])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onClick = () => {
|
||||
setIsThreadListVisible(false)
|
||||
}
|
||||
document.addEventListener('click', onClick)
|
||||
return () => {
|
||||
document.removeEventListener('click', onClick)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleThreadListVisibility = React.useCallback((event: React.SyntheticEvent) => {
|
||||
event.stopPropagation()
|
||||
setIsThreadListVisible(visible => !visible)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex text-sm font-semibold mx-4 mt-2">
|
||||
<button className="flex grow items-center" onClick={toggleThreadListVisibility}>
|
||||
<img
|
||||
className={`transition-transform duration-300 ${
|
||||
isThreadListVisible ? '-rotate-180' : ''
|
||||
}`}
|
||||
src={TriangleDownIcon}
|
||||
/>
|
||||
{/* Spacing. */}
|
||||
<div className="w-2" />
|
||||
<div className="grow">
|
||||
<input
|
||||
type="text"
|
||||
ref={titleInputRef}
|
||||
defaultValue={threadTitle}
|
||||
className="bg-transparent w-full leading-6"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
switch (event.key) {
|
||||
case 'Escape': {
|
||||
event.currentTarget.value = threadTitle
|
||||
break
|
||||
}
|
||||
case 'Enter': {
|
||||
event.currentTarget.blur()
|
||||
break
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={event => {
|
||||
const newTitle = event.currentTarget.value
|
||||
setThreadTitle(newTitle)
|
||||
if (threadId != null) {
|
||||
setThreads(oldThreads =>
|
||||
oldThreads.map(thread =>
|
||||
thread.id !== threadId
|
||||
? thread
|
||||
: { ...thread, title: newTitle }
|
||||
)
|
||||
)
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.renameThread,
|
||||
title: newTitle,
|
||||
threadId: threadId,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<button className="mx-1" onClick={doClose}>
|
||||
<img src={CloseLargeIcon} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative text-sm font-semibold">
|
||||
<div
|
||||
className={`grid absolute w-full bg-ide-bg shadow-soft clip-path-bottom-shadow overflow-hidden transition-grid-template-rows z-10 ${
|
||||
isThreadListVisible ? 'grid-rows-1fr' : 'grid-rows-0fr'
|
||||
}`}
|
||||
>
|
||||
<div className="min-h-0 max-h-70 overflow-y-auto">
|
||||
{threads.map(thread => (
|
||||
<div
|
||||
key={thread.id}
|
||||
className={`flex p-1 ${
|
||||
thread.id === threadId
|
||||
? 'cursor-default bg-gray-350'
|
||||
: 'cursor-pointer hover:bg-gray-300'
|
||||
}`}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (thread.id !== threadId) {
|
||||
switchThread(thread.id)
|
||||
setIsThreadListVisible(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="w-8 text-center">
|
||||
{/* {thread.hasUnreadMessages ? '(!) ' : ''} */}
|
||||
</div>
|
||||
<div>{thread.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ============
|
||||
// === Chat ===
|
||||
// ============
|
||||
|
||||
/** Props for a {@link Chat}. */
|
||||
export interface ChatProps {
|
||||
/** This should only be false when the panel is closing. */
|
||||
isOpen: boolean
|
||||
doClose: () => void
|
||||
}
|
||||
|
||||
/** Chat sidebar. */
|
||||
function Chat(props: ChatProps) {
|
||||
const { isOpen, doClose } = props
|
||||
const { accessToken: rawAccessToken } = authProvider.useNonPartialUserSession()
|
||||
const logger = loggerProvider.useLogger()
|
||||
|
||||
/** This is SAFE, because this component is only rendered when `accessToken` is present.
|
||||
* See `dashboard.tsx` for its sole usage. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const accessToken = rawAccessToken!
|
||||
|
||||
const [isPaidUser, setIsPaidUser] = React.useState(true)
|
||||
const [isReplyEnabled, setIsReplyEnabled] = React.useState(false)
|
||||
// `true` if and only if scrollback was triggered for the current thread.
|
||||
const [shouldIgnoreMessageLimit, setShouldIgnoreMessageLimit] = React.useState(false)
|
||||
const [isAtBeginning, setIsAtBeginning] = React.useState(false)
|
||||
const [threads, setThreads] = React.useState<chat.ThreadData[]>([])
|
||||
const [messages, setMessages] = React.useState<ChatDisplayMessage[]>([])
|
||||
const [threadId, setThreadId] = React.useState<chat.ThreadId | null>(null)
|
||||
const [threadTitle, setThreadTitle] = React.useState(DEFAULT_THREAD_TITLE)
|
||||
const [isAtTop, setIsAtTop] = React.useState(false)
|
||||
const [isAtBottom, setIsAtBottom] = React.useState(true)
|
||||
const [messagesHeightBeforeMessageHistory, setMessagesHeightBeforeMessageHistory] =
|
||||
React.useState<number | null>(null)
|
||||
// TODO: proper URL
|
||||
const [websocket] = React.useState(() => new WebSocket(config.ACTIVE_CONFIG.chatUrl))
|
||||
const [right, setTargetRight] = animations.useInterpolateOverTime(
|
||||
animations.interpolationFunctionEaseInOut,
|
||||
ANIMATION_DURATION_MS,
|
||||
-WIDTH_PX
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const messageInputRef = React.useRef<HTMLTextAreaElement>(null!)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const messagesRef = React.useRef<HTMLDivElement>(null!)
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsPaidUser(false)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
websocket.close()
|
||||
}
|
||||
}, [websocket])
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const element = messagesRef.current
|
||||
if (isAtTop && messagesHeightBeforeMessageHistory != null) {
|
||||
element.scrollTop = element.scrollHeight - messagesHeightBeforeMessageHistory
|
||||
setMessagesHeightBeforeMessageHistory(null)
|
||||
} else if (isAtBottom) {
|
||||
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||
}
|
||||
// Auto-scroll MUST only happen when the message list changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messages])
|
||||
|
||||
const sendMessage = React.useCallback(
|
||||
(message: chat.ChatClientMessageData) => {
|
||||
websocket.send(JSON.stringify(message))
|
||||
},
|
||||
[/* should never change */ websocket]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const onMessage = (data: MessageEvent) => {
|
||||
if (typeof data.data !== 'string') {
|
||||
logger.error('Chat cannot handle binary messages.')
|
||||
} else {
|
||||
// This is SAFE, as the format of server messages is known.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const message: chat.ChatServerMessageData = JSON.parse(data.data)
|
||||
switch (message.type) {
|
||||
case chat.ChatMessageDataType.serverThreads: {
|
||||
setThreads(message.threads)
|
||||
break
|
||||
}
|
||||
case chat.ChatMessageDataType.serverThread: {
|
||||
if (!threads.some(thread => thread.id === message.id)) {
|
||||
setThreads(oldThreads => {
|
||||
const newThread = {
|
||||
id: message.id,
|
||||
title: message.title,
|
||||
hasUnreadMessages: false,
|
||||
}
|
||||
if (!oldThreads.some(thread => thread.id === message.id)) {
|
||||
return [...oldThreads, newThread]
|
||||
} else {
|
||||
return oldThreads.map(oldThread =>
|
||||
oldThread.id === newThread.id ? newThread : oldThread
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
setShouldIgnoreMessageLimit(false)
|
||||
setThreadId(message.id)
|
||||
setThreadTitle(message.title)
|
||||
setIsAtBeginning(message.isAtBeginning)
|
||||
const newMessages = message.messages.flatMap(innerMessage => {
|
||||
switch (innerMessage.type) {
|
||||
case chat.ChatMessageDataType.serverMessage: {
|
||||
const displayMessage: ChatDisplayMessage = {
|
||||
id: innerMessage.id,
|
||||
isStaffMessage: true,
|
||||
content: innerMessage.content,
|
||||
reactions: innerMessage.reactions,
|
||||
avatar: innerMessage.authorAvatar,
|
||||
name: innerMessage.authorName,
|
||||
timestamp: innerMessage.timestamp,
|
||||
editedTimestamp: innerMessage.editedTimestamp,
|
||||
}
|
||||
return displayMessage
|
||||
}
|
||||
case chat.ChatMessageDataType.serverReplayedMessage: {
|
||||
const displayMessage: ChatDisplayMessage = {
|
||||
id: innerMessage.id,
|
||||
isStaffMessage: false,
|
||||
content: innerMessage.content,
|
||||
reactions: [],
|
||||
avatar: null,
|
||||
name: 'Me',
|
||||
timestamp: innerMessage.timestamp,
|
||||
editedTimestamp: null,
|
||||
}
|
||||
return displayMessage
|
||||
}
|
||||
}
|
||||
})
|
||||
switch (message.requestType) {
|
||||
case chat.ChatMessageDataType.historyBefore: {
|
||||
setMessages(oldMessages => [...newMessages, ...oldMessages])
|
||||
break
|
||||
}
|
||||
default: {
|
||||
setMessages(newMessages)
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case chat.ChatMessageDataType.serverMessage: {
|
||||
const newMessage: ChatDisplayMessage = {
|
||||
id: message.id,
|
||||
isStaffMessage: true,
|
||||
avatar: message.authorAvatar,
|
||||
name: message.authorName,
|
||||
content: message.content,
|
||||
reactions: [],
|
||||
timestamp: message.timestamp,
|
||||
editedTimestamp: null,
|
||||
}
|
||||
setMessages(oldMessages => {
|
||||
const newMessages = [...oldMessages, newMessage]
|
||||
return shouldIgnoreMessageLimit
|
||||
? newMessages
|
||||
: newMessages.slice(-MAX_MESSAGE_HISTORY)
|
||||
})
|
||||
break
|
||||
}
|
||||
case chat.ChatMessageDataType.serverEditedMessage: {
|
||||
setMessages(
|
||||
messages.map(otherMessage => {
|
||||
if (otherMessage.id !== message.id) {
|
||||
return otherMessage
|
||||
} else {
|
||||
return {
|
||||
...otherMessage,
|
||||
content: message.content,
|
||||
editedTimestamp: message.timestamp,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
break
|
||||
}
|
||||
case chat.ChatMessageDataType.serverReplayedMessage: {
|
||||
// This message is only sent as part of the `serverThread` message and
|
||||
// can safely be ignored.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const onOpen = () => {
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.authenticate,
|
||||
accessToken,
|
||||
})
|
||||
}
|
||||
websocket.addEventListener('message', onMessage)
|
||||
websocket.addEventListener('open', onOpen)
|
||||
return () => {
|
||||
websocket.removeEventListener('message', onMessage)
|
||||
websocket.removeEventListener('open', onOpen)
|
||||
}
|
||||
}, [
|
||||
websocket,
|
||||
shouldIgnoreMessageLimit,
|
||||
logger,
|
||||
threads,
|
||||
messages,
|
||||
accessToken,
|
||||
/* should never change */ sendMessage,
|
||||
])
|
||||
|
||||
const container = document.getElementById(HELP_CHAT_ID)
|
||||
|
||||
React.useEffect(() => {
|
||||
// The types come from a third-party API and cannot be changed.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
let handle: number | undefined
|
||||
if (container != null) {
|
||||
if (isOpen) {
|
||||
container.style.display = ''
|
||||
setTargetRight(0)
|
||||
} else {
|
||||
setTargetRight(-WIDTH_PX)
|
||||
handle = window.setTimeout(() => {
|
||||
container.style.display = 'none'
|
||||
}, ANIMATION_DURATION_MS)
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(handle)
|
||||
}
|
||||
}, [isOpen, container, setTargetRight])
|
||||
|
||||
const switchThread = React.useCallback(
|
||||
(newThreadId: chat.ThreadId) => {
|
||||
const threadData = threads.find(thread => thread.id === newThreadId)
|
||||
if (threadData == null) {
|
||||
const message = `Unknown thread id '${newThreadId}'.`
|
||||
toast.error(message)
|
||||
logger.error(message)
|
||||
} else {
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.switchThread,
|
||||
threadId: newThreadId,
|
||||
})
|
||||
}
|
||||
},
|
||||
[threads, /* should never change */ sendMessage, /* should never change */ logger]
|
||||
)
|
||||
|
||||
const sendCurrentMessage = React.useCallback(
|
||||
(event: React.SyntheticEvent, createNewThread?: boolean) => {
|
||||
event.preventDefault()
|
||||
const element = messageInputRef.current
|
||||
const content = element.value
|
||||
if (NON_WHITESPACE_CHARACTER_REGEX.test(content)) {
|
||||
setIsReplyEnabled(false)
|
||||
element.value = ''
|
||||
element.style.height = '0px'
|
||||
element.style.height = `${element.scrollHeight}px`
|
||||
const newMessage: ChatDisplayMessage = {
|
||||
// This MUST be unique.
|
||||
id: newtype.asNewtype<chat.MessageId>(String(Number(new Date()))),
|
||||
isStaffMessage: false,
|
||||
avatar: null,
|
||||
name: 'Me',
|
||||
content,
|
||||
reactions: [],
|
||||
timestamp: Number(new Date()),
|
||||
editedTimestamp: null,
|
||||
}
|
||||
if (threadId == null || createNewThread === true) {
|
||||
const newThreadTitle =
|
||||
threadId == null ? threadTitle : makeNewThreadTitle(threads)
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.newThread,
|
||||
title: newThreadTitle,
|
||||
content,
|
||||
})
|
||||
setThreadId(null)
|
||||
setThreadTitle(newThreadTitle)
|
||||
setMessages([newMessage])
|
||||
} else {
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.message,
|
||||
threadId,
|
||||
content,
|
||||
})
|
||||
setMessages(oldMessages => {
|
||||
const newMessages = [...oldMessages, newMessage]
|
||||
return shouldIgnoreMessageLimit
|
||||
? newMessages
|
||||
: newMessages.slice(-MAX_MESSAGE_HISTORY)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
threads,
|
||||
threadId,
|
||||
threadTitle,
|
||||
shouldIgnoreMessageLimit,
|
||||
/* should never change */ sendMessage,
|
||||
]
|
||||
)
|
||||
|
||||
const upgradeToPro = () => {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
if (container == null) {
|
||||
logger.error('Chat container not found.')
|
||||
return null
|
||||
} else {
|
||||
// This should be `findLast`, but that requires ES2023.
|
||||
const lastStaffMessage = [...messages].reverse().find(message => message.isStaffMessage)
|
||||
|
||||
return reactDom.createPortal(
|
||||
<div
|
||||
style={{ right }}
|
||||
className="text-xs-mini text-chat flex flex-col fixed top-0 right-0 h-screen bg-ide-bg border-ide-bg-dark border-l-2 w-83.5 py-1 z-10"
|
||||
>
|
||||
<ChatHeader
|
||||
threads={threads}
|
||||
setThreads={setThreads}
|
||||
threadId={threadId}
|
||||
threadTitle={threadTitle}
|
||||
setThreadTitle={setThreadTitle}
|
||||
switchThread={switchThread}
|
||||
sendMessage={sendMessage}
|
||||
doClose={doClose}
|
||||
/>
|
||||
<div
|
||||
ref={messagesRef}
|
||||
className="flex-1 overflow-scroll"
|
||||
onScroll={event => {
|
||||
const element = event.currentTarget
|
||||
const isNowAtTop = element.scrollTop === 0
|
||||
const isNowAtBottom =
|
||||
element.scrollTop + element.clientHeight === element.scrollHeight
|
||||
const firstMessage = messages[0]
|
||||
if (isNowAtTop && !isAtBeginning && firstMessage != null) {
|
||||
setShouldIgnoreMessageLimit(true)
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.historyBefore,
|
||||
messageId: firstMessage.id,
|
||||
})
|
||||
setMessagesHeightBeforeMessageHistory(element.scrollHeight)
|
||||
}
|
||||
if (isNowAtTop !== isAtTop) {
|
||||
setIsAtTop(isNowAtTop)
|
||||
}
|
||||
if (isNowAtBottom !== isAtBottom) {
|
||||
setIsAtBottom(isNowAtBottom)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{messages.map(message => (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
reactions={[]}
|
||||
doReact={reaction => {
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.reaction,
|
||||
messageId: message.id,
|
||||
reaction,
|
||||
})
|
||||
setMessages(oldMessages =>
|
||||
oldMessages.map(oldMessage =>
|
||||
oldMessage.id === message.id
|
||||
? {
|
||||
...message,
|
||||
reactions: [...oldMessage.reactions, reaction],
|
||||
}
|
||||
: oldMessage
|
||||
)
|
||||
)
|
||||
}}
|
||||
doRemoveReaction={reaction => {
|
||||
sendMessage({
|
||||
type: chat.ChatMessageDataType.removeReaction,
|
||||
messageId: message.id,
|
||||
reaction,
|
||||
})
|
||||
setMessages(oldMessages =>
|
||||
oldMessages.map(oldMessage =>
|
||||
oldMessage.id === message.id
|
||||
? {
|
||||
...message,
|
||||
reactions: oldMessage.reactions.filter(
|
||||
oldReaction => oldReaction !== reaction
|
||||
),
|
||||
}
|
||||
: oldMessage
|
||||
)
|
||||
)
|
||||
}}
|
||||
shouldShowReactionBar={
|
||||
message === lastStaffMessage || message.reactions.length !== 0
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white p-1 mx-2 my-1">
|
||||
<form onSubmit={sendCurrentMessage}>
|
||||
<div>
|
||||
<textarea
|
||||
ref={messageInputRef}
|
||||
rows={1}
|
||||
autoFocus
|
||||
required
|
||||
placeholder="Type your message ..."
|
||||
className="w-full rounded-lg resize-none p-1"
|
||||
onKeyDown={event => {
|
||||
switch (event.key) {
|
||||
case 'Enter': {
|
||||
// If the shift key is not pressed, submit the form.
|
||||
// If the shift key is pressed, keep the default
|
||||
// behavior of adding a newline.
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault()
|
||||
event.currentTarget.form?.requestSubmit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
onInput={event => {
|
||||
const element = event.currentTarget
|
||||
element.style.height = '0px'
|
||||
element.style.height =
|
||||
`min(${MAX_MESSAGE_INPUT_LINES}lh,` +
|
||||
`${element.scrollHeight + 1}px)`
|
||||
const newIsReplyEnabled = NON_WHITESPACE_CHARACTER_REGEX.test(
|
||||
element.value
|
||||
)
|
||||
if (newIsReplyEnabled !== isReplyEnabled) {
|
||||
setIsReplyEnabled(newIsReplyEnabled)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isReplyEnabled}
|
||||
className={`text-xxs text-white rounded-full grow text-left px-1.5 py-1 ${
|
||||
isReplyEnabled ? 'bg-gray-400' : 'bg-gray-300'
|
||||
}`}
|
||||
onClick={event => {
|
||||
sendCurrentMessage(event, true)
|
||||
}}
|
||||
>
|
||||
New question? Click to start a new thread!
|
||||
</button>
|
||||
{/* Spacing. */}
|
||||
<div className="w-0.5" />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isReplyEnabled}
|
||||
className={`text-white bg-blue-600 rounded-full px-1.5 py-1 ${
|
||||
isReplyEnabled ? '' : 'opacity-50'
|
||||
}`}
|
||||
>
|
||||
Reply!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{!isPaidUser && (
|
||||
<button
|
||||
className="text-left leading-5 rounded-2xl bg-call-to-action text-white p-2 mx-2 my-1"
|
||||
onClick={upgradeToPro}
|
||||
>
|
||||
Click here to upgrade to Enso Pro and get access to high-priority, live
|
||||
support!
|
||||
</button>
|
||||
)}
|
||||
</div>,
|
||||
container
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Chat
|
@ -30,6 +30,7 @@ import * as backendProvider from '../../providers/backend'
|
||||
import * as loggerProvider from '../../providers/logger'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
|
||||
import Chat, * as chat from './chat'
|
||||
import PermissionDisplay, * as permissionDisplay from './permissionDisplay'
|
||||
import ProjectActionButton, * as projectActionButton from './projectActionButton'
|
||||
import ContextMenu from './contextMenu'
|
||||
@ -289,6 +290,8 @@ function Dashboard(props: DashboardProps) {
|
||||
null
|
||||
)
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false)
|
||||
const [isHelpChatVisible, setIsHelpChatVisible] = React.useState(false)
|
||||
const [loadingProjectManagerDidFail, setLoadingProjectManagerDidFail] = React.useState(false)
|
||||
const [directoryId, setDirectoryId] = React.useState(
|
||||
session.organization != null ? rootDirectoryId(session.organization.id) : null
|
||||
@ -365,6 +368,22 @@ function Dashboard(props: DashboardProps) {
|
||||
}
|
||||
}, [doRefresh])
|
||||
|
||||
React.useEffect(() => {
|
||||
// The types come from a third-party API and cannot be changed.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
let handle: number | undefined
|
||||
if (isHelpChatOpen) {
|
||||
setIsHelpChatVisible(true)
|
||||
} else {
|
||||
handle = window.setTimeout(() => {
|
||||
setIsHelpChatVisible(false)
|
||||
}, chat.ANIMATION_DURATION_MS)
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(handle)
|
||||
}
|
||||
}, [isHelpChatOpen])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onProjectManagerLoadingFailed = () => {
|
||||
setLoadingProjectManagerDidFail(true)
|
||||
@ -1145,6 +1164,8 @@ function Dashboard(props: DashboardProps) {
|
||||
supportsLocalBackend={supportsLocalBackend}
|
||||
projectName={project?.name ?? null}
|
||||
tab={tab}
|
||||
isHelpChatOpen={isHelpChatOpen}
|
||||
setIsHelpChatOpen={setIsHelpChatOpen}
|
||||
toggleTab={() => {
|
||||
if (project && tab === Tab.dashboard) {
|
||||
switchToIdeTab()
|
||||
@ -1702,6 +1723,15 @@ function Dashboard(props: DashboardProps) {
|
||||
)}
|
||||
{/* This should be just `{modal}`, however TypeScript incorrectly throws an error. */}
|
||||
{project && <Ide project={project} appRunner={appRunner} />}
|
||||
{/* `session.accessToken` MUST be present in order for the `Chat` component to work. */}
|
||||
{isHelpChatVisible && session.accessToken != null && (
|
||||
<Chat
|
||||
isOpen={isHelpChatOpen}
|
||||
doClose={() => {
|
||||
setIsHelpChatOpen(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{modal && <>{modal}</>}
|
||||
</div>
|
||||
)
|
||||
|
@ -28,6 +28,8 @@ export interface TopBarProps {
|
||||
tab: dashboard.Tab
|
||||
toggleTab: () => void
|
||||
setBackendType: (backendType: backendModule.BackendType) => void
|
||||
isHelpChatOpen: boolean
|
||||
setIsHelpChatOpen: (isHelpChatOpen: boolean) => void
|
||||
query: string
|
||||
setQuery: (value: string) => void
|
||||
}
|
||||
@ -35,8 +37,17 @@ export interface TopBarProps {
|
||||
/** The {@link TopBarProps.setQuery} parameter is used to communicate with the parent component,
|
||||
* because `searchVal` may change parent component's project list. */
|
||||
function TopBar(props: TopBarProps) {
|
||||
const { supportsLocalBackend, projectName, tab, toggleTab, setBackendType, query, setQuery } =
|
||||
props
|
||||
const {
|
||||
supportsLocalBackend,
|
||||
projectName,
|
||||
tab,
|
||||
toggleTab,
|
||||
setBackendType,
|
||||
isHelpChatOpen,
|
||||
setIsHelpChatOpen,
|
||||
query,
|
||||
setQuery,
|
||||
} = props
|
||||
const [isUserMenuVisible, setIsUserMenuVisible] = React.useState(false)
|
||||
const { modal } = modalProvider.useModal()
|
||||
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||
@ -125,17 +136,20 @@ function TopBar(props: TopBarProps) {
|
||||
className="flex-1 mx-2 bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://discord.gg/enso"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center bg-help rounded-full px-2.5 text-white mx-2"
|
||||
>
|
||||
<span className="whitespace-nowrap">help chat</span>
|
||||
<div className="ml-2">
|
||||
<img src={SpeechBubbleIcon} />
|
||||
<div className="grow" />
|
||||
{!isHelpChatOpen && (
|
||||
<div
|
||||
className="flex cursor-pointer items-center bg-help rounded-full px-2.5 text-white mx-2"
|
||||
onClick={() => {
|
||||
setIsHelpChatOpen(true)
|
||||
}}
|
||||
>
|
||||
<span className="whitespace-nowrap">help chat</span>
|
||||
<div className="ml-2">
|
||||
<img src={SpeechBubbleIcon} />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
{/* User profile and menu. */}
|
||||
<div className="transform w-8">
|
||||
<div
|
||||
|
@ -0,0 +1,54 @@
|
||||
/** @file A wrapper for a twemoji image. */
|
||||
import * as React from 'react'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The base of hexadecimal numbers. */
|
||||
const HEXADECIMAL = 16
|
||||
|
||||
// ===============
|
||||
// === Twemoji ===
|
||||
// ===============
|
||||
|
||||
/** Props for a {@link Twemoji}. */
|
||||
export interface TwemojiProps {
|
||||
emoji: string
|
||||
}
|
||||
|
||||
// Only accepts strings that are two code points - for example, emojis.
|
||||
/** Returns the input type if it consists of two codepoints. Otherwise, it returns
|
||||
* an error message. */
|
||||
type MustBeLength2String<T extends string> = T extends `${string}${string}${infer Rest}`
|
||||
? Rest extends ''
|
||||
? T
|
||||
: 'Error: string must have a length of 2'
|
||||
: 'Error: string must have a length of 2'
|
||||
|
||||
/** Props for a {@link Twemoji}, but with extra validation. */
|
||||
interface InternalValidTwemojiProps<T extends string> {
|
||||
emoji: MustBeLength2String<T>
|
||||
size: number
|
||||
}
|
||||
|
||||
/** Serves a Twemoji image from the JSDelivr CDN. */
|
||||
function Twemoji<T extends string>(props: InternalValidTwemojiProps<T>) {
|
||||
const { emoji, size } = props
|
||||
|
||||
// This is safe as the that the string is required to be non-empty by the type of `props`.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const imagePath = emoji.codePointAt(0)!.toString(HEXADECIMAL)
|
||||
|
||||
return (
|
||||
<img
|
||||
src={`https://cdn.jsdelivr.net/npm/twemoji@latest/2/svg/${imagePath}.svg`}
|
||||
crossOrigin="anonymous"
|
||||
height={size}
|
||||
width={size}
|
||||
alt={emoji}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Twemoji
|
@ -1,6 +1,13 @@
|
||||
/** @file Utilities for manipulating and displaying dates and times */
|
||||
import * as newtype from '../newtype'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The number of hours in half a day. This is used to get the number of hours for AM/PM time. */
|
||||
const HALF_DAY_HOURS = 12
|
||||
|
||||
// ================
|
||||
// === DateTime ===
|
||||
// ================
|
||||
@ -18,6 +25,23 @@ export function formatDateTime(date: Date) {
|
||||
return `${year}-${month}-${dayOfMonth}, ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// TODO[sb]: Is this DD/MM/YYYY or MM/DD/YYYY?
|
||||
/** Formats date time into the preferred chat-frienly format: `DD/MM/YYYY, hh:mm PM`. */
|
||||
export function formatDateTimeChatFriendly(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const dayOfMonth = date.getDate().toString().padStart(2, '0')
|
||||
let hourRaw = date.getHours()
|
||||
let amOrPm = 'AM'
|
||||
if (hourRaw > HALF_DAY_HOURS) {
|
||||
hourRaw -= HALF_DAY_HOURS
|
||||
amOrPm = 'PM'
|
||||
}
|
||||
const hour = hourRaw.toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${dayOfMonth}/${month}/${year} ${hour}:${minute} ${amOrPm}`
|
||||
}
|
||||
|
||||
/** Formats a {@link Date} as a {@link Rfc3339DateTime} */
|
||||
export function toRfc3339(date: Date) {
|
||||
return newtype.asNewtype<Rfc3339DateTime>(date.toISOString())
|
||||
|
@ -126,6 +126,9 @@ export function useEvent<T>(): [event: T | null, dispatchEvent: (value: T | null
|
||||
|
||||
// === useDebugState ===
|
||||
|
||||
// `console.*` is allowed because this is for debugging purposes only.
|
||||
/* eslint-disable no-restricted-properties */
|
||||
|
||||
/** A modified `useState` that logs the old and new values when `setState` is called. */
|
||||
export function useDebugState<T>(
|
||||
initialState: T | (() => T),
|
||||
@ -138,29 +141,23 @@ export function useDebugState<T>(
|
||||
const setState = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<T>, source?: string) => {
|
||||
const fullDescription = `${description}${source != null ? ` from '${source}'` : ''}`
|
||||
if (typeof valueOrUpdater === 'function') {
|
||||
// This is UNSAFE, however React makes the same assumption.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const updater = valueOrUpdater as (prevState: T) => T
|
||||
rawSetState(oldState => {
|
||||
rawSetState(oldState => {
|
||||
const newState =
|
||||
typeof valueOrUpdater === 'function'
|
||||
? // This is UNSAFE when `T` is itself a function type,
|
||||
// however React makes the same assumption.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(valueOrUpdater as (prevState: T) => T)(oldState)
|
||||
: valueOrUpdater
|
||||
if (!Object.is(oldState, newState)) {
|
||||
console.group(description)
|
||||
console.trace(description)
|
||||
console.log(`Old ${fullDescription}:`, oldState)
|
||||
const newState = updater(oldState)
|
||||
console.log(`New ${fullDescription}:`, newState)
|
||||
console.groupEnd()
|
||||
return newState
|
||||
})
|
||||
} else {
|
||||
rawSetState(oldState => {
|
||||
if (!Object.is(oldState, valueOrUpdater)) {
|
||||
console.group(description)
|
||||
console.log(`Old ${fullDescription}:`, oldState)
|
||||
console.log(`New ${fullDescription}:`, valueOrUpdater)
|
||||
console.groupEnd()
|
||||
}
|
||||
return valueOrUpdater
|
||||
})
|
||||
}
|
||||
}
|
||||
return newState
|
||||
})
|
||||
},
|
||||
[description]
|
||||
)
|
||||
@ -194,6 +191,8 @@ function useMonitorDependencies(
|
||||
oldDependenciesRef.current = dependencies
|
||||
}
|
||||
|
||||
/* eslint-enable no-restricted-properties */
|
||||
|
||||
// === useDebugEffect ===
|
||||
|
||||
/** A modified `useEffect` that logs the old and new values of changed dependencies. */
|
||||
|
@ -25,6 +25,12 @@ interface NewtypeVariant<TypeName extends string> {
|
||||
* `a: string = asNewtype<Newtype<string, 'Name'>>(b)` successfully typechecks. */
|
||||
export type Newtype<T, TypeName extends string> = NewtypeVariant<TypeName> & T
|
||||
|
||||
/** Extracts the original type out of a {@link Newtype}.
|
||||
* Its only use is in {@link asNewtype}. */
|
||||
type UnNewtype<T extends Newtype<unknown, string>> = T extends infer U & NewtypeVariant<T['_$type']>
|
||||
? U
|
||||
: NotNewtype & Omit<T, '_$type'>
|
||||
|
||||
/** An interface that matches a type if and only if it is not a newtype. */
|
||||
interface NotNewtype {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@ -32,9 +38,7 @@ interface NotNewtype {
|
||||
}
|
||||
|
||||
/** Converts a value that is not a newtype, to a value that is a newtype. */
|
||||
export function asNewtype<T extends Newtype<unknown, string>>(
|
||||
s: NotNewtype & Omit<T, '_$type'>
|
||||
): T {
|
||||
export function asNewtype<T extends Newtype<unknown, string>>(s: UnNewtype<T>): T {
|
||||
// This cast is unsafe.
|
||||
// `T` has an extra property `_$type` which is used purely for typechecking
|
||||
// and does not exist at runtime.
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;600;700&display=swap");
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@ -6,7 +6,8 @@ body {
|
||||
|
||||
/* These styles MUST still be copied
|
||||
* as `.enso-dashboard body` and `.enso-dashboard html` make no sense. */
|
||||
.enso-dashboard {
|
||||
.enso-dashboard,
|
||||
.enso-chat {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-tab-size: 4;
|
||||
@ -16,7 +17,8 @@ body {
|
||||
font-feature-settings: normal;
|
||||
}
|
||||
|
||||
.enso-dashboard *:focus {
|
||||
.enso-dashboard *:focus,
|
||||
.enso-chat *:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@ -48,7 +50,8 @@ body {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.enso-dashboard {
|
||||
.enso-dashboard,
|
||||
.enso-chat {
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@ -90,6 +93,15 @@ body {
|
||||
);
|
||||
}
|
||||
|
||||
.clip-path-bottom-shadow {
|
||||
clip-path: polygon(
|
||||
0 0,
|
||||
100% 0,
|
||||
100% calc(100% + 100vh),
|
||||
0 calc(100% + 100vh)
|
||||
);
|
||||
}
|
||||
|
||||
.dasharray-5 {
|
||||
stroke-dasharray: calc(12 * 0.05 * 6.2832) calc(12 * 6.2832);
|
||||
}
|
||||
|
@ -21,6 +21,9 @@ export const theme = {
|
||||
// Should be `#3e515fe5`, but `bg-opacity` does not work with RGBA.
|
||||
/** The default color of all text. */
|
||||
primary: '#52636f',
|
||||
chat: '#484848',
|
||||
'ide-bg': '#ebeef1',
|
||||
'ide-bg-dark': '#d0d3d6',
|
||||
// Should be `#3e515f14`, but `bg-opacity` does not work with RGBA.
|
||||
label: '#f0f1f3',
|
||||
help: '#3f68ce',
|
||||
@ -34,14 +37,19 @@ export const theme = {
|
||||
'perm-docs-write': '#2db1c3',
|
||||
// Should be `#3e515f14`, but `bg-opacity` does not work with RGBA.
|
||||
'perm-none': '#f0f1f3',
|
||||
'call-to-action': '#fa6c08',
|
||||
'gray-350': '#b7bcc5',
|
||||
},
|
||||
flexGrow: {
|
||||
2: '2',
|
||||
},
|
||||
fontSize: {
|
||||
'xs-mini': '0.71875rem',
|
||||
vs: '0.8125rem',
|
||||
},
|
||||
spacing: {
|
||||
'83.5': '20.875rem',
|
||||
'70': '17.5rem',
|
||||
'10lh': '10lh',
|
||||
'140': '35rem',
|
||||
},
|
||||
@ -81,11 +89,16 @@ inset 0 -36px 51px -51px #00000014`,
|
||||
transitionProperty: {
|
||||
width: 'width',
|
||||
'stroke-dasharray': 'stroke-dasharray',
|
||||
'grid-template-rows': 'grid-template-rows',
|
||||
},
|
||||
transitionDuration: {
|
||||
'5000': '5000ms',
|
||||
'90000': '90000ms',
|
||||
},
|
||||
gridTemplateRows: {
|
||||
'0fr': '0fr',
|
||||
'1fr': '1fr',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
'fill-60': 'repeat(auto-fill, minmax(15rem, 1fr))',
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
/** @file File watch and compile service. */
|
||||
import * as module from 'node:module'
|
||||
import * as path from 'node:path'
|
||||
import * as url from 'node:url'
|
||||
|
||||
@ -32,12 +33,13 @@ OPTS.write = false
|
||||
OPTS.loader['.html'] = 'copy'
|
||||
OPTS.pure.splice(OPTS.pure.indexOf('assert'), 1)
|
||||
;(OPTS.inject = OPTS.inject ?? []).push(path.resolve(THIS_PATH, '..', '..', 'debugGlobals.ts'))
|
||||
const REQUIRE = module.default.createRequire(import.meta.url)
|
||||
OPTS.plugins.push({
|
||||
name: 'react-dom-profiling',
|
||||
setup: build => {
|
||||
build.onResolve({ filter: /^react-dom$/ }, args => {
|
||||
if (args.kind === 'import-statement') {
|
||||
return { path: 'react-dom/profiling' }
|
||||
return { path: REQUIRE.resolve('react-dom/profiling') }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
620
app/ide-desktop/package-lock.json
generated
620
app/ide-desktop/package-lock.json
generated
@ -441,6 +441,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"enso-chat": "git://github.com/enso-org/enso-bot#wip/sb/initial-implementation",
|
||||
"enso-content": "^1.0.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.8",
|
||||
@ -3380,6 +3381,110 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/builders": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.3.tgz",
|
||||
"integrity": "sha512-CTCh8NqED3iecTNuiz49mwSsrc2iQb4d0MjMdmS/8pb69Y4IlzJ/DIy/p5GFlgOrFbNO2WzMHkWKQSiJ3VNXaw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@discordjs/formatters": "^0.3.1",
|
||||
"@discordjs/util": "^0.3.1",
|
||||
"@sapphire/shapeshift": "^3.8.2",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.3",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/collection": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.1.tgz",
|
||||
"integrity": "sha512-aWEc9DCf3TMDe9iaJoOnO2+JVAjeRNuRxPZQA6GVvBf+Z3gqUuWYBy2NWh4+5CLYq5uoc3MOvUQ5H5m8CJBqOA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/formatters": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.1.tgz",
|
||||
"integrity": "sha512-M7X4IGiSeh4znwcRGcs+49B5tBkNDn4k5bmhxJDAUhRxRHTiFAOTVUNQ6yAKySu5jZTnCbSvTYHW3w0rAzV1MA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.37.41"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.7.1.tgz",
|
||||
"integrity": "sha512-Ofa9UqT0U45G/eX86cURQnX7gzOJLG2oC28VhIk/G6IliYgQF7jFByBJEykPSHE4MxPhqCleYvmsrtfKh1nYmQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^1.5.1",
|
||||
"@discordjs/util": "^0.3.0",
|
||||
"@sapphire/async-queue": "^1.5.0",
|
||||
"@sapphire/snowflake": "^3.4.2",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"file-type": "^18.3.0",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest/node_modules/file-type": {
|
||||
"version": "18.5.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-18.5.0.tgz",
|
||||
"integrity": "sha512-yvpl5U868+V6PqXHMmsESpg6unQ5GfnPssl4dxdJudBrr9qy7Fddt7EVX1VLlddFfe8Gj9N7goCZH22FXuSQXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"readable-web-to-node-stream": "^3.0.2",
|
||||
"strtok3": "^7.0.0",
|
||||
"token-types": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/util": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.3.1.tgz",
|
||||
"integrity": "sha512-HxXKYKg7vohx2/OupUN/4Sd02Ev3PBJ5q0gtjdcvXb0ErCva8jNHWfe/v5sU3UKjIB/uxOhc+TDOnhqffj9pRA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-0.8.3.tgz",
|
||||
"integrity": "sha512-hcYtppanjHecbdNyCKQNH2I4RP9UrphDgmRgLYrATEQF1oo4sYSve7ZmGsBEXSzH72MO2tBPdWSThunbxUVk0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^1.5.1",
|
||||
"@discordjs/rest": "^1.7.1",
|
||||
"@discordjs/util": "^0.3.1",
|
||||
"@sapphire/async-queue": "^1.5.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@vladfrangu/async_event_emitter": "^2.2.1",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"tslib": "^2.5.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "2.0.2",
|
||||
"dev": true,
|
||||
@ -4689,6 +4794,40 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz",
|
||||
"integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/shapeshift": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz",
|
||||
"integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/snowflake": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz",
|
||||
"integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.4",
|
||||
"license": "BSD-3-Clause",
|
||||
@ -4750,6 +4889,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
@ -5337,6 +5482,16 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vladfrangu/async_event_emitter": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz",
|
||||
"integrity": "sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/7zip-bin": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
@ -6015,6 +6170,17 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.4.0.tgz",
|
||||
"integrity": "sha512-NmsNW1CQvqMszu/CFAJ3pLct6NEFlNfuGM6vw72KHkjOD1UDnL96XNN1BMQc1hiHo8vE2GbOWQYIpZ+YM5wrZw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "2.4.0",
|
||||
"dev": true,
|
||||
@ -6031,6 +6197,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"license": "MIT",
|
||||
@ -6214,6 +6389,18 @@
|
||||
"version": "1.1.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
@ -7237,6 +7424,37 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.37.47",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.47.tgz",
|
||||
"integrity": "sha512-rNif8IAv6duS2z47BMXq/V9kkrLfkAoiwpFY3sLxxbyKprk065zqf3HLTg4bEoxRSmi+Lhc7yqGDrG8C3j8GFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/discord.js": {
|
||||
"version": "14.11.0",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.11.0.tgz",
|
||||
"integrity": "sha512-CkueWYFQ28U38YPR8HgsBR/QT35oPpMbEsTNM30Fs8loBIhnA4s70AwQEoy6JvLcpWWJO7GY0y2BUzZmuBMepQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.6.3",
|
||||
"@discordjs/collection": "^1.5.1",
|
||||
"@discordjs/formatters": "^0.3.1",
|
||||
"@discordjs/rest": "^1.7.1",
|
||||
"@discordjs/util": "^0.3.1",
|
||||
"@discordjs/ws": "^0.8.3",
|
||||
"@sapphire/snowflake": "^3.4.2",
|
||||
"@types/ws": "^8.5.4",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.22.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dlv": {
|
||||
"version": "1.1.3",
|
||||
"dev": true,
|
||||
@ -7432,6 +7650,18 @@
|
||||
"resolved": "lib/dashboard/src/authentication",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/enso-chat": {
|
||||
"name": "enso-support",
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+ssh://git@github.com/enso-org/enso-bot.git#3b76888ec6bd3579016e70ef83ba282714aec47d",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.4.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enso-common": {
|
||||
"resolved": "lib/common",
|
||||
"link": true
|
||||
@ -8478,6 +8708,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"dev": true,
|
||||
@ -10674,6 +10910,12 @@
|
||||
"version": "4.6.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"license": "MIT",
|
||||
@ -12551,6 +12793,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/peek-readable": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
|
||||
"integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
@ -13362,6 +13617,22 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-web-to-node-stream": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
|
||||
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"dev": true,
|
||||
@ -14552,6 +14823,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
@ -14698,6 +14978,23 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz",
|
||||
"integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"peek-readable": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/sudo-prompt": {
|
||||
"version": "9.2.1",
|
||||
"license": "MIT",
|
||||
@ -15122,6 +15419,23 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/token-types": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz",
|
||||
"integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"ieee754": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"dev": true,
|
||||
@ -15146,6 +15460,12 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-mixer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz",
|
||||
"integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.1",
|
||||
"dev": true,
|
||||
@ -15195,8 +15515,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.4.1",
|
||||
"license": "0BSD"
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
|
||||
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
|
||||
},
|
||||
"node_modules/tsutils": {
|
||||
"version": "3.21.0",
|
||||
@ -15344,6 +15665,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.22.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz",
|
||||
"integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"busboy": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unfetch": {
|
||||
"version": "4.2.0",
|
||||
"license": "MIT"
|
||||
@ -15823,6 +16156,27 @@
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xdg-basedir": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@ -17910,6 +18264,88 @@
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"@discordjs/builders": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.3.tgz",
|
||||
"integrity": "sha512-CTCh8NqED3iecTNuiz49mwSsrc2iQb4d0MjMdmS/8pb69Y4IlzJ/DIy/p5GFlgOrFbNO2WzMHkWKQSiJ3VNXaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@discordjs/formatters": "^0.3.1",
|
||||
"@discordjs/util": "^0.3.1",
|
||||
"@sapphire/shapeshift": "^3.8.2",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.3",
|
||||
"tslib": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"@discordjs/collection": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.1.tgz",
|
||||
"integrity": "sha512-aWEc9DCf3TMDe9iaJoOnO2+JVAjeRNuRxPZQA6GVvBf+Z3gqUuWYBy2NWh4+5CLYq5uoc3MOvUQ5H5m8CJBqOA==",
|
||||
"dev": true
|
||||
},
|
||||
"@discordjs/formatters": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.1.tgz",
|
||||
"integrity": "sha512-M7X4IGiSeh4znwcRGcs+49B5tBkNDn4k5bmhxJDAUhRxRHTiFAOTVUNQ6yAKySu5jZTnCbSvTYHW3w0rAzV1MA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"discord-api-types": "^0.37.41"
|
||||
}
|
||||
},
|
||||
"@discordjs/rest": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.7.1.tgz",
|
||||
"integrity": "sha512-Ofa9UqT0U45G/eX86cURQnX7gzOJLG2oC28VhIk/G6IliYgQF7jFByBJEykPSHE4MxPhqCleYvmsrtfKh1nYmQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@discordjs/collection": "^1.5.1",
|
||||
"@discordjs/util": "^0.3.0",
|
||||
"@sapphire/async-queue": "^1.5.0",
|
||||
"@sapphire/snowflake": "^3.4.2",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"file-type": "^18.3.0",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.22.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"file-type": {
|
||||
"version": "18.5.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-18.5.0.tgz",
|
||||
"integrity": "sha512-yvpl5U868+V6PqXHMmsESpg6unQ5GfnPssl4dxdJudBrr9qy7Fddt7EVX1VLlddFfe8Gj9N7goCZH22FXuSQXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-web-to-node-stream": "^3.0.2",
|
||||
"strtok3": "^7.0.0",
|
||||
"token-types": "^5.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@discordjs/util": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.3.1.tgz",
|
||||
"integrity": "sha512-HxXKYKg7vohx2/OupUN/4Sd02Ev3PBJ5q0gtjdcvXb0ErCva8jNHWfe/v5sU3UKjIB/uxOhc+TDOnhqffj9pRA==",
|
||||
"dev": true
|
||||
},
|
||||
"@discordjs/ws": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-0.8.3.tgz",
|
||||
"integrity": "sha512-hcYtppanjHecbdNyCKQNH2I4RP9UrphDgmRgLYrATEQF1oo4sYSve7ZmGsBEXSzH72MO2tBPdWSThunbxUVk0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@discordjs/collection": "^1.5.1",
|
||||
"@discordjs/rest": "^1.7.1",
|
||||
"@discordjs/util": "^0.3.1",
|
||||
"@sapphire/async-queue": "^1.5.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@vladfrangu/async_event_emitter": "^2.2.1",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"tslib": "^2.5.0",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"@electron/get": {
|
||||
"version": "2.0.2",
|
||||
"dev": true,
|
||||
@ -18729,6 +19165,28 @@
|
||||
"@remix-run/router": {
|
||||
"version": "1.3.3"
|
||||
},
|
||||
"@sapphire/async-queue": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz",
|
||||
"integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==",
|
||||
"dev": true
|
||||
},
|
||||
"@sapphire/shapeshift": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz",
|
||||
"integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"@sapphire/snowflake": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz",
|
||||
"integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==",
|
||||
"dev": true
|
||||
},
|
||||
"@sideway/address": {
|
||||
"version": "4.1.4",
|
||||
"peer": true,
|
||||
@ -18773,6 +19231,12 @@
|
||||
"defer-to-connect": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
||||
"dev": true
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"dev": true
|
||||
@ -19170,6 +19634,12 @@
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@vladfrangu/async_event_emitter": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz",
|
||||
"integrity": "sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==",
|
||||
"dev": true
|
||||
},
|
||||
"7zip-bin": {
|
||||
"version": "5.1.1",
|
||||
"dev": true
|
||||
@ -19610,6 +20080,16 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.4.0.tgz",
|
||||
"integrity": "sha512-NmsNW1CQvqMszu/CFAJ3pLct6NEFlNfuGM6vw72KHkjOD1UDnL96XNN1BMQc1hiHo8vE2GbOWQYIpZ+YM5wrZw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "2.4.0",
|
||||
"dev": true
|
||||
@ -19618,6 +20098,15 @@
|
||||
"version": "2.2.0",
|
||||
"dev": true
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.1.0",
|
||||
"requires": {
|
||||
@ -19733,6 +20222,15 @@
|
||||
"buffer-from": {
|
||||
"version": "1.1.2"
|
||||
},
|
||||
"busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"streamsearch": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.0.0",
|
||||
"peer": true
|
||||
@ -20377,6 +20875,34 @@
|
||||
"path-type": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"discord-api-types": {
|
||||
"version": "0.37.47",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.47.tgz",
|
||||
"integrity": "sha512-rNif8IAv6duS2z47BMXq/V9kkrLfkAoiwpFY3sLxxbyKprk065zqf3HLTg4bEoxRSmi+Lhc7yqGDrG8C3j8GFA==",
|
||||
"dev": true
|
||||
},
|
||||
"discord.js": {
|
||||
"version": "14.11.0",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.11.0.tgz",
|
||||
"integrity": "sha512-CkueWYFQ28U38YPR8HgsBR/QT35oPpMbEsTNM30Fs8loBIhnA4s70AwQEoy6JvLcpWWJO7GY0y2BUzZmuBMepQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@discordjs/builders": "^1.6.3",
|
||||
"@discordjs/collection": "^1.5.1",
|
||||
"@discordjs/formatters": "^0.3.1",
|
||||
"@discordjs/rest": "^1.7.1",
|
||||
"@discordjs/util": "^0.3.1",
|
||||
"@discordjs/ws": "^0.8.3",
|
||||
"@sapphire/snowflake": "^3.4.2",
|
||||
"@types/ws": "^8.5.4",
|
||||
"discord-api-types": "^0.37.41",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.22.0",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"dlv": {
|
||||
"version": "1.1.3",
|
||||
"dev": true
|
||||
@ -20770,6 +21296,16 @@
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
},
|
||||
"enso-chat": {
|
||||
"version": "git+ssh://git@github.com/enso-org/enso-bot.git#3b76888ec6bd3579016e70ef83ba282714aec47d",
|
||||
"dev": true,
|
||||
"from": "enso-chat@git://github.com/enso-org/enso-bot#wip/sb/initial-implementation",
|
||||
"requires": {
|
||||
"better-sqlite3": "^8.4.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"enso-common": {
|
||||
"version": "file:lib/common"
|
||||
},
|
||||
@ -20823,6 +21359,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"enso-chat": "git://github.com/enso-org/enso-bot#wip/sb/initial-implementation",
|
||||
"enso-content": "^1.0.0",
|
||||
"esbuild": "^0.17.15",
|
||||
"esbuild-plugin-time": "^1.0.0",
|
||||
@ -21562,6 +22099,12 @@
|
||||
"version": "3.9.0",
|
||||
"dev": true
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"dev": true
|
||||
},
|
||||
"filelist": {
|
||||
"version": "1.0.4",
|
||||
"dev": true,
|
||||
@ -22960,6 +23503,12 @@
|
||||
"lodash.merge": {
|
||||
"version": "4.6.2"
|
||||
},
|
||||
"lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"peer": true
|
||||
@ -24267,6 +24816,12 @@
|
||||
"version": "4.0.0",
|
||||
"dev": true
|
||||
},
|
||||
"peek-readable": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
|
||||
"integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==",
|
||||
"dev": true
|
||||
},
|
||||
"pend": {
|
||||
"version": "1.2.0",
|
||||
"dev": true
|
||||
@ -24765,6 +25320,15 @@
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"readable-web-to-node-stream": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
|
||||
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-stream": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"dev": true,
|
||||
@ -25566,6 +26130,12 @@
|
||||
"stream-to": "~0.2.0"
|
||||
}
|
||||
},
|
||||
"streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"dev": true
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"requires": {
|
||||
@ -25650,6 +26220,16 @@
|
||||
"version": "1.0.5",
|
||||
"peer": true
|
||||
},
|
||||
"strtok3": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz",
|
||||
"integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"peek-readable": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"sudo-prompt": {
|
||||
"version": "9.2.1",
|
||||
"peer": true
|
||||
@ -25950,6 +26530,16 @@
|
||||
"version": "1.0.1",
|
||||
"peer": true
|
||||
},
|
||||
"token-types": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz",
|
||||
"integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"dev": true,
|
||||
@ -25968,6 +26558,12 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"ts-mixer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz",
|
||||
"integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.1",
|
||||
"dev": true,
|
||||
@ -25993,7 +26589,9 @@
|
||||
"version": "3.3.0"
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.4.1"
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
|
||||
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
|
||||
},
|
||||
"tsutils": {
|
||||
"version": "3.21.0",
|
||||
@ -26086,6 +26684,15 @@
|
||||
"which-boxed-primitive": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"undici": {
|
||||
"version": "5.22.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz",
|
||||
"integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"busboy": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"unfetch": {
|
||||
"version": "4.2.0"
|
||||
},
|
||||
@ -26407,6 +27014,13 @@
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"xdg-basedir": {
|
||||
"version": "4.0.0",
|
||||
"dev": true
|
||||
|
Loading…
Reference in New Issue
Block a user