@overcrow/mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -2
- package/dist/audit/index.js +103 -0
- package/dist/audit/lexer.js +417 -0
- package/dist/audit/project.js +200 -0
- package/dist/audit/rules.js +661 -0
- package/dist/bootstrap/download.js +229 -0
- package/dist/bootstrap/install.js +280 -0
- package/dist/bootstrap/pin.js +85 -0
- package/dist/bootstrap/pin.json +119 -0
- package/dist/bootstrap/zip.js +258 -0
- package/dist/cli.js +200 -0
- package/dist/confine.js +244 -0
- package/dist/content/docs/en/cli.md +353 -0
- package/dist/content/docs/en/dev-channel.md +130 -0
- package/dist/content/docs/en/elements.md +469 -0
- package/dist/content/docs/en/forms.md +328 -0
- package/dist/content/docs/en/guide.md +253 -0
- package/dist/content/docs/en/index.md +85 -0
- package/dist/content/docs/en/limits.md +241 -0
- package/dist/content/docs/en/logic.md +446 -0
- package/dist/content/docs/en/manifest.md +381 -0
- package/dist/content/docs/en/mcp.md +165 -0
- package/dist/content/docs/en/package.md +131 -0
- package/dist/content/docs/en/publishing.md +162 -0
- package/dist/content/docs/en/reference.md +55 -0
- package/dist/content/docs/en/result-shapes.md +332 -0
- package/dist/content/docs/en/sdk.md +295 -0
- package/dist/content/docs/en/security.md +115 -0
- package/dist/content/docs/en/service-reference.md +344 -0
- package/dist/content/docs/en/services.md +517 -0
- package/dist/content/docs/en/style-properties.md +270 -0
- package/dist/content/docs/en/style.md +266 -0
- package/dist/content/docs/en/testing.md +400 -0
- package/dist/content/docs/en/view.md +338 -0
- package/dist/content/docs/en/widgets.md +76 -0
- package/dist/content/docs/fr/cli.md +367 -0
- package/dist/content/docs/fr/dev-channel.md +140 -0
- package/dist/content/docs/fr/elements.md +475 -0
- package/dist/content/docs/fr/forms.md +341 -0
- package/dist/content/docs/fr/guide.md +264 -0
- package/dist/content/docs/fr/index.md +90 -0
- package/dist/content/docs/fr/limits.md +244 -0
- package/dist/content/docs/fr/logic.md +461 -0
- package/dist/content/docs/fr/manifest.md +392 -0
- package/dist/content/docs/fr/mcp.md +173 -0
- package/dist/content/docs/fr/package.md +139 -0
- package/dist/content/docs/fr/publishing.md +172 -0
- package/dist/content/docs/fr/reference.md +59 -0
- package/dist/content/docs/fr/result-shapes.md +333 -0
- package/dist/content/docs/fr/sdk.md +301 -0
- package/dist/content/docs/fr/security.md +129 -0
- package/dist/content/docs/fr/service-reference.md +346 -0
- package/dist/content/docs/fr/services.md +540 -0
- package/dist/content/docs/fr/style-properties.md +272 -0
- package/dist/content/docs/fr/style.md +281 -0
- package/dist/content/docs/fr/testing.md +417 -0
- package/dist/content/docs/fr/view.md +352 -0
- package/dist/content/docs/fr/widgets.md +81 -0
- package/dist/content/examples/clock/LICENSE +21 -0
- package/dist/content/examples/clock/README.md +75 -0
- package/dist/content/examples/clock/listing.json +19 -0
- package/dist/content/examples/clock/locales/en.json +4 -0
- package/dist/content/examples/clock/locales/fr.json +4 -0
- package/dist/content/examples/clock/logic.ts +85 -0
- package/dist/content/examples/clock/manifest.json +44 -0
- package/dist/content/examples/clock/package.json +16 -0
- package/dist/content/examples/clock/style.ocss +26 -0
- package/dist/content/examples/clock/tests/hidden.scenario.json +68 -0
- package/dist/content/examples/clock/tests/load-logic.mjs +22 -0
- package/dist/content/examples/clock/tests/logic.test.mjs +89 -0
- package/dist/content/examples/clock/tests/options.scenario.json +108 -0
- package/dist/content/examples/clock/tests/preview.scenario.json +25 -0
- package/dist/content/examples/clock/tests/render.scenario.json +94 -0
- package/dist/content/examples/clock/tests/summer-time.scenario.json +74 -0
- package/dist/content/examples/clock/tests/ticks.scenario.json +120 -0
- package/dist/content/examples/clock/tsconfig.json +16 -0
- package/dist/content/examples/clock/view.ocml +8 -0
- package/dist/content/examples/countdown/LICENSE +20 -0
- package/dist/content/examples/countdown/listing.json +10 -0
- package/dist/content/examples/countdown/locales/en.json +15 -0
- package/dist/content/examples/countdown/locales/fr.json +15 -0
- package/dist/content/examples/countdown/logic.ts +179 -0
- package/dist/content/examples/countdown/manifest.json +34 -0
- package/dist/content/examples/countdown/package.json +15 -0
- package/dist/content/examples/countdown/style.ocss +94 -0
- package/dist/content/examples/countdown/tsconfig.json +16 -0
- package/dist/content/examples/countdown/view.ocml +42 -0
- package/dist/content/examples/fps/LICENSE +21 -0
- package/dist/content/examples/fps/README.md +64 -0
- package/dist/content/examples/fps/listing.json +19 -0
- package/dist/content/examples/fps/locales/en.json +11 -0
- package/dist/content/examples/fps/locales/fr.json +11 -0
- package/dist/content/examples/fps/logic.ts +64 -0
- package/dist/content/examples/fps/manifest.json +30 -0
- package/dist/content/examples/fps/package.json +16 -0
- package/dist/content/examples/fps/style.ocss +31 -0
- package/dist/content/examples/fps/tests/label.scenario.json +63 -0
- package/dist/content/examples/fps/tests/load-logic.mjs +22 -0
- package/dist/content/examples/fps/tests/logic.test.mjs +60 -0
- package/dist/content/examples/fps/tests/preview.scenario.json +29 -0
- package/dist/content/examples/fps/tests/refused.scenario.json +20 -0
- package/dist/content/examples/fps/tests/render.scenario.json +96 -0
- package/dist/content/examples/fps/tests/stale.scenario.json +91 -0
- package/dist/content/examples/fps/tests/unavailable.scenario.json +111 -0
- package/dist/content/examples/fps/tsconfig.json +16 -0
- package/dist/content/examples/fps/view.ocml +11 -0
- package/dist/content/examples/media/LICENSE +21 -0
- package/dist/content/examples/media/README.md +147 -0
- package/dist/content/examples/media/listing.json +19 -0
- package/dist/content/examples/media/locales/en.json +11 -0
- package/dist/content/examples/media/locales/fr.json +11 -0
- package/dist/content/examples/media/logic.ts +191 -0
- package/dist/content/examples/media/manifest.json +35 -0
- package/dist/content/examples/media/package.json +16 -0
- package/dist/content/examples/media/style.ocss +148 -0
- package/dist/content/examples/media/tests/covers.scenario.json +144 -0
- package/dist/content/examples/media/tests/gesture.scenario.json +173 -0
- package/dist/content/examples/media/tests/load-logic.mjs +22 -0
- package/dist/content/examples/media/tests/logic.test.mjs +176 -0
- package/dist/content/examples/media/tests/marquee.scenario.json +127 -0
- package/dist/content/examples/media/tests/preview.scenario.json +52 -0
- package/dist/content/examples/media/tests/read-only.scenario.json +53 -0
- package/dist/content/examples/media/tests/refused.scenario.json +22 -0
- package/dist/content/examples/media/tests/refused.test.mjs +31 -0
- package/dist/content/examples/media/tests/render.scenario.json +136 -0
- package/dist/content/examples/media/tests/states.scenario.json +257 -0
- package/dist/content/examples/media/tests/unavailable.scenario.json +120 -0
- package/dist/content/examples/media/tsconfig.json +16 -0
- package/dist/content/examples/media/view.ocml +41 -0
- package/dist/content/examples/notes/LICENSE +21 -0
- package/dist/content/examples/notes/README.md +219 -0
- package/dist/content/examples/notes/listing.json +19 -0
- package/dist/content/examples/notes/locales/en.json +25 -0
- package/dist/content/examples/notes/locales/fr.json +25 -0
- package/dist/content/examples/notes/logic.ts +799 -0
- package/dist/content/examples/notes/manifest.json +40 -0
- package/dist/content/examples/notes/package.json +16 -0
- package/dist/content/examples/notes/style.ocss +469 -0
- package/dist/content/examples/notes/tests/checks.scenario.json +237 -0
- package/dist/content/examples/notes/tests/create.scenario.json +260 -0
- package/dist/content/examples/notes/tests/delete.scenario.json +342 -0
- package/dist/content/examples/notes/tests/drafts.scenario.json +714 -0
- package/dist/content/examples/notes/tests/editor.scenario.json +232 -0
- package/dist/content/examples/notes/tests/frame.scenario.json +93 -0
- package/dist/content/examples/notes/tests/interactive.scenario.json +344 -0
- package/dist/content/examples/notes/tests/limits.scenario.json +1351 -0
- package/dist/content/examples/notes/tests/load-logic.mjs +22 -0
- package/dist/content/examples/notes/tests/logic.test.mjs +514 -0
- package/dist/content/examples/notes/tests/preview.scenario.json +99 -0
- package/dist/content/examples/notes/tests/read-only.scenario.json +105 -0
- package/dist/content/examples/notes/tests/refused.scenario.json +33 -0
- package/dist/content/examples/notes/tests/save.scenario.json +378 -0
- package/dist/content/examples/notes/tests/states.scenario.json +640 -0
- package/dist/content/examples/notes/tests/typing.scenario.json +562 -0
- package/dist/content/examples/notes/tests/unavailable.scenario.json +246 -0
- package/dist/content/examples/notes/tsconfig.json +16 -0
- package/dist/content/examples/notes/view.ocml +154 -0
- package/dist/content/examples/performance/LICENSE +21 -0
- package/dist/content/examples/performance/README.md +134 -0
- package/dist/content/examples/performance/listing.json +19 -0
- package/dist/content/examples/performance/locales/en.json +20 -0
- package/dist/content/examples/performance/locales/fr.json +20 -0
- package/dist/content/examples/performance/logic.ts +210 -0
- package/dist/content/examples/performance/manifest.json +83 -0
- package/dist/content/examples/performance/package.json +16 -0
- package/dist/content/examples/performance/style.ocss +75 -0
- package/dist/content/examples/performance/tests/fahrenheit.scenario.json +110 -0
- package/dist/content/examples/performance/tests/horizontal.scenario.json +96 -0
- package/dist/content/examples/performance/tests/load-logic.mjs +22 -0
- package/dist/content/examples/performance/tests/logic.test.mjs +226 -0
- package/dist/content/examples/performance/tests/menu.scenario.json +170 -0
- package/dist/content/examples/performance/tests/no-fps-source.scenario.json +47 -0
- package/dist/content/examples/performance/tests/preview.scenario.json +45 -0
- package/dist/content/examples/performance/tests/refused.scenario.json +20 -0
- package/dist/content/examples/performance/tests/refused.test.mjs +19 -0
- package/dist/content/examples/performance/tests/region.scenario.json +89 -0
- package/dist/content/examples/performance/tests/render.scenario.json +112 -0
- package/dist/content/examples/performance/tests/sources.scenario.json +151 -0
- package/dist/content/examples/performance/tests/stale.scenario.json +111 -0
- package/dist/content/examples/performance/tests/telemetry-only.scenario.json +44 -0
- package/dist/content/examples/performance/tests/thresholds.scenario.json +147 -0
- package/dist/content/examples/performance/tests/waiting.scenario.json +105 -0
- package/dist/content/examples/performance/tsconfig.json +16 -0
- package/dist/content/examples/performance/view.ocml +18 -0
- package/dist/content/examples/playervox-journal/LICENSE +21 -0
- package/dist/content/examples/playervox-journal/README.md +150 -0
- package/dist/content/examples/playervox-journal/listing.json +19 -0
- package/dist/content/examples/playervox-journal/locales/en.json +25 -0
- package/dist/content/examples/playervox-journal/locales/fr.json +25 -0
- package/dist/content/examples/playervox-journal/logic.ts +556 -0
- package/dist/content/examples/playervox-journal/manifest.json +19 -0
- package/dist/content/examples/playervox-journal/package.json +16 -0
- package/dist/content/examples/playervox-journal/style.ocss +142 -0
- package/dist/content/examples/playervox-journal/tests/changes.scenario.json +482 -0
- package/dist/content/examples/playervox-journal/tests/delete.scenario.json +432 -0
- package/dist/content/examples/playervox-journal/tests/frame.scenario.json +169 -0
- package/dist/content/examples/playervox-journal/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-journal/tests/logic.test.mjs +349 -0
- package/dist/content/examples/playervox-journal/tests/paging.scenario.json +428 -0
- package/dist/content/examples/playervox-journal/tests/preview.scenario.json +184 -0
- package/dist/content/examples/playervox-journal/tests/read-only.scenario.json +157 -0
- package/dist/content/examples/playervox-journal/tests/refused.scenario.json +39 -0
- package/dist/content/examples/playervox-journal/tests/scale.scenario.json +286 -0
- package/dist/content/examples/playervox-journal/tests/signed-out.scenario.json +134 -0
- package/dist/content/examples/playervox-journal/tests/states.scenario.json +741 -0
- package/dist/content/examples/playervox-journal/tests/unavailable.scenario.json +183 -0
- package/dist/content/examples/playervox-journal/tsconfig.json +16 -0
- package/dist/content/examples/playervox-journal/view.ocml +52 -0
- package/dist/content/examples/playervox-rating/LICENSE +21 -0
- package/dist/content/examples/playervox-rating/README.md +166 -0
- package/dist/content/examples/playervox-rating/listing.json +19 -0
- package/dist/content/examples/playervox-rating/locales/en.json +35 -0
- package/dist/content/examples/playervox-rating/locales/fr.json +35 -0
- package/dist/content/examples/playervox-rating/logic.ts +581 -0
- package/dist/content/examples/playervox-rating/manifest.json +19 -0
- package/dist/content/examples/playervox-rating/package.json +16 -0
- package/dist/content/examples/playervox-rating/style.ocss +333 -0
- package/dist/content/examples/playervox-rating/tests/account.scenario.json +125 -0
- package/dist/content/examples/playervox-rating/tests/bounds.scenario.json +73 -0
- package/dist/content/examples/playervox-rating/tests/draft.scenario.json +199 -0
- package/dist/content/examples/playervox-rating/tests/form.scenario.json +142 -0
- package/dist/content/examples/playervox-rating/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-rating/tests/logic.test.mjs +346 -0
- package/dist/content/examples/playervox-rating/tests/offline.scenario.json +142 -0
- package/dist/content/examples/playervox-rating/tests/preview.scenario.json +44 -0
- package/dist/content/examples/playervox-rating/tests/publish-keyboard.scenario.json +221 -0
- package/dist/content/examples/playervox-rating/tests/publish-pointer.scenario.json +314 -0
- package/dist/content/examples/playervox-rating/tests/read-only.scenario.json +70 -0
- package/dist/content/examples/playervox-rating/tests/refused.scenario.json +45 -0
- package/dist/content/examples/playervox-rating/tests/states.scenario.json +362 -0
- package/dist/content/examples/playervox-rating/tsconfig.json +16 -0
- package/dist/content/examples/playervox-rating/view.ocml +62 -0
- package/dist/content/examples/playervox-reviews/LICENSE +21 -0
- package/dist/content/examples/playervox-reviews/README.md +194 -0
- package/dist/content/examples/playervox-reviews/listing.json +19 -0
- package/dist/content/examples/playervox-reviews/locales/en.json +32 -0
- package/dist/content/examples/playervox-reviews/locales/fr.json +32 -0
- package/dist/content/examples/playervox-reviews/logic.ts +822 -0
- package/dist/content/examples/playervox-reviews/manifest.json +30 -0
- package/dist/content/examples/playervox-reviews/package.json +16 -0
- package/dist/content/examples/playervox-reviews/style.ocss +260 -0
- package/dist/content/examples/playervox-reviews/tests/account.scenario.json +249 -0
- package/dist/content/examples/playervox-reviews/tests/bounds.scenario.json +336 -0
- package/dist/content/examples/playervox-reviews/tests/changes.scenario.json +515 -0
- package/dist/content/examples/playervox-reviews/tests/filter.scenario.json +400 -0
- package/dist/content/examples/playervox-reviews/tests/frame.scenario.json +185 -0
- package/dist/content/examples/playervox-reviews/tests/keyboard.scenario.json +188 -0
- package/dist/content/examples/playervox-reviews/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-reviews/tests/logic.test.mjs +559 -0
- package/dist/content/examples/playervox-reviews/tests/offline-start.scenario.json +152 -0
- package/dist/content/examples/playervox-reviews/tests/offline.scenario.json +311 -0
- package/dist/content/examples/playervox-reviews/tests/paging.scenario.json +497 -0
- package/dist/content/examples/playervox-reviews/tests/preview.scenario.json +286 -0
- package/dist/content/examples/playervox-reviews/tests/reading.scenario.json +591 -0
- package/dist/content/examples/playervox-reviews/tests/refused.scenario.json +54 -0
- package/dist/content/examples/playervox-reviews/tests/scale.scenario.json +303 -0
- package/dist/content/examples/playervox-reviews/tests/states.scenario.json +700 -0
- package/dist/content/examples/playervox-reviews/tests/unavailable.scenario.json +192 -0
- package/dist/content/examples/playervox-reviews/tsconfig.json +16 -0
- package/dist/content/examples/playervox-reviews/view.ocml +110 -0
- package/dist/content/examples/playervox-score/LICENSE +21 -0
- package/dist/content/examples/playervox-score/README.md +131 -0
- package/dist/content/examples/playervox-score/listing.json +19 -0
- package/dist/content/examples/playervox-score/locales/en.json +23 -0
- package/dist/content/examples/playervox-score/locales/fr.json +23 -0
- package/dist/content/examples/playervox-score/logic.ts +430 -0
- package/dist/content/examples/playervox-score/manifest.json +47 -0
- package/dist/content/examples/playervox-score/package.json +16 -0
- package/dist/content/examples/playervox-score/style.ocss +247 -0
- package/dist/content/examples/playervox-score/tests/grades.scenario.json +279 -0
- package/dist/content/examples/playervox-score/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-score/tests/logic.test.mjs +229 -0
- package/dist/content/examples/playervox-score/tests/options.scenario.json +302 -0
- package/dist/content/examples/playervox-score/tests/preview.scenario.json +68 -0
- package/dist/content/examples/playervox-score/tests/pulse.scenario.json +157 -0
- package/dist/content/examples/playervox-score/tests/refused.scenario.json +21 -0
- package/dist/content/examples/playervox-score/tests/signed-out.scenario.json +46 -0
- package/dist/content/examples/playervox-score/tests/states.scenario.json +450 -0
- package/dist/content/examples/playervox-score/tests/unavailable.scenario.json +84 -0
- package/dist/content/examples/playervox-score/tsconfig.json +16 -0
- package/dist/content/examples/playervox-score/view.ocml +51 -0
- package/dist/content/examples/session/LICENSE +21 -0
- package/dist/content/examples/session/README.md +57 -0
- package/dist/content/examples/session/listing.json +19 -0
- package/dist/content/examples/session/locales/en.json +3 -0
- package/dist/content/examples/session/locales/fr.json +3 -0
- package/dist/content/examples/session/logic.ts +26 -0
- package/dist/content/examples/session/manifest.json +19 -0
- package/dist/content/examples/session/package.json +16 -0
- package/dist/content/examples/session/style.ocss +16 -0
- package/dist/content/examples/session/tests/hidden.scenario.json +45 -0
- package/dist/content/examples/session/tests/load-logic.mjs +22 -0
- package/dist/content/examples/session/tests/logic.test.mjs +37 -0
- package/dist/content/examples/session/tests/preview.scenario.json +28 -0
- package/dist/content/examples/session/tests/refused.scenario.json +20 -0
- package/dist/content/examples/session/tests/refused.test.mjs +16 -0
- package/dist/content/examples/session/tests/render.scenario.json +95 -0
- package/dist/content/examples/session/tests/ticks.scenario.json +139 -0
- package/dist/content/examples/session/tests/unknown.scenario.json +37 -0
- package/dist/content/examples/session/tsconfig.json +16 -0
- package/dist/content/examples/session/view.ocml +11 -0
- package/dist/content/examples/stopwatch/LICENSE +21 -0
- package/dist/content/examples/stopwatch/README.md +99 -0
- package/dist/content/examples/stopwatch/listing.json +19 -0
- package/dist/content/examples/stopwatch/locales/en.json +8 -0
- package/dist/content/examples/stopwatch/locales/fr.json +8 -0
- package/dist/content/examples/stopwatch/logic.ts +93 -0
- package/dist/content/examples/stopwatch/manifest.json +19 -0
- package/dist/content/examples/stopwatch/package.json +16 -0
- package/dist/content/examples/stopwatch/style.ocss +54 -0
- package/dist/content/examples/stopwatch/tests/errors.scenario.json +121 -0
- package/dist/content/examples/stopwatch/tests/gesture.scenario.json +154 -0
- package/dist/content/examples/stopwatch/tests/hidden.scenario.json +57 -0
- package/dist/content/examples/stopwatch/tests/hundredths.scenario.json +173 -0
- package/dist/content/examples/stopwatch/tests/load-logic.mjs +22 -0
- package/dist/content/examples/stopwatch/tests/logic.test.mjs +99 -0
- package/dist/content/examples/stopwatch/tests/preview.scenario.json +36 -0
- package/dist/content/examples/stopwatch/tests/read-only.scenario.json +42 -0
- package/dist/content/examples/stopwatch/tests/read-only.test.mjs +18 -0
- package/dist/content/examples/stopwatch/tests/refused.scenario.json +22 -0
- package/dist/content/examples/stopwatch/tests/refused.test.mjs +18 -0
- package/dist/content/examples/stopwatch/tests/render.scenario.json +105 -0
- package/dist/content/examples/stopwatch/tests/states.scenario.json +246 -0
- package/dist/content/examples/stopwatch/tests/unknown.scenario.json +40 -0
- package/dist/content/examples/stopwatch/tsconfig.json +16 -0
- package/dist/content/examples/stopwatch/view.ocml +29 -0
- package/dist/content/examples/twitch-chat/LICENSE +21 -0
- package/dist/content/examples/twitch-chat/README.md +283 -0
- package/dist/content/examples/twitch-chat/listing.json +19 -0
- package/dist/content/examples/twitch-chat/locales/en.json +52 -0
- package/dist/content/examples/twitch-chat/locales/fr.json +52 -0
- package/dist/content/examples/twitch-chat/logic.ts +887 -0
- package/dist/content/examples/twitch-chat/manifest.json +33 -0
- package/dist/content/examples/twitch-chat/package.json +16 -0
- package/dist/content/examples/twitch-chat/style.ocss +544 -0
- package/dist/content/examples/twitch-chat/tests/account.scenario.json +325 -0
- package/dist/content/examples/twitch-chat/tests/burst.scenario.json +1866 -0
- package/dist/content/examples/twitch-chat/tests/channel-refused.scenario.json +372 -0
- package/dist/content/examples/twitch-chat/tests/channel.scenario.json +480 -0
- package/dist/content/examples/twitch-chat/tests/draft.scenario.json +279 -0
- package/dist/content/examples/twitch-chat/tests/fade.scenario.json +269 -0
- package/dist/content/examples/twitch-chat/tests/failures.scenario.json +420 -0
- package/dist/content/examples/twitch-chat/tests/favorites.scenario.json +376 -0
- package/dist/content/examples/twitch-chat/tests/frame.scenario.json +152 -0
- package/dist/content/examples/twitch-chat/tests/lifetime.scenario.json +186 -0
- package/dist/content/examples/twitch-chat/tests/load-logic.mjs +22 -0
- package/dist/content/examples/twitch-chat/tests/logic.test.mjs +1053 -0
- package/dist/content/examples/twitch-chat/tests/passive.scenario.json +536 -0
- package/dist/content/examples/twitch-chat/tests/preview.scenario.json +147 -0
- package/dist/content/examples/twitch-chat/tests/read-only.scenario.json +202 -0
- package/dist/content/examples/twitch-chat/tests/read-only.test.mjs +63 -0
- package/dist/content/examples/twitch-chat/tests/refused.scenario.json +65 -0
- package/dist/content/examples/twitch-chat/tests/refused.test.mjs +43 -0
- package/dist/content/examples/twitch-chat/tests/render.scenario.json +347 -0
- package/dist/content/examples/twitch-chat/tests/reply.scenario.json +384 -0
- package/dist/content/examples/twitch-chat/tests/send-refused.scenario.json +383 -0
- package/dist/content/examples/twitch-chat/tests/send.scenario.json +300 -0
- package/dist/content/examples/twitch-chat/tests/states.scenario.json +455 -0
- package/dist/content/examples/twitch-chat/tests/unavailable.scenario.json +256 -0
- package/dist/content/examples/twitch-chat/tests/unread.scenario.json +997 -0
- package/dist/content/examples/twitch-chat/tsconfig.json +16 -0
- package/dist/content/examples/twitch-chat/view.ocml +141 -0
- package/dist/content/examples/warframe-market/LICENSE +21 -0
- package/dist/content/examples/warframe-market/README.md +176 -0
- package/dist/content/examples/warframe-market/listing.json +19 -0
- package/dist/content/examples/warframe-market/locales/en.json +44 -0
- package/dist/content/examples/warframe-market/locales/fr.json +44 -0
- package/dist/content/examples/warframe-market/logic.ts +1265 -0
- package/dist/content/examples/warframe-market/manifest.json +92 -0
- package/dist/content/examples/warframe-market/package.json +16 -0
- package/dist/content/examples/warframe-market/style.ocss +422 -0
- package/dist/content/examples/warframe-market/tests/bounds.scenario.json +60 -0
- package/dist/content/examples/warframe-market/tests/cached.scenario.json +70 -0
- package/dist/content/examples/warframe-market/tests/detail.scenario.json +251 -0
- package/dist/content/examples/warframe-market/tests/errors.scenario.json +105 -0
- package/dist/content/examples/warframe-market/tests/fixtures.json +295 -0
- package/dist/content/examples/warframe-market/tests/load-logic.mjs +22 -0
- package/dist/content/examples/warframe-market/tests/logic.test.mjs +505 -0
- package/dist/content/examples/warframe-market/tests/no-clipboard.scenario.json +104 -0
- package/dist/content/examples/warframe-market/tests/passive.scenario.json +87 -0
- package/dist/content/examples/warframe-market/tests/preview.scenario.json +146 -0
- package/dist/content/examples/warframe-market/tests/refresh.scenario.json +56 -0
- package/dist/content/examples/warframe-market/tests/refused.scenario.json +74 -0
- package/dist/content/examples/warframe-market/tests/render.scenario.json +210 -0
- package/dist/content/examples/warframe-market/tests/search.scenario.json +189 -0
- package/dist/content/examples/warframe-market/tests/unavailable.scenario.json +54 -0
- package/dist/content/examples/warframe-market/tsconfig.json +16 -0
- package/dist/content/examples/warframe-market/view.ocml +116 -0
- package/dist/content/examples/weather/LICENSE +20 -0
- package/dist/content/examples/weather/listing.json +10 -0
- package/dist/content/examples/weather/locales/en.json +9 -0
- package/dist/content/examples/weather/locales/fr.json +9 -0
- package/dist/content/examples/weather/logic.ts +62 -0
- package/dist/content/examples/weather/manifest.json +51 -0
- package/dist/content/examples/weather/package.json +15 -0
- package/dist/content/examples/weather/style.ocss +29 -0
- package/dist/content/examples/weather/tsconfig.json +16 -0
- package/dist/content/examples/weather/view.ocml +12 -0
- package/dist/content/index.json +3601 -0
- package/dist/content/reference/errors.json +1712 -0
- package/dist/content/reference/limits.json +1154 -0
- package/dist/content/reference/manifest.md +223 -0
- package/dist/content/reference/permissions.json +413 -0
- package/dist/content/templates/blank/locales/en.json +3 -0
- package/dist/content/templates/blank/locales/fr.json +3 -0
- package/dist/content/templates/blank/logic.ts +7 -0
- package/dist/content/templates/blank/manifest.json +16 -0
- package/dist/content/templates/blank/style.ocss +10 -0
- package/dist/content/templates/blank/tests/example.scenario.json +10 -0
- package/dist/content/templates/blank/view.ocml +6 -0
- package/dist/content/templates/chart/locales/en.json +3 -0
- package/dist/content/templates/chart/locales/fr.json +3 -0
- package/dist/content/templates/chart/logic.ts +27 -0
- package/dist/content/templates/chart/manifest.json +16 -0
- package/dist/content/templates/chart/style.ocss +27 -0
- package/dist/content/templates/chart/tests/example.scenario.json +10 -0
- package/dist/content/templates/chart/view.ocml +10 -0
- package/dist/content/templates/counter/locales/en.json +4 -0
- package/dist/content/templates/counter/locales/fr.json +4 -0
- package/dist/content/templates/counter/logic.ts +20 -0
- package/dist/content/templates/counter/manifest.json +16 -0
- package/dist/content/templates/counter/style.ocss +26 -0
- package/dist/content/templates/counter/tests/example.scenario.json +11 -0
- package/dist/content/templates/counter/view.ocml +13 -0
- package/dist/content/templates/list/locales/en.json +8 -0
- package/dist/content/templates/list/locales/fr.json +8 -0
- package/dist/content/templates/list/logic.ts +35 -0
- package/dist/content/templates/list/manifest.json +16 -0
- package/dist/content/templates/list/style.ocss +42 -0
- package/dist/content/templates/list/tests/example.scenario.json +10 -0
- package/dist/content/templates/list/view.ocml +17 -0
- package/dist/content/templates/shared/LICENSE +20 -0
- package/dist/content/templates/shared/package.json +14 -0
- package/dist/content/templates/shared/tsconfig.json +16 -0
- package/dist/content.js +68 -0
- package/dist/env.js +156 -0
- package/dist/fingerprint.js +32 -0
- package/dist/ids.js +27 -0
- package/dist/index.js +82 -0
- package/dist/knowledge.js +222 -0
- package/dist/npm.js +149 -0
- package/dist/prompts.js +47 -0
- package/dist/resources.js +82 -0
- package/dist/run.js +120 -0
- package/dist/server.js +43 -0
- package/dist/session.js +205 -0
- package/dist/sources.js +218 -0
- package/dist/text.js +80 -0
- package/dist/texts.js +44 -0
- package/dist/tools/audit.js +105 -0
- package/dist/tools/build.js +453 -0
- package/dist/tools/common.js +122 -0
- package/dist/tools/docs.js +196 -0
- package/dist/tools/preview.js +87 -0
- package/dist/tools/project.js +178 -0
- package/dist/tools/setup.js +146 -0
- package/dist/tools/submission.js +369 -0
- package/dist/version.js +3 -0
- package/npm-shrinkwrap.json +402 -0
- package/package.json +52 -4
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
// Twitch chat: the chat of one public Twitch channel the user chooses.
|
|
2
|
+
// OverCrow holds the Twitch account, the connection, the chosen channel and
|
|
3
|
+
// the favorites, and sends the chat as deltas at a bounded pace. In
|
|
4
|
+
// Interactive mode the widget shows the channel selector, the history and a
|
|
5
|
+
// composer whose message field belongs to OverCrow: it keeps the text and
|
|
6
|
+
// sends it itself when the user submits. The logic only follows it. In
|
|
7
|
+
// Passive mode the last messages show and fade out.
|
|
8
|
+
import {
|
|
9
|
+
hasGrant,
|
|
10
|
+
host,
|
|
11
|
+
initState,
|
|
12
|
+
onHost,
|
|
13
|
+
option,
|
|
14
|
+
t,
|
|
15
|
+
timers,
|
|
16
|
+
twitch,
|
|
17
|
+
type ChatMessage,
|
|
18
|
+
type ServiceErrorCode,
|
|
19
|
+
type ServiceErrorPayload,
|
|
20
|
+
type Subscription,
|
|
21
|
+
type Timer,
|
|
22
|
+
type TwitchChat,
|
|
23
|
+
} from "@overcrow/sdk";
|
|
24
|
+
|
|
25
|
+
/** One run of a chat line: text, or an emote image with its name. */
|
|
26
|
+
export interface Part {
|
|
27
|
+
key: string;
|
|
28
|
+
text: string;
|
|
29
|
+
/** The emote's image handle, or `""` for a text run. */
|
|
30
|
+
emote: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One row of the history: a message, or the mark of messages left out. */
|
|
34
|
+
export interface Row {
|
|
35
|
+
key: string;
|
|
36
|
+
/** The message's ID; `""` for a gap. */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Messages OverCrow left out here; 0 for a message. */
|
|
39
|
+
gap: number;
|
|
40
|
+
author: string;
|
|
41
|
+
color: string | null;
|
|
42
|
+
parts: Part[];
|
|
43
|
+
/** "Author: text" of the message replied to, or `""`. */
|
|
44
|
+
context: string;
|
|
45
|
+
/** The reply icon's tooltip, "Reply to {name}". */
|
|
46
|
+
tip: string;
|
|
47
|
+
receivedAt: number;
|
|
48
|
+
/** Scene nodes the row takes. */
|
|
49
|
+
nodes: number;
|
|
50
|
+
/** Style classes: the row's kind and, in Passive mode, its fade. */
|
|
51
|
+
cls: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The message being replied to. */
|
|
55
|
+
export interface ReplyTarget {
|
|
56
|
+
id: string;
|
|
57
|
+
author: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare module "@overcrow/sdk" {
|
|
61
|
+
interface WidgetState {
|
|
62
|
+
/** The host answered the subscription at least once. */
|
|
63
|
+
ready: boolean;
|
|
64
|
+
/** The subscription ended with a failure; the widget asks again. */
|
|
65
|
+
unavailable: boolean;
|
|
66
|
+
account: TwitchChat["account"];
|
|
67
|
+
channel: string | null;
|
|
68
|
+
joinState: TwitchChat["joinState"];
|
|
69
|
+
failure: TwitchChat["failure"];
|
|
70
|
+
favorites: readonly string[];
|
|
71
|
+
canSend: boolean;
|
|
72
|
+
generation: number;
|
|
73
|
+
/** The history, oldest first. */
|
|
74
|
+
rows: Row[];
|
|
75
|
+
/** What the list shows: the history, or the fading last messages. */
|
|
76
|
+
shown: Row[];
|
|
77
|
+
interactive: boolean;
|
|
78
|
+
/** The channel selector is open over a joined channel. */
|
|
79
|
+
selecting: boolean;
|
|
80
|
+
/** The channel field as the user typed it. */
|
|
81
|
+
channelDraft: string;
|
|
82
|
+
/** The channel selector's key: a new one is a new, empty field. */
|
|
83
|
+
selector: number;
|
|
84
|
+
/** The list follows its end; the user scrolled away otherwise. */
|
|
85
|
+
following: boolean;
|
|
86
|
+
/** Messages received since the user scrolled away. */
|
|
87
|
+
unread: number;
|
|
88
|
+
reply: ReplyTarget | null;
|
|
89
|
+
/** The message field as OverCrow last reported it. */
|
|
90
|
+
draft: string;
|
|
91
|
+
/** A message was submitted and OverCrow has not answered yet. */
|
|
92
|
+
sending: boolean;
|
|
93
|
+
/** Message key of the last send's failure, or `""`. */
|
|
94
|
+
sendError: string;
|
|
95
|
+
/** Message key of a failed channel or favorite change, or `""`. */
|
|
96
|
+
notice: string;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Messages kept in the history (the host's own ring). */
|
|
101
|
+
export const HISTORY_MAX = 200;
|
|
102
|
+
/** Messages shown in Passive mode. */
|
|
103
|
+
export const PASSIVE_MAX = 12;
|
|
104
|
+
/**
|
|
105
|
+
* Scene nodes the history may take. A row takes 4 to 21 nodes; 200 rows
|
|
106
|
+
* of the longest kind would pass `MAX_SCENE_NODES` (4096) with the rest of
|
|
107
|
+
* the view, so the oldest rows leave first.
|
|
108
|
+
*/
|
|
109
|
+
export const ROW_NODE_BUDGET = 3600;
|
|
110
|
+
/** A subscription that ended is asked again this much later. */
|
|
111
|
+
export const RETRY_MS = 5_000;
|
|
112
|
+
/**
|
|
113
|
+
* OverCrow answers a send; should no answer come at all, the composer is
|
|
114
|
+
* given back after this long.
|
|
115
|
+
*/
|
|
116
|
+
export const ANSWER_MS = 40_000;
|
|
117
|
+
/** Opacity levels of the Passive fade (`fade-1` … `fade-32` in the style). */
|
|
118
|
+
export const FADE_LEVELS = 32;
|
|
119
|
+
/** The shortest step of the fade: OverCrow's shortest timer, in ms. */
|
|
120
|
+
export const FADE_STEP_MIN_MS = 100;
|
|
121
|
+
/** `MAX_CHAT_CHANNEL_BYTES`. */
|
|
122
|
+
const CHANNEL_MAX = 25;
|
|
123
|
+
/** `MAX_CHAT_FAVORITES`. */
|
|
124
|
+
const FAVORITES_MAX = 20;
|
|
125
|
+
/** `MAX_LABEL_BYTES`. */
|
|
126
|
+
const LABEL_MAX = 256;
|
|
127
|
+
|
|
128
|
+
const canRead = hasGrant("twitch.chat.read");
|
|
129
|
+
const canWrite = hasGrant("twitch.chat.compose");
|
|
130
|
+
|
|
131
|
+
const state = initState({
|
|
132
|
+
ready: false,
|
|
133
|
+
unavailable: false,
|
|
134
|
+
account: "connected" as TwitchChat["account"],
|
|
135
|
+
channel: null as string | null,
|
|
136
|
+
joinState: "idle" as TwitchChat["joinState"],
|
|
137
|
+
failure: null as TwitchChat["failure"],
|
|
138
|
+
favorites: [] as readonly string[],
|
|
139
|
+
canSend: false,
|
|
140
|
+
generation: 0,
|
|
141
|
+
rows: [] as Row[],
|
|
142
|
+
shown: [] as Row[],
|
|
143
|
+
interactive: host.mode === "interactive",
|
|
144
|
+
selecting: false,
|
|
145
|
+
channelDraft: "",
|
|
146
|
+
selector: 0,
|
|
147
|
+
following: true,
|
|
148
|
+
unread: 0,
|
|
149
|
+
reply: null as ReplyTarget | null,
|
|
150
|
+
draft: "",
|
|
151
|
+
sending: false,
|
|
152
|
+
sendError: "",
|
|
153
|
+
notice: "",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/** What the view reads of the state. */
|
|
157
|
+
export interface View {
|
|
158
|
+
ready: boolean;
|
|
159
|
+
unavailable: boolean;
|
|
160
|
+
account: TwitchChat["account"];
|
|
161
|
+
channel: string | null;
|
|
162
|
+
joinState: TwitchChat["joinState"];
|
|
163
|
+
failure: TwitchChat["failure"];
|
|
164
|
+
favorites: readonly string[];
|
|
165
|
+
canSend: boolean;
|
|
166
|
+
generation: number;
|
|
167
|
+
shown: Row[];
|
|
168
|
+
interactive: boolean;
|
|
169
|
+
selecting: boolean;
|
|
170
|
+
channelDraft: string;
|
|
171
|
+
selector: number;
|
|
172
|
+
following: boolean;
|
|
173
|
+
unread: number;
|
|
174
|
+
reply: ReplyTarget | null;
|
|
175
|
+
draft: string;
|
|
176
|
+
sending: boolean;
|
|
177
|
+
sendError: string;
|
|
178
|
+
notice: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Text helpers.
|
|
182
|
+
|
|
183
|
+
/** Provider text on one line. */
|
|
184
|
+
export function oneLine(text: string): string {
|
|
185
|
+
return text.replace(/\s*[\r\n]+\s*/g, " ").trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** `text` cut to `limit` UTF-8 bytes on a character boundary. */
|
|
189
|
+
export function cutBytes(text: string, limit: number): string {
|
|
190
|
+
let bytes = 0;
|
|
191
|
+
let end = 0;
|
|
192
|
+
for (const character of text) {
|
|
193
|
+
const code = character.codePointAt(0) ?? 0;
|
|
194
|
+
const size = code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
195
|
+
if (bytes + size > limit) {
|
|
196
|
+
return text.slice(0, end);
|
|
197
|
+
}
|
|
198
|
+
bytes += size;
|
|
199
|
+
end += character.length;
|
|
200
|
+
}
|
|
201
|
+
return text;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A channel login as Twitch writes it: trimmed, an optional `#` removed,
|
|
206
|
+
* ASCII letters, digits and `_`, lowercased, 1 to 25 characters; `null`
|
|
207
|
+
* for anything else.
|
|
208
|
+
*/
|
|
209
|
+
export function normalizeChannel(value: string): string | null {
|
|
210
|
+
const trimmed = value.trim();
|
|
211
|
+
const login = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
212
|
+
if (login.length === 0 || login.length > CHANNEL_MAX || !/^[A-Za-z0-9_]+$/.test(login)) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return login.toLowerCase();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The history.
|
|
219
|
+
|
|
220
|
+
let gaps = 0;
|
|
221
|
+
|
|
222
|
+
/** The row of a delivered message. */
|
|
223
|
+
export function messageRow(message: ChatMessage): Row {
|
|
224
|
+
const parts: Part[] = message.fragments.map((fragment, index) =>
|
|
225
|
+
"emote" in fragment
|
|
226
|
+
? { key: `${index}`, text: cutBytes(fragment.alt, LABEL_MAX), emote: fragment.emote }
|
|
227
|
+
: { key: `${index}`, text: fragment.text, emote: "" },
|
|
228
|
+
);
|
|
229
|
+
const context =
|
|
230
|
+
message.reply === null
|
|
231
|
+
? ""
|
|
232
|
+
: `${oneLine(message.reply.author)}: ${oneLine(message.reply.text)}`;
|
|
233
|
+
return {
|
|
234
|
+
key: `m:${message.id}`,
|
|
235
|
+
id: message.id,
|
|
236
|
+
gap: 0,
|
|
237
|
+
author: `${message.author}: `,
|
|
238
|
+
color: message.color,
|
|
239
|
+
parts,
|
|
240
|
+
context,
|
|
241
|
+
tip: replyTip(message.author),
|
|
242
|
+
receivedAt: message.receivedAt,
|
|
243
|
+
// The row, its text, the author, the reply icon, then each run and the
|
|
244
|
+
// reply context.
|
|
245
|
+
nodes: 4 + parts.length + (context === "" ? 0 : 1),
|
|
246
|
+
cls: context === "" ? "row" : "row replied",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The mark of `count` messages OverCrow left out. */
|
|
251
|
+
export function gapRow(count: number, at: number): Row {
|
|
252
|
+
gaps += 1;
|
|
253
|
+
return {
|
|
254
|
+
key: `g:${gaps}`,
|
|
255
|
+
id: "",
|
|
256
|
+
gap: count,
|
|
257
|
+
author: "",
|
|
258
|
+
color: null,
|
|
259
|
+
parts: [],
|
|
260
|
+
context: "",
|
|
261
|
+
tip: "",
|
|
262
|
+
receivedAt: at,
|
|
263
|
+
nodes: 1,
|
|
264
|
+
cls: "gap",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* `rows` after a delta: the removed messages gone, the mark of the
|
|
270
|
+
* messages left out, each delivered message appended or, when the history
|
|
271
|
+
* already holds its ID, put in its place; then the oldest rows dropped
|
|
272
|
+
* beyond `HISTORY_MAX` messages and `ROW_NODE_BUDGET` nodes. `appended`
|
|
273
|
+
* counts what arrived, left out or not.
|
|
274
|
+
*/
|
|
275
|
+
export function applyDelta(
|
|
276
|
+
rows: readonly Row[],
|
|
277
|
+
update: Pick<TwitchChat, "reset" | "messages" | "removed" | "skipped">,
|
|
278
|
+
now: number,
|
|
279
|
+
): { rows: Row[]; appended: number } {
|
|
280
|
+
let next: Row[] = update.reset ? [] : rows.slice();
|
|
281
|
+
if (update.removed.length > 0) {
|
|
282
|
+
const removed = new Set(update.removed);
|
|
283
|
+
next = next.filter((row) => !removed.has(row.id));
|
|
284
|
+
}
|
|
285
|
+
let appended = 0;
|
|
286
|
+
if (!update.reset && update.skipped > 0) {
|
|
287
|
+
const last = next[next.length - 1];
|
|
288
|
+
if (last !== undefined && last.gap > 0) {
|
|
289
|
+
// Two gaps in a row are one: nothing was shown between them.
|
|
290
|
+
next[next.length - 1] = { ...last, gap: last.gap + update.skipped };
|
|
291
|
+
} else {
|
|
292
|
+
next.push(gapRow(update.skipped, now));
|
|
293
|
+
}
|
|
294
|
+
appended += update.skipped;
|
|
295
|
+
}
|
|
296
|
+
for (const message of update.messages) {
|
|
297
|
+
const index = next.findIndex((row) => row.id === message.id);
|
|
298
|
+
if (message.deleted) {
|
|
299
|
+
if (index >= 0) {
|
|
300
|
+
next.splice(index, 1);
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const row = messageRow(message);
|
|
305
|
+
if (index >= 0) {
|
|
306
|
+
next[index] = row;
|
|
307
|
+
} else {
|
|
308
|
+
next.push(row);
|
|
309
|
+
appended += 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { rows: trimmed(next), appended };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** `rows` without its oldest rows beyond the history's two bounds. */
|
|
316
|
+
export function trimmed(rows: Row[]): Row[] {
|
|
317
|
+
let messages = 0;
|
|
318
|
+
let nodes = 0;
|
|
319
|
+
let first = rows.length;
|
|
320
|
+
while (first > 0) {
|
|
321
|
+
const row = rows[first - 1];
|
|
322
|
+
if (row === undefined) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
const counted = row.gap === 0 ? 1 : 0;
|
|
326
|
+
if (messages + counted > HISTORY_MAX || nodes + row.nodes > ROW_NODE_BUDGET) {
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
messages += counted;
|
|
330
|
+
nodes += row.nodes;
|
|
331
|
+
first -= 1;
|
|
332
|
+
}
|
|
333
|
+
// A gap that would open the history marks nothing the user can see.
|
|
334
|
+
while (first < rows.length && (rows[first]?.gap ?? 0) > 0) {
|
|
335
|
+
first += 1;
|
|
336
|
+
}
|
|
337
|
+
return first === 0 ? rows : rows.slice(first);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// The Passive fade.
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The fade's step: the time one opacity level lasts, and no shorter than
|
|
344
|
+
* OverCrow's shortest timer. Each step is one render and one frame: the
|
|
345
|
+
* fade has no transition, which would draw a frame at every refresh for as
|
|
346
|
+
* long as a message fades.
|
|
347
|
+
*/
|
|
348
|
+
export function fadeStepMs(lifetimeMs: number): number {
|
|
349
|
+
return Math.max(FADE_STEP_MIN_MS, Math.round(lifetimeMs / 3 / FADE_LEVELS));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The opacity level of a message of `ageMs`: 0 (opaque) until two thirds
|
|
354
|
+
* of the lifetime, then linear up to `FADE_LEVELS` (gone) at the lifetime.
|
|
355
|
+
*/
|
|
356
|
+
export function fadeLevel(ageMs: number, lifetimeMs: number): number {
|
|
357
|
+
const start = (lifetimeMs * 2) / 3;
|
|
358
|
+
if (ageMs <= start) {
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
const level = Math.round(((ageMs - start) / (lifetimeMs - start)) * FADE_LEVELS);
|
|
362
|
+
return Math.min(FADE_LEVELS, Math.max(1, level));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The rows of Passive mode: the last `PASSIVE_MAX` messages younger than
|
|
367
|
+
* the lifetime, each with the opacity level of its age.
|
|
368
|
+
*/
|
|
369
|
+
export function passiveRows(rows: readonly Row[], now: number, lifetimeMs: number): Row[] {
|
|
370
|
+
const recent = rows.filter((row) => row.gap === 0).slice(-PASSIVE_MAX);
|
|
371
|
+
const shown: Row[] = [];
|
|
372
|
+
for (const row of recent) {
|
|
373
|
+
const age = now - row.receivedAt;
|
|
374
|
+
if (age >= lifetimeMs) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const level = fadeLevel(age, lifetimeMs);
|
|
378
|
+
shown.push(level === 0 ? row : { ...row, cls: `${row.cls} fade-${level}` });
|
|
379
|
+
}
|
|
380
|
+
return shown;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* How long until a row of Passive mode changes: one step while a message
|
|
385
|
+
* fades, the time until the first fade starts otherwise; `null` when
|
|
386
|
+
* nothing shows.
|
|
387
|
+
*/
|
|
388
|
+
export function nextFadeMs(rows: readonly Row[], now: number, lifetimeMs: number): number | null {
|
|
389
|
+
const step = fadeStepMs(lifetimeMs);
|
|
390
|
+
const start = (lifetimeMs * 2) / 3;
|
|
391
|
+
let next: number | null = null;
|
|
392
|
+
for (const row of rows) {
|
|
393
|
+
const age = now - row.receivedAt;
|
|
394
|
+
const wait = Math.max(step, start - age);
|
|
395
|
+
next = next === null ? wait : Math.min(next, wait);
|
|
396
|
+
}
|
|
397
|
+
return next;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The lifetime of a message in Passive mode, in ms (menu row, 5 to 120 s). */
|
|
401
|
+
export function lifetimeMs(): number {
|
|
402
|
+
const seconds = option("passive-lifetime", 30);
|
|
403
|
+
return Math.min(120, Math.max(5, Number.isFinite(seconds) ? seconds : 30)) * 1000;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let fade: Timer | null = null;
|
|
407
|
+
|
|
408
|
+
/** Shows the history, or the fading last messages, and arms the fade. */
|
|
409
|
+
function show(): void {
|
|
410
|
+
fade?.cancel();
|
|
411
|
+
fade = null;
|
|
412
|
+
if (state.interactive) {
|
|
413
|
+
state.shown = state.rows;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const lifetime = lifetimeMs();
|
|
417
|
+
const now = Date.now();
|
|
418
|
+
state.shown = passiveRows(state.rows, now, lifetime);
|
|
419
|
+
const wait = nextFadeMs(state.shown, now, lifetime);
|
|
420
|
+
// Hidden, a timer waits; nothing ticks once the last message is gone.
|
|
421
|
+
if (wait !== null) {
|
|
422
|
+
fade = timers.after(wait, show);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// What the view shows.
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* `permission` without the grant, `unavailable` after the host's source
|
|
430
|
+
* failed, `waiting` before its first answer, `signed-out` and `pending`
|
|
431
|
+
* under the host's account panel, `no-channel`, or `chat`.
|
|
432
|
+
*/
|
|
433
|
+
export function phase(view: View): string {
|
|
434
|
+
if (!canRead) {
|
|
435
|
+
return "permission";
|
|
436
|
+
}
|
|
437
|
+
if (view.unavailable) {
|
|
438
|
+
return "unavailable";
|
|
439
|
+
}
|
|
440
|
+
if (!view.ready) {
|
|
441
|
+
return "waiting";
|
|
442
|
+
}
|
|
443
|
+
if (view.account === "pending") {
|
|
444
|
+
return "pending";
|
|
445
|
+
}
|
|
446
|
+
if (view.account !== "connected") {
|
|
447
|
+
return "signed-out";
|
|
448
|
+
}
|
|
449
|
+
return view.channel === null ? "no-channel" : "chat";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** The header's title: the channel, or "Twitch". */
|
|
453
|
+
export function heading(view: View): string {
|
|
454
|
+
return view.channel === null || phase(view) !== "chat" ? t("twitch") : `#${view.channel}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Message key of the connection's state. */
|
|
458
|
+
export function statusKey(view: View): string {
|
|
459
|
+
if (phase(view) !== "chat" && phase(view) !== "no-channel") {
|
|
460
|
+
return "inactive";
|
|
461
|
+
}
|
|
462
|
+
switch (view.joinState) {
|
|
463
|
+
case "connecting":
|
|
464
|
+
return "connecting";
|
|
465
|
+
case "joined":
|
|
466
|
+
return "connected";
|
|
467
|
+
case "reconnecting":
|
|
468
|
+
return "reconnecting";
|
|
469
|
+
case "failed":
|
|
470
|
+
switch (view.failure) {
|
|
471
|
+
case "channel_unavailable":
|
|
472
|
+
return "channel-unavailable";
|
|
473
|
+
case "connection":
|
|
474
|
+
return "connection-error";
|
|
475
|
+
case "limit":
|
|
476
|
+
return "limit";
|
|
477
|
+
default:
|
|
478
|
+
return "twitch-error";
|
|
479
|
+
}
|
|
480
|
+
default:
|
|
481
|
+
return "disconnected";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** The status dot's classes: green joined, amber on its way, red failed. */
|
|
486
|
+
export function dotClass(view: View): string {
|
|
487
|
+
switch (statusKey(view)) {
|
|
488
|
+
case "connected":
|
|
489
|
+
return "dot ok";
|
|
490
|
+
case "connecting":
|
|
491
|
+
case "reconnecting":
|
|
492
|
+
return "dot warn";
|
|
493
|
+
case "inactive":
|
|
494
|
+
case "disconnected":
|
|
495
|
+
return "dot";
|
|
496
|
+
default:
|
|
497
|
+
return "dot failed";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Message key of a failed connection, shown under the header; or `""`. */
|
|
502
|
+
export function failureKey(view: View): string {
|
|
503
|
+
return phase(view) === "chat" && view.joinState === "failed" ? statusKey(view) : "";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** The channel selector shows: Interactive mode, no channel or a change. */
|
|
507
|
+
export function selecting(view: View): boolean {
|
|
508
|
+
const current = phase(view);
|
|
509
|
+
return view.interactive && (current === "no-channel" || (current === "chat" && view.selecting));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The selector's key. Its field is the host's: the logic cannot empty it,
|
|
514
|
+
* so a selector that starts over is a new one.
|
|
515
|
+
*/
|
|
516
|
+
export function selectors(view: View): number[] {
|
|
517
|
+
return [view.selector];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** The channel typed is one the host can join. */
|
|
521
|
+
export function canJoin(view: View): boolean {
|
|
522
|
+
return normalizeChannel(view.channelDraft) !== null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** The current channel is a favorite. */
|
|
526
|
+
export function isFavorite(view: View): boolean {
|
|
527
|
+
return view.channel !== null && view.favorites.includes(view.channel);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** The star adds a favorite the host has no room for. */
|
|
531
|
+
export function favoriteFull(view: View): boolean {
|
|
532
|
+
return !isFavorite(view) && view.favorites.length >= FAVORITES_MAX;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** The star's accessible name: "Favorite #channel" or "Unfavorite #channel". */
|
|
536
|
+
export function favoriteLabel(view: View): string {
|
|
537
|
+
return t(isFavorite(view) ? "unfavorite" : "favorite", { channel: view.channel ?? "" });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Message key of the star's tooltip. */
|
|
541
|
+
export function favoriteTip(view: View): string {
|
|
542
|
+
if (favoriteFull(view)) {
|
|
543
|
+
return "favorite-limit";
|
|
544
|
+
}
|
|
545
|
+
return isFavorite(view) ? "favorite-remove" : "favorite-add";
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function starClass(view: View): string {
|
|
549
|
+
return isFavorite(view) ? "star on" : "star";
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Message key of an empty history. */
|
|
553
|
+
export function emptyKey(view: View): string {
|
|
554
|
+
return view.interactive ? "no-messages" : "no-recent";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** "1 new message" or "{n} new messages". */
|
|
558
|
+
export function unreadText(count: number): string {
|
|
559
|
+
return count === 1 ? t("new-message") : t("new-messages", { count });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** "1 message not shown" or "{n} messages not shown". */
|
|
563
|
+
export function gapText(count: number): string {
|
|
564
|
+
return count === 1 ? t("skipped-one") : t("skipped", { count });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** The reply icon's tooltip: "Reply to {name}". */
|
|
568
|
+
export function replyTip(author: string): string {
|
|
569
|
+
return cutBytes(t("reply-to", { name: author }), LABEL_MAX);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** "Replying to {name}", above the composer. */
|
|
573
|
+
export function replyingTo(view: View): string {
|
|
574
|
+
return view.reply === null ? "" : t("replying-to", { name: view.reply.author });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The ID the composer's form replies to; `null` without a reply. */
|
|
578
|
+
export function replyTarget(view: View): string | null {
|
|
579
|
+
return view.reply === null ? null : view.reply.id;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The composer's key: a new channel or account is a new form, so that the
|
|
584
|
+
* text OverCrow held for the previous one is gone.
|
|
585
|
+
*/
|
|
586
|
+
export function composers(view: View): number[] {
|
|
587
|
+
return [view.generation];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** The composer shows: Interactive mode, a channel, the compose grant. */
|
|
591
|
+
export function composing(view: View): boolean {
|
|
592
|
+
return view.interactive && canWrite && phase(view) === "chat";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function composerClass(view: View): string {
|
|
596
|
+
return composing(view) ? "composer" : "composer hidden";
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Message key of the message field's placeholder. */
|
|
600
|
+
export function composerHint(view: View): string {
|
|
601
|
+
return view.canSend ? "send-hint" : "waiting-hint";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** A message can be submitted: joined, something typed, nothing in flight. */
|
|
605
|
+
export function canSubmit(view: View): boolean {
|
|
606
|
+
return composing(view) && view.canSend && !view.sending && view.draft.trim() !== "";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Message key beside the field: "sending…", "not sent", or `""`. */
|
|
610
|
+
export function sendMark(view: View): string {
|
|
611
|
+
if (view.sending) {
|
|
612
|
+
return "sending";
|
|
613
|
+
}
|
|
614
|
+
return view.sendError === "" ? "" : "not-sent";
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function sendMarkClass(view: View): string {
|
|
618
|
+
return view.sending ? "pending" : "unsent";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Message key of a refused send. */
|
|
622
|
+
export function sendErrorKey(code: ServiceErrorCode | null): string {
|
|
623
|
+
switch (code) {
|
|
624
|
+
case "busy":
|
|
625
|
+
return "error-too-fast";
|
|
626
|
+
case "permission_denied":
|
|
627
|
+
return "error-restricted";
|
|
628
|
+
case "cancelled":
|
|
629
|
+
return "error-not-sent";
|
|
630
|
+
default:
|
|
631
|
+
// `unavailable`, `not_connected`, `stale_context`, `invalid_request`,
|
|
632
|
+
// `timeout` and the rest.
|
|
633
|
+
return "error-not-accepted";
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Message key of a refused channel or favorite change; `""` for none. */
|
|
638
|
+
export function noticeKey(code: ServiceErrorCode | null): string {
|
|
639
|
+
switch (code) {
|
|
640
|
+
case "busy":
|
|
641
|
+
return "error-busy";
|
|
642
|
+
case "quota_exceeded":
|
|
643
|
+
return "error-favorite-limit";
|
|
644
|
+
case "not_connected":
|
|
645
|
+
case "stale_context":
|
|
646
|
+
// The account or the channel changed meanwhile: the state says so.
|
|
647
|
+
return "";
|
|
648
|
+
default:
|
|
649
|
+
return "error-settings";
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// The subscription.
|
|
654
|
+
|
|
655
|
+
/** Drops what belonged to the previous channel or account. */
|
|
656
|
+
function forget(): void {
|
|
657
|
+
state.reply = null;
|
|
658
|
+
state.unread = 0;
|
|
659
|
+
state.following = true;
|
|
660
|
+
state.sending = false;
|
|
661
|
+
state.sendError = "";
|
|
662
|
+
state.draft = "";
|
|
663
|
+
state.notice = "";
|
|
664
|
+
state.selecting = false;
|
|
665
|
+
restartSelector();
|
|
666
|
+
answer?.cancel();
|
|
667
|
+
answer = null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Takes an update of the host: the chat's state and a delta of messages. */
|
|
671
|
+
function apply(update: TwitchChat): void {
|
|
672
|
+
const moved = state.ready && update.generation !== state.generation;
|
|
673
|
+
if (moved) {
|
|
674
|
+
forget();
|
|
675
|
+
}
|
|
676
|
+
state.ready = true;
|
|
677
|
+
state.account = update.account;
|
|
678
|
+
state.channel = update.channel;
|
|
679
|
+
state.joinState = update.joinState;
|
|
680
|
+
state.failure = update.failure;
|
|
681
|
+
state.favorites = update.favorites;
|
|
682
|
+
state.canSend = update.canSend;
|
|
683
|
+
state.generation = update.generation;
|
|
684
|
+
const delta = applyDelta(moved ? [] : state.rows, update, Date.now());
|
|
685
|
+
state.rows = delta.rows;
|
|
686
|
+
if (update.reset) {
|
|
687
|
+
state.following = true;
|
|
688
|
+
state.unread = 0;
|
|
689
|
+
} else if (!state.following) {
|
|
690
|
+
state.unread = Math.min(HISTORY_MAX, state.unread + delta.appended);
|
|
691
|
+
}
|
|
692
|
+
const reply = state.reply;
|
|
693
|
+
if (reply !== null && !state.rows.some((row) => row.id === reply.id)) {
|
|
694
|
+
state.reply = null;
|
|
695
|
+
}
|
|
696
|
+
show();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
let subscription: Subscription | null = null;
|
|
700
|
+
|
|
701
|
+
function subscribe(): void {
|
|
702
|
+
const current = twitch.chat.subscribe((update) => {
|
|
703
|
+
if (update.ok) {
|
|
704
|
+
state.unavailable = false;
|
|
705
|
+
apply(update.value);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
// The subscription ended: the host's source failed. Say so and ask
|
|
709
|
+
// again later; hidden, the timer waits until the widget shows.
|
|
710
|
+
if (subscription === current) {
|
|
711
|
+
subscription = null;
|
|
712
|
+
state.unavailable = true;
|
|
713
|
+
timers.after(RETRY_MS, subscribe);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
subscription = current;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// The user's gestures.
|
|
720
|
+
|
|
721
|
+
/** A new, empty channel field. */
|
|
722
|
+
function restartSelector(): void {
|
|
723
|
+
state.channelDraft = "";
|
|
724
|
+
state.selector += 1;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function refused(error: unknown): void {
|
|
728
|
+
const code = (error as { code?: ServiceErrorCode } | null)?.code ?? null;
|
|
729
|
+
state.notice = noticeKey(code);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** The channel field's text. */
|
|
733
|
+
export function channelTyped(value: string): void {
|
|
734
|
+
state.channelDraft = value;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function joinChannel(channel: string): void {
|
|
738
|
+
state.notice = "";
|
|
739
|
+
twitch.chat.join({ channel }).then(() => {
|
|
740
|
+
// The selector leaves once the host publishes the channel; what was
|
|
741
|
+
// typed stays in its field until then.
|
|
742
|
+
state.selecting = false;
|
|
743
|
+
}, refused);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Enter in the channel field, or "Join chat". */
|
|
747
|
+
export function join(): void {
|
|
748
|
+
const channel = normalizeChannel(state.channelDraft);
|
|
749
|
+
if (channel !== null) {
|
|
750
|
+
joinChannel(channel);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** A favorite's chip joins its channel. */
|
|
755
|
+
export function joinFavorite(channel: string): void {
|
|
756
|
+
joinChannel(channel);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** "Change channel" opens the selector; the chat stays until a choice. */
|
|
760
|
+
export function toggleSelector(): void {
|
|
761
|
+
state.selecting = !state.selecting;
|
|
762
|
+
restartSelector();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** "Disconnect channel": the host forgets the channel. */
|
|
766
|
+
export function leave(): void {
|
|
767
|
+
state.notice = "";
|
|
768
|
+
twitch.chat.leave().catch(refused);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** The star adds the current channel to the favorites, or removes it. */
|
|
772
|
+
export function toggleFavorite(): void {
|
|
773
|
+
const channel = state.channel;
|
|
774
|
+
if (channel === null || favoriteFull(state)) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
state.notice = "";
|
|
778
|
+
twitch.chat.favorite({ channel, favorite: !isFavorite(state) }).catch(refused);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** The list left its end, or came back to it, under the user's scroll. */
|
|
782
|
+
export function stuck(atEnd: boolean): void {
|
|
783
|
+
state.following = atEnd;
|
|
784
|
+
if (atEnd) {
|
|
785
|
+
state.unread = 0;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** "N new messages": back to the latest message. */
|
|
790
|
+
export function latest(): void {
|
|
791
|
+
state.following = true;
|
|
792
|
+
state.unread = 0;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** The reply icon of a message: the next send answers it. */
|
|
796
|
+
export function replyTo(id: string): void {
|
|
797
|
+
const row = state.rows.find((each) => each.id === id);
|
|
798
|
+
if (row !== undefined && row.gap === 0) {
|
|
799
|
+
state.reply = { id, author: row.author.slice(0, -2) };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function cancelReply(): void {
|
|
804
|
+
state.reply = null;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** The message field's text, as OverCrow holds it. */
|
|
808
|
+
export function drafted(value: string): void {
|
|
809
|
+
state.draft = value;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
let answer: Timer | null = null;
|
|
813
|
+
|
|
814
|
+
/** OverCrow is sending the form: the composer waits for the outcome. */
|
|
815
|
+
function sending(): void {
|
|
816
|
+
if (!canSubmit(state)) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
state.sending = true;
|
|
820
|
+
state.sendError = "";
|
|
821
|
+
answer?.cancel();
|
|
822
|
+
answer = timers.after(ANSWER_MS, () => {
|
|
823
|
+
answer = null;
|
|
824
|
+
if (state.sending) {
|
|
825
|
+
state.sending = false;
|
|
826
|
+
state.sendError = "error-not-accepted";
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** The Send button: OverCrow submits the form on the same gesture. */
|
|
832
|
+
export function sendPressed(): void {
|
|
833
|
+
sending();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Enter in the message field submits the form too. */
|
|
837
|
+
export function keyed(key: string): void {
|
|
838
|
+
if (key === "Enter") {
|
|
839
|
+
sending();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* The outcome of a message OverCrow sent: the form's `submit` event, with
|
|
845
|
+
* the failure's code when it was refused. OverCrow empties the field once
|
|
846
|
+
* the message is accepted and keeps it otherwise.
|
|
847
|
+
*/
|
|
848
|
+
export function submitted(outcome: string, error: ServiceErrorPayload | undefined): void {
|
|
849
|
+
const awaited = state.sending;
|
|
850
|
+
state.sending = false;
|
|
851
|
+
answer?.cancel();
|
|
852
|
+
answer = null;
|
|
853
|
+
if (outcome === "accepted") {
|
|
854
|
+
state.sendError = "";
|
|
855
|
+
state.draft = "";
|
|
856
|
+
state.reply = null;
|
|
857
|
+
} else if (awaited || state.draft.trim() !== "") {
|
|
858
|
+
// Enter on an empty field is refused too: nothing to say about it.
|
|
859
|
+
state.sendError = sendErrorKey(outcome === "cancelled" ? "cancelled" : (error?.code ?? null));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
onHost((changed) => {
|
|
864
|
+
if (changed.includes("mode")) {
|
|
865
|
+
state.interactive = host.mode === "interactive";
|
|
866
|
+
}
|
|
867
|
+
if (changed.includes("locale")) {
|
|
868
|
+
// The tooltips of the rows are written in the interface's language.
|
|
869
|
+
state.rows = state.rows.map((row) =>
|
|
870
|
+
row.gap === 0 ? { ...row, tip: replyTip(row.author.slice(0, -2)) } : row,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
if (
|
|
874
|
+
changed.includes("mode") ||
|
|
875
|
+
changed.includes("options") ||
|
|
876
|
+
changed.includes("visible") ||
|
|
877
|
+
changed.includes("locale")
|
|
878
|
+
) {
|
|
879
|
+
show();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Without the grant the host would refuse the subscription: say so instead
|
|
884
|
+
// of asking.
|
|
885
|
+
if (canRead) {
|
|
886
|
+
subscribe();
|
|
887
|
+
}
|