mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
Merge branch 'master' into fix/emotes-older-vods
This commit is contained in:
commit
3cccebab17
75 changed files with 3760 additions and 1295 deletions
|
@ -785,6 +785,26 @@
|
|||
"css": "list-bullet",
|
||||
"code": 61642,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "7655b7161cf9660beeb1af4036db1198",
|
||||
"css": "mastodon",
|
||||
"code": 59463,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M932.9 220.7C918.5 114.6 825 31 714.2 14.8 695.5 12 624.7 2.1 460.6 2.1H459.4C295.3 2.1 260.1 12 241.4 14.8 133.7 30.5 35.3 105.6 11.4 213 0 265.9-1.3 324.5 0.9 378.2 3.9 455.3 4.5 532.3 11.6 609.1 16.5 660.1 25 710.7 37.1 760.5 59.8 852.5 151.6 929 241.6 960.3 337.9 992.8 441.5 998.2 540.8 975.9 551.7 973.4 562.5 970.4 573.2 967.1 597.3 959.5 625.6 951.1 646.3 936.2 646.6 935.9 646.9 935.7 647 935.4 647.2 935.1 647.3 934.7 647.3 934.4V860C647.3 859.6 647.2 859.3 647.1 859 646.9 858.7 646.7 858.5 646.4 858.3 646.2 858.1 645.9 857.9 645.6 857.8 645.2 857.8 644.9 857.8 644.6 857.8 581 872.9 515.8 880.4 450.4 880.3 337.9 880.3 307.6 827.5 299 805.5 292 786.5 287.6 766.7 285.8 746.5 285.8 746.2 285.9 745.8 286 745.5 286.1 745.2 286.3 744.9 286.6 744.7 286.9 744.5 287.2 744.4 287.5 744.3 287.9 744.2 288.2 744.2 288.5 744.3 351.1 759.2 415.2 766.8 479.5 766.8 495 766.8 510.4 766.8 525.9 766.3 590.6 764.6 658.8 761.3 722.4 749 724 748.7 725.6 748.4 727 748 827.4 728.9 922.9 669.1 932.7 517.5 933 511.5 933.9 455 933.9 448.8 934 427.7 940.8 299.5 932.9 220.7ZM778.4 598.9H672.8V343.1C672.8 289.3 650.1 261.8 604 261.8 553.2 261.8 527.8 294.3 527.8 358.5V498.5H422.9V358.5C422.9 294.3 397.4 261.8 346.7 261.8 300.8 261.8 277.9 289.3 277.9 343.1V598.9H172.4V335.4C172.4 281.5 186.3 238.7 214.1 207 242.7 175.4 280.4 159.1 327.1 159.1 381.1 159.1 421.9 179.7 449.2 220.8L475.4 264.4 501.7 220.8C529 179.7 569.8 159.1 623.8 159.1 670.4 159.1 708 175.4 736.8 207 764.6 238.7 778.5 281.5 778.5 335.4L778.4 598.9Z",
|
||||
"width": 937
|
||||
},
|
||||
"search": [
|
||||
"logo-black"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "012ff5762ccb18c16bdfdd6baf187406",
|
||||
"css": "volume-up",
|
||||
"code": 59464,
|
||||
"src": "elusive"
|
||||
}
|
||||
]
|
||||
}
|
14
package.json
14
package.json
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.36.1",
|
||||
"version": "4.39.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --config webpack.web.dev.js",
|
||||
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --config webpack.web.dev.js",
|
||||
"eslint": "eslint \"src/**/*.{js,jsx,vue}\"",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "webpack-dev-server --config webpack.web.dev.js",
|
||||
"dev:prod": "webpack-dev-server --config webpack.web.dev.prod.js",
|
||||
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --config webpack.web.dev.js",
|
||||
"dev:prod": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --config webpack.web.dev.prod.js",
|
||||
"build": "pnpm build:prod",
|
||||
"build:stats": "cross-env NODE_ENV=production webpack --config webpack.web.prod.js --json > stats.json",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack --config webpack.web.prod.js",
|
||||
"build:dev": "pnpm clean && webpack --config webpack.web.dev.js",
|
||||
"build:stats": "cross-env NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=production webpack --config webpack.web.prod.js --json > stats.json",
|
||||
"build:prod": "cross-env NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=production webpack --config webpack.web.prod.js",
|
||||
"build:dev": "pnpm clean && cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config webpack.web.dev.js",
|
||||
"font": "pnpm font:edit",
|
||||
"font:edit": "fontello-cli --cli-config fontello.client.json edit",
|
||||
"font:save": "fontello-cli --cli-config fontello.client.json save && pnpm font:update",
|
||||
|
|
438
pnpm-lock.yaml
generated
438
pnpm-lock.yaml
generated
|
@ -1,4 +1,4 @@
|
|||
lockfileVersion: 5.3
|
||||
lockfileVersion: 5.4
|
||||
|
||||
overrides:
|
||||
ansi-regex@>2.1.1 <5.0.1: '>=5.0.1'
|
||||
|
@ -52,7 +52,7 @@ specifiers:
|
|||
semver: ^7.3.5
|
||||
sortablejs: ^1.14.0
|
||||
sourcemapped-stacktrace: ^1.1.11
|
||||
terser-webpack-plugin: '4'
|
||||
terser-webpack-plugin: ^4.2.3
|
||||
text-diff: ^1.0.1
|
||||
vue: ^2.6.14
|
||||
vue-clickaway: ^2.2.2
|
||||
|
@ -97,7 +97,7 @@ dependencies:
|
|||
|
||||
devDependencies:
|
||||
'@babel/core': 7.16.0
|
||||
'@babel/eslint-parser': 7.16.0_@babel+core@7.16.0+eslint@7.32.0
|
||||
'@babel/eslint-parser': 7.16.0_q3hdp35agahhlc67sgxrhsgj5a
|
||||
'@babel/plugin-proposal-class-properties': 7.16.0_@babel+core@7.16.0
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator': 7.16.0_@babel+core@7.16.0
|
||||
'@babel/plugin-proposal-object-rest-spread': 7.16.0_@babel+core@7.16.0
|
||||
|
@ -105,8 +105,8 @@ devDependencies:
|
|||
'@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.16.0
|
||||
'@babel/plugin-transform-react-jsx': 7.16.0_@babel+core@7.16.0
|
||||
'@ffz/fontello-cli': 1.0.4
|
||||
'@webpack-cli/serve': 1.6.0_90245a0e5d2744c77d25a53c1b9ef3e7
|
||||
babel-loader: 8.2.3_1bd60a6cd0f7024f034efd75ae733a3f
|
||||
'@webpack-cli/serve': 1.6.0_sasfuds5e5cmo7jfuu6bxhxt44
|
||||
babel-loader: 8.2.3_dplau3gq64be6a2o7v2244z2h4
|
||||
clean-webpack-plugin: 3.0.0_webpack@4.46.0
|
||||
copy-webpack-plugin: 5.1.2_webpack@4.46.0
|
||||
cross-env: 7.0.3
|
||||
|
@ -123,11 +123,11 @@ devDependencies:
|
|||
sass-loader: 7.3.1_webpack@4.46.0
|
||||
semver: 7.3.5
|
||||
terser-webpack-plugin: 4.2.3_webpack@4.46.0
|
||||
vue-loader: 15.9.8_e15c8784917900cf55a453471769391c
|
||||
vue-loader: 15.9.8_4jxi6swgjym7ovrhfrafs526hm
|
||||
vue-template-compiler: 2.6.14
|
||||
webpack: 4.46.0_webpack-cli@4.9.1
|
||||
webpack-cli: 4.9.1_703ab6c6d02792f100f7002d09038fa7
|
||||
webpack-dev-server: 4.4.0_webpack-cli@4.9.1+webpack@4.46.0
|
||||
webpack-cli: 4.9.1_oa5lnrwqe6jpcahxaawqsa4pu4
|
||||
webpack-dev-server: 4.4.0_7d675yvzq6xw7ta76a6hebfduq
|
||||
webpack-manifest-plugin: 4.0.2_webpack@4.46.0
|
||||
webpack-merge: 4.2.2
|
||||
|
||||
|
@ -174,7 +174,7 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@babel/eslint-parser/7.16.0_@babel+core@7.16.0+eslint@7.32.0:
|
||||
/@babel/eslint-parser/7.16.0_q3hdp35agahhlc67sgxrhsgj5a:
|
||||
resolution: {integrity: sha512-c+AsYOHjI+FgCa+ifLd8sDXp4U4mjkfFgL9NdQWhuA731kAUJs0WdJIXET4A14EJAR9Jv9FFF/MzPWJfV9Oirw==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
|
||||
peerDependencies:
|
||||
|
@ -366,6 +366,8 @@ packages:
|
|||
resolution: {integrity: sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.16.0
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-proposal-class-properties/7.16.0_@babel+core@7.16.0:
|
||||
|
@ -682,10 +684,10 @@ packages:
|
|||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
||||
/@vue/component-compiler-utils/3.3.0:
|
||||
/@vue/component-compiler-utils/3.3.0_react@17.0.2:
|
||||
resolution: {integrity: sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==}
|
||||
dependencies:
|
||||
consolidate: 0.15.1
|
||||
consolidate: 0.15.1_react@17.0.2
|
||||
hash-sum: 1.0.2
|
||||
lru-cache: 4.1.5
|
||||
merge-source-map: 1.1.0
|
||||
|
@ -694,7 +696,61 @@ packages:
|
|||
source-map: 0.6.1
|
||||
vue-template-es2015-compiler: 1.9.1
|
||||
optionalDependencies:
|
||||
prettier: 2.4.1
|
||||
prettier: 2.7.1
|
||||
transitivePeerDependencies:
|
||||
- arc-templates
|
||||
- atpl
|
||||
- babel-core
|
||||
- bracket-template
|
||||
- coffee-script
|
||||
- dot
|
||||
- dust
|
||||
- dustjs-helpers
|
||||
- dustjs-linkedin
|
||||
- eco
|
||||
- ect
|
||||
- ejs
|
||||
- haml-coffee
|
||||
- hamlet
|
||||
- hamljs
|
||||
- handlebars
|
||||
- hogan.js
|
||||
- htmling
|
||||
- jade
|
||||
- jazz
|
||||
- jqtpl
|
||||
- just
|
||||
- liquid-node
|
||||
- liquor
|
||||
- lodash
|
||||
- marko
|
||||
- mote
|
||||
- mustache
|
||||
- nunjucks
|
||||
- plates
|
||||
- pug
|
||||
- qejs
|
||||
- ractive
|
||||
- razor-tmpl
|
||||
- react
|
||||
- react-dom
|
||||
- slm
|
||||
- squirrelly
|
||||
- swig
|
||||
- swig-templates
|
||||
- teacup
|
||||
- templayed
|
||||
- then-jade
|
||||
- then-pug
|
||||
- tinyliquid
|
||||
- toffee
|
||||
- twig
|
||||
- twing
|
||||
- underscore
|
||||
- vash
|
||||
- velocityjs
|
||||
- walrus
|
||||
- whiskers
|
||||
dev: true
|
||||
|
||||
/@webassemblyjs/ast/1.9.0:
|
||||
|
@ -824,14 +880,14 @@ packages:
|
|||
'@xtuc/long': 4.2.2
|
||||
dev: true
|
||||
|
||||
/@webpack-cli/configtest/1.1.0_webpack-cli@4.9.1+webpack@4.46.0:
|
||||
/@webpack-cli/configtest/1.1.0_7d675yvzq6xw7ta76a6hebfduq:
|
||||
resolution: {integrity: sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==}
|
||||
peerDependencies:
|
||||
webpack: 4.x.x || 5.x.x
|
||||
webpack-cli: 4.x.x
|
||||
dependencies:
|
||||
webpack: 4.46.0_webpack-cli@4.9.1
|
||||
webpack-cli: 4.9.1_703ab6c6d02792f100f7002d09038fa7
|
||||
webpack-cli: 4.9.1_oa5lnrwqe6jpcahxaawqsa4pu4
|
||||
dev: true
|
||||
|
||||
/@webpack-cli/info/1.4.0_webpack-cli@4.9.1:
|
||||
|
@ -840,10 +896,10 @@ packages:
|
|||
webpack-cli: 4.x.x
|
||||
dependencies:
|
||||
envinfo: 7.8.1
|
||||
webpack-cli: 4.9.1_703ab6c6d02792f100f7002d09038fa7
|
||||
webpack-cli: 4.9.1_oa5lnrwqe6jpcahxaawqsa4pu4
|
||||
dev: true
|
||||
|
||||
/@webpack-cli/serve/1.6.0_90245a0e5d2744c77d25a53c1b9ef3e7:
|
||||
/@webpack-cli/serve/1.6.0_sasfuds5e5cmo7jfuu6bxhxt44:
|
||||
resolution: {integrity: sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==}
|
||||
peerDependencies:
|
||||
webpack-cli: 4.x.x
|
||||
|
@ -852,8 +908,8 @@ packages:
|
|||
webpack-dev-server:
|
||||
optional: true
|
||||
dependencies:
|
||||
webpack-cli: 4.9.1_703ab6c6d02792f100f7002d09038fa7
|
||||
webpack-dev-server: 4.4.0_webpack-cli@4.9.1+webpack@4.46.0
|
||||
webpack-cli: 4.9.1_oa5lnrwqe6jpcahxaawqsa4pu4
|
||||
webpack-dev-server: 4.4.0_7d675yvzq6xw7ta76a6hebfduq
|
||||
dev: true
|
||||
|
||||
/@xtuc/ieee754/1.2.0:
|
||||
|
@ -892,6 +948,12 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/acorn/8.8.0:
|
||||
resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/aggregate-error/3.1.0:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -979,6 +1041,8 @@ packages:
|
|||
dependencies:
|
||||
micromatch: 3.1.10
|
||||
normalize-path: 2.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -1116,7 +1180,7 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/babel-loader/8.2.3_1bd60a6cd0f7024f034efd75ae733a3f:
|
||||
/babel-loader/8.2.3_dplau3gq64be6a2o7v2244z2h4:
|
||||
resolution: {integrity: sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==}
|
||||
engines: {node: '>= 8.9'}
|
||||
peerDependencies:
|
||||
|
@ -1221,6 +1285,8 @@ packages:
|
|||
qs: 6.7.0
|
||||
raw-body: 2.4.0
|
||||
type-is: 1.6.18
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/bonjour/3.5.0:
|
||||
|
@ -1255,6 +1321,8 @@ packages:
|
|||
snapdragon-node: 2.1.1
|
||||
split-string: 3.1.0
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/braces/3.0.2:
|
||||
|
@ -1328,7 +1396,7 @@ packages:
|
|||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001278
|
||||
caniuse-lite: 1.0.30001387
|
||||
electron-to-chromium: 1.3.890
|
||||
escalade: 3.1.1
|
||||
node-releases: 2.0.1
|
||||
|
@ -1421,6 +1489,8 @@ packages:
|
|||
ssri: 8.0.1
|
||||
tar: 6.1.11
|
||||
unique-filename: 1.1.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
dev: true
|
||||
|
||||
/cache-base/1.0.1:
|
||||
|
@ -1455,8 +1525,8 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/caniuse-lite/1.0.30001278:
|
||||
resolution: {integrity: sha512-mpF9KeH8u5cMoEmIic/cr7PNS+F5LWBk0t2ekGT60lFf0Wq+n9LspAj0g3P+o7DQhD3sUdlMln4YFAWhFYn9jg==}
|
||||
/caniuse-lite/1.0.30001387:
|
||||
resolution: {integrity: sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==}
|
||||
dev: true
|
||||
|
||||
/chainsaw/0.1.0:
|
||||
|
@ -1484,7 +1554,7 @@ packages:
|
|||
|
||||
/chokidar/2.1.8:
|
||||
resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==}
|
||||
deprecated: Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.
|
||||
deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
|
||||
dependencies:
|
||||
anymatch: 2.0.0
|
||||
async-each: 1.0.3
|
||||
|
@ -1499,6 +1569,8 @@ packages:
|
|||
upath: 1.2.0
|
||||
optionalDependencies:
|
||||
fsevents: 1.2.13
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -1518,6 +1590,23 @@ packages:
|
|||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/chokidar/3.5.3:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
anymatch: 3.1.2
|
||||
braces: 3.0.2
|
||||
glob-parent: 5.1.2
|
||||
is-binary-path: 2.1.0
|
||||
is-glob: 4.0.3
|
||||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/chownr/1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: true
|
||||
|
@ -1662,6 +1751,8 @@ packages:
|
|||
on-headers: 1.0.2
|
||||
safe-buffer: 5.1.2
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/concat-map/0.0.1:
|
||||
|
@ -1687,11 +1778,173 @@ packages:
|
|||
resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==}
|
||||
dev: true
|
||||
|
||||
/consolidate/0.15.1:
|
||||
/consolidate/0.15.1_react@17.0.2:
|
||||
resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
peerDependencies:
|
||||
arc-templates: ^0.5.3
|
||||
atpl: '>=0.7.6'
|
||||
babel-core: ^6.26.3
|
||||
bracket-template: ^1.1.5
|
||||
coffee-script: ^1.12.7
|
||||
dot: ^1.1.3
|
||||
dust: ^0.3.0
|
||||
dustjs-helpers: ^1.7.4
|
||||
dustjs-linkedin: ^2.7.5
|
||||
eco: ^1.1.0-rc-3
|
||||
ect: ^0.5.9
|
||||
ejs: ^3.1.5
|
||||
haml-coffee: ^1.14.1
|
||||
hamlet: ^0.3.3
|
||||
hamljs: ^0.6.2
|
||||
handlebars: ^4.7.6
|
||||
hogan.js: ^3.0.2
|
||||
htmling: ^0.0.8
|
||||
jade: ^1.11.0
|
||||
jazz: ^0.0.18
|
||||
jqtpl: ~1.1.0
|
||||
just: ^0.1.8
|
||||
liquid-node: ^3.0.1
|
||||
liquor: ^0.0.5
|
||||
lodash: ^4.17.20
|
||||
marko: ^3.14.4
|
||||
mote: ^0.2.0
|
||||
mustache: ^3.0.0
|
||||
nunjucks: ^3.2.2
|
||||
plates: ~0.4.11
|
||||
pug: ^3.0.0
|
||||
qejs: ^3.0.5
|
||||
ractive: ^1.3.12
|
||||
razor-tmpl: ^1.3.1
|
||||
react: ^16.13.1
|
||||
react-dom: ^16.13.1
|
||||
slm: ^2.0.0
|
||||
squirrelly: ^5.1.0
|
||||
swig: ^1.4.2
|
||||
swig-templates: ^2.0.3
|
||||
teacup: ^2.0.0
|
||||
templayed: '>=0.2.3'
|
||||
then-jade: '*'
|
||||
then-pug: '*'
|
||||
tinyliquid: ^0.2.34
|
||||
toffee: ^0.3.6
|
||||
twig: ^1.15.2
|
||||
twing: ^5.0.2
|
||||
underscore: ^1.11.0
|
||||
vash: ^0.13.0
|
||||
velocityjs: ^2.0.1
|
||||
walrus: ^0.10.1
|
||||
whiskers: ^0.4.0
|
||||
peerDependenciesMeta:
|
||||
arc-templates:
|
||||
optional: true
|
||||
atpl:
|
||||
optional: true
|
||||
babel-core:
|
||||
optional: true
|
||||
bracket-template:
|
||||
optional: true
|
||||
coffee-script:
|
||||
optional: true
|
||||
dot:
|
||||
optional: true
|
||||
dust:
|
||||
optional: true
|
||||
dustjs-helpers:
|
||||
optional: true
|
||||
dustjs-linkedin:
|
||||
optional: true
|
||||
eco:
|
||||
optional: true
|
||||
ect:
|
||||
optional: true
|
||||
ejs:
|
||||
optional: true
|
||||
haml-coffee:
|
||||
optional: true
|
||||
hamlet:
|
||||
optional: true
|
||||
hamljs:
|
||||
optional: true
|
||||
handlebars:
|
||||
optional: true
|
||||
hogan.js:
|
||||
optional: true
|
||||
htmling:
|
||||
optional: true
|
||||
jade:
|
||||
optional: true
|
||||
jazz:
|
||||
optional: true
|
||||
jqtpl:
|
||||
optional: true
|
||||
just:
|
||||
optional: true
|
||||
liquid-node:
|
||||
optional: true
|
||||
liquor:
|
||||
optional: true
|
||||
lodash:
|
||||
optional: true
|
||||
marko:
|
||||
optional: true
|
||||
mote:
|
||||
optional: true
|
||||
mustache:
|
||||
optional: true
|
||||
nunjucks:
|
||||
optional: true
|
||||
plates:
|
||||
optional: true
|
||||
pug:
|
||||
optional: true
|
||||
qejs:
|
||||
optional: true
|
||||
ractive:
|
||||
optional: true
|
||||
razor-tmpl:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
slm:
|
||||
optional: true
|
||||
squirrelly:
|
||||
optional: true
|
||||
swig:
|
||||
optional: true
|
||||
swig-templates:
|
||||
optional: true
|
||||
teacup:
|
||||
optional: true
|
||||
templayed:
|
||||
optional: true
|
||||
then-jade:
|
||||
optional: true
|
||||
then-pug:
|
||||
optional: true
|
||||
tinyliquid:
|
||||
optional: true
|
||||
toffee:
|
||||
optional: true
|
||||
twig:
|
||||
optional: true
|
||||
twing:
|
||||
optional: true
|
||||
underscore:
|
||||
optional: true
|
||||
vash:
|
||||
optional: true
|
||||
velocityjs:
|
||||
optional: true
|
||||
walrus:
|
||||
optional: true
|
||||
whiskers:
|
||||
optional: true
|
||||
dependencies:
|
||||
bluebird: 3.7.2
|
||||
react: 17.0.2
|
||||
dev: true
|
||||
|
||||
/constants-browserify/1.0.0:
|
||||
|
@ -1872,12 +2125,22 @@ packages:
|
|||
|
||||
/debug/2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
dev: true
|
||||
|
||||
/debug/3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: true
|
||||
|
@ -2435,6 +2698,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/express/4.17.1:
|
||||
|
@ -2471,6 +2736,8 @@ packages:
|
|||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/extend-shallow/2.0.1:
|
||||
|
@ -2500,6 +2767,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/extract-loader/2.0.1:
|
||||
|
@ -2609,6 +2878,8 @@ packages:
|
|||
parseurl: 1.3.3
|
||||
statuses: 1.5.0
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/find-cache-dir/2.1.0:
|
||||
|
@ -2743,7 +3014,7 @@ packages:
|
|||
requiresBuild: true
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
nan: 2.15.0
|
||||
nan: 2.16.0
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -3819,6 +4090,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/micromatch/4.0.4:
|
||||
|
@ -3996,8 +4269,8 @@ packages:
|
|||
thunky: 1.1.0
|
||||
dev: true
|
||||
|
||||
/nan/2.15.0:
|
||||
resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==}
|
||||
/nan/2.16.0:
|
||||
resolution: {integrity: sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
@ -4017,6 +4290,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/natural-compare/1.4.0:
|
||||
|
@ -4483,6 +4758,8 @@ packages:
|
|||
async: 2.6.3
|
||||
debug: 3.2.7
|
||||
mkdirp: 0.5.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/posix-character-classes/0.1.1:
|
||||
|
@ -4547,8 +4824,8 @@ packages:
|
|||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/prettier/2.4.1:
|
||||
resolution: {integrity: sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==}
|
||||
/prettier/2.7.1:
|
||||
resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
|
@ -4570,6 +4847,11 @@ packages:
|
|||
|
||||
/promise-inflight/1.0.1:
|
||||
resolution: {integrity: sha1-mEcocL8igTL8vdhoEputEsPAKeM=}
|
||||
peerDependencies:
|
||||
bluebird: '*'
|
||||
peerDependenciesMeta:
|
||||
bluebird:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/prop-types/15.7.2:
|
||||
|
@ -4715,7 +4997,6 @@ packages:
|
|||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
dev: false
|
||||
|
||||
/readable-stream/2.3.7:
|
||||
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
|
||||
|
@ -4744,6 +5025,8 @@ packages:
|
|||
graceful-fs: 4.2.8
|
||||
micromatch: 3.1.10
|
||||
readable-stream: 2.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -5012,6 +5295,8 @@ packages:
|
|||
on-finished: 2.3.0
|
||||
range-parser: 1.2.1
|
||||
statuses: 1.5.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/serialize-javascript/4.0.0:
|
||||
|
@ -5037,6 +5322,8 @@ packages:
|
|||
http-errors: 1.6.3
|
||||
mime-types: 2.1.33
|
||||
parseurl: 1.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/serve-static/1.14.1:
|
||||
|
@ -5047,6 +5334,8 @@ packages:
|
|||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.17.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/set-immediate-shim/1.0.1:
|
||||
|
@ -5160,6 +5449,8 @@ packages:
|
|||
source-map: 0.5.7
|
||||
source-map-resolve: 0.5.3
|
||||
use: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/sockjs/0.3.21:
|
||||
|
@ -5472,6 +5763,8 @@ packages:
|
|||
terser: 5.9.0
|
||||
webpack: 4.46.0_webpack-cli@4.9.1
|
||||
webpack-sources: 1.4.3
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
dev: true
|
||||
|
||||
/terser/4.8.0:
|
||||
|
@ -5479,6 +5772,7 @@ packages:
|
|||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
acorn: 8.8.0
|
||||
commander: 2.20.3
|
||||
source-map: 0.6.1
|
||||
source-map-support: 0.5.20
|
||||
|
@ -5489,6 +5783,7 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
acorn: 8.8.0
|
||||
commander: 2.20.3
|
||||
source-map: 0.7.3
|
||||
source-map-support: 0.5.20
|
||||
|
@ -5781,20 +6076,23 @@ packages:
|
|||
resolution: {integrity: sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==}
|
||||
dev: true
|
||||
|
||||
/vue-loader/15.9.8_e15c8784917900cf55a453471769391c:
|
||||
/vue-loader/15.9.8_4jxi6swgjym7ovrhfrafs526hm:
|
||||
resolution: {integrity: sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==}
|
||||
peerDependencies:
|
||||
'@vue/compiler-sfc': ^3.0.8
|
||||
cache-loader: '*'
|
||||
css-loader: '*'
|
||||
vue-template-compiler: '*'
|
||||
webpack: ^3.0.0 || ^4.1.0 || ^5.0.0-0
|
||||
peerDependenciesMeta:
|
||||
'@vue/compiler-sfc':
|
||||
optional: true
|
||||
cache-loader:
|
||||
optional: true
|
||||
vue-template-compiler:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@vue/component-compiler-utils': 3.3.0
|
||||
'@vue/component-compiler-utils': 3.3.0_react@17.0.2
|
||||
css-loader: 3.6.0_webpack@4.46.0
|
||||
hash-sum: 1.0.2
|
||||
loader-utils: 1.4.0
|
||||
|
@ -5802,6 +6100,60 @@ packages:
|
|||
vue-style-loader: 4.1.3
|
||||
vue-template-compiler: 2.6.14
|
||||
webpack: 4.46.0_webpack-cli@4.9.1
|
||||
transitivePeerDependencies:
|
||||
- arc-templates
|
||||
- atpl
|
||||
- babel-core
|
||||
- bracket-template
|
||||
- coffee-script
|
||||
- dot
|
||||
- dust
|
||||
- dustjs-helpers
|
||||
- dustjs-linkedin
|
||||
- eco
|
||||
- ect
|
||||
- ejs
|
||||
- haml-coffee
|
||||
- hamlet
|
||||
- hamljs
|
||||
- handlebars
|
||||
- hogan.js
|
||||
- htmling
|
||||
- jade
|
||||
- jazz
|
||||
- jqtpl
|
||||
- just
|
||||
- liquid-node
|
||||
- liquor
|
||||
- lodash
|
||||
- marko
|
||||
- mote
|
||||
- mustache
|
||||
- nunjucks
|
||||
- plates
|
||||
- pug
|
||||
- qejs
|
||||
- ractive
|
||||
- razor-tmpl
|
||||
- react
|
||||
- react-dom
|
||||
- slm
|
||||
- squirrelly
|
||||
- swig
|
||||
- swig-templates
|
||||
- teacup
|
||||
- templayed
|
||||
- then-jade
|
||||
- then-pug
|
||||
- tinyliquid
|
||||
- toffee
|
||||
- twig
|
||||
- twing
|
||||
- underscore
|
||||
- vash
|
||||
- velocityjs
|
||||
- walrus
|
||||
- whiskers
|
||||
dev: true
|
||||
|
||||
/vue-observe-visibility/1.0.0:
|
||||
|
@ -5841,6 +6193,8 @@ packages:
|
|||
requiresBuild: true
|
||||
dependencies:
|
||||
chokidar: 2.1.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -5850,8 +6204,10 @@ packages:
|
|||
graceful-fs: 4.2.8
|
||||
neo-async: 2.6.2
|
||||
optionalDependencies:
|
||||
chokidar: 3.5.2
|
||||
chokidar: 3.5.3
|
||||
watchpack-chokidar2: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/wbuf/1.7.3:
|
||||
|
@ -5864,7 +6220,7 @@ packages:
|
|||
resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=}
|
||||
dev: true
|
||||
|
||||
/webpack-cli/4.9.1_703ab6c6d02792f100f7002d09038fa7:
|
||||
/webpack-cli/4.9.1_oa5lnrwqe6jpcahxaawqsa4pu4:
|
||||
resolution: {integrity: sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
@ -5885,9 +6241,9 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
'@discoveryjs/json-ext': 0.5.5
|
||||
'@webpack-cli/configtest': 1.1.0_webpack-cli@4.9.1+webpack@4.46.0
|
||||
'@webpack-cli/configtest': 1.1.0_7d675yvzq6xw7ta76a6hebfduq
|
||||
'@webpack-cli/info': 1.4.0_webpack-cli@4.9.1
|
||||
'@webpack-cli/serve': 1.6.0_90245a0e5d2744c77d25a53c1b9ef3e7
|
||||
'@webpack-cli/serve': 1.6.0_sasfuds5e5cmo7jfuu6bxhxt44
|
||||
colorette: 2.0.16
|
||||
commander: 7.2.0
|
||||
execa: 5.1.1
|
||||
|
@ -5896,7 +6252,7 @@ packages:
|
|||
interpret: 2.2.0
|
||||
rechoir: 0.7.1
|
||||
webpack: 4.46.0_webpack-cli@4.9.1
|
||||
webpack-dev-server: 4.4.0_webpack-cli@4.9.1+webpack@4.46.0
|
||||
webpack-dev-server: 4.4.0_7d675yvzq6xw7ta76a6hebfduq
|
||||
webpack-merge: 5.8.0
|
||||
dev: true
|
||||
|
||||
|
@ -5914,7 +6270,7 @@ packages:
|
|||
webpack: 4.46.0_webpack-cli@4.9.1
|
||||
dev: true
|
||||
|
||||
/webpack-dev-server/4.4.0_webpack-cli@4.9.1+webpack@4.46.0:
|
||||
/webpack-dev-server/4.4.0_7d675yvzq6xw7ta76a6hebfduq:
|
||||
resolution: {integrity: sha512-+S0XRIbsopVjPFjCO8I07FXYBWYqkFmuP56ucGMTs2hA/gV4q2M9xTmNo5Tg4o8ffRR+Nm3AsXnQXxKRyYovrA==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
hasBin: true
|
||||
|
@ -5949,7 +6305,7 @@ packages:
|
|||
strip-ansi: 7.0.1
|
||||
url: 0.11.0
|
||||
webpack: 4.46.0_webpack-cli@4.9.1
|
||||
webpack-cli: 4.9.1_703ab6c6d02792f100f7002d09038fa7
|
||||
webpack-cli: 4.9.1_oa5lnrwqe6jpcahxaawqsa4pu4
|
||||
webpack-dev-middleware: 5.2.1_webpack@4.46.0
|
||||
ws: 8.2.3
|
||||
transitivePeerDependencies:
|
||||
|
@ -6042,8 +6398,10 @@ packages:
|
|||
tapable: 1.1.3
|
||||
terser-webpack-plugin: 1.4.5_webpack@4.46.0
|
||||
watchpack: 1.7.5
|
||||
webpack-cli: 4.9.1_703ab6c6d02792f100f7002d09038fa7
|
||||
webpack-cli: 4.9.1_oa5lnrwqe6jpcahxaawqsa4pu4
|
||||
webpack-sources: 1.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/websocket-driver/0.7.4:
|
||||
|
|
Binary file not shown.
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Copyright (C) 2021 by original authors @ fontello.com</metadata>
|
||||
<metadata>Copyright (C) 2022 by original authors @ fontello.com</metadata>
|
||||
<defs>
|
||||
<font id="ffz-fontello" horiz-adv-x="1000" >
|
||||
<font-face font-family="ffz-fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
|
||||
|
@ -148,6 +148,10 @@
|
|||
|
||||
<glyph glyph-name="right-open" unicode="" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
|
||||
|
||||
<glyph glyph-name="mastodon" unicode="" d="M933 629c-14 106-108 190-219 206-18 3-89 13-253 13h-2c-164 0-199-10-218-13-107-15-206-91-230-198-11-53-12-111-10-165 3-77 4-154 11-231 5-51 13-102 25-151 23-93 115-169 205-200 96-33 200-38 299-16 11 3 22 6 32 9 24 7 53 16 73 31 1 0 1 0 1 1 0 0 0 0 0 1v74c0 0 0 1 0 1 0 0 0 0-1 1 0 0 0 0 0 0-1 0-1 0-1 0-64-15-129-22-195-22-112 0-142 53-151 75-7 19-11 38-13 59 0 0 0 0 0 1 0 0 0 0 1 0 0 1 0 1 1 1 0 0 0 0 1 0 62-15 126-23 191-23 15 0 30 0 46 1 65 1 133 5 196 17 2 0 4 1 5 1 100 19 196 79 206 231 0 6 1 62 1 68 0 21 7 150-1 228z m-155-378h-105v256c0 54-23 81-69 81-51 0-76-32-76-96v-140h-105v140c0 64-26 96-76 96-46 0-69-27-69-81v-256h-106v264c0 54 14 96 42 128 29 32 66 48 113 48 54 0 95-21 122-62l26-43 27 43c27 41 68 62 122 62 46 0 84-16 113-48 28-32 42-74 42-128l-1-264z" horiz-adv-x="937" />
|
||||
|
||||
<glyph glyph-name="volume-up" unicode="" d="M0 169l0 360 203 0 307 250 0-858-307 248-203 0z m563 33q62 63 62 151t-62 152l60 65q90-90 92-219 0-125-92-213z m101-105q106 105 106 256t-106 258l66 62q131-133 131-319t-131-321z m100-98q146 147 146 354t-146 353l62 65q82-82 128-190t46-227-46-229-128-190z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="move" unicode="" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="link-ext" unicode="" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
|
||||
|
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -2,7 +2,7 @@
|
|||
'use strict';
|
||||
(() => {
|
||||
// Don't run on certain sub-domains.
|
||||
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev)\./.test(location.hostname) )
|
||||
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
|
||||
return;
|
||||
|
||||
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev'),
|
||||
|
|
|
@ -48,6 +48,8 @@ export default class ExperimentManager extends Module {
|
|||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.get = this.getAssignment;
|
||||
|
||||
this.inject('settings');
|
||||
|
||||
this.settings.addUI('experiments', {
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
{
|
||||
"line_renderer": {
|
||||
"name": "Modular Chat Line Rendering",
|
||||
"description": "Enable a newer, modular chat line renderer.",
|
||||
"groups": [
|
||||
{"value": true, "weight": 0},
|
||||
{"value": false, "weight": 100}
|
||||
]
|
||||
},
|
||||
"api_load": {
|
||||
"name": "New API Stress Testing",
|
||||
"description": "Send duplicate requests to the new API server for load testing.",
|
||||
|
|
29
src/modules/chat/actions/components/edit-copy.vue
Normal file
29
src/modules/chat/actions/components/edit-copy.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template lang="html">
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<label for="edit_format" class="tw-mg-y-05">
|
||||
{{ t('setting.actions.format', 'Format') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-full-width">
|
||||
<input
|
||||
id="edit_format"
|
||||
v-model="value.format"
|
||||
:placeholder="defaults.format"
|
||||
class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
|
||||
@input="$emit('input', value)"
|
||||
>
|
||||
|
||||
<div class="tw-c-text-alt-2 tw-mg-b-1">
|
||||
{{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['value', 'defaults', 'vars'],
|
||||
}
|
||||
|
||||
</script>
|
40
src/modules/chat/actions/components/edit-emote.vue
Normal file
40
src/modules/chat/actions/components/edit-emote.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template lang="html">
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<label class="tw-mg-y-05">
|
||||
{{ t('setting.actions.emote', 'Emote') }}
|
||||
</label>
|
||||
|
||||
<emote-picker
|
||||
:value="val"
|
||||
class="tw-full-width"
|
||||
@input="change"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
val: this.value
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
value() {
|
||||
this.val = this.value;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(val) {
|
||||
this.val = val;
|
||||
this.$emit('input', this.val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
5
src/modules/chat/actions/components/preview-emote.vue
Normal file
5
src/modules/chat/actions/components/preview-emote.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template functional>
|
||||
<figure class="mod-icon__image">
|
||||
<img :src="props.data.src">
|
||||
</figure>
|
||||
</template>
|
|
@ -38,6 +38,18 @@ export default class Actions extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.actions.hover-size', {
|
||||
default: 30,
|
||||
ui: {
|
||||
path: 'Chat > Actions > Message Hover >> Appearance',
|
||||
title: 'Action Size',
|
||||
description: "How tall hover actions should be, in pixels. This may be affected by your browser's zoom and font size settings.",
|
||||
component: 'setting-text-box',
|
||||
process: 'to_int',
|
||||
bounds: [1]
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.actions.reasons', {
|
||||
default: [
|
||||
{v: {text: 'One-Man Spam', i18n: 'chat.reasons.spam'}},
|
||||
|
@ -67,6 +79,42 @@ export default class Actions extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.actions.hover', {
|
||||
process: (ctx, val) =>
|
||||
val.filter(x => x.appearance &&
|
||||
this.renderers[x.appearance.type] &&
|
||||
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
|
||||
(! x.action || this.actions[x.action])
|
||||
),
|
||||
|
||||
default: [
|
||||
{v: {action: 'pin', appearance: {type: 'icon', icon: 'ffz-i-pin'}, options: {}, display: {mod_icons: true}}},
|
||||
{v: {action: 'reply', appearance: {type: 'dynamic'}, options: {}, display: {}}}
|
||||
],
|
||||
|
||||
type: 'array_merge',
|
||||
inherit_default: true,
|
||||
|
||||
ui: {
|
||||
path: 'Chat > Actions > Message Hover @{"description": "Here, you can define custom actions that will appear on top of messages in chat when you hover over them. If you aren\'t seeing an action you\'ve defined here in chat, please make sure that you have enabled Mod Icons in the chat settings menu."}',
|
||||
component: 'chat-actions',
|
||||
context: ['user', 'room', 'message'],
|
||||
inline: true,
|
||||
modifiers: true,
|
||||
hover_modifier: false,
|
||||
|
||||
data: () => {
|
||||
const chat = this.resolve('site.chat');
|
||||
|
||||
return {
|
||||
color: val => chat && chat.colors ? chat.colors.process(val) : val,
|
||||
actions: deep_copy(this.actions),
|
||||
renderers: deep_copy(this.renderers)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.actions.inline', {
|
||||
// Filter out actions
|
||||
process: (ctx, val) =>
|
||||
|
@ -80,8 +128,7 @@ export default class Actions extends Module {
|
|||
{v: {action: 'ban', appearance: {type: 'icon', icon: 'ffz-i-block'}, options: {}, display: {mod: true, mod_icons: true, deleted: false}}},
|
||||
{v: {action: 'unban', appearance: {type: 'icon', icon: 'ffz-i-ok'}, options: {}, display: {mod: true, mod_icons: true, deleted: true}}},
|
||||
{v: {action: 'timeout', appearance: {type: 'icon', icon: 'ffz-i-clock'}, display: {mod: true, mod_icons: true}}},
|
||||
{v: {action: 'msg_delete', appearance: {type: 'icon', icon: 'ffz-i-trash'}, options: {}, display: {mod: true, mod_icons: true}}},
|
||||
{v: {action: 'reply', appearance: {type: 'icon', icon: 'ffz-i-reply'}, options: {}, display: {}}}
|
||||
{v: {action: 'msg_delete', appearance: {type: 'icon', icon: 'ffz-i-trash'}, options: {}, display: {mod: true, mod_icons: true}}}
|
||||
],
|
||||
|
||||
type: 'array_merge',
|
||||
|
@ -223,8 +270,12 @@ export default class Actions extends Module {
|
|||
|
||||
this.actions[key] = data;
|
||||
|
||||
for(const ctx of this.settings.__contexts)
|
||||
for(const ctx of this.settings.__contexts) {
|
||||
ctx.update('chat.actions.inline');
|
||||
ctx.update('chat.actions.hover');
|
||||
ctx.update('chat.actions.user-context');
|
||||
ctx.update('chat.actions.room');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -234,8 +285,13 @@ export default class Actions extends Module {
|
|||
|
||||
this.renderers[key] = data;
|
||||
|
||||
for(const ctx of this.settings.__contexts)
|
||||
for(const ctx of this.settings.__contexts) {
|
||||
ctx.update('chat.actions.inline');
|
||||
ctx.update('chat.actions.inline');
|
||||
ctx.update('chat.actions.hover');
|
||||
ctx.update('chat.actions.user-context');
|
||||
ctx.update('chat.actions.room');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -501,6 +557,8 @@ export default class Actions extends Module {
|
|||
if ( ! data )
|
||||
continue;
|
||||
|
||||
data.ctx = 'room';
|
||||
|
||||
const type = data.type;
|
||||
if ( type ) {
|
||||
if ( type === 'new-line' ) {
|
||||
|
@ -545,6 +603,12 @@ export default class Actions extends Module {
|
|||
if ( maybe_call(act.hidden, this, data, null, current_room, current_user, mod_icons) )
|
||||
continue;
|
||||
|
||||
if ( ap.type === 'dynamic' ) {
|
||||
const out = act.dynamicAppearance && act.dynamicAppearance.call(this, Object.assign({}, ap), data, null, current_room, current_user, mod_icons);
|
||||
if ( out )
|
||||
ap = out;
|
||||
}
|
||||
|
||||
if ( act.override_appearance ) {
|
||||
const out = act.override_appearance.call(this, Object.assign({}, ap), data, null, current_room, current_user, mod_icons);
|
||||
if ( out )
|
||||
|
@ -615,16 +679,17 @@ export default class Actions extends Module {
|
|||
const u = site.getUser(),
|
||||
r = {id: line.props.channelID, login: room};
|
||||
|
||||
const has_replies = line.chatRepliesTreatment ? line.chatRepliesTreatment !== 'control' : false,
|
||||
can_replies = has_replies && ! msg.deleted && ! line.props.disableReplyClick,
|
||||
can_reply = can_replies && u.login !== msg.user?.login && ! msg.reply;
|
||||
const has_replies = !!(line.props.hasReply || line.props.reply || ! line.props.replyRestrictedReason),
|
||||
can_replies = has_replies && msg.message && ! msg.deleted && ! line.props.disableReplyClick,
|
||||
can_reply = can_replies && (has_replies || (u && u.login !== msg.user?.login));
|
||||
|
||||
msg.roomId = r.id;
|
||||
|
||||
if ( u ) {
|
||||
u.moderator = line.props.isCurrentUserModerator;
|
||||
u.staff = line.props.isCurrentUserStaff;
|
||||
u.can_reply = this.parent.context.get('chat.replies.style') === 2 && can_reply;
|
||||
u.reply_mode = this.parent.context.get('chat.replies.style'),
|
||||
u.can_reply = can_reply;
|
||||
}
|
||||
|
||||
const current_level = this.getUserLevel(r, u),
|
||||
|
@ -649,6 +714,8 @@ export default class Actions extends Module {
|
|||
if ( ! data )
|
||||
continue;
|
||||
|
||||
data.ctx = 'user_context';
|
||||
|
||||
if ( data.type === 'new-line' ) {
|
||||
line = null;
|
||||
continue;
|
||||
|
@ -681,11 +748,17 @@ export default class Actions extends Module {
|
|||
(disp.deleted != null && disp.deleted !== !!msg.deleted) )
|
||||
continue;
|
||||
|
||||
if ( maybe_call(act.hidden, this, data, msg, r, u, mod_icons) )
|
||||
if ( maybe_call(act.hidden, this, data, msg, r, u, mod_icons, chat_line) )
|
||||
continue;
|
||||
|
||||
if ( ap.type === 'dynamic' ) {
|
||||
const out = act.dynamicAppearance && act.dynamicAppearance.call(this, Object.assign({}, ap), data, msg, r, u, mod_icons, chat_line);
|
||||
if ( out )
|
||||
ap = out;
|
||||
}
|
||||
|
||||
if ( act.override_appearance ) {
|
||||
const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, r, u, mod_icons);
|
||||
const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, r, u, mod_icons, chat_line);
|
||||
if ( out )
|
||||
ap = out;
|
||||
}
|
||||
|
@ -695,7 +768,7 @@ export default class Actions extends Module {
|
|||
continue;
|
||||
|
||||
const has_color = def.colored && ap.color,
|
||||
disabled = maybe_call(act.disabled, this, data, msg, r, u, mod_icons) || false,
|
||||
disabled = maybe_call(act.disabled, this, data, msg, r, u, mod_icons, chat_line) || false,
|
||||
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
|
||||
contents = def.render.call(this, ap, createElement, color);
|
||||
|
||||
|
@ -704,7 +777,7 @@ export default class Actions extends Module {
|
|||
|
||||
const btn = (<button
|
||||
class={`ffz-tooltip ffz-tooltip--no-mouse tw-button tw-button--text${disabled ? ' tw-button--disabled disabled' : ''}`}
|
||||
disabled={disabled}
|
||||
//disabled={disabled}
|
||||
data-tooltip-type="action"
|
||||
data-action={data.action}
|
||||
data-options={data.options ? JSON.stringify(data.options) : null}
|
||||
|
@ -743,7 +816,95 @@ export default class Actions extends Module {
|
|||
}
|
||||
|
||||
|
||||
renderInline(msg, mod_icons, current_user, current_room, createElement) {
|
||||
renderHover(msg, mod_icons, current_user, current_room, createElement, instance = null) {
|
||||
const actions = [];
|
||||
|
||||
const current_level = this.getUserLevel(current_room, current_user),
|
||||
msg_level = this.getUserLevel(current_room, msg.user),
|
||||
is_self = msg.user && current_user && current_user.login === msg.user.login;
|
||||
|
||||
if ( current_level < 3 )
|
||||
mod_icons = false;
|
||||
|
||||
const chat = this.resolve('site.chat');
|
||||
|
||||
let had_action = false;
|
||||
|
||||
for(const data of this.parent.context.get('chat.actions.hover')) {
|
||||
if ( ! data.action || ! data.appearance )
|
||||
continue;
|
||||
|
||||
data.ctx = 'hover';
|
||||
|
||||
let ap = data.appearance || {};
|
||||
const disp = data.display || {},
|
||||
keys = disp.keys,
|
||||
act = this.actions[data.action];
|
||||
|
||||
if ( ! act || disp.disabled ||
|
||||
(disp.mod_icons != null && disp.mod_icons !== !!mod_icons) ||
|
||||
(disp.mod != null && disp.mod !== (current_level > msg_level)) ||
|
||||
(disp.staff != null && disp.staff !== (current_user ? !!current_user.staff : false)) ||
|
||||
(disp.deleted != null && disp.deleted !== !!msg.deleted) )
|
||||
continue;
|
||||
|
||||
if ( is_self && ! act.can_self )
|
||||
continue;
|
||||
|
||||
if ( maybe_call(act.hidden, this, data, msg, current_room, current_user, mod_icons, instance) )
|
||||
continue;
|
||||
|
||||
if ( ap.type === 'dynamic' ) {
|
||||
const out = act.dynamicAppearance && act.dynamicAppearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons, instance);
|
||||
if ( out )
|
||||
ap = out;
|
||||
}
|
||||
|
||||
if ( act.override_appearance ) {
|
||||
const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons, instance);
|
||||
if ( out )
|
||||
ap = out;
|
||||
}
|
||||
|
||||
const def = this.renderers[ap.type];
|
||||
if ( ! def )
|
||||
continue;
|
||||
|
||||
const has_color = def.colored && ap.color,
|
||||
disabled = maybe_call(act.disabled, this, data, msg, current_room, current_user, mod_icons, instance) || false,
|
||||
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
|
||||
contents = def.render.call(this, ap, createElement, color);
|
||||
|
||||
had_action = true;
|
||||
actions.push(<div class={`ffz-hover-action${keys ? ` ffz-has-modifier ffz-modifier-${keys}` : ''}`}>
|
||||
<button
|
||||
class={`ffz-tooltip ffz-mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}`}
|
||||
//disabled={disabled}
|
||||
data-tooltip-type="action"
|
||||
data-action={data.action}
|
||||
data-options={data.options ? JSON.stringify(data.options) : null}
|
||||
data-tip={ap.tooltip}
|
||||
onClick={this.handleClick}
|
||||
onContextMenu={this.handleContext}
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
</div>);
|
||||
}
|
||||
|
||||
if ( ! had_action )
|
||||
return null;
|
||||
|
||||
return (<div
|
||||
class={`ffz--hover-actions ffz-action-data tw-mg-r-05`}
|
||||
data-source="line"
|
||||
>
|
||||
{actions}
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
renderInline(msg, mod_icons, current_user, current_room, createElement, instance = null) {
|
||||
const actions = [];
|
||||
|
||||
const current_level = this.getUserLevel(current_room, current_user),
|
||||
|
@ -762,6 +923,8 @@ export default class Actions extends Module {
|
|||
if ( ! data.action || ! data.appearance )
|
||||
continue;
|
||||
|
||||
data.ctx = 'inline';
|
||||
|
||||
let ap = data.appearance || {};
|
||||
const disp = data.display || {},
|
||||
keys = disp.keys,
|
||||
|
@ -778,11 +941,17 @@ export default class Actions extends Module {
|
|||
if ( is_self && ! act.can_self )
|
||||
continue;
|
||||
|
||||
if ( maybe_call(act.hidden, this, data, msg, current_room, current_user, mod_icons) )
|
||||
if ( maybe_call(act.hidden, this, data, msg, current_room, current_user, mod_icons, instance) )
|
||||
continue;
|
||||
|
||||
if ( ap.type === 'dynamic' ) {
|
||||
const out = act.dynamicAppearance && act.dynamicAppearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons, instance);
|
||||
if ( out )
|
||||
ap = out;
|
||||
}
|
||||
|
||||
if ( act.override_appearance ) {
|
||||
const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons);
|
||||
const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons, instance);
|
||||
if ( out )
|
||||
ap = out;
|
||||
}
|
||||
|
@ -792,7 +961,7 @@ export default class Actions extends Module {
|
|||
continue;
|
||||
|
||||
const has_color = def.colored && ap.color,
|
||||
disabled = maybe_call(act.disabled, this, data, msg, current_room, current_user, mod_icons) || false,
|
||||
disabled = maybe_call(act.disabled, this, data, msg, current_room, current_user, mod_icons, instance) || false,
|
||||
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
|
||||
contents = def.render.call(this, ap, createElement, color);
|
||||
|
||||
|
@ -804,7 +973,7 @@ export default class Actions extends Module {
|
|||
had_action = true;
|
||||
list.push(<button
|
||||
class={`ffz-tooltip mod-icon ffz-mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}${keys ? ` ffz-modifier-${keys}` : ''}${hover ? ' ffz-hover' : ''}`}
|
||||
disabled={disabled}
|
||||
//disabled={disabled}
|
||||
data-tooltip-type="action"
|
||||
data-action={data.action}
|
||||
data-options={data.options ? JSON.stringify(data.options) : null}
|
||||
|
@ -819,14 +988,6 @@ export default class Actions extends Module {
|
|||
if ( ! had_action )
|
||||
return null;
|
||||
|
||||
/*const room = current_room && JSON.stringify(current_room),
|
||||
user = msg.user && JSON.stringify({
|
||||
login: msg.user.login,
|
||||
displayName: msg.user.displayName,
|
||||
id: msg.user.id,
|
||||
type: msg.user.type
|
||||
});*/
|
||||
|
||||
let out = null;
|
||||
if ( actions.length )
|
||||
out = (<div
|
||||
|
|
|
@ -3,6 +3,22 @@
|
|||
import {load as loadFontAwesome} from 'utilities/font-awesome';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Dynamic
|
||||
// ============================================================================
|
||||
|
||||
export const dynamic = {
|
||||
title: 'Dynamic',
|
||||
title_i18n: 'setting.actions.appearance.dynamic',
|
||||
|
||||
colored: true,
|
||||
|
||||
render(data, createElement, color) {
|
||||
return <figure style={{color}} class={`${data.icon||'ffz-i-zreknarf'}`} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Text
|
||||
// ============================================================================
|
||||
|
@ -21,6 +37,21 @@ export const text = {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Emote
|
||||
// ============================================================================
|
||||
|
||||
export const emote = {
|
||||
title: 'Emote',
|
||||
title_i18n: 'setting.actions.appearance.emote',
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-emote.vue'),
|
||||
|
||||
component: () => import(/* webpackChunkName: 'main-menu' */ './components/preview-emote.vue'),
|
||||
render(data, createElement) {
|
||||
return <figure class="mod-icon__image"><img src={data.src} /></figure>;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Icon
|
||||
|
|
|
@ -3,6 +3,73 @@
|
|||
import {createElement} from 'utilities/dom';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Pin Message
|
||||
// ============================================================================
|
||||
|
||||
export const pin = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-pin'
|
||||
}
|
||||
}],
|
||||
|
||||
required_context: ['message'],
|
||||
|
||||
title: 'Pin This Message',
|
||||
description: "Allows you to pin a chat message if you're a moderator.",
|
||||
|
||||
can_self: true,
|
||||
|
||||
tooltip(data) {
|
||||
const pinned = data.line?.props?.pinnedMessage?.message?.id === data.message_id;
|
||||
if (pinned)
|
||||
return this.i18n.t('chat.actions.pin.already', 'This message is already pinned.');
|
||||
|
||||
return this.i18n.t('chat.actions.pin', 'Pin This Message')
|
||||
},
|
||||
|
||||
disabled(data, message, current_room, current_user, mod_icons, instance) {
|
||||
const line = instance ?? data.line,
|
||||
props = line?.props,
|
||||
pinned = props?.pinnedMessage?.message?.id === message.id && message.id != null;
|
||||
|
||||
return pinned;
|
||||
},
|
||||
|
||||
hidden(data, message, current_room, current_user, mod_icons, instance) {
|
||||
let line = instance;
|
||||
|
||||
if ( ! line )
|
||||
return true;
|
||||
|
||||
if ( ! line.props.isPinnable || ! line.onPinMessageClick )
|
||||
return true;
|
||||
|
||||
// If the message is empty or deleted, we can't pin it.
|
||||
if ( ! message.message || ! message.message.length || message.deleted )
|
||||
return true;
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
let line = data.line;
|
||||
if ( ! line ) {
|
||||
const fine = this.resolve('site.fine');
|
||||
line = fine ? fine.searchParent(event.target, n => n.setMessageTray && n.props && n.props.message) : null;
|
||||
}
|
||||
|
||||
if ( ! line || ! line.props.isPinnable || ! line.onPinMessageClick )
|
||||
return;
|
||||
|
||||
if ( line.props.pinnedMessage?.message?.id === data.message_id )
|
||||
return;
|
||||
|
||||
line.onPinMessageClick();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Send Reply
|
||||
// ============================================================================
|
||||
|
@ -10,19 +77,37 @@ import {createElement} from 'utilities/dom';
|
|||
export const reply = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-reply'
|
||||
type: 'dynamic'
|
||||
}
|
||||
}],
|
||||
|
||||
required_context: ['message'],
|
||||
supports_dynamic: true,
|
||||
|
||||
title: 'Reply to Message',
|
||||
description: 'Allows you to directly reply to another user\'s message. Only functions when the Chat Replies Style is "FrankerFaceZ".',
|
||||
description: "Allows you to directly reply to another user's message.",
|
||||
|
||||
can_self: true,
|
||||
|
||||
tooltip() {
|
||||
dynamicAppearance(ap, data, message, current_room, current_user, mod_icons, instance) {
|
||||
const line = instance ?? data.line,
|
||||
props = line?.props,
|
||||
has_reply = props?.hasReply || props?.reply;
|
||||
|
||||
return {
|
||||
type: 'icon',
|
||||
icon: has_reply ? 'ffz-i-threads' : 'ffz-i-reply',
|
||||
color: ap.color
|
||||
}
|
||||
},
|
||||
|
||||
tooltip(data) {
|
||||
const props = data.line?.props,
|
||||
has_reply = props?.hasReply || props?.reply;
|
||||
|
||||
if (has_reply)
|
||||
return this.i18n.t('chat.actions.reply.thread', 'Open Thread');
|
||||
|
||||
return this.i18n.t('chat.actions.reply', 'Reply to Message')
|
||||
},
|
||||
|
||||
|
@ -31,10 +116,16 @@ export const reply = {
|
|||
if ( typeof id !== 'string' || ! /^[0-9a-f]+-[0-9a-f]+/.test(id) )
|
||||
return true;
|
||||
|
||||
if ( ! message.message || message.deleted || (current_user && current_user.login === message.user?.login) || ! current_user?.can_reply )
|
||||
// Users must be able to reply.
|
||||
if ( ! current_user?.can_reply )
|
||||
return true;
|
||||
|
||||
if ( message?.reply )
|
||||
// If reply mode is set to 0 (Disabled), don't show the action.
|
||||
if ( current_user?.reply_mode === 0 )
|
||||
return true;
|
||||
|
||||
// If the message is empty or deleted, don't show the action.
|
||||
if ( ! message.message || message.deleted )
|
||||
return true;
|
||||
},
|
||||
|
||||
|
@ -83,6 +174,51 @@ export const edit_overrides = {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Copy to Clipboard
|
||||
// ============================================================================
|
||||
|
||||
export const copy_message = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-docs'
|
||||
}
|
||||
}],
|
||||
|
||||
defaults: {
|
||||
format: '{{user.displayName}}: {{message.text}}'
|
||||
},
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-copy.vue'),
|
||||
|
||||
required_context: ['user', 'message'],
|
||||
|
||||
title: 'Copy Message',
|
||||
description: 'Allows you to quickly copy a chat message to your clipboard.',
|
||||
|
||||
can_self: true,
|
||||
|
||||
tooltip(data) {
|
||||
const msg = this.replaceVariables(data.options.format, data);
|
||||
|
||||
return [
|
||||
(<div class="tw-border-b tw-mg-b-05">{ // eslint-disable-line react/jsx-key
|
||||
this.i18n.t('chat.actions.copy_message', 'Copy Message')
|
||||
}</div>),
|
||||
(<div class="tw-align-left">{ // eslint-disable-line react/jsx-key
|
||||
msg
|
||||
}</div>)
|
||||
];
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
const msg = this.replaceVariables(data.options.format, data);
|
||||
navigator.clipboard.writeText(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Open URL
|
||||
// ============================================================================
|
||||
|
|
|
@ -513,7 +513,7 @@ export default class Emotes extends Module {
|
|||
return;
|
||||
|
||||
const line = fine.searchParent(target, n => n.props && n.props.message),
|
||||
opener = fine.searchParent(target, n => n.onShowEmoteCard, 200);
|
||||
opener = fine.searchParent(target, n => n.onShowEmoteCard, 250);
|
||||
|
||||
if ( ! line || ! opener )
|
||||
return;
|
||||
|
|
|
@ -20,6 +20,7 @@ import Room from './room';
|
|||
import User from './user';
|
||||
import * as TOKENIZERS from './tokenizers';
|
||||
import * as RICH_PROVIDERS from './rich_providers';
|
||||
import * as LINK_PROVIDERS from './link_providers';
|
||||
|
||||
import Actions from './actions';
|
||||
import { getFontsList } from 'src/utilities/fonts';
|
||||
|
@ -99,6 +100,9 @@ export default class Chat extends Module {
|
|||
this.rich_providers = {};
|
||||
this.__rich_providers = [];
|
||||
|
||||
this.link_providers = {};
|
||||
this.__link_providers = [];
|
||||
|
||||
this._hl_reasons = {};
|
||||
this.addHighlightReason('mention', 'Mentioned');
|
||||
this.addHighlightReason('user', 'Highlight User');
|
||||
|
@ -1241,6 +1245,8 @@ export default class Chat extends Module {
|
|||
onEnable() {
|
||||
this.socket = this.resolve('socket');
|
||||
|
||||
this.on('site.subpump:pubsub-message', this.onPubSub, this);
|
||||
|
||||
if ( this.context.get('chat.filtering.color-mentions') )
|
||||
this.createColorCache().then(() => this.emit(':update-line-tokens'));
|
||||
|
||||
|
@ -1251,6 +1257,47 @@ export default class Chat extends Module {
|
|||
for(const key in RICH_PROVIDERS)
|
||||
if ( has(RICH_PROVIDERS, key) )
|
||||
this.addRichProvider(RICH_PROVIDERS[key]);
|
||||
|
||||
for(const key in LINK_PROVIDERS)
|
||||
if ( has(LINK_PROVIDERS, key) )
|
||||
this.addLinkProvider(LINK_PROVIDERS[key]);
|
||||
}
|
||||
|
||||
|
||||
onPubSub(event) {
|
||||
if ( event.prefix === 'stream-chat-room-v1' && event.message.type === 'chat_rich_embed' ) {
|
||||
const data = event.message.data,
|
||||
url = data.request_url,
|
||||
|
||||
providers = this.__link_providers;
|
||||
|
||||
// Don't re-cache.
|
||||
if ( this._link_info[url] )
|
||||
return;
|
||||
|
||||
for(const provider of providers) {
|
||||
const match = provider.test.call(this, url);
|
||||
if ( match ) {
|
||||
const processed = provider.receive ? provider.receive.call(this, match, data) : data;
|
||||
let result = provider.process.call(this, match, processed);
|
||||
|
||||
if ( !(result instanceof Promise) )
|
||||
result = Promise.resolve(result);
|
||||
|
||||
result.then(value => {
|
||||
// If something is already running, don't override it.
|
||||
let info = this._link_info[url];
|
||||
if ( info )
|
||||
return;
|
||||
|
||||
// Save the value.
|
||||
this._link_info[url] = [true, Date.now() + 120000, value];
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1855,6 +1902,11 @@ export default class Chat extends Module {
|
|||
|
||||
addTokenizer(tokenizer) {
|
||||
const type = tokenizer.type;
|
||||
if ( has(this.tokenizers, type) ) {
|
||||
this.log.warn(`Tried adding tokenizer of type '${type}' when one was already present.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tokenizers[type] = tokenizer;
|
||||
if ( tokenizer.priority == null )
|
||||
tokenizer.priority = 0;
|
||||
|
@ -1894,8 +1946,48 @@ export default class Chat extends Module {
|
|||
return tokenizer;
|
||||
}
|
||||
|
||||
addLinkProvider(provider) {
|
||||
const type = provider.type;
|
||||
if ( has(this.link_providers, type) ) {
|
||||
this.log.warn(`Tried adding link provider of type '${type}' when one was already present.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.link_providers[type] = provider;
|
||||
if ( provider.priority == null )
|
||||
provider.priority = 0;
|
||||
|
||||
this.__link_providers.push(provider);
|
||||
this.__link_providers.sort((a,b) => {
|
||||
if ( a.priority > b.priority ) return -1;
|
||||
if ( a.priority < b.priority ) return 1;
|
||||
return a.type < b.type;
|
||||
});
|
||||
}
|
||||
|
||||
removeLinkProvider(provider) {
|
||||
let type;
|
||||
if ( typeof provider === 'string' ) type = provider;
|
||||
else type = provider.type;
|
||||
|
||||
provider = this.link_providers[type];
|
||||
if ( ! provider )
|
||||
return null;
|
||||
|
||||
const idx = this.__link_providers.indexOf(provider);
|
||||
if ( idx !== -1 )
|
||||
this.__link_providers.splice(idx, 1);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
addRichProvider(provider) {
|
||||
const type = provider.type;
|
||||
if ( has(this.rich_providers, type) ) {
|
||||
this.log.warn(`Tried adding rich provider of type '${type}' when one was already present.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.rich_providers[type] = provider;
|
||||
if ( provider.priority == null )
|
||||
provider.priority = 0;
|
||||
|
@ -2108,6 +2200,17 @@ export default class Chat extends Module {
|
|||
cbs[success ? 0 : 1](data);
|
||||
}
|
||||
|
||||
// Try using a link provider.
|
||||
for(const lp of this.__link_providers) {
|
||||
const match = lp.test.call(this, url);
|
||||
if ( match ) {
|
||||
timeout(lp.process.call(this, match), 15000)
|
||||
.then(data => handle(true, data))
|
||||
.catch(err => handle(false, err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let provider = this.settings.get('debug.link-resolver.source');
|
||||
if ( provider == null )
|
||||
provider = this.experiments.getAssignment('api_links') ? 'test' : 'socket';
|
||||
|
|
459
src/modules/chat/link_providers.js
Normal file
459
src/modules/chat/link_providers.js
Normal file
|
@ -0,0 +1,459 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Rich Content Providers
|
||||
// ============================================================================
|
||||
|
||||
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/([a-z0-9-_=]+)(?:\/)?(\w+)?(?:\/edit)?/i;
|
||||
const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:(?:www|m)\.)?twitch\.tv\/\w+\/clip\/([a-z0-9-_=]+)/i;
|
||||
const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
|
||||
const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/;
|
||||
|
||||
const BAD_USERS = [
|
||||
'directory', '_deck', 'p', 'downloads', 'jobs', 'turbo', 'settings', 'friends',
|
||||
'subscriptions', 'inventory', 'wallet'
|
||||
];
|
||||
|
||||
import GET_CLIP from './clip_info.gql';
|
||||
import GET_VIDEO from './video_info.gql';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Clips
|
||||
// ============================================================================
|
||||
|
||||
export const Clip = {
|
||||
type: 'clip',
|
||||
|
||||
test(url) {
|
||||
const match = CLIP_URL.exec(url) || NEW_CLIP_URL.exec(url);
|
||||
if ( match && match[1] && match[1] !== 'create' )
|
||||
return match[1];
|
||||
},
|
||||
|
||||
receive(match, data) {
|
||||
const cd = data?.twitch_metadata?.clip_metadata;
|
||||
if ( ! cd )
|
||||
return;
|
||||
|
||||
return {
|
||||
id: cd.id,
|
||||
slug: cd.slug,
|
||||
title: data.title,
|
||||
thumbnailURL: data.thumbnail_url,
|
||||
curator: {
|
||||
id: cd.curator_id,
|
||||
displayName: data.author_name
|
||||
},
|
||||
broadcaster: {
|
||||
id: cd.broadcaster_id,
|
||||
displayName: cd.channel_display_name
|
||||
},
|
||||
game: {
|
||||
displayName: cd.game
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async process(match, received) {
|
||||
let clip = received;
|
||||
|
||||
if ( ! clip ) {
|
||||
const apollo = this.resolve('site.apollo');
|
||||
if ( ! apollo )
|
||||
return null;
|
||||
|
||||
const result = await apollo.client.query({
|
||||
query: GET_CLIP,
|
||||
variables: {
|
||||
slug: match
|
||||
}
|
||||
});
|
||||
|
||||
clip = result?.data?.clip;
|
||||
}
|
||||
|
||||
if ( ! clip || ! clip.broadcaster )
|
||||
return null;
|
||||
|
||||
const game = clip.game,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
let user = {
|
||||
type: 'style', weight: 'semibold', color: 'alt-2',
|
||||
content: clip.broadcaster.displayName
|
||||
};
|
||||
|
||||
if ( clip.broadcaster.login )
|
||||
user = {
|
||||
type: 'link', url: `https://www.twitch.tv/${clip.broadcaster.login}`,
|
||||
content: user
|
||||
};
|
||||
|
||||
const subtitle = game_display ? {
|
||||
type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: {
|
||||
user,
|
||||
game: {type: 'style', weight: 'semibold', content: game_display}
|
||||
}
|
||||
} : {type: 'i18n', key: 'clip.desc.1', phrase: 'Clip of {user}', content: {user}};
|
||||
|
||||
let curator = clip.curator ? {
|
||||
type: 'style', color: 'alt-2',
|
||||
content: clip.curator.displayName
|
||||
} : {type: 'i18n', key: 'clip.unknown', phrase: 'Unknown'};
|
||||
|
||||
if ( clip.curator?.login )
|
||||
curator = {
|
||||
type: 'link', url: `https://www.twitch.tv/${clip.curator.login}`,
|
||||
content: curator
|
||||
};
|
||||
|
||||
let extra;
|
||||
|
||||
if ( clip.viewCount > 0 )
|
||||
extra = {
|
||||
type: 'i18n', key: 'clip.desc.2',
|
||||
phrase: 'Clipped by {curator} — {views, plural, one {# View} other {# Views}}',
|
||||
content: {
|
||||
curator,
|
||||
views: clip.viewCount
|
||||
}
|
||||
};
|
||||
else
|
||||
extra = {
|
||||
type: 'i18n', key: 'clip.desc.no-views',
|
||||
phrase: 'Clipped by {curator}',
|
||||
content: {
|
||||
curator
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
accent: '#6441a4',
|
||||
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: clip.thumbnailURL, sfw: true, aspect: 16/9},
|
||||
title: clip.title,
|
||||
subtitle,
|
||||
extra
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Users
|
||||
// ============================================================================
|
||||
|
||||
export const User = {
|
||||
type: 'user',
|
||||
|
||||
test(url) {
|
||||
const match = USER_URL.exec(url);
|
||||
if ( match && ! BAD_USERS.includes(match[1]) )
|
||||
return match[1];
|
||||
},
|
||||
|
||||
async process(match) {
|
||||
const twitch_data = this.resolve('site.twitch_data'),
|
||||
user = twitch_data ? await twitch_data.getUser(null, match) : null;
|
||||
|
||||
if ( ! user || ! user.id )
|
||||
return null;
|
||||
|
||||
const game = user.broadcastSettings?.game?.displayName,
|
||||
stream_id = user.stream?.id;
|
||||
|
||||
const fragments = {
|
||||
avatar: {
|
||||
type: 'image',
|
||||
url: user.profileImageURL,
|
||||
rounding: -1,
|
||||
aspect: 1
|
||||
},
|
||||
desc: user.description,
|
||||
title: [user.displayName]
|
||||
};
|
||||
|
||||
if ( stream_id && game )
|
||||
fragments.game = {type: 'style', weight: 'semibold', content: game};
|
||||
|
||||
if ( user.displayName.trim().toLowerCase() !== user.login )
|
||||
fragments.title.push({
|
||||
type: 'style', color: 'alt-2',
|
||||
content: [' (', user.login, ')']
|
||||
});
|
||||
|
||||
if ( user.roles?.isPartner )
|
||||
fragments.title.push({
|
||||
type: 'style', color: 'link',
|
||||
content: {type: 'icon', name: 'verified'}
|
||||
});
|
||||
|
||||
const full = [
|
||||
{
|
||||
type: 'header',
|
||||
image: {type: 'ref', name: 'avatar'},
|
||||
title: {type: 'ref', name: 'title'},
|
||||
},
|
||||
{
|
||||
type: 'box',
|
||||
'mg-y': 'small',
|
||||
wrap: 'pre-wrap',
|
||||
lines: 5,
|
||||
content: {
|
||||
type: 'ref',
|
||||
name: 'desc'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if ( stream_id && game ) {
|
||||
const thumb_url = user.stream.previewImageURL
|
||||
? user.stream.previewImageURL
|
||||
.replace('{width}', '320')
|
||||
.replace('{height}', '180')
|
||||
: null;
|
||||
|
||||
full.push({
|
||||
type: 'link',
|
||||
url: `https://www.twitch.tv/${user.login}`,
|
||||
embed: true,
|
||||
interactive: true,
|
||||
tooltip: false,
|
||||
content: [
|
||||
{
|
||||
type: 'conditional',
|
||||
media: true,
|
||||
content: {
|
||||
type: 'gallery',
|
||||
items: [
|
||||
{
|
||||
type: 'image',
|
||||
url: thumb_url,
|
||||
aspect: 16/9
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'box',
|
||||
'mg-y': 'small',
|
||||
lines: 2,
|
||||
content: user.broadcastSettings.title
|
||||
},
|
||||
{
|
||||
type: 'ref',
|
||||
name: 'game'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
full.push({
|
||||
type: 'header',
|
||||
compact: true,
|
||||
subtitle: [
|
||||
{
|
||||
type: 'icon',
|
||||
name: 'twitch'
|
||||
},
|
||||
' Twitch'
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
v: 5,
|
||||
|
||||
accent: user.primaryColorHex ? `#${user.primaryColorHex}` : null,
|
||||
fragments,
|
||||
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'ref', name: 'avatar'},
|
||||
title: {type: 'ref', name: 'title'},
|
||||
subtitle: {type: 'ref', name: 'desc'},
|
||||
extra: stream_id ? {
|
||||
type: 'i18n',
|
||||
key: 'cards.user.streaming',
|
||||
phrase: 'streaming {game}',
|
||||
content: {
|
||||
game: {type: 'ref', name: 'game'}
|
||||
}
|
||||
} : null
|
||||
},
|
||||
|
||||
full
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Videos
|
||||
// ============================================================================
|
||||
|
||||
export const Video = {
|
||||
type: 'video',
|
||||
|
||||
test(url) {
|
||||
const match = VIDEO_URL.exec(url);
|
||||
if ( match )
|
||||
return match[1];
|
||||
},
|
||||
|
||||
async process(match) {
|
||||
const apollo = this.resolve('site.apollo');
|
||||
if ( ! apollo )
|
||||
return null;
|
||||
|
||||
const result = await apollo.client.query({
|
||||
query: GET_VIDEO,
|
||||
variables: {
|
||||
id: match
|
||||
}
|
||||
});
|
||||
|
||||
if ( ! result || ! result.data || ! result.data.video || ! result.data.video.owner )
|
||||
return null;
|
||||
|
||||
const video = result.data.video,
|
||||
game = video.game,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
const fragments = {
|
||||
title: video.title,
|
||||
thumbnail: {
|
||||
type: 'image',
|
||||
url: video.previewThumbnailURL,
|
||||
aspect: 16/9
|
||||
}
|
||||
};
|
||||
|
||||
const user = {
|
||||
type: 'link',
|
||||
url: `https://www.twitch.tv/${video.owner.login}`,
|
||||
content: {
|
||||
type: 'style',
|
||||
weight: 'semibold',
|
||||
color: 'alt-2',
|
||||
content: video.owner.displayName
|
||||
}
|
||||
};
|
||||
|
||||
fragments.subtitle = video.game?.displayName
|
||||
? {
|
||||
type: 'i18n',
|
||||
key: 'video.desc.1.playing',
|
||||
phrase: 'Video of {user} playing {game}',
|
||||
content: {
|
||||
user,
|
||||
game: {
|
||||
type: 'style',
|
||||
weight: 'semibold',
|
||||
content: video.game.displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
: {
|
||||
type: 'i18n',
|
||||
key: 'video.desc.1',
|
||||
phrase: 'Video of {user}',
|
||||
content: {
|
||||
user
|
||||
}
|
||||
};
|
||||
|
||||
let length = video.lengthSeconds;
|
||||
|
||||
return {
|
||||
v: 5,
|
||||
|
||||
fragments,
|
||||
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'ref', name: 'thumbnail'},
|
||||
title: {type: 'ref', name: 'title'},
|
||||
subtitle: {type: 'ref', name: 'subtitle'},
|
||||
extra: {
|
||||
type: 'i18n',
|
||||
key: 'video.desc.2',
|
||||
phrase: '{length,duration} — {views,number} Views — {date,datetime}',
|
||||
content: {
|
||||
length,
|
||||
views: video.viewCount,
|
||||
date: video.publishedAt
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
full: [
|
||||
{
|
||||
type: 'header',
|
||||
image: {
|
||||
type: 'image',
|
||||
url: video.owner.profileImageURL,
|
||||
rounding: -1,
|
||||
aspect: 1
|
||||
},
|
||||
title: {type: 'ref', name: 'title'},
|
||||
subtitle: {type: 'ref', name: 'subtitle'}
|
||||
},
|
||||
{
|
||||
type: 'box',
|
||||
'mg-y': 'small',
|
||||
lines: 5,
|
||||
wrap: 'pre-wrap',
|
||||
content: video.description
|
||||
},
|
||||
{
|
||||
type: 'conditional',
|
||||
media: true,
|
||||
content: {
|
||||
type: 'gallery',
|
||||
items: [
|
||||
{
|
||||
type: 'overlay',
|
||||
content: {type: 'ref', name: 'thumbnail'},
|
||||
'top-left': {
|
||||
type: 'format',
|
||||
format: 'duration',
|
||||
value: length
|
||||
},
|
||||
'bottom-left': {
|
||||
type: 'i18n',
|
||||
key: 'video.views',
|
||||
phrase: '{views,number} views',
|
||||
content: {
|
||||
views: video.viewCount
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
compact: true,
|
||||
subtitle: [
|
||||
{
|
||||
type: 'icon',
|
||||
name: 'twitch'
|
||||
},
|
||||
" Twitch • ",
|
||||
{
|
||||
type: 'format',
|
||||
format: 'datetime',
|
||||
value: video.publishedAt
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -4,24 +4,6 @@
|
|||
// Rich Content Providers
|
||||
// ============================================================================
|
||||
|
||||
//const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
|
||||
//const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/;
|
||||
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/([a-z0-9-_=]+)(?:\/)?(\w+)?(?:\/edit)?/i;
|
||||
const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:(?:www|m)\.)?twitch\.tv\/\w+\/clip\/([a-z0-9-_=]+)/i;
|
||||
const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
|
||||
const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/;
|
||||
|
||||
const BAD_USERS = [
|
||||
'directory', '_deck', 'p', 'downloads', 'jobs', 'turbo', 'settings', 'friends',
|
||||
'subscriptions', 'inventory', 'wallet'
|
||||
];
|
||||
|
||||
import GET_CLIP from './clip_info.gql';
|
||||
import GET_VIDEO from './video_info.gql';
|
||||
|
||||
import {truncate} from 'utilities/object';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// General Links
|
||||
// ============================================================================
|
||||
|
@ -32,10 +14,18 @@ export const Links = {
|
|||
priority: -10,
|
||||
|
||||
test(token) {
|
||||
if ( ! this.context.get('chat.rich.all-links') && ! token.force_rich )
|
||||
if ( token.type !== 'link' )
|
||||
return false;
|
||||
|
||||
return token.type === 'link'
|
||||
const url = token.url;
|
||||
|
||||
// Link providers always result in embeds.
|
||||
for(const provider of this.__link_providers) {
|
||||
if ( provider.test.call(this, url) )
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.context.get('chat.rich.all-links') || token.force_rich;
|
||||
},
|
||||
|
||||
process(token, want_mid) {
|
||||
|
@ -70,279 +60,3 @@ export const Links = {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Users
|
||||
// ============================================================================
|
||||
|
||||
export const Users = {
|
||||
type: 'user',
|
||||
can_hide_token: true,
|
||||
|
||||
test(token) {
|
||||
if ( token.type !== 'link' || (! this.context.get('chat.rich.all-links') && ! token.force_rich) )
|
||||
return false;
|
||||
|
||||
return USER_URL.test(token.url);
|
||||
},
|
||||
|
||||
process(token) {
|
||||
const match = USER_URL.exec(token.url),
|
||||
twitch_data = this.resolve('site.twitch_data');
|
||||
|
||||
if ( ! twitch_data || ! match || BAD_USERS.includes(match[1]) )
|
||||
return;
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
|
||||
getData: async () => {
|
||||
const user = await twitch_data.getUser(null, match[1]);
|
||||
if ( ! user || ! user.id )
|
||||
return null;
|
||||
|
||||
const game = user.broadcastSettings?.game?.displayName,
|
||||
stream_id = user.stream?.id;
|
||||
|
||||
let subtitle
|
||||
if ( stream_id && game )
|
||||
subtitle = {
|
||||
type: 'i18n',
|
||||
key: 'cards.user.streaming', phrase: 'streaming {game}', content: {
|
||||
game: {type: 'style', weight: 'semibold', content: game}
|
||||
}
|
||||
};
|
||||
|
||||
const extra = truncate(user.description);
|
||||
const title = [user.displayName];
|
||||
|
||||
if ( user.displayName.trim().toLowerCase() !== user.login )
|
||||
title.push({
|
||||
type: 'style', color: 'alt-2',
|
||||
content: [' (', user.login, ')']
|
||||
});
|
||||
|
||||
if ( user.roles?.isPartner )
|
||||
title.push({
|
||||
type: 'style', color: 'link',
|
||||
content: {type: 'icon', name: 'verified'}
|
||||
});
|
||||
|
||||
/*const full = [{
|
||||
type: 'header',
|
||||
image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1},
|
||||
title,
|
||||
subtitle,
|
||||
extra: stream_id ? extra : null
|
||||
}];
|
||||
|
||||
if ( stream_id ) {
|
||||
full.push({type: 'box', 'mg-y': 'small', lines: 1, content: user.broadcastSettings.title});
|
||||
full.push({type: 'conditional', content: {
|
||||
type: 'gallery', items: [{
|
||||
type: 'image', aspect: 16/9, sfw: false, url: user.stream.previewImageURL
|
||||
}]
|
||||
}});
|
||||
} else
|
||||
full.push({type: 'box', 'mg-y': 'small', wrap: 'pre-wrap', lines: 5, content: truncate(user.description, 1000, undefined, undefined, false)})
|
||||
|
||||
full.push({
|
||||
type: 'fieldset',
|
||||
fields: [
|
||||
{
|
||||
name: {type: 'i18n', key: 'embed.twitch.views', phrase: 'Views'},
|
||||
value: {type: 'format', format: 'number', value: user.profileViewCount},
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: {type: 'i18n', key: 'embed.twitch.followers', phrase: 'Followers'},
|
||||
value: {type: 'format', format: 'number', value: user.followers?.totalCount},
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
full.push({
|
||||
type: 'header',
|
||||
subtitle: [{type: 'icon', name: 'twitch'}, ' Twitch']
|
||||
});*/
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
accent: user.primaryColorHex ? `#${user.primaryColorHex}` : null,
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1},
|
||||
title,
|
||||
subtitle,
|
||||
extra
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Clips
|
||||
// ============================================================================
|
||||
|
||||
export const Clips = {
|
||||
type: 'clip',
|
||||
can_hide_token: true,
|
||||
|
||||
test(token) {
|
||||
if ( token.type !== 'link' )
|
||||
return false;
|
||||
|
||||
return CLIP_URL.test(token.url) || NEW_CLIP_URL.test(token.url);
|
||||
},
|
||||
|
||||
process(token) {
|
||||
let match = CLIP_URL.exec(token.url);
|
||||
if ( ! match )
|
||||
match = NEW_CLIP_URL.exec(token.url);
|
||||
|
||||
const apollo = this.resolve('site.apollo');
|
||||
if ( ! apollo || ! match || match[1] === 'create' )
|
||||
return;
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
|
||||
getData: async () => {
|
||||
const result = await apollo.client.query({
|
||||
query: GET_CLIP,
|
||||
variables: {
|
||||
slug: match[1]
|
||||
}
|
||||
});
|
||||
|
||||
if ( ! result || ! result.data || ! result.data.clip || ! result.data.clip.broadcaster )
|
||||
return null;
|
||||
|
||||
const clip = result.data.clip,
|
||||
game = clip.game,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
const user = {
|
||||
type: 'link', url: `https://www.twitch.tv/${clip.broadcaster.login}`,
|
||||
content: {
|
||||
type: 'style', weight: 'semibold', color: 'alt-2',
|
||||
content: clip.broadcaster.displayName
|
||||
}
|
||||
};
|
||||
|
||||
const subtitle = game_display ? {
|
||||
type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: {
|
||||
user,
|
||||
game: {type: 'style', weight: 'semibold', content: game_display}
|
||||
}
|
||||
} : {type: 'i18n', key: 'clip.desc.1', phrase: 'Clip of {user}', content: {user}};
|
||||
|
||||
const curator = clip.curator ? {
|
||||
type: 'link', url: `https://www.twitch.tv/${clip.curator.login}`,
|
||||
content: {
|
||||
type: 'style', color: 'alt-2',
|
||||
content: clip.curator.displayName
|
||||
}
|
||||
} : {type: 'i18n', key: 'clip.unknown', phrase: 'Unknown'};
|
||||
|
||||
const extra = {
|
||||
type: 'i18n', key: 'clip.desc.2',
|
||||
phrase: 'Clipped by {curator} — {views, plural, one {# View} other {# Views}}',
|
||||
content: {
|
||||
curator,
|
||||
views: clip.viewCount
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
accent: '#6441a4',
|
||||
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: clip.thumbnailURL, sfw: true, aspect: 16/9},
|
||||
title: clip.title,
|
||||
subtitle,
|
||||
extra
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const Videos = {
|
||||
type: 'video',
|
||||
can_hide_token: true,
|
||||
|
||||
test(token) {
|
||||
return token.type === 'link' && VIDEO_URL.test(token.url)
|
||||
},
|
||||
|
||||
process(token) {
|
||||
const match = VIDEO_URL.exec(token.url),
|
||||
apollo = this.resolve('site.apollo');
|
||||
|
||||
if ( ! apollo || ! match )
|
||||
return;
|
||||
|
||||
return {
|
||||
getData: async () => {
|
||||
const result = await apollo.client.query({
|
||||
query: GET_VIDEO,
|
||||
variables: {
|
||||
id: match[1]
|
||||
}
|
||||
});
|
||||
|
||||
if ( ! result || ! result.data || ! result.data.video || ! result.data.video.owner )
|
||||
return null;
|
||||
|
||||
const video = result.data.video,
|
||||
game = video.game,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
const user = {
|
||||
type: 'link', url: `https://www.twitch.tv/${video.owner.login}`,
|
||||
content: {
|
||||
type: 'style', weight: 'semibold', color: 'alt-2',
|
||||
content: video.owner.displayName
|
||||
}
|
||||
};
|
||||
|
||||
const subtitle = game_display ? {
|
||||
type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: {
|
||||
user,
|
||||
game: {type: 'style', weight: 'semibold', content: game_display}
|
||||
}
|
||||
} : {type: 'i18n', key: 'video.desc.1', phrase: 'Video of {user}', content: {user}};
|
||||
|
||||
const extra = {
|
||||
type: 'i18n', key: 'video.desc.2',
|
||||
phrase: '{length,duration} — {views,number} Views — {date,datetime}', content: {
|
||||
length: video.lengthSeconds,
|
||||
views: video.viewCount,
|
||||
date: video.publishedAt
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: video.previewThumbnailURL, sfw: true, aspect: 16/9},
|
||||
title: video.title,
|
||||
subtitle,
|
||||
extra
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ query FFZ_GetVideoInfo($id: ID!) {
|
|||
video(id: $id) {
|
||||
id
|
||||
title
|
||||
previewThumbnailURL(width: 86, height: 45)
|
||||
previewThumbnailURL(width: 320, height: 180)
|
||||
lengthSeconds
|
||||
publishedAt
|
||||
viewCount
|
||||
|
@ -14,6 +14,7 @@ query FFZ_GetVideoInfo($id: ID!) {
|
|||
id
|
||||
login
|
||||
displayName
|
||||
profileImageURL(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,6 +51,7 @@
|
|||
>
|
||||
<option
|
||||
v-for="(r, key) in data.renderers"
|
||||
v-if="supportsRenderer(key)"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
|
@ -330,7 +331,7 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-r-1 ffz-checkbox">
|
||||
<div v-if="has_hover_modifier" class="tw-pd-r-1 ffz-checkbox">
|
||||
<input
|
||||
:id="'key_hover$' + id"
|
||||
ref="key_hover"
|
||||
|
@ -388,8 +389,8 @@
|
|||
|
||||
<div v-if="canPreview" class="tw-mg-l-1 tw-border-l tw-pd-l-1 tw-pd-y-05 tw-flex tw-flex-shrink-0 tw-align-items-start">
|
||||
<action-preview
|
||||
:act="display"
|
||||
:color="display.appearance && data.color(display.appearance.color)"
|
||||
:act="maybeDynamic(display)"
|
||||
:process-color="data.color"
|
||||
:renderers="data.renderers"
|
||||
/>
|
||||
</div>
|
||||
|
@ -458,7 +459,7 @@ import {has, maybe_call, deep_copy} from 'utilities/object';
|
|||
let id = 0;
|
||||
|
||||
export default {
|
||||
props: ['action', 'data', 'inline', 'mod_icons', 'context', 'modifiers'],
|
||||
props: ['vuectx', 'action', 'data', 'inline', 'mod_icons', 'context', 'modifiers', 'hover_modifier'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
@ -494,6 +495,10 @@ export default {
|
|||
return this.modifiers
|
||||
},
|
||||
|
||||
has_hover_modifier() {
|
||||
return this.hover_modifier !== false
|
||||
},
|
||||
|
||||
fmts() {
|
||||
const out = [];
|
||||
|
||||
|
@ -552,6 +557,9 @@ export default {
|
|||
if ( this.action.t === 'inherit' )
|
||||
return this.t('setting.inheritance', 'Inheritance Point');
|
||||
|
||||
if ( this.action.t === 'skip' )
|
||||
return this.t('setting.inheritance.skip', 'Not Inheriting');
|
||||
|
||||
else if ( ! this.display )
|
||||
return this.t('setting.unknown', 'Unknown Value');
|
||||
|
||||
|
@ -585,7 +593,10 @@ export default {
|
|||
|
||||
description() {
|
||||
if ( this.action.t === 'inherit' )
|
||||
return this.t('setting.inheritance.desc', 'Inherit values from lower priority profiles at this position.');
|
||||
return this.t('setting.inheritance.desc', 'Inherit values from lower priority profiles or the default values at this position.');
|
||||
|
||||
if ( this.action.t === 'skip' )
|
||||
return this.t('setting.inheritance.skip.desc', 'This profile does not inherit values from lower priority profiles or the default values, despite having no values of its own.');
|
||||
|
||||
const type = this.display && this.display.type;
|
||||
|
||||
|
@ -691,7 +702,7 @@ export default {
|
|||
}));
|
||||
}
|
||||
|
||||
if ( disp.hover )
|
||||
if ( this.has_hover_modifier && disp.hover )
|
||||
out.push(this.t('setting.actions.visible.hover', 'when hovering'));
|
||||
|
||||
if ( ! out.length )
|
||||
|
@ -766,6 +777,29 @@ export default {
|
|||
this.edit_data = null;
|
||||
},
|
||||
|
||||
maybeDynamic(data) {
|
||||
let ap = data.appearance;
|
||||
if (ap?.type === 'dynamic') {
|
||||
const act = this.action_def,
|
||||
ffz = this.vuectx.getFFZ(),
|
||||
actions = ffz && ffz.resolve('chat.actions');
|
||||
|
||||
const out = actions && act?.dynamicAppearance && act.dynamicAppearance
|
||||
.call(actions, deep_copy(ap), data, null, null, null, null);
|
||||
if ( out )
|
||||
return Object.assign({}, data, {appearance: out});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
supportsRenderer(key) {
|
||||
if (key !== 'dynamic')
|
||||
return true;
|
||||
|
||||
return this.action_def?.supports_dynamic;
|
||||
},
|
||||
|
||||
getData() {
|
||||
const def = this.display && this.data.actions[this.display.action];
|
||||
if ( ! def )
|
||||
|
|
|
@ -19,9 +19,13 @@
|
|||
<script>
|
||||
|
||||
export default {
|
||||
props: ['act', 'color', 'tooltip', 'pad', 'renderers'],
|
||||
props: ['act', 'process-color', 'tooltip', 'pad', 'renderers'],
|
||||
|
||||
computed: {
|
||||
color() {
|
||||
return this['processColor'](this.act.appearance.color);
|
||||
},
|
||||
|
||||
renderer() {
|
||||
return this.renderers[this.act.appearance.type]
|
||||
}
|
||||
|
|
|
@ -207,8 +207,8 @@
|
|||
<action-preview
|
||||
v-else
|
||||
:key="act.id"
|
||||
:act="act.v"
|
||||
:color="color(act.v.appearance.color)"
|
||||
:act="maybeDynamic(act.v)"
|
||||
:process-color="color"
|
||||
:renderers="data.renderers"
|
||||
tooltip="true"
|
||||
pad="true"
|
||||
|
@ -291,7 +291,12 @@
|
|||
<div class="tw-flex-grow-1 tw-mg-r-1">
|
||||
{{ preset.title_i18n ? t(preset.title_i18n, preset.title, preset) : preset.title }}
|
||||
</div>
|
||||
<action-preview v-if="preset.appearance" :act="preset" :renderers="data.renderers" />
|
||||
<action-preview
|
||||
v-if="preset.appearance"
|
||||
:act="maybeDynamic(preset)"
|
||||
:process-color="color"
|
||||
:renderers="data.renderers"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
@ -300,7 +305,7 @@
|
|||
</balloon>
|
||||
</div>
|
||||
<button
|
||||
v-if="! maybe_clear && val.length"
|
||||
v-if="! maybe_clear && strip_skip_val.length"
|
||||
class="tw-mg-l-1 tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="maybe_clear = true"
|
||||
>
|
||||
|
@ -333,7 +338,7 @@
|
|||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="! val.length && has_default"
|
||||
v-if="! strip_skip_val.length && has_default"
|
||||
class="tw-mg-l-1 tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="populate"
|
||||
>
|
||||
|
@ -349,6 +354,20 @@
|
|||
<div ref="list" class="ffz--action-list">
|
||||
<div v-if="! val.length" class="tw-c-text-alt-2 tw-font-size-4 tw-align-center tw-c-text-alt-2 tw-pd-1">
|
||||
{{ t('setting.actions.no-actions', 'no actions are defined in this profile') }}
|
||||
|
||||
<div class="tw-mg-t-1">
|
||||
<button
|
||||
class="tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="addSkip"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-block">
|
||||
{{ t('setting.actions.do-not-inherit', 'Do Not Inherit') }}
|
||||
</span>
|
||||
<span class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-center">
|
||||
{{ t('setting.actions.do-not-inherit.desc', 'Do not inherit values from lower priority profiles or the defaults.') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<section v-for="act in val" :key="act.id">
|
||||
<action-editor
|
||||
|
@ -357,7 +376,9 @@
|
|||
:inline="item.inline"
|
||||
:mod_icons="has_icons"
|
||||
:context="item.context"
|
||||
:vuectx="context"
|
||||
:modifiers="item.modifiers"
|
||||
:hover_modifier="item.hover_modifier"
|
||||
@remove="remove(act)"
|
||||
@save="save(act, $event)"
|
||||
/>
|
||||
|
@ -409,6 +430,14 @@ export default {
|
|||
return false;
|
||||
},
|
||||
|
||||
hasSkip() {
|
||||
for(const val of this.val)
|
||||
if ( val.t === 'skip' )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
sample_user() {
|
||||
return this.has_user ? {
|
||||
displayName: 'SirStendec',
|
||||
|
@ -560,6 +589,10 @@ export default {
|
|||
return out;
|
||||
},
|
||||
|
||||
strip_skip_val() {
|
||||
return this.val.filter(x => x.t !== 'skip');
|
||||
},
|
||||
|
||||
val() {
|
||||
if ( ! this.has_value )
|
||||
return [];
|
||||
|
@ -657,8 +690,28 @@ export default {
|
|||
this.set(deep_copy(this.default_value));
|
||||
},
|
||||
|
||||
addSkip() {
|
||||
const vals = Array.from(this.val);
|
||||
if(vals.length > 0)
|
||||
return;
|
||||
|
||||
vals.push({
|
||||
t: 'skip'
|
||||
});
|
||||
|
||||
this.set(deep_copy(vals));
|
||||
},
|
||||
|
||||
add(val) {
|
||||
const vals = Array.from(this.val);
|
||||
|
||||
// Remove any skip entry.
|
||||
let i = vals.length;
|
||||
while(i--) {
|
||||
if (vals[i]?.t === 'skip')
|
||||
vals.splice(i, 1);
|
||||
}
|
||||
|
||||
vals.push(val);
|
||||
this.set(deep_copy(vals));
|
||||
this.add_open = false;
|
||||
|
@ -739,6 +792,22 @@ export default {
|
|||
return true;
|
||||
},
|
||||
|
||||
maybeDynamic(data) {
|
||||
let ap = data.appearance;
|
||||
if (ap?.type === 'dynamic') {
|
||||
const act = this.data.actions[data.action],
|
||||
ffz = this.context.getFFZ(),
|
||||
actions = ffz && ffz.resolve('chat.actions');
|
||||
|
||||
const out = actions && act?.dynamicAppearance && act.dynamicAppearance
|
||||
.call(actions, deep_copy(ap), data, this.sample_message, this.sample_room, this.sample_user, this.with_mod_icons);
|
||||
if ( out )
|
||||
return Object.assign({}, data, {appearance: out});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
color(input) {
|
||||
if ( ! input )
|
||||
return input;
|
||||
|
|
699
src/modules/main_menu/components/chat-tester.vue
Normal file
699
src/modules/main_menu/components/chat-tester.vue
Normal file
|
@ -0,0 +1,699 @@
|
|||
<template>
|
||||
<div class="ffz--chat-tester">
|
||||
<div v-if="context.exclusive" class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-2">
|
||||
<h3 class="ffz-i-attention">
|
||||
{{ t('debug.chat-tester.exclusive', "Hey! This won't work here!") }}
|
||||
</h3>
|
||||
<markdown :source="t('debug.chat-tester.exclusive-explain', 'This feature does not work when the FFZ Control Center is popped out. It needs to be used in a window where you can see chat.')" />
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<label for="selector" class="tw-mg-y-05">
|
||||
{{ t('debug.chat-tester.message', 'Test Message') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-flex tw-flex-column tw-mg-05 tw-full-width">
|
||||
<select
|
||||
id="selector"
|
||||
ref="selector"
|
||||
class="tw-full-width tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
|
||||
@change="onSelectChange"
|
||||
>
|
||||
<option :selected="is_custom" value="custom">
|
||||
{{ t('setting.combo-box.custom', 'Custom') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(sample, idx) in samples"
|
||||
:key="idx"
|
||||
:selected="sample.data === message"
|
||||
:value="sample.data"
|
||||
>
|
||||
{{ sample.name }}
|
||||
</option>
|
||||
</select>
|
||||
<textarea
|
||||
ref="message"
|
||||
class="tw-block tw-font-size-6 tw-full-width ffz-textarea ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium"
|
||||
rows="10"
|
||||
@blur="updateMessage"
|
||||
@input="onMessageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-t-1 tw-flex tw-align-items-center">
|
||||
<div class="tw-flex-grow-1" />
|
||||
|
||||
<div class="tw-pd-x-1 ffz-checkbox">
|
||||
<input
|
||||
id="replay_fix"
|
||||
ref="replay_fix"
|
||||
:checked="replay_fix"
|
||||
type="checkbox"
|
||||
class="ffz-checkbox__input"
|
||||
@change="onCheck"
|
||||
>
|
||||
|
||||
<label for="replay_fix" class="ffz-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('debug.chat-tester.replay-fix', 'Fix ID and Channel') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="playMessage"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-play">
|
||||
{{ t('debug.chat-tester.play', 'Play Message') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-t-1 tw-border-t tw-mg-t-1 tw-flex tw-mg-b-1 tw-align-items-center">
|
||||
<div class="tw-flex-grow-1" />
|
||||
|
||||
<div class="tw-pd-x-1 ffz-checkbox">
|
||||
<input
|
||||
id="capture_chat"
|
||||
ref="capture_chat"
|
||||
:checked="capture_chat"
|
||||
type="checkbox"
|
||||
class="ffz-checkbox__input"
|
||||
@change="onCheck"
|
||||
>
|
||||
|
||||
<label for="capture_chat" class="ffz-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('debug.chat-tester.capture-chat', 'Capture Chat') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1 ffz-checkbox">
|
||||
<input
|
||||
id="ignore_privmsg"
|
||||
ref="ignore_privmsg"
|
||||
:checked="ignore_privmsg"
|
||||
type="checkbox"
|
||||
class="ffz-checkbox__input"
|
||||
@change="onCheck"
|
||||
>
|
||||
|
||||
<label for="ignore_privmsg" class="ffz-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('debug.chat-tester.ignore-privmsg', 'Ignore PRIVMSG') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1 ffz-checkbox">
|
||||
<input
|
||||
id="capture_pubsub"
|
||||
ref="capture_pubsub"
|
||||
:checked="capture_pubsub"
|
||||
type="checkbox"
|
||||
class="ffz-checkbox__input"
|
||||
@change="onCheck"
|
||||
>
|
||||
|
||||
<label for="capture_pubsub" class="ffz-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('debug.chat-tester.capture-pubsub', 'Capture PubSub') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="clearLog"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('debug.chat-tester.clear-log', 'Clear Log') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in log"
|
||||
:key="item._id"
|
||||
class="tw-elevation-1 tw-border tw-pd-y-05 tw-pd-r-1 tw-mg-y-05 tw-flex tw-flex-nowrap tw-align-items-center"
|
||||
:class="{'tw-c-background-base': item.pubsub, 'tw-c-background-alt-2': !item.pubsub}"
|
||||
>
|
||||
<time class="tw-mg-l-05 tw-mg-r-1 tw-flex-shrink-0">
|
||||
{{ tTime(item.timestamp, 'HH:mm:ss') }}
|
||||
</time>
|
||||
<div v-if="item.pubsub" class="tw-flex-grow-1">
|
||||
<div class="tw-mg-b-05 tw-border-b tw-pd-b-05">{{ item.topic }}</div>
|
||||
<div v-html="highlightJson(item.data)" />
|
||||
</div>
|
||||
<div v-else-if="item.chat" class="tw-flex-grow-1">
|
||||
<div v-if="item.tags" class="ffz-ct--tags">
|
||||
@<template v-for="(tag, key) in item.tags"><span class="ffz-ct--tag">{{ key }}</span>=<span class="ffz-ct--tag-value">{{ tag }}</span>;</template>
|
||||
</div>
|
||||
<div class="ffz-ct--prefix">
|
||||
<template v-if="item.prefix">:<span v-if="item.user" class="ffz-ct--user">{{ item.user }}</span><span class="ffz-ct--prefix">{{ item.prefix }}</span></template>
|
||||
<span class="ffz-ct--command">{{ item.command }}</span>
|
||||
<template v-if="item.channel">#<span class="ffz-ct--channel">{{ item.channel }}</span></template>
|
||||
</div>
|
||||
<div v-if="item.last_param" class="ffz-ct--params">
|
||||
<span v-for="para in item.params" class="ffz-ct--param">{{ para }}</span>
|
||||
:<span class="ffz-ct--param">{{ item.last_param }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tw-flex-grow-1">
|
||||
{{ item.data }}
|
||||
</div>
|
||||
<div class="tw-mg-l-1 tw-flex tw-flex-wrap tw-flex-column tw-justify-content-start tw-align-items-start">
|
||||
<button
|
||||
v-if="item.chat || item.pubsub"
|
||||
class="tw-button tw-button--text"
|
||||
@click="replayItem(item)"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-arrows-cw">
|
||||
{{ t('debug.chat-tester.replay', 'Replay') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="tw-button tw-button--text"
|
||||
@click="copyItem(item)"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-docs">
|
||||
{{ t('setting.copy-json', 'Copy') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { sanitize } from 'src/utilities/dom';
|
||||
import { DEBUG, SERVER } from 'utilities/constants';
|
||||
import { deep_copy, generateUUID } from 'utilities/object';
|
||||
import { getBuster } from 'utilities/time';
|
||||
|
||||
import SAMPLES from '../sample-chat-messages.json'; // eslint-disable-line no-unused-vars
|
||||
|
||||
const IGNORE_COMMANDS = [
|
||||
'PONG',
|
||||
'PING',
|
||||
'366',
|
||||
'353'
|
||||
];
|
||||
|
||||
let LOADED_SAMPLES = [
|
||||
{
|
||||
"name": "Ping",
|
||||
"data": "PING :tmi.twitch.tv"
|
||||
}
|
||||
];
|
||||
|
||||
let has_loaded_samples = false;
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
const state = window.history.state;
|
||||
const samples = deep_copy(LOADED_SAMPLES);
|
||||
const message = state?.ffz_ct_message ?? samples[0].data;
|
||||
|
||||
let is_custom = true;
|
||||
for(const item of samples) {
|
||||
if (item.data === message) {
|
||||
is_custom = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
has_client: false,
|
||||
|
||||
samples,
|
||||
is_custom,
|
||||
message,
|
||||
|
||||
replay_fix: state?.ffz_ct_replay ?? true,
|
||||
ignore_privmsg: state?.ffz_ct_privmsg ?? false,
|
||||
capture_chat: state?.ffz_ct_chat ?? false,
|
||||
capture_pubsub: state?.ffz_ct_pubsub ?? false,
|
||||
|
||||
log: [],
|
||||
logi: 0
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
message() {
|
||||
if ( ! this.is_custom )
|
||||
this.$refs.message.value = this.message;
|
||||
},
|
||||
|
||||
capture_chat() {
|
||||
if ( this.capture_chat )
|
||||
this.listenChat();
|
||||
else
|
||||
this.unlistenChat();
|
||||
},
|
||||
|
||||
capture_pubsub() {
|
||||
if ( this.capture_pubsub )
|
||||
this.listenPubsub();
|
||||
else
|
||||
this.unlistenPubsub();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadSamples();
|
||||
|
||||
this.chat = this.item.getChat();
|
||||
|
||||
this.client = this.chat.ChatService.first?.client;
|
||||
this.has_client = !!this.client;
|
||||
|
||||
if ( this.capture_chat )
|
||||
this.listenChat();
|
||||
|
||||
if ( this.capture_pubsub )
|
||||
this.listenPubsub();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.unlistenChat();
|
||||
this.unlistenPubsub();
|
||||
|
||||
this.client = null;
|
||||
this.chat = null;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$refs.message.value = this.message;
|
||||
},
|
||||
|
||||
methods: {
|
||||
highlightJson(object, depth = 1) {
|
||||
if ( depth > 10 )
|
||||
return `<span class="ffz-ct--obj-literal"><nested>`;
|
||||
|
||||
if (object == null)
|
||||
return `<span class="ffz-ct--literal" depth="${depth}">null</span>`;
|
||||
|
||||
if ( typeof object === 'number' || typeof object === 'boolean' )
|
||||
return `<span class="ffz-ct--literal" depth="${depth}">${object}</span>`;
|
||||
|
||||
if ( typeof object === 'string' )
|
||||
return `<span class=ffz-ct--string depth="${depth}">"${sanitize(object)}"</span>`;
|
||||
|
||||
if ( Array.isArray(object) )
|
||||
return `<span class="ffz-ct--obj-open" depth="${depth}">[</span>`
|
||||
+ object.map(x => this.highlightJson(x, depth + 1)).join(`<span class="ffz-ct--obj-sep" depth="${depth}">, </span>`)
|
||||
+ `<span class="ffz-ct--obj-close" depth="${depth}">]</span>`;
|
||||
|
||||
const out = [];
|
||||
|
||||
for(const [key, val] of Object.entries(object)) {
|
||||
if ( out.length > 0 )
|
||||
out.push(`<span class="ffz-ct--obj-sep" depth="${depth}">, </span>`);
|
||||
|
||||
out.push(`<span class="ffz-ct--obj-key" depth="${depth}">"${sanitize(key)}"</span><span class="ffz-ct--obj-key-sep" depth="${depth}">: </span>`);
|
||||
out.push(this.highlightJson(val, depth + 1));
|
||||
}
|
||||
|
||||
return `<span class="ffz-ct--obj-open" depth="${depth}">{</span>${out.join('')}<span class="ffz-ct--obj-close" depth="${depth}">}</span>`;
|
||||
},
|
||||
|
||||
// Samples
|
||||
async loadSamples() {
|
||||
if ( has_loaded_samples )
|
||||
return;
|
||||
|
||||
const values = await fetch(DEBUG ? SAMPLES : `${SERVER}/script/sample-chat-messages.json?_=${getBuster()}`).then(r => r.ok ? r.json() : null);
|
||||
if ( Array.isArray(values) && values.length > 0 ) {
|
||||
has_loaded_samples = true;
|
||||
|
||||
for(const item of values) {
|
||||
if ( Array.isArray(item.data) )
|
||||
item.data = item.data.join('\n\n');
|
||||
}
|
||||
|
||||
LOADED_SAMPLES = values;
|
||||
this.samples = deep_copy(values);
|
||||
|
||||
let is_custom = true;
|
||||
for(const item of this.samples) {
|
||||
if (item.data === this.message) {
|
||||
is_custom = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.is_custom = is_custom;
|
||||
}
|
||||
},
|
||||
|
||||
// Chat
|
||||
listenChat() {
|
||||
if ( this.listening_chat )
|
||||
return;
|
||||
|
||||
// Ensure we have the chat client.
|
||||
if ( ! this.has_client ) {
|
||||
this.client = this.chat.ChatService.first?.client;
|
||||
this.has_client = !!this.client;
|
||||
|
||||
if ( ! this.has_client )
|
||||
return;
|
||||
}
|
||||
|
||||
// Hook into the connection.
|
||||
const conn = this.client.connection;
|
||||
|
||||
if ( ! conn.ffzOnSocketMessage )
|
||||
conn.ffzOnSocketMessage = conn.onSocketMessage;
|
||||
|
||||
conn.onSocketMessage = event => {
|
||||
try {
|
||||
this.handleChat(event);
|
||||
} catch(err) {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
return conn.ffzOnSocketMessage(event);
|
||||
}
|
||||
|
||||
if ( conn.ws )
|
||||
conn.ws.onmessage = conn.onSocketMessage;
|
||||
|
||||
this.addLog("Started capturing chat.");
|
||||
|
||||
this.listening_chat = true;
|
||||
},
|
||||
|
||||
unlistenChat() {
|
||||
if ( ! this.listening_chat )
|
||||
return;
|
||||
|
||||
const conn = this.client.connection;
|
||||
|
||||
conn.onSocketMessage = conn.ffzOnSocketMessage;
|
||||
|
||||
if ( conn.ws )
|
||||
conn.ws.onmessage = conn.onSocketMessage;
|
||||
|
||||
this.addLog("Stopped capturing chat.");
|
||||
|
||||
this.listening_chat = false;
|
||||
},
|
||||
|
||||
handleChat(event) {
|
||||
for(const raw of event.data.split(/\r?\n/g)) {
|
||||
const msg = this.parseChat(raw);
|
||||
if ( msg ) {
|
||||
if ( this.ignore_privmsg && msg.command === 'PRIVMSG' )
|
||||
continue;
|
||||
|
||||
if ( IGNORE_COMMANDS.includes(msg.command) )
|
||||
continue;
|
||||
|
||||
this.addLog(msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
parseChat(raw) {
|
||||
const msg = this.client.parser.msg(raw);
|
||||
msg.chat = true;
|
||||
|
||||
if ( Object.keys(msg.tags).length === 0 )
|
||||
msg.tags = null;
|
||||
|
||||
if ( msg.params.length > 0 && msg.params[0].startsWith('#') )
|
||||
msg.channel = msg.params.shift().slice(1);
|
||||
|
||||
if ( msg.params.length > 0 )
|
||||
msg.last_param = msg.params.pop();
|
||||
|
||||
const idx = msg.prefix ? msg.prefix.indexOf('!') : -1;
|
||||
|
||||
if ( idx === -1 )
|
||||
msg.user = null;
|
||||
else {
|
||||
msg.user = msg.prefix.substr(0, idx);
|
||||
msg.prefix = msg.prefix.substr(idx);
|
||||
}
|
||||
|
||||
return msg;
|
||||
},
|
||||
|
||||
// Pubsub
|
||||
listenPubsub() {
|
||||
if ( this.listening_pubsub )
|
||||
return;
|
||||
|
||||
this.chat.on('site.subpump:pubsub-message', this.handlePubsub, this);
|
||||
this.addLog("Started capturing PubSub.");
|
||||
|
||||
this.listening_pubsub = true;
|
||||
},
|
||||
|
||||
unlistenPubsub() {
|
||||
if ( ! this.listening_pubsub )
|
||||
return;
|
||||
|
||||
this.chat.off('site.subpump:pubsub-message', this.handlePubsub, this);
|
||||
this.addLog("Stopped capturing PubSub.");
|
||||
|
||||
this.listening_pubsub = false;
|
||||
},
|
||||
|
||||
handlePubsub(event) {
|
||||
|
||||
if ( event.prefix === 'video-playback-by-id' )
|
||||
return;
|
||||
|
||||
this.addLog({
|
||||
pubsub: true,
|
||||
topic: event.topic,
|
||||
data: deep_copy(event.message)
|
||||
});
|
||||
},
|
||||
|
||||
// State
|
||||
|
||||
saveState() {
|
||||
try {
|
||||
window.history.replaceState({
|
||||
...window.history.state,
|
||||
ffz_ct_replay: this.replay_fix,
|
||||
ffz_ct_message: this.message,
|
||||
ffz_ct_chat: this.capture_chat,
|
||||
ffz_ct_pubsub: this.capture_pubsub,
|
||||
ffz_ct_privmsg: this.ignore_privmsg
|
||||
}, document.title);
|
||||
|
||||
} catch(err) {
|
||||
/* no-op */
|
||||
}
|
||||
},
|
||||
|
||||
// Event Handlers
|
||||
|
||||
onSelectChange() {
|
||||
const raw_value = this.$refs.selector.value;
|
||||
|
||||
if ( raw_value && raw_value !== 'custom' ) {
|
||||
this.message = raw_value;
|
||||
this.is_custom = false;
|
||||
} else
|
||||
this.is_custom = true;
|
||||
},
|
||||
|
||||
updateMessage() {
|
||||
const value = this.$refs.message.value;
|
||||
|
||||
let is_custom = true;
|
||||
for(const item of this.samples) {
|
||||
if (item.data === value) {
|
||||
is_custom = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.is_custom = is_custom;
|
||||
if ( this.is_custom )
|
||||
this.message = value;
|
||||
},
|
||||
|
||||
onMessageChange() {
|
||||
this.updateMessage();
|
||||
},
|
||||
|
||||
onCheck() {
|
||||
this.replay_fix = this.$refs.replay_fix.checked;
|
||||
this.capture_chat = this.$refs.capture_chat.checked;
|
||||
this.capture_pubsub = this.$refs.capture_pubsub.checked;
|
||||
this.ignore_privmsg = this.$refs.ignore_privmsg.checked;
|
||||
|
||||
this.saveState();
|
||||
},
|
||||
|
||||
// Log
|
||||
|
||||
addLog(msg) {
|
||||
if ( typeof msg !== 'object' )
|
||||
msg = {
|
||||
data: msg
|
||||
};
|
||||
|
||||
msg.timestamp = Date.now();
|
||||
msg._id = this.logi++;
|
||||
|
||||
this.log.unshift(msg);
|
||||
const extra = this.log.length - 100;
|
||||
if ( extra > 0 )
|
||||
this.log.splice(100, extra);
|
||||
},
|
||||
|
||||
clearLog() {
|
||||
this.log = [];
|
||||
this.addLog('Cleared log.');
|
||||
},
|
||||
|
||||
// Item Actions
|
||||
|
||||
copyItem(item) {
|
||||
let value;
|
||||
if ( item.raw )
|
||||
value = item.raw;
|
||||
else if ( item.data )
|
||||
value = item.data;
|
||||
else
|
||||
value = item;
|
||||
|
||||
if ( typeof value !== 'string' )
|
||||
value = JSON.stringify(value);
|
||||
|
||||
navigator.clipboard.writeText(value);
|
||||
},
|
||||
|
||||
playMessage() {
|
||||
const msgs = [];
|
||||
const parts = this.message.split(/\r?\n/g);
|
||||
|
||||
for(const part of parts) {
|
||||
try {
|
||||
if ( part && part.length > 0 )
|
||||
msgs.push(this.parseChat(part));
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Unable to parse message.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for(const msg of msgs)
|
||||
this.replayItem(msg);
|
||||
},
|
||||
|
||||
replayItem(item) {
|
||||
if ( item.chat ) {
|
||||
// While building the string, also build colors for the console log.
|
||||
const out = [];
|
||||
const colors = [];
|
||||
|
||||
if ( item.tags ) {
|
||||
out.push('@');
|
||||
colors.push('gray');
|
||||
|
||||
for(const [key, val] of Object.entries(item.tags)) {
|
||||
let v = val;
|
||||
|
||||
// If the tag is "id", return a new id so the message
|
||||
// won't be deduplicated automatically.
|
||||
if ( key === 'id' && this.replay_fix )
|
||||
v = generateUUID();
|
||||
|
||||
out.push(key);
|
||||
out.push('=');
|
||||
out.push(`${v}`);
|
||||
out.push(';');
|
||||
colors.push('orange');
|
||||
colors.push('gray');
|
||||
colors.push('white');
|
||||
colors.push('gray');
|
||||
}
|
||||
}
|
||||
|
||||
if ( item.user || item.prefix ) {
|
||||
if ( out.length ) {
|
||||
out.push(' ');
|
||||
colors.push('');
|
||||
}
|
||||
|
||||
out.push(':');
|
||||
colors.push('gray');
|
||||
if (item.user) {
|
||||
out.push(item.user);
|
||||
colors.push('green');
|
||||
}
|
||||
if (item.prefix) {
|
||||
out.push(item.prefix);
|
||||
colors.push('gray');
|
||||
}
|
||||
}
|
||||
|
||||
if ( out.length ) {
|
||||
out.push(' ');
|
||||
colors.push('');
|
||||
}
|
||||
|
||||
out.push(item.command);
|
||||
colors.push('orange');
|
||||
|
||||
// If there's a channel, use the current channel rather
|
||||
// than the logged channel.
|
||||
if ( item.channel ) {
|
||||
out.push(` #`);
|
||||
colors.push('gray');
|
||||
out.push(this.replay_fix ? this.chat.ChatService.first?.props?.channelLogin ?? item.channel : item.channel);
|
||||
colors.push('green');
|
||||
}
|
||||
|
||||
for(const para of item.params) {
|
||||
out.push(` ${para}`);
|
||||
colors.push('skyblue');
|
||||
}
|
||||
|
||||
if ( item.last_param ) {
|
||||
out.push(` :`);
|
||||
colors.push('gray');
|
||||
out.push(item.last_param);
|
||||
colors.push('skyblue');
|
||||
}
|
||||
|
||||
const msg = out.join(''),
|
||||
conn = this.client.connection,
|
||||
handler = conn.ffzOnSocketMessage ?? conn.onSocketMessage;
|
||||
|
||||
const log_msg = out.join('%c'),
|
||||
log_colors = colors.map(x => x?.length ? `color: ${x};` : '');
|
||||
|
||||
this.chat.log.debugColor(`Injecting chat message: %c${log_msg}`, log_colors);
|
||||
|
||||
handler.call(conn, {
|
||||
data: msg
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
|
@ -80,6 +80,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
adding: false,
|
||||
resetting: false,
|
||||
editing: this.copyValue()
|
||||
}
|
||||
},
|
||||
|
@ -91,9 +92,16 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
value() {
|
||||
this.resetting = true;
|
||||
this.editing = this.copyValue();
|
||||
},
|
||||
|
||||
editing: {
|
||||
handler() {
|
||||
this.$emit('input', this.editing)
|
||||
if (!this.resetting)
|
||||
this.$emit('input', this.editing)
|
||||
this.resetting = false;
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
|
|
|
@ -45,23 +45,23 @@
|
|||
</div>
|
||||
|
||||
<template v-if="deleting">
|
||||
<button class="tw-button tw-button--text tw-relative ffz-il-tooltip__container" @click="$emit('delete')">
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text tw-relative ffz-il-tooltip__container" @click="deleting = false">
|
||||
<span class="tw-button__text ffz-i-cancel" />
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--up ffz-il-tooltip--align-right">
|
||||
{{ t('setting.cancel', 'Cancel') }}
|
||||
</div>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text tw-relative ffz-il-tooltip__container" @click="$emit('delete')">
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--up ffz-il-tooltip--align-right">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="tw-button tw-button--text tw-relative ffz-il-tooltip__container" @click="deleting = true">
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--up ffz-il-tooltip--align-right">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
@ -366,6 +366,11 @@ export default {
|
|||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
this.es = null;
|
||||
}
|
||||
|
||||
this.chat.off('chat:update-link-resolver', this.checkRefreshRaw, this);
|
||||
this.settings.off(':changed:debug.link-resolver.source', this.changeProvider, this);
|
||||
this.chat = null;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
@focusin="focus"
|
||||
@focusout="blur"
|
||||
>
|
||||
<div class="scrollable-area tw-border-b" data-simplebar>
|
||||
<div ref="scroller" class="scrollable-area tw-border-b" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div ref="popup" class="simplebar-content">
|
||||
<div
|
||||
|
@ -111,7 +111,22 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
opened() {
|
||||
if (this.opened)
|
||||
this.$nextTick(() => this.updateScroller());
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateScroller() {
|
||||
const scroller = this.$refs.scroller;
|
||||
if (!scroller || ! window.ffzSimplebar || scroller.SimpleBar)
|
||||
return;
|
||||
|
||||
new ffzSimplebar(scroller, ffzSimplebar.getElOptions(scroller));
|
||||
},
|
||||
|
||||
openConfigure() {
|
||||
this.hide();
|
||||
this.$emit('navigate', 'data_management.profiles');
|
||||
|
|
|
@ -8,6 +8,59 @@
|
|||
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center tw-pd-b-05">
|
||||
<div class="tw-flex-grow-1">
|
||||
{{ t('setting.filter.drag', 'Drag entries to re-order them.') }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="! maybe_clear && rules.length"
|
||||
class="tw-mg-l-1 tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="maybe_clear = true"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete-all', 'Delete All') }}
|
||||
</span>
|
||||
<span class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
||||
{{ t('setting.filter.delete-all', "Delete all of this profile's entries.") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="maybe_clear"
|
||||
class="tw-mg-l-1 tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="doClear"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete-all', 'Delete All') }}
|
||||
</span>
|
||||
<span class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
||||
{{ t('setting.filter.delete-all', "Delete all of this profile's entries.") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="maybe_clear"
|
||||
class="tw-mg-l-1 tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="maybe_clear = false"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-cancel">
|
||||
{{ t('setting.cancel', 'Cancel') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="! rules.length && has_default"
|
||||
class="tw-mg-l-1 tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="populate"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.filter.add-default', 'Add Defaults') }}
|
||||
</span>
|
||||
<span class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
||||
{{ t('setting.filter.add-default-tip', 'Add all of the default values to this profile.') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<filter-editor
|
||||
:value="rules"
|
||||
:filters="filters"
|
||||
|
@ -15,6 +68,7 @@
|
|||
:preview="preview"
|
||||
@input="onInput"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -31,26 +85,42 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
filters: this.item.data(),
|
||||
maybe_clear: false,
|
||||
test_context: this.item.test_context ? this.item.test_context() : {},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
has_default() {
|
||||
return this.default_value && this.default_value.length
|
||||
},
|
||||
|
||||
preview() {
|
||||
return this.item.preview || false
|
||||
},
|
||||
|
||||
rules() {
|
||||
if ( ! Array.isArray(this.value) || this.value.length <= 0 )
|
||||
if ( ! this.has_value || ! Array.isArray(this.value) )
|
||||
return [];
|
||||
|
||||
return this.value.filter(x => x.v).map(x => x.v);
|
||||
return this.value.filter(x => x?.v).map(x => x.v);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
doClear() {
|
||||
this.maybe_clear = false;
|
||||
this.clear();
|
||||
},
|
||||
|
||||
populate() {
|
||||
this.set(deep_copy(this.default_value));
|
||||
},
|
||||
|
||||
onInput(data) {
|
||||
const val = deep_copy(data);
|
||||
const val = deep_copy(data).map(x => ({v: x}));
|
||||
if (val.length == 0)
|
||||
val.push({t: 'skip'});
|
||||
this.set(val);
|
||||
}
|
||||
}
|
||||
|
|
29
src/modules/main_menu/sample-chat-messages.json
Normal file
29
src/modules/main_menu/sample-chat-messages.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
[
|
||||
{
|
||||
"name": "Cheer",
|
||||
"data": "@bits=42069;badge-info=subscriber/69;badges=subscriber/3024,bits/100;color=#FF526F;display-name=SirStendec;id=813a1ef5-f8dd-406c-a2dd-f74d99db2799 :sirstendec!sirstendec@sirstendec.tmi.twitch.tv PRIVMSG #sirstendec :Hello. cheer42069"
|
||||
},
|
||||
{
|
||||
"name": "Ping",
|
||||
"data": "PING :tmi.twitch.tv"
|
||||
},
|
||||
{
|
||||
"name": "Subscribe (Tier 1, No Message)",
|
||||
"data": "@badge-info=subscriber/1;badges=subscriber/0,premium/1;color=#007ECC;display-name=Kerokai;emotes=;flags=;id=80b7174c-830b-487b-8bce-99bab02b6378;login=kerokai;mod=0;msg-id=sub;msg-param-cumulative-months=1;msg-param-months=0;msg-param-multimonth-duration=1;msg-param-multimonth-tenure=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(maxisgoatlord);msg-param-sub-plan=1000;msg-param-was-gifted=false;room-id=42490770;subscriber=1;system-msg=Kerokai\\ssubscribed\\sat\\sTier\\s1.;tmi-sent-ts=1671231043209;user-id=32357552;user-type=; :tmi.twitch.tv USERNOTICE #maximum"
|
||||
},
|
||||
{
|
||||
"name": "Resubscribe (Tier 1, 17 Months, Message)",
|
||||
"data": "@badge-info=subscriber/17;badges=subscriber/12,moments/1;color=#2E8B57;display-name=FaustDaimos;emotes=;flags=;id=cdb378a9-4e56-4933-a78b-f4bcf2a3961a;login=faustdaimos;mod=0;msg-id=resub;msg-param-cumulative-months=17;msg-param-months=0;msg-param-multimonth-duration=0;msg-param-multimonth-tenure=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Baka\\sBrigade!;msg-param-sub-plan=1000;msg-param-was-gifted=false;room-id=24761645;subscriber=1;system-msg=FaustDaimos\\ssubscribed\\sat\\sTier\\s1.\\sThey've\\ssubscribed\\sfor\\s17\\smonths!;tmi-sent-ts=1671231277242;user-id=37613709;user-type= :tmi.twitch.tv USERNOTICE #cirno_tv :when do we try it out"
|
||||
},
|
||||
{
|
||||
"name": "Mass Gift Sub (Tier 1, 5 Total)",
|
||||
"data": [
|
||||
"@badge-info=;badges=premium/1;color=;display-name=HellbirDza;emotes=;flags=;id=d0e4a1d9-75c7-406d-8423-cfa3dfb514b5;login=hellbirdza;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=5;msg-param-origin-id=ee\\se2\\s3f\\s01\\s75\\s45\\se1\\sfa\\s24\\s47\\s29\\s08\\sdb\\sf8\\sab\\se1\\s56\\s27\\s83\\s2e;msg-param-sender-count=15;msg-param-sub-plan=1000;room-id=26261471;subscriber=0;system-msg=HellbirDza\\sis\\sgifting\\s5\\sTier\\s1\\sSubs\\sto\\sAsmongold's\\scommunity!\\sThey've\\sgifted\\sa\\stotal\\sof\\s15\\sin\\sthe\\schannel!;tmi-sent-ts=1671302976346;user-id=28678305;user-type= :tmi.twitch.tv USERNOTICE #asmongold",
|
||||
"@badge-info=;badges=premium/1;color=;display-name=HellbirDza;emotes=;flags=;id=182675e7-db1b-49d3-9650-54c31d938203;login=hellbirdza;mod=0;msg-id=subgift;msg-param-gift-months=1;msg-param-months=1;msg-param-origin-id=ee\\se2\\s3f\\s01\\s75\\s45\\se1\\sfa\\s24\\s47\\s29\\s08\\sdb\\sf8\\sab\\se1\\s56\\s27\\s83\\s2e;msg-param-recipient-display-name=buddyunique1;msg-param-recipient-id=431251927;msg-param-recipient-user-name=buddyunique1;msg-param-sender-count=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(asmongold);msg-param-sub-plan=1000;room-id=26261471;subscriber=0;system-msg=HellbirDza\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sbuddyunique1!;tmi-sent-ts=1671302976704;user-id=28678305;user-type= :tmi.twitch.tv USERNOTICE #asmongold",
|
||||
"@badge-info=;badges=premium/1;color=;display-name=HellbirDza;emotes=;flags=;id=594ce86d-f956-43cd-8b5d-1b7e8499dca1;login=hellbirdza;mod=0;msg-id=subgift;msg-param-gift-months=1;msg-param-months=1;msg-param-origin-id=ee\\se2\\s3f\\s01\\s75\\s45\\se1\\sfa\\s24\\s47\\s29\\s08\\sdb\\sf8\\sab\\se1\\s56\\s27\\s83\\s2e;msg-param-recipient-display-name=tartarin_e;msg-param-recipient-id=144049812;msg-param-recipient-user-name=tartarin_e;msg-param-sender-count=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(asmongold);msg-param-sub-plan=1000;room-id=26261471;subscriber=0;system-msg=HellbirDza\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\startarin_e!;tmi-sent-ts=1671302976722;user-id=28678305;user-type= :tmi.twitch.tv USERNOTICE #asmongold",
|
||||
"@badge-info=;badges=premium/1;color=;display-name=HellbirDza;emotes=;flags=;id=527abc39-e599-4c1d-a480-e724a9c69823;login=hellbirdza;mod=0;msg-id=subgift;msg-param-gift-months=1;msg-param-months=1;msg-param-origin-id=ee\\se2\\s3f\\s01\\s75\\s45\\se1\\sfa\\s24\\s47\\s29\\s08\\sdb\\sf8\\sab\\se1\\s56\\s27\\s83\\s2e;msg-param-recipient-display-name=haaryho_stracene_vlasy;msg-param-recipient-id=96664018;msg-param-recipient-user-name=haaryho_stracene_vlasy;msg-param-sender-count=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(asmongold);msg-param-sub-plan=1000;room-id=26261471;subscriber=0;system-msg=HellbirDza\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\shaaryho_stracene_vlasy!;tmi-sent-ts=1671302976759;user-id=28678305;user-type= :tmi.twitch.tv USERNOTICE #asmongold",
|
||||
"@badge-info=;badges=premium/1;color=;display-name=HellbirDza;emotes=;flags=;id=f694b0fc-0b5e-4adf-8002-03dae340e9b5;login=hellbirdza;mod=0;msg-id=subgift;msg-param-gift-months=1;msg-param-months=1;msg-param-origin-id=ee\\se2\\s3f\\s01\\s75\\s45\\se1\\sfa\\s24\\s47\\s29\\s08\\sdb\\sf8\\sab\\se1\\s56\\s27\\s83\\s2e;msg-param-recipient-display-name=corette0;msg-param-recipient-id=149790291;msg-param-recipient-user-name=corette0;msg-param-sender-count=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(asmongold);msg-param-sub-plan=1000;room-id=26261471;subscriber=0;system-msg=HellbirDza\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\scorette0!;tmi-sent-ts=1671302976767;user-id=28678305;user-type= :tmi.twitch.tv USERNOTICE #asmongold",
|
||||
"@badge-info=;badges=premium/1;color=;display-name=HellbirDza;emotes=;flags=;id=5893d8a8-5eb3-46d6-9737-f1b2b76400d4;login=hellbirdza;mod=0;msg-id=subgift;msg-param-gift-months=1;msg-param-months=2;msg-param-origin-id=ee\\se2\\s3f\\s01\\s75\\s45\\se1\\sfa\\s24\\s47\\s29\\s08\\sdb\\sf8\\sab\\se1\\s56\\s27\\s83\\s2e;msg-param-recipient-display-name=yo_adg;msg-param-recipient-id=465861822;msg-param-recipient-user-name=yo_adg;msg-param-sender-count=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(asmongold);msg-param-sub-plan=1000;room-id=26261471;subscriber=0;system-msg=HellbirDza\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\syo_adg!;tmi-sent-ts=1671302976798;user-id=28678305;user-type= :tmi.twitch.tv USERNOTICE #asmongold"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -75,7 +75,7 @@ export const array_merge = {
|
|||
default(val) {
|
||||
const values = [];
|
||||
for(const v of val)
|
||||
if ( v.t !== 'inherit' && v.v )
|
||||
if ( v.t !== 'inherit' && v.t !== 'skip' && v.v )
|
||||
values.push(v.v);
|
||||
|
||||
return values;
|
||||
|
@ -119,6 +119,8 @@ export const array_merge = {
|
|||
had_value = true;
|
||||
if ( val.t === 'inherit' )
|
||||
is_trailing = true;
|
||||
else if ( val.t === 'skip' )
|
||||
continue;
|
||||
else if ( is_trailing )
|
||||
trail.push(val.v);
|
||||
else
|
||||
|
|
4
src/sites/player/css_tweaks/player-fade-paused.scss
Normal file
4
src/sites/player/css_tweaks/player-fade-paused.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.video-player__container[data-paused="true"] video,
|
||||
.video-player__container[data-buffering="true"] video {
|
||||
filter: grayscale(0.25) brightness(0.5);
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
.video-player__overlay[data-controls="false"][data-paused="false"][data-ended="false"] {
|
||||
.video-player__container[data-controls="false"][data-paused="false"][data-ended="false"] {
|
||||
cursor: none;
|
||||
}
|
|
@ -94,6 +94,17 @@ export default class PlayerBase extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('player.fade-pause-buffer', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Player > General >> Playback',
|
||||
title: 'Fade the player when paused or buffering to make the UI easier to see.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: val => this.css_tweaks.toggle('player-fade-paused', val)
|
||||
});
|
||||
|
||||
if ( HAS_COMPRESSOR ) {
|
||||
this.settings.add('player.compressor.enable', {
|
||||
default: true,
|
||||
|
@ -559,6 +570,15 @@ export default class PlayerBase extends Module {
|
|||
},
|
||||
changed: val => this.css_tweaks.toggle('player-hide-mouse', val)
|
||||
});
|
||||
|
||||
this.settings.add('player.single-click-pause', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Player > General >> Playback',
|
||||
title: "Pause/Unpause the player by clicking.",
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onEnable() {
|
||||
|
@ -569,6 +589,7 @@ export default class PlayerBase extends Module {
|
|||
this.css_tweaks.toggle('player-volume', this.settings.get('player.volume-always-shown'));
|
||||
this.css_tweaks.toggle('player-ext-mouse', !this.settings.get('player.ext-interaction'));
|
||||
this.css_tweaks.toggle('player-hide-mouse', this.settings.get('player.hide-mouse'));
|
||||
this.css_tweaks.toggle('player-fade-paused', this.settings.get('player.fade-pause-buffer'));
|
||||
|
||||
this.installVisibilityHook();
|
||||
this.updateHideExtensions();
|
||||
|
@ -643,8 +664,6 @@ export default class PlayerBase extends Module {
|
|||
|
||||
|
||||
onShortcut(e) {
|
||||
this.log.info('Compressor Hotkey', e);
|
||||
|
||||
for(const inst of this.Player.instances)
|
||||
this.compressPlayer(inst, e);
|
||||
}
|
||||
|
@ -699,6 +718,7 @@ export default class PlayerBase extends Module {
|
|||
|
||||
const events = this.props.playerEvents;
|
||||
if ( events ) {
|
||||
on(events, 'Buffering', this._ffzUpdateState);
|
||||
on(events, 'Playing', this._ffzUpdateState);
|
||||
on(events, 'PlayerError', this._ffzUpdateState);
|
||||
on(events, 'PlayerError', this._ffzErrorReset);
|
||||
|
@ -811,6 +831,7 @@ export default class PlayerBase extends Module {
|
|||
|
||||
ds.ended = state === 'Ended';
|
||||
ds.paused = state === 'Idle';
|
||||
ds.buffering = state === 'Buffering';
|
||||
}
|
||||
|
||||
cls.prototype.ffzAttachListeners = function() {
|
||||
|
@ -825,10 +846,14 @@ export default class PlayerBase extends Module {
|
|||
if ( ! this._ffz_click_handler )
|
||||
this._ffz_click_handler = this.ffzClickHandler.bind(this);
|
||||
|
||||
if ( ! this._ffz_dblclick_handler )
|
||||
this._ffz_dblclick_handler = this.ffzDblClickHandler.bind(this);
|
||||
|
||||
if ( ! this._ffz_menu_handler )
|
||||
this._ffz_menu_handler = this.ffzMenuHandler.bind(this);
|
||||
|
||||
on(cont, 'wheel', this._ffz_scroll_handler);
|
||||
on(cont, 'dblclick', this._ffz_dblclick_handler);
|
||||
on(cont, 'mousedown', this._ffz_click_handler);
|
||||
on(cont, 'contextmenu', this._ffz_menu_handler);
|
||||
}
|
||||
|
@ -853,23 +878,60 @@ export default class PlayerBase extends Module {
|
|||
this._ffz_menu_handler = null;
|
||||
}
|
||||
|
||||
if ( this._ffz_dblclick_handler ) {
|
||||
off(cont, 'dblclick', this._ffz_dblclick_handler);
|
||||
this._ffz_dblclick_handler = null;
|
||||
}
|
||||
|
||||
this._ffz_listeners = false;
|
||||
}
|
||||
|
||||
cls.prototype.ffzDelayPause = function() {
|
||||
if ( this._ffz_pause_timer )
|
||||
clearTimeout(this._ffz_pause_timer);
|
||||
|
||||
const player = this.props?.mediaPlayerInstance;
|
||||
if (! player.isPaused())
|
||||
this._ffz_pause_timer = setTimeout(() => {
|
||||
const player = this.props?.mediaPlayerInstance;
|
||||
if (!player.isPaused())
|
||||
player.pause();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
cls.prototype.ffzDblClickHandler = function(event) {
|
||||
if ( ! event )
|
||||
return;
|
||||
|
||||
if ( this._ffz_pause_timer )
|
||||
clearTimeout(this._ffz_pause_timer);
|
||||
}
|
||||
|
||||
cls.prototype.ffzClickHandler = function(event) {
|
||||
if ( ! event )
|
||||
return;
|
||||
|
||||
const vol_scroll = t.settings.get('player.volume-scroll'),
|
||||
gain_scroll = t.settings.get('player.gain.scroll'),
|
||||
click_pause = t.settings.get('player.single-click-pause'),
|
||||
|
||||
wants_rmb = wantsRMB(vol_scroll) || wantsRMB(gain_scroll);
|
||||
|
||||
// Left Click
|
||||
if (click_pause && event.button === 0) {
|
||||
if (! event.target || ! event.target.classList.contains('click-handler'))
|
||||
return;
|
||||
|
||||
this.ffzDelayPause();
|
||||
}
|
||||
|
||||
// Right Click
|
||||
if ( wants_rmb && event.button === 2 ) {
|
||||
this.ffz_rmb = true;
|
||||
this.ffz_scrolled = false;
|
||||
}
|
||||
|
||||
// Middle Click
|
||||
if ( ! t.settings.get('player.mute-click') || event.button !== 1 )
|
||||
return;
|
||||
|
||||
|
@ -1321,7 +1383,7 @@ export default class PlayerBase extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
let icon, tip, extra, ff_el, btn, cont = container.querySelector('.ffz--player-comp');
|
||||
let icon, tip, extra, btn, cont = container.querySelector('.ffz--player-comp');
|
||||
if ( ! has_comp || this.areControlsDisabled(inst) ) {
|
||||
if ( cont )
|
||||
cont.remove();
|
||||
|
@ -1346,7 +1408,6 @@ export default class PlayerBase extends Module {
|
|||
<div>
|
||||
{tip = (<div class="ffz--p-tip" />)}
|
||||
{extra = (<div class="ffz--p-extra tw-pd-t-05 ffz--tooltip-explain" />)}
|
||||
{ff_el = IS_FIREFOX ? (<div class="ffz--p-ff tw-pd-t-05 ffz--tooltip-explain" />) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
|
@ -1374,9 +1435,6 @@ export default class PlayerBase extends Module {
|
|||
if ( can_apply && this._shortcut_bound )
|
||||
label = `${label} (${this._shortcut_bound})`;
|
||||
|
||||
if ( ff_el )
|
||||
ff_el.textContent += `\n${this.i18n.t('player.comp_button.firefox', 'Playback Speed controls will not function for Firefox users when the Compressor has been enabled.')}`;
|
||||
|
||||
icon.classList.toggle('ffz-i-comp-on', comp_active);
|
||||
icon.classList.toggle('ffz-i-comp-off', ! comp_active);
|
||||
btn.disabled = ! can_apply;
|
||||
|
@ -1543,12 +1601,16 @@ export default class PlayerBase extends Module {
|
|||
if ( ! video )
|
||||
return false;
|
||||
|
||||
if ( ! video.src && ! video.srcObject )
|
||||
return false;
|
||||
|
||||
if ( video.src ) {
|
||||
const url = new URL(video.src);
|
||||
if ( url.protocol !== 'blob:' )
|
||||
return false;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Validation for srcObject (if we need it)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -67,6 +67,11 @@ export default class Twilight extends BaseSite {
|
|||
onEnable() {
|
||||
this.settings = this.resolve('settings');
|
||||
|
||||
this.web_munch.findModule('simplebar').then(sb => {
|
||||
if (! window.ffzSimplebar && sb )
|
||||
window.ffzSimplebar = sb;
|
||||
}).catch(() => {});
|
||||
|
||||
const thing = this.fine.searchNode(null, n => n?.pendingProps?.store?.getState),
|
||||
store = this.store = thing?.pendingProps?.store;
|
||||
|
||||
|
@ -249,19 +254,20 @@ Twilight.KNOWN_MODULES = {
|
|||
}
|
||||
}
|
||||
|
||||
const VEND_CHUNK = n => ! n || n.includes('vendor');
|
||||
//const VEND_CHUNK = n => ! n || n.includes('vendor');
|
||||
const VEND_CORE = n => ! n || n.includes('vendor') || n.includes('core');
|
||||
|
||||
Twilight.KNOWN_MODULES.core.use_result = true;
|
||||
//Twilight.KNOWN_MODULES.core.chunks = 'core';
|
||||
|
||||
Twilight.KNOWN_MODULES.simplebar.chunks = VEND_CHUNK;
|
||||
Twilight.KNOWN_MODULES.react.chunks = VEND_CHUNK;
|
||||
Twilight.KNOWN_MODULES.cookie.chunks = VEND_CHUNK;
|
||||
Twilight.KNOWN_MODULES.simplebar.chunks = VEND_CORE;
|
||||
Twilight.KNOWN_MODULES.react.chunks = VEND_CORE;
|
||||
Twilight.KNOWN_MODULES.cookie.chunks = VEND_CORE;
|
||||
|
||||
Twilight.KNOWN_MODULES['gql-printer'].use_result = true;
|
||||
Twilight.KNOWN_MODULES['gql-printer'].chunks = VEND_CHUNK;
|
||||
Twilight.KNOWN_MODULES['gql-printer'].chunks = VEND_CORE;
|
||||
|
||||
Twilight.KNOWN_MODULES.mousetrap.chunks = VEND_CHUNK;
|
||||
Twilight.KNOWN_MODULES.mousetrap.chunks = VEND_CORE;
|
||||
|
||||
const CHAT_CHUNK = n => ! n || n.includes('chat');
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ export default class Channel extends Module {
|
|||
changed: () => this.updateLinks()
|
||||
});
|
||||
|
||||
this.settings.add('channel.hosting.enable', {
|
||||
/*this.settings.add('channel.hosting.enable', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Channel > Behavior >> Hosting',
|
||||
|
@ -87,8 +87,7 @@ export default class Channel extends Module {
|
|||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => ! val && this.InfoBar.each(el => this.updateBar(el))
|
||||
});
|
||||
|
||||
});*/
|
||||
|
||||
this.ChannelPanels = this.fine.define(
|
||||
'channel-panels',
|
||||
|
@ -116,7 +115,7 @@ export default class Channel extends Module {
|
|||
{childNodes: true, subtree: true}, 1
|
||||
);
|
||||
|
||||
const strip_host = resp => {
|
||||
/*const strip_host = resp => {
|
||||
if ( this.settings.get('channel.hosting.enable') )
|
||||
return;
|
||||
|
||||
|
@ -130,7 +129,7 @@ export default class Channel extends Module {
|
|||
};
|
||||
|
||||
this.apollo.registerModifier('UseHosting', strip_host, false);
|
||||
this.apollo.registerModifier('PlayerTrackingContextQuery', strip_host, false);
|
||||
this.apollo.registerModifier('PlayerTrackingContextQuery', strip_host, false);*/
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
@ -162,7 +161,7 @@ export default class Channel extends Module {
|
|||
this.InfoBar.on('unmount', this.removeBar, this);
|
||||
this.InfoBar.each(el => this.updateBar(el));
|
||||
|
||||
this.subpump.on(':pubsub-message', this.onPubSub, this);
|
||||
//this.subpump.on(':pubsub-message', this.onPubSub, this);
|
||||
|
||||
this.router.on(':route', this.checkNavigation, this);
|
||||
this.checkNavigation();
|
||||
|
@ -230,7 +229,7 @@ export default class Channel extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
setHost(channel_id, channel_login, target_id, target_login) {
|
||||
/*setHost(channel_id, channel_login, target_id, target_login) {
|
||||
const topic = `stream-chat-room-v1.${channel_id}`;
|
||||
|
||||
this.subpump.inject(topic, {
|
||||
|
@ -272,7 +271,7 @@ export default class Channel extends Module {
|
|||
event.message.data.num_viewers = 0;
|
||||
event.markChanged();
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
updateSubscription(login) {
|
||||
|
@ -441,8 +440,8 @@ export default class Channel extends Module {
|
|||
});
|
||||
}*/
|
||||
|
||||
if ( ! this.settings.get('channel.hosting.enable') && props.hostLogin )
|
||||
this.setHost(props.channelID, props.channelLogin, null, null);
|
||||
//if ( ! this.settings.get('channel.hosting.enable') && props.hostLogin )
|
||||
// this.setHost(props.channelID, props.channelLogin, null, null);
|
||||
|
||||
this.updateSubscription(props.channelLogin);
|
||||
this.updateMetadata(el);
|
||||
|
@ -492,10 +491,10 @@ export default class Channel extends Module {
|
|||
live_since: props.liveSince
|
||||
},
|
||||
props,
|
||||
hosted: {
|
||||
/*hosted: {
|
||||
login: props.hostLogin,
|
||||
display_name: props.hostDisplayName
|
||||
},
|
||||
},*/
|
||||
el,
|
||||
getViewerCount: () => {
|
||||
const thing = cont.querySelector('p[data-a-target="animated-channel-viewers-count"]'),
|
||||
|
|
|
@ -188,6 +188,15 @@ export default class EmoteMenu extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.emote-menu.clear-search', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Chat > Emote Menu >> General',
|
||||
title: 'Reset search when closing the Emote Menu.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.emote-menu.enabled', {
|
||||
default: true,
|
||||
ui: {
|
||||
|
@ -760,24 +769,24 @@ export default class EmoteMenu extends Module {
|
|||
if ( renews > 0 ) {
|
||||
calendar = {
|
||||
icon: 'calendar',
|
||||
message: t.i18n.t('emote-menu.sub-renews', 'This sub renews in {seconds,humantime}.', {seconds: renews})
|
||||
message: t.i18n.t('emote-menu.sub-renews', 'This sub renews {seconds,humantime}.', {seconds: renews})
|
||||
}
|
||||
|
||||
} else if ( ends ) {
|
||||
if ( data.prime )
|
||||
calendar = {
|
||||
icon: 'crown',
|
||||
message: t.i18n.t('emote-menu.sub-prime', 'This is your free sub with Prime Gaming.\nIt ends in {seconds,humantime}.', {seconds: ends})
|
||||
message: t.i18n.t('emote-menu.sub-prime', 'This is your free sub with Prime Gaming.\nIt ends {seconds,humantime}.', {seconds: ends})
|
||||
}
|
||||
else if ( data.gift )
|
||||
calendar = {
|
||||
icon: 'gift',
|
||||
message: t.i18n.t('emote-menu.sub-gift-ends', 'This gifted sub ends in {seconds,humantime}.', {seconds: ends})
|
||||
message: t.i18n.t('emote-menu.sub-gift-ends', 'This gifted sub ends {seconds,humantime}.', {seconds: ends})
|
||||
}
|
||||
else
|
||||
calendar = {
|
||||
icon: 'calendar-empty',
|
||||
message: t.i18n.t('emote-menu.sub-ends', 'This sub ends in {seconds,humantime}.', {seconds: ends})
|
||||
message: t.i18n.t('emote-menu.sub-ends', 'This sub ends {seconds,humantime}.', {seconds: ends})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1078,6 +1087,7 @@ export default class EmoteMenu extends Module {
|
|||
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
|
||||
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
|
||||
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
|
||||
clearSearch: t.chat.context.get('chat.emote-menu.clear-search'),
|
||||
tone: t.settings.provider.get('emoji-tone', null)
|
||||
}
|
||||
|
||||
|
@ -1226,6 +1236,7 @@ export default class EmoteMenu extends Module {
|
|||
t.chat.context.on('changed:chat.emote-menu.show-heading', this.updateSettingState, this);
|
||||
t.chat.context.on('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
|
||||
t.chat.context.on('changed:chat.emote-menu.show-search', this.updateSettingState, this);
|
||||
t.chat.context.on('changed:chat.emote-menu.clear-search', this.updateSettingState, this);
|
||||
t.chat.context.on('changed:chat.emote-menu.tall', this.updateSettingState, this);
|
||||
|
||||
window.ffz_menu = this;
|
||||
|
@ -1241,6 +1252,7 @@ export default class EmoteMenu extends Module {
|
|||
t.chat.context.off('changed:chat.emote-menu.reduced-padding', this.updateSettingState, this);
|
||||
t.chat.context.off('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
|
||||
t.chat.context.off('changed:chat.emote-menu.show-search', this.updateSettingState, this);
|
||||
t.chat.context.off('changed:chat.emote-menu.clear-search', this.updateSettingState, this);
|
||||
t.chat.context.off('changed:chat.emote-menu.tall', this.updateSettingState, this);
|
||||
|
||||
if ( window.ffz_menu === this )
|
||||
|
@ -1256,6 +1268,7 @@ export default class EmoteMenu extends Module {
|
|||
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
|
||||
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
|
||||
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
|
||||
clearSearch: t.chat.context.get('chat.emote-menu.clear-search'),
|
||||
tall: t.chat.context.get('chat.emote-menu.tall')
|
||||
});
|
||||
}
|
||||
|
@ -2309,6 +2322,13 @@ export default class EmoteMenu extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
if ( ! this.props.visible && old_props.visible ) {
|
||||
if ( this.state.clearSearch ) {
|
||||
this.setState(this.filterState('', this.state));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const cd = this.props.channel_data,
|
||||
old_cd = old_props.channel_data,
|
||||
cd_diff = cd?.user !== old_cd?.user || cd?.channel !== old_cd?.channel,
|
||||
|
|
|
@ -20,6 +20,7 @@ import SettingsMenu from './settings_menu';
|
|||
import EmoteMenu from './emote_menu';
|
||||
import Input from './input';
|
||||
import ViewerCards from './viewer_card';
|
||||
import { isHighlightedReward } from './points';
|
||||
|
||||
|
||||
/*const REGEX_EMOTES = {
|
||||
|
@ -201,7 +202,7 @@ export default class ChatHook extends Module {
|
|||
|
||||
this.ChatController = this.fine.define(
|
||||
'chat-controller',
|
||||
n => n.hostingHandler && n.onRoomStateUpdated,
|
||||
n => n.parseOutgoingMessage && n.onRoomStateUpdated && n.renderNotifications,
|
||||
Twilight.CHAT_ROUTES
|
||||
);
|
||||
|
||||
|
@ -275,6 +276,13 @@ export default class ChatHook extends Module {
|
|||
|
||||
// Settings
|
||||
|
||||
this.settings.addUI('debug.chat-test', {
|
||||
path: 'Debugging > Chat >> Chat',
|
||||
component: 'chat-tester',
|
||||
getChat: () => this,
|
||||
force_seen: true
|
||||
});
|
||||
|
||||
this.settings.add('chat.filtering.blocked-types', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
|
@ -652,7 +660,34 @@ export default class ChatHook extends Module {
|
|||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Input >> Appearance',
|
||||
title: 'Display the Mod View button in relevant channels.',
|
||||
title: 'Allow the "Mod View" button to appear in relevant channels.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.input.show-highlight', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Input >> Appearance',
|
||||
title: 'Allow the "Chat Highlight Settings" button to appear in relevant channels.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.input.show-shield', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Input >> Appearance',
|
||||
title: 'Allow the "Shield Mode" button to appear in relevant channels.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.input.show-elevate-your-message', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Input >> Appearance',
|
||||
title: 'Allow the "Elevate Your Message" button to be displayed.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
@ -724,11 +759,15 @@ export default class ChatHook extends Module {
|
|||
|
||||
const width = this.chat.context.get('chat.effective-width'),
|
||||
action_size = this.chat.context.get('chat.actions.size'),
|
||||
hover_action_size = this.chat.context.get('chat.actions.hover-size'),
|
||||
ts_size = this.chat.context.get('chat.timestamp-size'),
|
||||
size = this.chat.context.get('chat.font-size'),
|
||||
emote_alignment = this.chat.context.get('chat.lines.emote-alignment'),
|
||||
lh = Math.round((20/12) * size);
|
||||
|
||||
const hover_action_icon = Math.round(hover_action_size * (2/3)),
|
||||
hover_action_padding = hover_action_size - hover_action_icon;
|
||||
|
||||
let font = this.chat.context.get('chat.font-family') || 'inherit';
|
||||
const [processed, unloader] = useFont(font);
|
||||
font = processed;
|
||||
|
@ -747,6 +786,8 @@ export default class ChatHook extends Module {
|
|||
this.css_tweaks.delete('ts-size');
|
||||
|
||||
this.css_tweaks.setVariable('chat-actions-size', `${action_size/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-actions-hover-size', `${hover_action_icon/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-actions-hover-padding', `${hover_action_padding/20}rem`);
|
||||
this.css_tweaks.setVariable('chat-font-size', `${size/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-line-height', `${lh/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-font-family', font);
|
||||
|
@ -872,6 +913,7 @@ export default class ChatHook extends Module {
|
|||
this.chat.context.on('changed:chat.effective-width', this.updateChatCSS, this);
|
||||
this.settings.main_context.on('changed:chat.use-width', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.actions.size', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.actions.hover-size', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.timestamp-size', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.font-family', this.updateChatCSS, this);
|
||||
|
@ -911,7 +953,13 @@ export default class ChatHook extends Module {
|
|||
this.css_tweaks.toggleHide('last-x-events', ! val));
|
||||
|
||||
this.chat.context.getChanges('chat.input.show-mod-view', val =>
|
||||
this.css_tweaks.toggleHide('mod-view', ! val));
|
||||
this.css_tweaks.toggleHide('ci-mod-view', ! val));
|
||||
|
||||
this.chat.context.getChanges('chat.input.show-highlight', val =>
|
||||
this.css_tweaks.toggleHide('ci-highlight-settings', !val));
|
||||
|
||||
this.chat.context.getChanges('chat.input.show-shield', val =>
|
||||
this.css_tweaks.toggleHide('ci-shield-mode', ! val));
|
||||
|
||||
this.chat.context.getChanges('chat.lines.padding', val =>
|
||||
this.css_tweaks.toggle('chat-padding', val));
|
||||
|
@ -941,6 +989,9 @@ export default class ChatHook extends Module {
|
|||
this.updateMentionCSS();
|
||||
});
|
||||
|
||||
this.chat.context.getChanges('chat.input.show-elevate-your-message', val =>
|
||||
this.css_tweaks.toggleHide('elevate-your-message', ! val));
|
||||
|
||||
this.updateChatCSS();
|
||||
this.updateColors();
|
||||
this.updateLineBorders();
|
||||
|
@ -1296,6 +1347,7 @@ export default class ChatHook extends Module {
|
|||
type: this.chat_types.Message,
|
||||
ffz_type: 'points',
|
||||
ffz_reward: reward,
|
||||
ffz_reward_highlight: isHighlightedReward(reward),
|
||||
messageParts: [],
|
||||
user: {
|
||||
id: data.user.id,
|
||||
|
@ -2429,6 +2481,7 @@ export default class ChatHook extends Module {
|
|||
|
||||
out.ffz_type = 'points';
|
||||
out.ffz_reward = reward;
|
||||
out.ffz_reward_highlight = isHighlightedReward(reward);
|
||||
|
||||
return i.postMessageToCurrentChannel(e, out);
|
||||
}
|
||||
|
@ -2441,7 +2494,7 @@ export default class ChatHook extends Module {
|
|||
return old_points.call(i, e);
|
||||
}
|
||||
|
||||
const old_host = this.onHostingEvent;
|
||||
/*const old_host = this.onHostingEvent;
|
||||
this.onHostingEvent = function (e, _t) {
|
||||
t.emit('tmi:host', e, _t);
|
||||
return old_host.call(i, e, _t);
|
||||
|
@ -2451,7 +2504,7 @@ export default class ChatHook extends Module {
|
|||
this.onUnhostEvent = function (e, _t) {
|
||||
t.emit('tmi:unhost', e, _t);
|
||||
return old_unhost.call(i, e, _t);
|
||||
}
|
||||
}*/
|
||||
|
||||
const old_add = this.addMessage;
|
||||
this.addMessage = function(e) {
|
||||
|
|
|
@ -722,6 +722,7 @@ export default class Input extends Module {
|
|||
return {emotes: [], length: 0};
|
||||
|
||||
const out = [],
|
||||
seen = new Set,
|
||||
anim = this.chat.context.get('chat.emotes.animated') > 0,
|
||||
hidden_sets = this.settings.provider.get('emote-menu.hidden-sets'),
|
||||
has_hidden = Array.isArray(hidden_sets) && hidden_sets.length > 0,
|
||||
|
@ -760,9 +761,11 @@ export default class Input extends Module {
|
|||
const id = emote.id,
|
||||
token = KNOWN_CODES[emote.token] || emote.token;
|
||||
|
||||
if ( ! token )
|
||||
if ( ! token || seen.has(token) )
|
||||
continue;
|
||||
|
||||
seen.add(token);
|
||||
|
||||
const replacement = REPLACEMENTS[id];
|
||||
let srcSet;
|
||||
|
||||
|
@ -942,18 +945,18 @@ export default class Input extends Module {
|
|||
|
||||
|
||||
getEmoteSuggestions(input, inst) {
|
||||
if ( ! inst._ffz_channel_login ) {
|
||||
const parent = this.fine.searchParent(inst, 'chat-input', 50);
|
||||
if ( parent )
|
||||
this.updateEmoteCompletion(parent, inst);
|
||||
}
|
||||
|
||||
const user = inst._ffz_user,
|
||||
channel_id = inst._ffz_channel_id,
|
||||
channel_login = inst._ffz_channel_login;
|
||||
|
||||
if ( ! channel_login ) {
|
||||
const parent = this.fine.searchParent(inst, 'chat-input', 50);
|
||||
if ( parent )
|
||||
this.updateEmoteCompletion(parent, inst);
|
||||
|
||||
if ( ! channel_login )
|
||||
return [];
|
||||
}
|
||||
if ( ! channel_login )
|
||||
return [];
|
||||
|
||||
let cache = inst.ffz_ffz_cache;
|
||||
if ( ! cache || cache.user_id !== user?.id || cache.user_login !== user?.login || cache.channel_id !== channel_id || cache.channel_login !== channel_login )
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -108,6 +108,11 @@ export default class Scroller extends Module {
|
|||
this.use_keys = true;
|
||||
break;
|
||||
}
|
||||
for(const act of this.chat.context.get('chat.actions.hover'))
|
||||
if ( act && act.display && act.display.keys ) {
|
||||
this.use_keys = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( this.use_keys !== old_use ) {
|
||||
for(const inst of this.ChatScroller.instances)
|
||||
|
@ -679,6 +684,16 @@ export default class Scroller extends Module {
|
|||
this.off('tooltips:mousemove', inst.ffzTooltipHover, inst);
|
||||
this.off('tooltips:leave', inst.ffzTooltipLeave, inst);
|
||||
|
||||
if ( inst._ffz_hover_timer ) {
|
||||
clearInterval(inst._ffz_hover_timer);
|
||||
inst._ffz_hover_timer = null;
|
||||
}
|
||||
|
||||
if ( inst._ffz_outside_timer ) {
|
||||
clearTimeout(inst._ffz_outside_timer);
|
||||
inst._ffz_outside_timer = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('keydown', inst.ffzHandleKey);
|
||||
window.removeEventListener('keyup', inst.ffzHandleKey);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ const CLASSES = {
|
|||
'modview-hide-info': '.modview-player-widget__hide-stream-info',
|
||||
|
||||
'community-highlights': '.community-highlight-stack__card',
|
||||
'elevate-your-message': '.chat-input__input-icons button[aria-label="ElevatedMessage"]',
|
||||
|
||||
'prime-offers': '.top-nav__prime',
|
||||
'discover-luna': '.top-nav__external-link[data-a-target="try-presto-link"]',
|
||||
|
@ -38,7 +39,7 @@ const CLASSES = {
|
|||
'player-event-bar': '.channel-root .live-event-banner-ui__header',
|
||||
'player-rerun-bar': '.channel-root__player-container div.tw-c-text-overlay:not([data-a-target="hosting-ui-header"])',
|
||||
|
||||
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2,.channel-leaderboard',
|
||||
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2,.channel-leaderboard,.channel-leaderboard-marquee,div[data-test-selector="channel-leaderboard-container"]',
|
||||
'whispers': 'body .whispers-open-threads,.tw-core-button[data-a-target="whisper-box-button"],.whispers__pill',
|
||||
|
||||
'dir-live-ind': '.live-channel-card[data-ffz-type="live"] .tw-channel-status-text-indicator, article[data-ffz-type="live"] .tw-channel-status-text-indicator',
|
||||
|
@ -46,8 +47,12 @@ const CLASSES = {
|
|||
'not-live-bar': 'div[data-test-selector="non-live-video-banner-layout"]',
|
||||
'channel-live-ind': '.channel-header__user .tw-channel-status-text-indicator,.channel-info-content .tw-halo__indicator',
|
||||
'celebration': 'body .celebration__overlay',
|
||||
'mod-view': '.chat-input__buttons-container a[href*="/moderator"]',
|
||||
'last-x-events': '.last-x-events_container'
|
||||
|
||||
'last-x-events': '.last-x-events_container',
|
||||
|
||||
'ci-mod-view': '.chat-input__buttons-container a[href*="/moderator"]',
|
||||
'ci-highlight-settings': '.chat-input__buttons-container button[data-highlight-selector="chat-highlights-shortcut"]',
|
||||
'ci-shield-mode': '.chat-input__buttons-container > div:last-child button[class|="ScCoreButton"]:not([data-highlight-selector]):not([data-a-target])'
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.video-player__container[data-paused="true"] video,
|
||||
.video-player__container[data-buffering="true"] video {
|
||||
filter: grayscale(0.25) brightness(0.5);
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
.video-player__overlay[data-controls="false"][data-paused="false"][data-ended="false"] {
|
||||
.video-player__container[data-controls="false"][data-paused="false"][data-ended="false"] {
|
||||
cursor: none;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
.channel-root__scroll-area--theatre-mode {
|
||||
.channel-info-content > div:first-child {
|
||||
#live-channel-stream-information { //.channel-info-content > div:first-child {
|
||||
right: 40rem !important;
|
||||
bottom: calc(10rem + var(--ffz-chat-height)) !important;
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ body .whispers--theatre-mode.whispers--right-column-expanded-beside {
|
|||
}
|
||||
|
||||
.channel-root__scroll-area--theatre-mode {
|
||||
.channel-info-content > div:first-child, .channel-info-bar {
|
||||
#live-channel-stream-information, .channel-info-bar { // .channel-info-content > div:first-child, .channel-info-bar {
|
||||
left: calc(var(--ffz-chat-width) + 5rem) !important;
|
||||
right: 40rem !important;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.channel-root__scroll-area--theatre-mode {
|
||||
.channel-info-content > div:first-child,
|
||||
#live-channel-stream-information, //.channel-info-content > section:first-child,
|
||||
.channel-info-bar {
|
||||
position: fixed;
|
||||
bottom: 17rem;
|
||||
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
.channel-info-content > div:first-child,
|
||||
#live-channel-stream-information, //.channel-info-content > section:first-child,
|
||||
.channel-info-bar {
|
||||
background-color: var(--color-background-base);
|
||||
opacity: 0.75;
|
||||
|
|
|
@ -54,7 +54,7 @@ export default class Dashboard extends Module {
|
|||
this.settings.updateContext({
|
||||
channel: get('props.channelLogin', inst),
|
||||
channelID: get('props.channelID', inst),
|
||||
hosting: !! inst.props?.hostedChannel?.id
|
||||
//hosting: !! inst.props?.hostedChannel?.id
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ export default class Dashboard extends Module {
|
|||
this.settings.updateContext({
|
||||
channel: null,
|
||||
channelID: null,
|
||||
hosting: false
|
||||
//hosting: false
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,249 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Following Page
|
||||
// ============================================================================
|
||||
|
||||
import {SiteModule} from 'utilities/module';
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {get} from 'utilities/object';
|
||||
|
||||
import {createPopper} from '@popperjs/core';
|
||||
import {makeReference} from 'utilities/tooltip';
|
||||
|
||||
export default class Following extends SiteModule {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('site.fine');
|
||||
this.inject('site.router');
|
||||
this.inject('site.apollo');
|
||||
this.inject('site.css_tweaks');
|
||||
|
||||
this.inject('i18n');
|
||||
this.inject('settings');
|
||||
|
||||
this.settings.add('directory.following.group-hosts', {
|
||||
default: true,
|
||||
|
||||
ui: {
|
||||
path: 'Directory > Following @{"description": "**Note:** These settings do not currently work due to changes made by Twitch to how the directory works."} >> Hosts',
|
||||
title: 'Group Hosts',
|
||||
description: 'Only show a given hosted channel once in the directory.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: () => {
|
||||
this.apollo.maybeRefetch('FollowedIndex_CurrentUser');
|
||||
this.apollo.maybeRefetch('FollowingHosts_CurrentUser');
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('directory.following.host-menus', {
|
||||
default: 1,
|
||||
|
||||
ui: {
|
||||
path: 'Directory > Following >> Hosts',
|
||||
title: 'Hosted Channel Menus',
|
||||
description: 'Display a menu to select which channel to visit when clicking a hosted channel in the directory.',
|
||||
|
||||
component: 'setting-select-box',
|
||||
|
||||
data: [
|
||||
{value: 0, title: 'Disabled'},
|
||||
{value: 1, title: 'When Multiple are Hosting'},
|
||||
{value: 2, title: 'Always'}
|
||||
]
|
||||
},
|
||||
|
||||
changed: () => this.parent.DirectoryCard.forceUpdate()
|
||||
});
|
||||
|
||||
this.hosts = new WeakMap;
|
||||
}
|
||||
|
||||
modifyLiveUsers(res, path = 'followedLiveUsers') {
|
||||
const followed_live = get(`data.currentUser.${path}`, res);
|
||||
if ( ! followed_live )
|
||||
return;
|
||||
|
||||
if ( followed_live.nodes )
|
||||
followed_live.nodes = this.parent.processNodes(followed_live.nodes);
|
||||
|
||||
else if ( followed_live.edges )
|
||||
followed_live.edges = this.parent.processNodes(followed_live.edges);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
modifyLiveHosts(res) {
|
||||
const blocked_games = this.settings.provider.get('directory.game.blocked-games', []),
|
||||
do_grouping = this.settings.get('directory.following.group-hosts'),
|
||||
edges = get('data.currentUser.followedHosts.nodes', res);
|
||||
|
||||
if ( ! edges || ! edges.length )
|
||||
return res;
|
||||
|
||||
this.hosts = new WeakMap();
|
||||
const out = [];
|
||||
|
||||
for(const edge of edges) {
|
||||
if ( ! edge )
|
||||
continue;
|
||||
|
||||
const node = edge.node || edge,
|
||||
hosted = node.hosting,
|
||||
stream = hosted && hosted.stream;
|
||||
|
||||
if ( ! stream || stream.game && blocked_games.includes(stream.game.name) )
|
||||
continue;
|
||||
|
||||
if ( ! stream.viewersCount ) {
|
||||
if ( ! do_grouping || ! this.hosts[hosted.login] )
|
||||
out.push(edge);
|
||||
continue;
|
||||
}
|
||||
|
||||
const store = stream.viewersCount = new Number(stream.viewersCount || 0);
|
||||
|
||||
store.createdAt = stream.createdAt;
|
||||
store.title = stream.title;
|
||||
//store.game = stream.game;
|
||||
|
||||
if ( do_grouping ) {
|
||||
const host_nodes = this.hosts[hosted.login];
|
||||
if ( host_nodes ) {
|
||||
host_nodes.push(node);
|
||||
this.hosts.set(store, host_nodes);
|
||||
|
||||
} else {
|
||||
this.hosts.set(store, this.hosts[hosted.login] = [node]);
|
||||
out.push(edge);
|
||||
}
|
||||
|
||||
} else
|
||||
out.push(edge);
|
||||
}
|
||||
|
||||
res.data.currentUser.followedHosts.nodes = out;
|
||||
return res;
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
|
||||
}
|
||||
|
||||
destroyHostMenu(event) {
|
||||
if (!event || ! this.hostMenu || event && event.target && event.target.closest('.ffz-channel-selector-outer') === null && Date.now() > this.hostMenuBuffer) {
|
||||
this.hostMenuPopper && this.hostMenuPopper.destroy();
|
||||
this.hostMenu && this.hostMenu.remove();
|
||||
this.hostMenuPopper = this.hostMenu = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
showHostMenu(inst, channels, event) {
|
||||
if (this.settings.get('directory.following.host-menus') === 0 || this.settings.get('directory.following.host-menus') === 1 && channels.length < 2) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.hostMenuPopper && this.hostMenuPopper.destroy();
|
||||
|
||||
this.hostMenu && this.hostMenu.remove();
|
||||
|
||||
const simplebarContentChildren = [];
|
||||
|
||||
// Hosted Channel Header
|
||||
simplebarContentChildren.push(<p class="tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2">
|
||||
{this.i18n.t('directory.hosted', 'Hosted Channel')}
|
||||
</p>);
|
||||
|
||||
// Hosted Channel Content
|
||||
simplebarContentChildren.push(<a
|
||||
class="tw-block tw-border-radius-small tw-full-width ffz-interactable ffz-interactable--default ffz-interactable--hover-enabled tw-interactive"
|
||||
href={`/${inst.props.channelLogin}`}
|
||||
onClick={e => this.parent.hijackUserClick(e, inst.props.channelLogin, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05">
|
||||
<div class="ffz-channel-avatar">
|
||||
<img src={inst.props.channelImageProps.src} alt={inst.props.channelDisplayName} />
|
||||
</div>
|
||||
<p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">
|
||||
{inst.props.channelDisplayName}
|
||||
</p>
|
||||
</div>
|
||||
</a>);
|
||||
|
||||
// Hosting Channels Header
|
||||
simplebarContentChildren.push(<p class="tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2">
|
||||
{this.i18n.t('directory.hosting', 'Hosting Channels')}
|
||||
</p>);
|
||||
|
||||
// Hosting Channels Content
|
||||
for (const channel of channels) {
|
||||
simplebarContentChildren.push(<a
|
||||
class="tw-block tw-border-radius-small tw-full-width ffz-interactable ffz-interactable--default ffz-interactable--hover-enabled tw-interactive"
|
||||
href={`/${channel.login}`}
|
||||
onClick={e => this.parent.hijackUserClick(e, channel.login, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05">
|
||||
<div class="ffz-channel-avatar">
|
||||
<img src={channel.profileImageURL} alt={channel.displayName} />
|
||||
</div>
|
||||
<p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">
|
||||
{channel.displayName}
|
||||
</p>
|
||||
</div>
|
||||
</a>);
|
||||
}
|
||||
|
||||
this.hostMenu = (<div class="ffz-host-menu ffz-balloon tw-block">
|
||||
<div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base tw-pd-05">
|
||||
<div class="scrollable-area" data-simplebar>
|
||||
{simplebarContentChildren}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
const root = (document.body.querySelector('#root>div') || document.body);
|
||||
root.appendChild(this.hostMenu);
|
||||
|
||||
this.hostMenuPopper = createPopper(
|
||||
makeReference(event.clientX - 60, event.clientY - 60),
|
||||
this.hostMenu,
|
||||
{
|
||||
placement: 'bottom-start',
|
||||
modifiers: {
|
||||
flip: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.hostMenuBuffer = Date.now() + 50;
|
||||
}
|
||||
|
||||
updateChannelCard(inst) {
|
||||
const card = this.fine.getChildNode(inst);
|
||||
|
||||
if ( ! card )
|
||||
return;
|
||||
|
||||
const login = inst.props.channelLogin,
|
||||
hosting = inst.props.channelLinkTo && inst.props.channelLinkTo.state.content === 'live_host' && this.hosts && this.hosts[login];
|
||||
|
||||
if ( hosting && this.settings.get('directory.following.group-hosts') ) {
|
||||
const host_data = this.hosts[login];
|
||||
|
||||
const title_link = card.querySelector('a[data-test-selector="preview-card-titles__primary-link"]'),
|
||||
thumbnail_link = card.querySelector('a[data-a-target="preview-card-image-link"]');
|
||||
|
||||
if ( title_link )
|
||||
title_link.addEventListener('click', this.showHostMenu.bind(this, inst, host_data));
|
||||
|
||||
if ( thumbnail_link )
|
||||
thumbnail_link.addEventListener('click', this.showHostMenu.bind(this, inst, host_data));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,7 +48,6 @@ export default class Directory extends SiteModule {
|
|||
this.inject('i18n');
|
||||
this.inject('settings');
|
||||
|
||||
//this.inject(Following);
|
||||
this.inject(Game);
|
||||
|
||||
this.DirectoryCard = this.elemental.define(
|
||||
|
|
|
@ -1,379 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Host Button
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {get} from 'utilities/object';
|
||||
import {createElement} from 'utilities/dom';
|
||||
|
||||
const HOST_ERRORS = {
|
||||
COMMAND_EXECUTION: {
|
||||
key: 'command-execution',
|
||||
text: 'There was an error executing the host command. Please try again later.',
|
||||
},
|
||||
CHAT_CONNECTION: {
|
||||
key: 'chat-connection',
|
||||
text: 'There was an issue connecting to chat. Please try again later.',
|
||||
}
|
||||
};
|
||||
|
||||
export default class HostButton extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('site');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.chat');
|
||||
this.inject('site.twitch_data');
|
||||
this.inject('i18n');
|
||||
this.inject('metadata');
|
||||
this.inject('settings');
|
||||
|
||||
this.settings.add('metadata.host-button', {
|
||||
default: true,
|
||||
|
||||
ui: {
|
||||
path: 'Channel > Metadata >> Player',
|
||||
title: 'Host Button',
|
||||
description: 'Show a host button with the current hosted channel in the tooltip.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: () => {
|
||||
const ffz_user = this.site.getUser(),
|
||||
userLogin = ffz_user && ffz_user.login;
|
||||
|
||||
if (userLogin)
|
||||
this.joinChannel(userLogin);
|
||||
|
||||
this.metadata.updateMetadata('host');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isChannelHosted(channelLogin) {
|
||||
return this._last_hosted_channel === channelLogin;
|
||||
}
|
||||
|
||||
sendHostUnhostCommand(channel) {
|
||||
if (!this._chat_con) {
|
||||
this._host_error = HOST_ERRORS.CHAT_CONNECTION;
|
||||
this._host_updating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const ffz_user = this.site.getUser(),
|
||||
userLogin = ffz_user && ffz_user.login;
|
||||
|
||||
const commandData = {channel: userLogin, username: channel};
|
||||
|
||||
this._host_updating = true;
|
||||
this.metadata.updateMetadata('host');
|
||||
|
||||
this._host_feedback = setTimeout(() => {
|
||||
if (this._last_hosted_channel === null) {
|
||||
this._host_error = HOST_ERRORS.COMMAND_EXECUTION;
|
||||
this._host_updating = false;
|
||||
this.metadata.updateMetadata('host');
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
if (this.isChannelHosted(channel)) {
|
||||
this._chat_con.commands.unhost.execute(commandData);
|
||||
} else {
|
||||
this._chat_con.commands.host.execute(commandData);
|
||||
}
|
||||
}
|
||||
|
||||
joinChannel(channel) {
|
||||
if (this._chat_con) {
|
||||
if (this.settings.get('metadata.host-button') && !this._chat_con.session.channelstate[`#${channel}`]) {
|
||||
this._chat_con.joinChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hookIntoChatConnection(inst) {
|
||||
const userLogin = inst.props.currentUserLogin;
|
||||
|
||||
if (this._chat_con) {
|
||||
this.joinChannel(userLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
this.on('tmi:host', e => {
|
||||
if (e.channel.substring(1) !== userLogin) return;
|
||||
|
||||
clearTimeout(this._host_feedback);
|
||||
this._host_error = false;
|
||||
this._last_hosted_channel = e.target;
|
||||
|
||||
this._host_updating = false;
|
||||
this.metadata.updateMetadata('host');
|
||||
});
|
||||
|
||||
this.on('tmi:unhost', e => {
|
||||
if (e.channel.substring(1) !== userLogin) return;
|
||||
|
||||
clearTimeout(this._host_feedback);
|
||||
this._host_error = false;
|
||||
this._last_hosted_channel = null;
|
||||
|
||||
this._host_updating = false;
|
||||
this.metadata.updateMetadata('host');
|
||||
});
|
||||
|
||||
const chatServiceClient = inst.client;
|
||||
|
||||
this._chat_con = chatServiceClient;
|
||||
if (this.settings.get('metadata.host-button'))
|
||||
this.joinChannel(userLogin);
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.on('i18n:update', () => this.metadata.updateMetadata('host'));
|
||||
|
||||
this.metadata.definitions.host = {
|
||||
order: 150,
|
||||
border: true,
|
||||
button: true,
|
||||
fade_in: true,
|
||||
modview: true,
|
||||
|
||||
disabled: () => this._host_updating || this._host_error,
|
||||
|
||||
click: data => {
|
||||
if ( data.channel )
|
||||
this.sendHostUnhostCommand(data.channel.login);
|
||||
},
|
||||
|
||||
popup: async (data, tip) => {
|
||||
const vue = this.resolve('vue'),
|
||||
_host_options_vue = import(/* webpackChunkName: "host-options" */ './host-options.vue'),
|
||||
_autoHosts = this.fetchAutoHosts(),
|
||||
_autoHostSettings = this.fetchAutoHostSettings();
|
||||
|
||||
const [, host_options_vue, autoHosts, autoHostSettings] = await Promise.all([vue.enable(), _host_options_vue, _autoHosts, _autoHostSettings]);
|
||||
|
||||
this._auto_host_tip = tip;
|
||||
tip.element.classList.remove('tw-pd-1');
|
||||
tip.element.classList.add('ffz-balloon--lg');
|
||||
vue.component('host-options', host_options_vue.default);
|
||||
return this.buildAutoHostMenu(vue, autoHosts, autoHostSettings, data.channel);
|
||||
},
|
||||
|
||||
label: data => {
|
||||
const ffz_user = this.site.getUser();
|
||||
|
||||
if ( ! this.settings.get('metadata.host-button') || ! ffz_user || ! data.channel || data.channel.login === ffz_user.login )
|
||||
return;
|
||||
|
||||
if ( data.channel.video && ! this.isChannelHosted(data.channel.login) )
|
||||
return;
|
||||
|
||||
if ( this._host_updating )
|
||||
return this.i18n.t('metadata.host-button.updating', 'Updating...');
|
||||
|
||||
return (this._last_hosted_channel && this.isChannelHosted(data.channel && data.channel.login))
|
||||
? this.i18n.t('metadata.host-button.unhost', 'Unhost')
|
||||
: this.i18n.t('metadata.host-button.host', 'Host');
|
||||
},
|
||||
|
||||
tooltip: () => {
|
||||
if (this._host_error) {
|
||||
return this.i18n.t(
|
||||
`metadata.host-button.tooltip.error.${this._host_error.key}`,
|
||||
this._host_error.text);
|
||||
} else {
|
||||
return this.i18n.t('metadata.host-button.tooltip',
|
||||
'Currently hosting: {channel}',
|
||||
{
|
||||
channel: this._last_hosted_channel || this.i18n.t('metadata.host-button.tooltip.none', 'None')
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.metadata.updateMetadata('host');
|
||||
|
||||
this.chat.ChatService.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.hookIntoChatConnection(inst);
|
||||
})
|
||||
|
||||
this.chat.ChatService.on('mount', this.hookIntoChatConnection, this);
|
||||
}
|
||||
|
||||
buildAutoHostMenu(vue, hosts, autoHostSettings, data) {
|
||||
this._current_channel_id = data.id;
|
||||
this.activeTab = this.activeTab || 'auto-host';
|
||||
|
||||
const vueEl = new vue.Vue({
|
||||
el: createElement('div'),
|
||||
render: h => this.vueHostMenu = h('host-options', {
|
||||
hosts,
|
||||
autoHostSettings,
|
||||
activeTab: this.activeTab,
|
||||
|
||||
addedToHosts: this.currentRoomInHosts(),
|
||||
addToAutoHosts: () => this.addCurrentRoomToHosts(),
|
||||
rearrangeHosts: event => this.rearrangeHosts(event.oldIndex, event.newIndex),
|
||||
removeFromHosts: event => this.removeUserFromHosts(event),
|
||||
setActiveTab: tab => {
|
||||
this.vueHostMenu.data.activeTab = this.activeTab = tab;
|
||||
},
|
||||
updatePopper: () => {
|
||||
if (this._auto_host_tip) this._auto_host_tip.update();
|
||||
},
|
||||
updateCheckbox: e => {
|
||||
const t = e.target;
|
||||
let setting = t.dataset.setting,
|
||||
state = t.checked;
|
||||
|
||||
if ( setting === 'enabled' )
|
||||
setting = 'isEnabled';
|
||||
else if ( setting === 'teamHost' )
|
||||
setting = 'willAutohostTeam';
|
||||
else if ( setting === 'strategy' )
|
||||
state = state ? 'RANDOM' : 'ORDERED';
|
||||
else if ( setting === 'deprioritizeVodcast' ) {
|
||||
setting = 'willPrioritizeAutohost';
|
||||
}
|
||||
|
||||
this.updateAutoHostSetting(setting, state);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return vueEl.$el;
|
||||
}
|
||||
|
||||
async fetchAutoHosts() {
|
||||
const user = this.site.getUser();
|
||||
if ( ! user )
|
||||
return;
|
||||
|
||||
const result = await this.twitch_data.queryApollo(
|
||||
await import(/* webpackChunkName: 'host-options' */ './autohost_list.gql'),
|
||||
{
|
||||
id: user.id
|
||||
},
|
||||
{
|
||||
fetchPolicy: 'network-only'
|
||||
}
|
||||
);
|
||||
|
||||
return this.autoHosts = get('data.user.autohostChannels.nodes', result);
|
||||
}
|
||||
|
||||
async fetchAutoHostSettings() {
|
||||
const user = this.site.getUser();
|
||||
if ( ! user )
|
||||
return;
|
||||
|
||||
const result = await this.twitch_data.queryApollo(
|
||||
await import(/* webpackChunkName: 'host-options' */ './autohost_settings.gql'),
|
||||
{
|
||||
id: user.id
|
||||
},
|
||||
{
|
||||
fetchPolicy: 'network-only'
|
||||
}
|
||||
);
|
||||
|
||||
return this.autoHostSettings = get('data.user.autohostSettings', result);
|
||||
}
|
||||
|
||||
queueHostUpdate() {
|
||||
if (this._host_update_timer) clearTimeout(this._host_update_timer);
|
||||
|
||||
this._host_update_timer = setTimeout(() => {
|
||||
this._host_update_timer = undefined;
|
||||
this.updateAutoHosts(this.autoHosts);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
rearrangeHosts(oldIndex, newIndex) {
|
||||
const host = this.autoHosts.splice(oldIndex, 1)[0];
|
||||
this.autoHosts.splice(newIndex, 0, host);
|
||||
|
||||
this.queueHostUpdate();
|
||||
}
|
||||
|
||||
currentRoomInHosts() {
|
||||
return this.getAutoHostIDs(this.autoHosts).includes(this._current_channel_id);
|
||||
}
|
||||
|
||||
addCurrentRoomToHosts() {
|
||||
const newHosts = this.autoHosts.slice(0);
|
||||
newHosts.push({ id: this._current_channel_id});
|
||||
|
||||
this.updateAutoHosts(newHosts);
|
||||
}
|
||||
|
||||
removeUserFromHosts(event) {
|
||||
const id = event.target.closest('.ffz--host-user').dataset.id;
|
||||
|
||||
const newHosts = [];
|
||||
for (let i = 0; i < this.autoHosts.length; i++) {
|
||||
if (this.autoHosts[i].id != id) newHosts.push(this.autoHosts[i]);
|
||||
}
|
||||
|
||||
this.updateAutoHosts(newHosts);
|
||||
}
|
||||
|
||||
getAutoHostIDs(hosts) { // eslint-disable-line class-methods-use-this
|
||||
const ids = [];
|
||||
if (hosts) {
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
ids.push(hosts[i].id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
async updateAutoHosts(newHosts) {
|
||||
const user = this.site.getUser();
|
||||
if ( ! user )
|
||||
return;
|
||||
|
||||
const autoHosts = this.getAutoHostIDs(newHosts);
|
||||
|
||||
const result = await this.twitch_data.mutate({
|
||||
mutation: await import(/* webpackChunkName: 'host-options' */ './autohost_list_mutate.gql'),
|
||||
variables: {
|
||||
userID: user.id,
|
||||
channelIDs: autoHosts
|
||||
}
|
||||
});
|
||||
|
||||
this.autoHosts = get('data.setAutohostChannels.user.autohostChannels.nodes', result);
|
||||
if (this.vueHostMenu) {
|
||||
this.vueHostMenu.data.hosts = this.autoHosts;
|
||||
this.vueHostMenu.data.addedToHosts = this.currentRoomInHosts();
|
||||
}
|
||||
}
|
||||
|
||||
async updateAutoHostSetting(setting, newValue) {
|
||||
const user = this.site.getUser();
|
||||
if ( ! user )
|
||||
return;
|
||||
|
||||
const result = await this.twitch_data.mutate({
|
||||
mutation: await import(/* webpackChunkName: 'host-options' */ './autohost_settings_mutate.gql'),
|
||||
variables: {
|
||||
userID: user.id,
|
||||
[setting]: newValue
|
||||
}
|
||||
});
|
||||
|
||||
this.autoHostSettings = get('data.updateAutohostSettings.user.autohostSettings', result);
|
||||
if (this.vueHostMenu) {
|
||||
this.vueHostMenu.data.autoHostSettings = this.autoHostSettings;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -172,7 +172,7 @@ export default class Layout extends Module {
|
|||
});
|
||||
|
||||
this.settings.add('layout.portrait-extra-height', {
|
||||
requires: ['context.new_channel', 'context.squad_bar', 'context.hosting', 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
|
||||
requires: ['context.new_channel', 'context.squad_bar', /*'context.hosting',*/ 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
|
||||
process(ctx) {
|
||||
let height = 0;
|
||||
if ( ctx.get('context.ui.theatreModeEnabled') ) {
|
||||
|
@ -192,8 +192,8 @@ export default class Layout extends Module {
|
|||
|
||||
height += ctx.get('context.new_channel') ? 1 : 5;
|
||||
|
||||
if ( ctx.get('context.hosting') )
|
||||
height += 4;
|
||||
/*if ( ctx.get('context.hosting') )
|
||||
height += 4;*/
|
||||
}
|
||||
|
||||
return height;
|
||||
|
|
|
@ -196,7 +196,7 @@ export default class Player extends PlayerBase {
|
|||
|
||||
|
||||
checkCarousel(inst) {
|
||||
if ( this.settings.get('channel.hosting.enable') )
|
||||
/*if ( this.settings.get('channel.hosting.enable') )
|
||||
return;
|
||||
|
||||
if ( inst.props?.playerType === 'channel_home_carousel' ) {
|
||||
|
@ -211,7 +211,7 @@ export default class Player extends PlayerBase {
|
|||
events = inst.props.playerEvents;
|
||||
|
||||
this.stopPlayer(player, events, inst);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -271,6 +271,7 @@ export default class ThemeEngine extends Module {
|
|||
`--color-background-input-focus:${color.toCSS()};`,
|
||||
`--color-background-base:${hsla._l(luma + (dark ? 0.05 : -0.05)).toCSS()};`,
|
||||
`--color-background-alt:${hsla._l(luma + (dark ? 0.1 : -0.1)).toCSS()};`,
|
||||
`--color-background-float:var(--color-background-alt);`,
|
||||
`--color-background-alt-2:${hsla._l(luma + (dark ? 0.15 : -0.15)).toCSS()};`
|
||||
].join('');
|
||||
}
|
||||
|
|
|
@ -89,6 +89,8 @@ export default class VideoChatHook extends Module {
|
|||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.active_room = null;
|
||||
}
|
||||
|
||||
|
||||
|
@ -561,6 +563,7 @@ export default class VideoChatHook extends Module {
|
|||
if ( ! this.addRoom(chat, props) )
|
||||
return;
|
||||
|
||||
this.active_room = chat._ffz_room;
|
||||
this.chat.badges.updateTwitchBadges(get('data.badges', props));
|
||||
|
||||
this.updateRoomBadges(chat, get('data.video.owner.broadcastBadges', props));
|
||||
|
@ -607,6 +610,9 @@ export default class VideoChatHook extends Module {
|
|||
|
||||
|
||||
chatUnmounted(chat) {
|
||||
if (this.active_room === chat._ffz_room)
|
||||
this.active_room = null;
|
||||
|
||||
this.removeRoom(chat);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
}
|
||||
}*/
|
||||
|
||||
.video-player__container--theatre .stream-display-ad__wrapper .player-controls__right-control-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.tw-root--theme-ffz, .tw-root--theme-ffz.tw-root--theme-dark, .tw-root--theme-dark, body {
|
||||
.ffz-stat > .tw-button--text,
|
||||
.ffz-stat.tw-button--text {
|
||||
|
|
|
@ -452,4 +452,50 @@
|
|||
padding: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ffz--hover-actions {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: -1rem;
|
||||
display: none;
|
||||
|
||||
--ffz-chat-actions-size: var(--ffz-chat-actions-hover-size);
|
||||
--ffz-chat-actions-padding: var(--ffz-chat-actions-hover-padding);
|
||||
|
||||
.ffz-notice-line:hover &,
|
||||
.chat-line__message:hover & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ffz-hover-action {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
box-shadow: var(--shadow-elevation-1);
|
||||
background-color: var(--color-background-base);
|
||||
border-radius: var(--border-radius-medium);
|
||||
|
||||
figure {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
&.ffz-has-modifier {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > .ffz-mod-icon {
|
||||
padding: var(--ffz-chat-actions-padding);
|
||||
|
||||
background-color: var(--color-background-button-text-default);
|
||||
color: var(--color-fill-button-icon);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-button-text-hover);
|
||||
color: var(--color-fill-button-icon-hover);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
@import 'fixes';
|
||||
|
||||
@import 'host_options';
|
||||
//@import 'host_options';
|
||||
@import 'featured_follow';
|
||||
@import 'mod_card';
|
||||
@import 'easteregg';
|
259
src/std-components/emote-picker.vue
Normal file
259
src/std-components/emote-picker.vue
Normal file
|
@ -0,0 +1,259 @@
|
|||
<template lang="html">
|
||||
<div v-on-clickaway="close" class="ffz--emote-picker tw-relative">
|
||||
<div class="tw-search-input tw-full-width">
|
||||
<label v-if="isOpen" :for="'emote-search$' + id" class="tw-hide-accessible">{{ t('setting.emote.search', 'Search for Emote') }}</label>
|
||||
<div class="tw-relative">
|
||||
<div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height ffz-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default">
|
||||
<figure class="tw-mg-y-05 tw-mg-x-05">
|
||||
<img class="ffz-preview-emote" v-if="val.src" :src="val.src" />
|
||||
</figure>
|
||||
</div>
|
||||
<input
|
||||
:id="'emote-search$' + id"
|
||||
ref="input"
|
||||
:placeholder="t('setting.emote.search', 'Search for Emote')"
|
||||
:value="isOpen ? search : valName"
|
||||
:class="[clearable ? 'tw-pd-r-5' : 'tw-pd-r-1']"
|
||||
type="text"
|
||||
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-3 tw-pd-y-05"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
@input="update"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.escape="open = false"
|
||||
/>
|
||||
<button
|
||||
v-if="clearable"
|
||||
class="tw-absolute tw-right-0 tw-top-0 tw-button tw-button--text ffz-il-tooltip__container"
|
||||
@click="change('', false)"
|
||||
@keydown.escape="open = false"
|
||||
@focus="onFocus(false)"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--up ffz-il-tooltip--align-right">
|
||||
{{ t('setting.icon.clear', 'Clear') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<balloon v-if="isOpen" :dir="direction" color="background-base">
|
||||
<div ref="list">
|
||||
<simplebar classes="scrollable-area--suppress-scroll-x ffz--emote-picker__list">
|
||||
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap tw-justify-content-between">
|
||||
<div
|
||||
v-for="i of visible"
|
||||
:key="`${i.provider}:${i.id}`"
|
||||
:aria-checked="val.provider === i.provider && val.id === i.id"
|
||||
:class="{'ffz-interactable--selected': val.provider === i.provider && val.id === i.id}"
|
||||
:data-provider="i.provider"
|
||||
:data-id="i.id"
|
||||
:data-set="i.set_id"
|
||||
:data-name="i.name"
|
||||
class="ffz-tooltip ffz-icon ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
data-tooltip-type="emote"
|
||||
@keydown.space.stop.prevent=""
|
||||
@keyup.space="change(i)"
|
||||
@keyup.enter="change(i)"
|
||||
@click="change(i)"
|
||||
@focus="onFocus(false)"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<figure :class="`tw-mg-y-05 tw-mg-x-1`">
|
||||
<img :src="i.src" />
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="! emotes.length" class="tw-align-center tw-pd-1 tw-c-text-alt-2">
|
||||
{{ t('setting.emote.none', 'unable to load emote data') }}
|
||||
<div class="tw-mg-t-05">
|
||||
{{ t('setting.emote.none-about', 'Please make sure you have the FFZ Emote Menu enabled, and that you use this from a page that loads chat.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tw-align-center tw-pd-1 tw-c-text-alt-2">
|
||||
{{ t('setting.actions.empty-search', 'no results') }}
|
||||
</div>
|
||||
</simplebar>
|
||||
</div>
|
||||
</balloon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { debounce } from 'utilities/object';
|
||||
|
||||
let id = 0;
|
||||
|
||||
function readEmoteMenuEmotes(input, out, seen) {
|
||||
if ( Array.isArray(input) ) {
|
||||
for(const item of input)
|
||||
readEmoteMenuEmotes(item, out, seen);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! Array.isArray(input?.emotes) )
|
||||
return;
|
||||
|
||||
for(const emote of input.emotes) {
|
||||
if ( emote.locked || seen.has(emote.name) )
|
||||
continue;
|
||||
|
||||
seen.add(emote.name);
|
||||
|
||||
out.push({
|
||||
provider: emote.provider,
|
||||
id: emote.id,
|
||||
set_id: emote.set_id,
|
||||
name: emote.name,
|
||||
lname: emote.name && emote.name.toLowerCase(),
|
||||
src: emote.src
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: Object,
|
||||
alwaysOpen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'down'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
id: id++,
|
||||
open: false,
|
||||
val: this.value,
|
||||
search: '',
|
||||
emotes: []
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
visible() {
|
||||
if ( ! this.search || ! this.search.length )
|
||||
return this.emotes;
|
||||
|
||||
const search = this.search.toLowerCase();
|
||||
return this.emotes.filter(x => x.lname && x.lname.indexOf(search) !== -1);
|
||||
},
|
||||
|
||||
valName() {
|
||||
return this.val?.name;
|
||||
},
|
||||
|
||||
isOpen() {
|
||||
return this.alwaysOpen || this.open
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value() {
|
||||
this.val = this.value;
|
||||
},
|
||||
|
||||
isOpen() {
|
||||
if ( ! this.isOpen ) {
|
||||
requestAnimationFrame(() => {
|
||||
const ffz = FrankerFaceZ.get();
|
||||
if ( ffz )
|
||||
ffz.emit('tooltips:cleanup');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.maybeLoadEmotes();
|
||||
|
||||
this.$nextTick(() => {
|
||||
if ( this.val ) {
|
||||
const root = this.$refs.list,
|
||||
el = root && root.querySelector('.ffz-interactable--selected');
|
||||
|
||||
if ( el )
|
||||
el.scrollIntoViewIfNeeded();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.maybeClose = debounce(this.maybeClose, 10);
|
||||
},
|
||||
|
||||
methods: {
|
||||
maybeLoadEmotes() {
|
||||
if ( ! this.emotes || ! this.emotes.length ) {
|
||||
const emotes = [],
|
||||
seen = new Set,
|
||||
menu = window.ffz_menu,
|
||||
state = menu?.state;
|
||||
|
||||
if ( menu ) {
|
||||
menu.loadData();
|
||||
readEmoteMenuEmotes(state?.channel_sets, emotes, seen);
|
||||
readEmoteMenuEmotes(state?.all_sets, emotes, seen);
|
||||
}
|
||||
|
||||
this.emotes = emotes;
|
||||
}
|
||||
},
|
||||
|
||||
update() {
|
||||
if ( this.isOpen )
|
||||
this.search = this.$refs.input.value;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
change(val, close = true) {
|
||||
this.val = {
|
||||
type: 'emote',
|
||||
provider: val.provider,
|
||||
id: val.id,
|
||||
name: val.name,
|
||||
src: val.src
|
||||
},
|
||||
this.$emit('input', this.val);
|
||||
if ( close )
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
onFocus(open = true) {
|
||||
this.focused = true;
|
||||
if ( open )
|
||||
this.open = true;
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
this.maybeClose();
|
||||
},
|
||||
|
||||
maybeClose() {
|
||||
if ( ! this.focused )
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -31,6 +31,14 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const scroller = this.$refs.scroller;
|
||||
if (!scroller || ! window.ffzSimplebar || scroller.SimpleBar)
|
||||
return;
|
||||
|
||||
new ffzSimplebar(scroller, ffzSimplebar.getElOptions(scroller));
|
||||
},
|
||||
|
||||
methods: {
|
||||
onScroll() {
|
||||
// We do this to avoid the scroll position getting screwed up on
|
||||
|
|
|
@ -40,6 +40,7 @@ export const RERENDER_SETTINGS = [
|
|||
'chat.subs.show',
|
||||
'chat.subs.compact',
|
||||
'chat.actions.inline',
|
||||
'chat.actions.hover',
|
||||
'chat.timestamp-format',
|
||||
'chat.points.allow-highlight',
|
||||
'chat.filtering.display-deleted',
|
||||
|
|
|
@ -104,5 +104,7 @@ export default [
|
|||
"reply",
|
||||
"threads",
|
||||
"right-open",
|
||||
"list-bullet"
|
||||
"list-bullet",
|
||||
"mastodon",
|
||||
"volume-up"
|
||||
];
|
|
@ -69,26 +69,50 @@ export class Logger {
|
|||
return this.invoke(Logger.VERBOSE, args);
|
||||
}
|
||||
|
||||
verboseColor(msg, colors, ...args) {
|
||||
return this.invokeColor(Logger.VERBOSE, msg, colors, args);
|
||||
}
|
||||
|
||||
debug(...args) {
|
||||
return this.invoke(Logger.DEBUG, args);
|
||||
}
|
||||
|
||||
debugColor(msg, colors, ...args) {
|
||||
return this.invokeColor(Logger.DEBUG, msg, colors, args);
|
||||
}
|
||||
|
||||
info(...args) {
|
||||
return this.invoke(Logger.INFO, args);
|
||||
}
|
||||
|
||||
infoColor(msg, colors, ...args) {
|
||||
return this.invokeColor(Logger.INFO, msg, colors, args);
|
||||
}
|
||||
|
||||
warn(...args) {
|
||||
return this.invoke(Logger.WARN, args);
|
||||
}
|
||||
|
||||
warnColor(msg, colors, ...args) {
|
||||
return this.invokeColor(Logger.WARN, msg, colors, args);
|
||||
}
|
||||
|
||||
warning(...args) {
|
||||
return this.invoke(Logger.WARN, args);
|
||||
}
|
||||
|
||||
warningColor(msg, colors, ...args) {
|
||||
return this.invokeColor(Logger.WARN, msg, colors, args);
|
||||
}
|
||||
|
||||
error(...args) {
|
||||
return this.invoke(Logger.ERROR, args);
|
||||
}
|
||||
|
||||
errorColor(msg, colors, ...args) {
|
||||
return this.invokeColor(Logger.ERROR, msg, colors, args);
|
||||
}
|
||||
|
||||
crumb(...args) {
|
||||
if ( this.raven )
|
||||
return this.raven.captureBreadcrumb(...args);
|
||||
|
@ -107,6 +131,62 @@ export class Logger {
|
|||
return this.error(...args);
|
||||
}
|
||||
|
||||
invokeColor(level, msg, colors, args) {
|
||||
if ( ! this.enabled || level < this.level )
|
||||
return;
|
||||
|
||||
if ( ! Array.isArray(colors) )
|
||||
colors = [colors];
|
||||
|
||||
const message = args ? Array.prototype.slice.call(args) : [];
|
||||
|
||||
if ( level !== Logger.VERBOSE ) {
|
||||
const out = msg.replace(/%c/g, '') + ' ' + message.join(' ');
|
||||
|
||||
if ( this.root.init )
|
||||
this.root.captured_init.push({
|
||||
time: Date.now(),
|
||||
category: this.name,
|
||||
message: out,
|
||||
level: RAVEN_LEVELS[level] || level
|
||||
});
|
||||
|
||||
this.crumb({
|
||||
message: out,
|
||||
category: this.name,
|
||||
level: RAVEN_LEVELS[level] || level
|
||||
});
|
||||
}
|
||||
|
||||
message.unshift(msg);
|
||||
|
||||
if ( this.name ) {
|
||||
message[0] = `%c${this.root.label} [%c${this.name}%c]:%c ${message[0]}`;
|
||||
colors.unshift('color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', '');
|
||||
|
||||
} else {
|
||||
message[0] = `%c${this.root.label}:%c ${message[0]}`;
|
||||
colors.unshift('color:#755000; font-weight:bold', '');
|
||||
}
|
||||
|
||||
message.splice(1, 0, ...colors);
|
||||
|
||||
if ( level === Logger.DEBUG || level === Logger.VERBOSE )
|
||||
console.debug(...message);
|
||||
|
||||
else if ( level === Logger.INFO )
|
||||
console.info(...message);
|
||||
|
||||
else if ( level === Logger.WARN )
|
||||
console.warn(...message);
|
||||
|
||||
else if ( level === Logger.ERROR )
|
||||
console.error(...message);
|
||||
|
||||
else
|
||||
console.log(...message);
|
||||
}
|
||||
|
||||
/* eslint no-console: "off" */
|
||||
invoke(level, args) {
|
||||
if ( ! this.enabled || level < this.level )
|
||||
|
|
|
@ -802,8 +802,13 @@ function render_image(token, createElement, ctx) {
|
|||
const image = createElement('img', {
|
||||
className: `${token.class || ''} ${round}`,
|
||||
src: token.url,
|
||||
alt: token.alt || token.title || '',
|
||||
title: token.title || '',
|
||||
onLoad: ctx.onload
|
||||
onLoad: ctx.onload,
|
||||
style: {
|
||||
width: token.width,
|
||||
height: token.height
|
||||
}
|
||||
});
|
||||
|
||||
if ( ! aspect )
|
||||
|
|
|
@ -373,7 +373,12 @@
|
|||
&[data-meta="true"][data-alt="true"][data-ctrl="true"] .ffz-modifier-13,
|
||||
&[data-meta="true"][data-alt="true"][data-shift="true"] .ffz-modifier-14,
|
||||
&[data-meta="true"][data-alt="true"][data-ctrl="true"][data-shift="true"] .ffz-modifier-15 {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
&:ffz-hover-action {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,6 +70,8 @@
|
|||
.ffz-i-threads:before { content: '\e844'; } /* '' */
|
||||
.ffz-i-volume-off:before { content: '\e845'; } /* '' */
|
||||
.ffz-i-right-open:before { content: '\e846'; } /* '' */
|
||||
.ffz-i-mastodon:before { content: '\e847'; } /* '' */
|
||||
.ffz-i-volume-up:before { content: '\e848'; } /* '' */
|
||||
.ffz-i-move:before { content: '\f047'; } /* '' */
|
||||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -70,6 +70,8 @@
|
|||
.ffz-i-threads { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-mastodon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -81,6 +81,8 @@
|
|||
.ffz-i-threads { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-mastodon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'ffz-fontello';
|
||||
src: url('../font/ffz-fontello.eot?79984461');
|
||||
src: url('../font/ffz-fontello.eot?79984461#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?79984461') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?79984461') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?79984461') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?79984461#ffz-fontello') format('svg');
|
||||
src: url('../font/ffz-fontello.eot?19069837');
|
||||
src: url('../font/ffz-fontello.eot?19069837#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?19069837') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?19069837') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?19069837') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?19069837#ffz-fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'ffz-fontello';
|
||||
src: url('../font/ffz-fontello.svg?79984461#ffz-fontello') format('svg');
|
||||
src: url('../font/ffz-fontello.svg?19069837#ffz-fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -125,6 +125,8 @@
|
|||
.ffz-i-threads:before { content: '\e844'; } /* '' */
|
||||
.ffz-i-volume-off:before { content: '\e845'; } /* '' */
|
||||
.ffz-i-right-open:before { content: '\e846'; } /* '' */
|
||||
.ffz-i-mastodon:before { content: '\e847'; } /* '' */
|
||||
.ffz-i-volume-up:before { content: '\e848'; } /* '' */
|
||||
.ffz-i-move:before { content: '\f047'; } /* '' */
|
||||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
|
||||
}
|
||||
|
||||
.ffz-mod-icon .ffz-i-reply {
|
||||
.ffz--inline-actions .ffz-mod-icon .ffz-i-reply {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
@import "./widgets/color-picker.scss";
|
||||
@import "./widgets/icon-picker.scss";
|
||||
|
||||
@import "./widgets/chat-tester.scss";
|
||||
|
||||
@import "./widgets/check-box.scss";
|
||||
|
||||
.tw-display-inline { display: inline !important }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.ffz--emote-picker__list,
|
||||
.ffz--icon-picker__list {
|
||||
max-height: 15rem;
|
||||
font-size: 1.6rem;
|
||||
|
@ -9,4 +10,12 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ffz--emote-picker {
|
||||
.ffz-preview-emote {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue