mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-02 16:08:31 +00:00
The In-Line Actions Update
* Add extensible actions system. * Add extensive UI for configuring the actions system. * Add setting to disable channel hosting. * Fix the stupid Rooms thing popping up every time you open a channel. * Fix how we grab chat types from React. * Refactor how we handle incoming chat messages. * Add a hook for outgoing chat messages. * Fix emoji appearing squished with baseline emote alignment. * Display arrows on balloons. * Fix an issue generating emoji URLs. * Do not use the default values for settings with merge strategies if profiles have those settings, just empty. * Display a message in the chat settings menu if we tried opening FFZ's settings and failed. * Wait a bit for webpack's loader if it's not immediately there for some reason. * Probably other stuff. * Not mod cards. Yet.
This commit is contained in:
parent
e9214bb46a
commit
fdde05030f
67 changed files with 7689 additions and 226 deletions
|
@ -1,3 +1,20 @@
|
|||
<div class="list-header">4.0.0-rc1<span>@a1a7fb774d62948bacc5</span> <time datetime="2018-04-28">(2018-04-28)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Custom In-Line Chat Actions</li>
|
||||
<li>Added: Option to disable Channel Hosting.</li>
|
||||
<li>Changed: Minor refactoring of how we wrap incoming chat events, since Twitch is now filtering by chat room themselves for most events.</li>
|
||||
<li>Fixed: Grab chat types from React correctly.</li>
|
||||
<li>Fixed: Emoji appearing squashed in chat when using the baseline emote alignment option.</li>
|
||||
<li>Fixed: Strip new lines from outgoing chat messages to ensure we process them correctly, locally.</li>
|
||||
<li>Fixed: Display arrows on balloons for channel metadata.</li>
|
||||
<li>Fixed: Validate the requested emoji style when getting image URLs.</li>
|
||||
<li>Fixed: Do not use default values for settings with merge strategies if profiles are present and empty.</li>
|
||||
<li>Fixed: Display a message in the chat settings menu if we tried opening FFZ settings in a pop-up and failed.</li>
|
||||
<li>Fixed: Wait a bit for webpack's loader to be found if it is not immediately present in window.</li>
|
||||
<li>Fixed: Forcibly close the chat room picker when first loading a channel.</li>
|
||||
<li>Fixed: Vertically align FFZ icon for emote menu with bits icon.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">4.0.0-beta2.18.2<span>@a1a7fb774d62948bacc5</span> <time datetime="2018-04-16">(2018-04-16)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Rewrite stacktraces for automatic error reporting to use the permanent URL for the current FrankerFaceZ build.</li>
|
||||
|
|
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -5384,6 +5384,11 @@
|
|||
"integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
},
|
||||
"lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
|
@ -5474,6 +5479,11 @@
|
|||
"object-visit": "1.0.1"
|
||||
}
|
||||
},
|
||||
"material-colors": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.5.tgz",
|
||||
"integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE="
|
||||
},
|
||||
"math-expression-evaluator": {
|
||||
"version": "1.2.17",
|
||||
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
|
||||
|
@ -8797,6 +8807,11 @@
|
|||
"setimmediate": "1.0.5"
|
||||
}
|
||||
},
|
||||
"tinycolor2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
|
||||
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
|
@ -9356,6 +9371,16 @@
|
|||
"loose-envify": "1.3.1"
|
||||
}
|
||||
},
|
||||
"vue-color": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.4.6.tgz",
|
||||
"integrity": "sha512-kGz1LNu1DKIcT8jQmUeSjQNqh9NGYBCvMFXU43MRF4kg7A33Z4tZMQFLrPrX7d8r3ptbmLq+MaclJiVQFMCMqg==",
|
||||
"requires": {
|
||||
"lodash.throttle": "4.1.1",
|
||||
"material-colors": "1.2.5",
|
||||
"tinycolor2": "1.4.1"
|
||||
}
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz",
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"sortablejs": "^1.7.0",
|
||||
"vue": "^2.5.16",
|
||||
"vue-clickaway": "^2.1.0",
|
||||
"vue-color": "^2.4.6",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"vuedraggable": "^2.16.0"
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -84,6 +84,10 @@
|
|||
|
||||
<glyph glyph-name="pin-outline" unicode="" d="M856 554q30-30 30-73t-30-75q-16-16-36-24-106-51-144-125-51-102-51-246 0-45-29-74t-75-30q-47 0-75 30l-167 169-279-199 199 279-169 168q-20 18-27 49t5 64q27 65 96 65 146 0 246 50l11 7q69 37 118 141 7 20 21 34 30 30 74 29t74-31z m-273-250q53 107 196 175l-205 211q-69-146-176-200l-13-6q-120-57-281-57l416-416q0 168 63 293z" horiz-adv-x="886" />
|
||||
|
||||
<glyph glyph-name="gift" unicode="" d="M518 93v400h-179v-400q0-14 10-21t26-8h107q16 0 26 8t10 21z m-255 471h109l-70 90q-15 17-39 17-22 0-38-15t-15-38 15-38 38-16z m384 54q0 22-15 38t-38 15q-24 0-39-17l-69-90h108q22 0 38 16t15 38z m210-143v-179q0-7-5-12t-13-5h-53v-233q0-22-16-37t-38-16h-607q-22 0-38 16t-16 37v233h-53q-8 0-13 5t-5 12v179q0 8 5 13t13 5h245q-51 0-88 36t-37 89 37 88 88 37q60 0 94-43l72-92 71 92q34 43 94 43 52 0 88-37t37-88-37-89-88-36h245q8 0 13-5t5-13z" horiz-adv-x="857.1" />
|
||||
|
||||
<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" />
|
||||
|
||||
<glyph glyph-name="twitter" unicode="" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="gauge" unicode="" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
|
||||
|
@ -104,6 +108,8 @@
|
|||
|
||||
<glyph glyph-name="trash" unicode="" d="M286 82v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m143 0v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m142 0v393q0 8-5 13t-12 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q7 0 12 5t5 13z m-303 554h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
|
||||
|
||||
<glyph glyph-name="eyedropper" unicode="" d="M948 798q52-53 52-127t-52-126l-126-124 58-58q6-6 6-13t-6-13l-117-117q-6-6-13-6t-13 6l-58 59-337-337q-21-21-50-21h-113l-143-71-36 36 71 143v113q0 29 21 50l337 337-59 58q-6 6-6 13t6 13l117 117q6 6 13 6t13-6l58-58 124 126q52 52 126 52t127-52z m-662-769l321 321-107 107-321-321v-107h107z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="window-maximize" unicode="" d="M143 64h714v429h-714v-429z m857 625v-678q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v678q0 37 26 63t63 27h822q37 0 63-27t26-63z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="window-minimize" unicode="" d="M1000 118v-107q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v107q0 37 26 63t63 26h822q37 0 63-26t26-63z" horiz-adv-x="1000" />
|
||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
res/fontawesome/fontawesome-webfont.eot
Normal file
BIN
res/fontawesome/fontawesome-webfont.eot
Normal file
Binary file not shown.
2671
res/fontawesome/fontawesome-webfont.svg
Normal file
2671
res/fontawesome/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 434 KiB |
BIN
res/fontawesome/fontawesome-webfont.ttf
Normal file
BIN
res/fontawesome/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
res/fontawesome/fontawesome-webfont.woff
Normal file
BIN
res/fontawesome/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
res/fontawesome/fontawesome-webfont.woff2
Normal file
BIN
res/fontawesome/fontawesome-webfont.woff2
Normal file
Binary file not shown.
|
@ -13,6 +13,7 @@
|
|||
script = document.createElement('script');
|
||||
|
||||
script.id = 'ffz-script';
|
||||
script.async = true;
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.src = `${SERVER}/script/${BABEL}${FLAVOR}.js?_=${Date.now()}`;
|
||||
document.head.appendChild(script);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// ============================================================================
|
||||
|
||||
import {SERVER} from 'utilities/constants';
|
||||
import {pick_random, has} from 'utilities/object';
|
||||
import {get, pick_random, has} from 'utilities/object';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
|
||||
|
@ -519,10 +519,10 @@ export function transformPhrase(phrase, substitutions, locale, token_regex, form
|
|||
|
||||
if ( typeof result === 'string' )
|
||||
result = REPLACE.call(result, token_regex, (expr, arg, fmt) => {
|
||||
if ( ! has(options, arg) )
|
||||
let val = get(arg, options);
|
||||
if ( val == null )
|
||||
return '';
|
||||
|
||||
let val = options[arg];
|
||||
const formatter = formatters[fmt];
|
||||
if ( typeof formatter === 'function' )
|
||||
val = formatter(val, locale, options);
|
||||
|
|
|
@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 0, revision: 0, extra: '-beta2.18.3',
|
||||
major: 4, minor: 0, revision: 0, extra: '-rc1',
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||
|
|
24
src/modules/chat/actions/edit-ban.vue
Normal file
24
src/modules/chat/actions/edit-ban.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template lang="html">
|
||||
<div>
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="edit_reason">
|
||||
{{ t('setting.actions.reason', 'Default Reason') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="edit_reason"
|
||||
v-model.trim="value.reason"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
@input="$emit('input', value)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['value', 'defaults']
|
||||
}
|
||||
|
||||
</script>
|
29
src/modules/chat/actions/edit-chat.vue
Normal file
29
src/modules/chat/actions/edit-chat.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template lang="html">
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<label for="edit_chat" class="tw-mg-y-05">
|
||||
{{ t('setting.actions.chat', 'Chat Command') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-full-width">
|
||||
<input
|
||||
id="edit_chat"
|
||||
v-model="value.command"
|
||||
:placeholder="defaults.command"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
@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>
|
132
src/modules/chat/actions/edit-icon.vue
Normal file
132
src/modules/chat/actions/edit-icon.vue
Normal file
|
@ -0,0 +1,132 @@
|
|||
<template lang="html">
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<label class="tw-mg-y-05">
|
||||
{{ t('setting.actions.icon', 'Icon') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-full-width">
|
||||
<div class="tw-search-input">
|
||||
<label for="icon-search" class="tw-hide-accessible">
|
||||
{{ t('setting.actions.icon.search', 'Search for Icon') }}
|
||||
</label>
|
||||
<div class="tw-relative tw-mg-t-05">
|
||||
<div class="tw-input__icon-group tw-top-0 tw-left-0 tw-z-default tw-absolute">
|
||||
<div class="tw-input__icon tw-c-text-alt-2 tw-align-items-center tw-flex tw-justify-content-center">
|
||||
<figure class="ffz-i-search" />
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
id="icon-search"
|
||||
:placeholder="t('setting.actions.icon.search', 'Search for Icon')"
|
||||
v-model="search"
|
||||
type="search"
|
||||
class="tw-input tw-pd-l-3"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<simplebar classes="tw-c-background-alt-2 tw-border-l tw-border-r tw-border-b ffz-icon-picker tw-mg-b-05">
|
||||
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap" >
|
||||
<div
|
||||
v-for="i of visible"
|
||||
:key="i[0]"
|
||||
:aria-checked="value.icon === i[0]"
|
||||
:class="{'tw-interactable--selected': value.icon === i[0]}"
|
||||
class="ffz-icon tw-interactable"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
@keydown.space.stop.prevent=""
|
||||
@keyup.space="change(i[0])"
|
||||
@keyup.enter="change(i[0])"
|
||||
@click="change(i[0])"
|
||||
>
|
||||
<figure :class="`tw-mg-y-05 tw-mg-x-1 ${i[0]}`" />
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {escape_regex, deep_copy} from 'utilities/object';
|
||||
import {load, ICONS as FA_ICONS, ALIASES as FA_ALIASES} from 'utilities/font-awesome';
|
||||
|
||||
const FFZ_ICONS = [
|
||||
'zreknarf',
|
||||
'crown',
|
||||
'verified',
|
||||
'inventory',
|
||||
'ignore',
|
||||
'pin-outline',
|
||||
'pin',
|
||||
'block',
|
||||
'ok',
|
||||
'clock'
|
||||
];
|
||||
|
||||
const FFZ_ALIASES = {
|
||||
'block': ['ban', 'block'],
|
||||
'ok': ['ok', 'unban', 'untimeout', 'checkmark'],
|
||||
'clock': ['clock', 'clock-o', 'timeout']
|
||||
};
|
||||
|
||||
|
||||
const ICONS = FFZ_ICONS
|
||||
.map(x => [`ffz-i-${x}`, FFZ_ALIASES[x] ? FFZ_ALIASES[x].join(' ') : x])
|
||||
.concat(FA_ICONS.filter(x => ! FFZ_ICONS.includes(x)).map(x => [`ffz-fa fa-${x}`, FA_ALIASES[x] ? FA_ALIASES[x].join(' ') : x]));
|
||||
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
icons: deep_copy(ICONS)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
visible() {
|
||||
if ( ! this.search || ! this.search.length )
|
||||
return this.icons;
|
||||
|
||||
const search = this.search.toLowerCase().replace(' ', '-'),
|
||||
reg = new RegExp('(?:^|-| )' + escape_regex(search), 'i');
|
||||
|
||||
return this.icons.filter(x => reg.test(x[1]));
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
load();
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(val) {
|
||||
this.value.icon = val;
|
||||
this.$emit('input', this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ffz-icon-picker {
|
||||
max-height: 15rem;
|
||||
font-size: 1.6rem;
|
||||
|
||||
.ffz-icon {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
22
src/modules/chat/actions/edit-image.vue
Normal file
22
src/modules/chat/actions/edit-image.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template lang="html">
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="edit_image">
|
||||
{{ t('setting.actions.url', 'URL') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="edit_image"
|
||||
v-model="value.image"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
@input="$emit('input', value)"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['value']
|
||||
}
|
||||
|
||||
</script>
|
22
src/modules/chat/actions/edit-text.vue
Normal file
22
src/modules/chat/actions/edit-text.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template lang="html">
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="edit_text">
|
||||
{{ t('setting.actions.text', 'Label') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="edit_text"
|
||||
v-model="value.text"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
@input="$emit('input', value)"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['value']
|
||||
}
|
||||
|
||||
</script>
|
39
src/modules/chat/actions/edit-timeout.vue
Normal file
39
src/modules/chat/actions/edit-timeout.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template lang="html">
|
||||
<div>
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="edit_duration">
|
||||
{{ t('setting.actions.duration', 'Duration') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="edit_duration"
|
||||
v-model.number="value.duration"
|
||||
:placeholder="defaults.duration"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
type="number"
|
||||
@input="$emit('input', value)"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="edit_reason">
|
||||
{{ t('setting.actions.reason', 'Default Reason') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="edit_reason"
|
||||
v-model.trim="value.reason"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
@input="$emit('input', value)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['value', 'defaults']
|
||||
}
|
||||
|
||||
</script>
|
29
src/modules/chat/actions/edit-url.vue
Normal file
29
src/modules/chat/actions/edit-url.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template lang="html">
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<label for="edit_url" class="tw-mg-y-05">
|
||||
{{ t('setting.actions.url', 'URL') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-full-width">
|
||||
<input
|
||||
id="edit_url"
|
||||
v-model="value.url"
|
||||
:placeholder="defaults.url"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
@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>
|
303
src/modules/chat/actions/index.jsx
Normal file
303
src/modules/chat/actions/index.jsx
Normal file
|
@ -0,0 +1,303 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Emoji Handling
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {has, maybe_call, deep_copy} from 'utilities/object';
|
||||
import {ClickOutside} from 'utilities/dom';
|
||||
import Tooltip from 'utilities/tooltip';
|
||||
|
||||
import * as ACTIONS from './types';
|
||||
import * as RENDERERS from './renderers';
|
||||
|
||||
|
||||
export default class Actions extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('tooltips');
|
||||
this.inject('i18n');
|
||||
|
||||
this.actions = {};
|
||||
this.renderers = {};
|
||||
|
||||
this.settings.add('chat.actions.inline', {
|
||||
// Filter out actions
|
||||
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: 'ban', appearance: {type: 'icon', icon: 'ffz-i-block'}, display: {mod: true, mod_icons: true, deleted: false}}},
|
||||
{v: {action: 'unban', appearance: {type: 'icon', icon: 'ffz-i-ok'}, display: {mod: true, mod_icons: true, deleted: true}}},
|
||||
{v: {action: 'timeout', appearance: {type: 'icon', icon: 'ffz-i-clock'}, display: {mod: true, mod_icons: true}}},
|
||||
],
|
||||
|
||||
type: 'array_merge',
|
||||
ui: {
|
||||
path: 'Chat > In-Line Actions',
|
||||
component: 'chat-actions',
|
||||
inline: true,
|
||||
|
||||
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.handleClick = this.handleClick.bind(this);
|
||||
this.handleContext = this.handleContext.bind(this);
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
this.tooltips.types.action = (target, tip) => {
|
||||
const data = this.getData(target);
|
||||
if ( ! data )
|
||||
return this.i18n.t('chat.actions.unknown', 'Unknown Action Type');
|
||||
|
||||
if ( ! data.definition.tooltip )
|
||||
return `Error: The "${data.action}" action provider does not have tooltip support.`;
|
||||
|
||||
if ( data.tip && data.tip.length )
|
||||
return data.tip;
|
||||
|
||||
return maybe_call(data.definition.tooltip, this, data, target, tip);
|
||||
}
|
||||
|
||||
|
||||
for(const key in ACTIONS)
|
||||
if ( has(ACTIONS, key) )
|
||||
this.addAction(key, ACTIONS[key]);
|
||||
|
||||
for(const key in RENDERERS)
|
||||
if ( has(RENDERERS, key) )
|
||||
this.addRenderer(key, RENDERERS[key]);
|
||||
}
|
||||
|
||||
|
||||
addAction(key, data) {
|
||||
if ( has(this.actions, key) )
|
||||
return this.log.warn(`Attempted to add action "${key}" which is already defined.`);
|
||||
|
||||
this.actions[key] = data;
|
||||
|
||||
for(const ctx of this.settings.__contexts)
|
||||
ctx.update('chat.actions.inline');
|
||||
}
|
||||
|
||||
|
||||
addRenderer(key, data) {
|
||||
if ( has(this.renderers, key) )
|
||||
return this.log.warn(`Attempted to add renderer "${key}" which is already defined.`);
|
||||
|
||||
this.renderers[key] = data;
|
||||
|
||||
for(const ctx of this.settings.__contexts)
|
||||
ctx.update('chat.actions.inline');
|
||||
}
|
||||
|
||||
|
||||
renderInlineContext(target, data) {
|
||||
if ( target._ffz_destroy )
|
||||
return target._ffz_destroy();
|
||||
|
||||
const destroy = target._ffz_destroy = () => {
|
||||
if ( target._ffz_outside )
|
||||
target._ffz_outside.destroy();
|
||||
|
||||
if ( target._ffz_popup ) {
|
||||
const fp = target._ffz_popup;
|
||||
target._ffz_popup = null;
|
||||
fp.destroy();
|
||||
}
|
||||
|
||||
target._ffz_destroy = target._ffz_outside = null;
|
||||
}
|
||||
|
||||
const parent = document.body.querySelector('.twilight-root,.twilight-minimal-root') || document.body,
|
||||
tt = target._ffz_popup = new Tooltip(parent, target, {
|
||||
logger: this.log,
|
||||
manual: true,
|
||||
html: true,
|
||||
|
||||
tooltipClass: 'ffz-action-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background',
|
||||
arrowClass: 'tw-balloon__tail tw-overflow-hidden tw-absolute',
|
||||
arrowInner: 'tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background tw-absolute',
|
||||
innerClass: 'tw-pd-1',
|
||||
|
||||
popper: {
|
||||
placement: 'bottom',
|
||||
modifiers: {
|
||||
preventOverflow: {
|
||||
boundariesElement: parent
|
||||
},
|
||||
flip: {
|
||||
behavior: ['bottom', 'top', 'left', 'right']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
content: (t, tip) => data.definition.context.call(this, data, t, tip),
|
||||
onShow: (t, tip) =>
|
||||
setTimeout(() => {
|
||||
target._ffz_outside = new ClickOutside(tip.outer, destroy)
|
||||
}),
|
||||
|
||||
onHide: destroy
|
||||
});
|
||||
|
||||
tt._enter(target);
|
||||
}
|
||||
|
||||
|
||||
renderInline(msg, mod_icons, current_user, current_room, createElement) {
|
||||
const actions = [];
|
||||
|
||||
if ( msg.user && current_user.login === msg.user.userLogin )
|
||||
return;
|
||||
|
||||
const chat = this.resolve('site.chat');
|
||||
|
||||
for(const data of this.parent.context.get('chat.actions.inline')) {
|
||||
if ( ! data.action || ! data.appearance )
|
||||
continue;
|
||||
|
||||
const ap = data.appearance || {},
|
||||
disp = data.display || {},
|
||||
|
||||
def = this.renderers[ap.type];
|
||||
|
||||
if ( ! def || disp.disabled ||
|
||||
(disp.mod_icons != null && disp.mod_icons !== mod_icons) ||
|
||||
(disp.mod != null && disp.mod !== current_user.moderator) ||
|
||||
(disp.staff != null && disp.staff !== current_user.staff) ||
|
||||
(disp.deleted != null && disp.deleted !== msg.deleted) )
|
||||
continue;
|
||||
|
||||
const has_color = def.colored && ap.color,
|
||||
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
|
||||
contents = def.render.call(this, ap, createElement, color);
|
||||
|
||||
actions.push(<button
|
||||
class={`ffz-tooltip ffz-mod-icon mod-icon tw-c-text-alt-2${has_color ? ' colored' : ''}`}
|
||||
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>);
|
||||
}
|
||||
|
||||
if ( ! actions.length )
|
||||
return null;
|
||||
|
||||
const room = current_room && JSON.stringify(current_room),
|
||||
user = msg.user && JSON.stringify({
|
||||
login: msg.user.userLogin,
|
||||
displayName: msg.user.userDisplayName,
|
||||
id: msg.user.userID,
|
||||
type: msg.user.userType
|
||||
});
|
||||
|
||||
return (<div
|
||||
class="ffz--inline-actions tw-inline tw-mg-r-05"
|
||||
data-msg-id={msg.id}
|
||||
data-user={user}
|
||||
data-room={room}
|
||||
>
|
||||
{actions}
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
getData(element) {
|
||||
const ds = element.dataset,
|
||||
parent = element.parentElement,
|
||||
pds = parent && parent.dataset,
|
||||
action = ds && ds.action,
|
||||
definition = this.actions[action];
|
||||
|
||||
if ( ! definition )
|
||||
return null;
|
||||
|
||||
const user = pds && pds.user ? JSON.parse(pds.user) : null,
|
||||
room = pds && pds.room ? JSON.parse(pds.room) : null,
|
||||
message_id = pds && pds.msgId,
|
||||
|
||||
data = {
|
||||
action,
|
||||
definition,
|
||||
tip: ds.tip,
|
||||
options: ds.options ? JSON.parse(ds.options) : null,
|
||||
user,
|
||||
room,
|
||||
message_id
|
||||
};
|
||||
|
||||
if ( definition.defaults )
|
||||
data.options = Object.assign({}, maybe_call(definition.defaults, this, data, element), data.options);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
handleClick(event) {
|
||||
const target = event.target,
|
||||
data = this.getData(target);
|
||||
if ( ! data )
|
||||
return;
|
||||
|
||||
if ( ! data.definition.click ) {
|
||||
if ( data.definition.context )
|
||||
return this.handleContext(event);
|
||||
|
||||
return this.log.warn(`No click handler for action provider "${data.action}"`);
|
||||
}
|
||||
|
||||
if ( target._ffz_tooltip$0 )
|
||||
target._ffz_tooltip$0.hide();
|
||||
|
||||
return data.definition.click.call(this, event, data);
|
||||
}
|
||||
|
||||
handleContext(event) {
|
||||
if ( event.shiftKey )
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const target = event.target,
|
||||
data = this.getData(event.target);
|
||||
if ( ! data )
|
||||
return;
|
||||
|
||||
if ( ! data.definition.context )
|
||||
return;
|
||||
|
||||
if ( target._ffz_tooltip$0 )
|
||||
target._ffz_tooltip$0.hide();
|
||||
|
||||
this.renderInlineContext(event.target, data);
|
||||
}
|
||||
|
||||
|
||||
sendMessage(room, message) {
|
||||
return this.resolve('site.chat').sendMessage(room, message);
|
||||
}
|
||||
}
|
14
src/modules/chat/actions/preview-icon.vue
Normal file
14
src/modules/chat/actions/preview-icon.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<figure
|
||||
:class="`${data.icon||'ffz-i-zreknarf'}`"
|
||||
:style="{color}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['data', 'color']
|
||||
}
|
||||
|
||||
</script>
|
13
src/modules/chat/actions/preview-image.vue
Normal file
13
src/modules/chat/actions/preview-image.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<figure class="mod-icon__image">
|
||||
<img :src="data.image">
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['data', 'color']
|
||||
}
|
||||
|
||||
</script>
|
13
src/modules/chat/actions/preview-text.vue
Normal file
13
src/modules/chat/actions/preview-text.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<span :style="{color}">
|
||||
{{ data.text }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['data', 'color']
|
||||
}
|
||||
|
||||
</script>
|
63
src/modules/chat/actions/renderers.jsx
Normal file
63
src/modules/chat/actions/renderers.jsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
'use strict';
|
||||
|
||||
import {load as loadFontAwesome} from 'utilities/font-awesome';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Text
|
||||
// ============================================================================
|
||||
|
||||
export const text = {
|
||||
title: 'Text',
|
||||
title_i18n: 'setting.actions.appearance.text',
|
||||
|
||||
colored: true,
|
||||
preview: () => import(/* webpackChunkName: 'main-menu' */ './preview-text.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-text.vue'),
|
||||
|
||||
render(data, createElement, color) {
|
||||
return <span style={{color}}>{data.text}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Icon
|
||||
// ============================================================================
|
||||
|
||||
export const icon = {
|
||||
title: 'Icon',
|
||||
title_i18n: 'setting.actions.appearance.icon',
|
||||
|
||||
colored: true,
|
||||
preview: () => import(/* webpackChunkName: 'main-menu' */ './preview-icon.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-icon.vue'),
|
||||
|
||||
load(data) {
|
||||
if ( data.icon && data.icon.startsWith('ffz-fa') )
|
||||
loadFontAwesome();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
render(data, createElement, color) {
|
||||
return <figure style={{color}} class={`${data.icon||'ffz-i-zreknarf'}`} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Image
|
||||
// ============================================================================
|
||||
|
||||
export const image = {
|
||||
title: 'Image',
|
||||
title_i18n: 'setting.actions.appearance.image',
|
||||
|
||||
preview: () => import(/* webpackChunkName: 'main-menu' */ './preview-image.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-image.vue'),
|
||||
|
||||
render(data, createElement) {
|
||||
return <figure class="mod-icon__image"><img src={data.image} /></figure>;
|
||||
}
|
||||
}
|
291
src/modules/chat/actions/types.jsx
Normal file
291
src/modules/chat/actions/types.jsx
Normal file
|
@ -0,0 +1,291 @@
|
|||
'use strict';
|
||||
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {transformPhrase} from 'src/i18n';
|
||||
|
||||
const VAR_REPLACE = /\{\{(.*?)(?:\|(.*?))?\}\}/g;
|
||||
|
||||
const process = (input, data, locale = 'en') => transformPhrase(input, data, locale, VAR_REPLACE, {});
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Open URL
|
||||
// ============================================================================
|
||||
|
||||
export const open_url = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-link-ext'
|
||||
}
|
||||
}],
|
||||
|
||||
defaults: {
|
||||
url: 'https://link.example/{{user.login}}'
|
||||
},
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-url.vue'),
|
||||
|
||||
title: 'Open URL',
|
||||
description: '%{options.url}',
|
||||
|
||||
tooltip(data) {
|
||||
const url = process(data.options.url, data, this.i18n.locale);
|
||||
|
||||
return [
|
||||
(<div class="tw-border-b tw-mg-b-05">{ // eslint-disable-line react/jsx-key
|
||||
this.i18n.t('chat.actions.open_url', 'Open URL')
|
||||
}</div>),
|
||||
(<div class="tw-align-left">{ // eslint-disable-line react/jsx-key
|
||||
url
|
||||
}</div>)
|
||||
]
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
const url = process(data.options.url, data, this.i18n.locale);
|
||||
|
||||
const win = window.open();
|
||||
win.opener = null;
|
||||
win.location = url;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Chat
|
||||
// ============================================================================
|
||||
|
||||
export const chat = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'text',
|
||||
text: 'C'
|
||||
}
|
||||
}],
|
||||
|
||||
defaults: {
|
||||
command: '@{{user.login}} HeyGuys'
|
||||
},
|
||||
|
||||
title: 'Chat Command',
|
||||
description: '%{options.command}',
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-chat.vue'),
|
||||
|
||||
process(data) {
|
||||
return transformPhrase(
|
||||
data.options.command,
|
||||
data,
|
||||
this.i18n.locale,
|
||||
VAR_REPLACE,
|
||||
{}
|
||||
)
|
||||
},
|
||||
|
||||
tooltip(data) {
|
||||
const msg = process(data.options.command, data, this.i18n.locale);
|
||||
|
||||
return [
|
||||
(<div class="tw-border-b tw-mg-b-05">{ // eslint-disable-line react/jsx-key
|
||||
this.i18n.t('chat.actions.chat', 'Chat Command')
|
||||
}</div>),
|
||||
(<div class="tw-align-left">{ // eslint-disable-line react/jsx-key
|
||||
msg
|
||||
}</div>)
|
||||
]
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
const msg = data.definition.process.call(this, data);
|
||||
this.sendMessage(data.room.login, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Timeout
|
||||
// ============================================================================
|
||||
|
||||
|
||||
export const ban = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-block'
|
||||
},
|
||||
|
||||
display: {
|
||||
mod: true,
|
||||
mod_icons: true
|
||||
}
|
||||
}],
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-ban.vue'),
|
||||
|
||||
title: 'Ban User',
|
||||
|
||||
tooltip(data) {
|
||||
return this.i18n.t('chat.actions.ban', 'Ban %{user.login}', {user: data.user});
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
this.sendMessage(data.room.login, `/ban ${data.user.login} ${data.reason||''}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const timeout = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-clock'
|
||||
},
|
||||
|
||||
display: {
|
||||
mod: true,
|
||||
mod_icons: true
|
||||
}
|
||||
}],
|
||||
|
||||
defaults: {
|
||||
duration: 600
|
||||
},
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-timeout.vue'),
|
||||
|
||||
title: 'Timeout User',
|
||||
description: '%{options.duration} second%{options.duration|en_plural}',
|
||||
|
||||
tooltip(data) {
|
||||
return this.i18n.t(
|
||||
'chat.actions.timeout',
|
||||
'Timeout %{user.login} for %{duration} second%{duration|en_plural}',
|
||||
{
|
||||
user: data.user,
|
||||
duration: data.options.duration
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
this.sendMessage(data.room.login, `/timeout ${data.user.login} ${data.options.duration} ${data.reason||''}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const unban = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-ok'
|
||||
},
|
||||
|
||||
display: {
|
||||
mod: true,
|
||||
mod_icons: true
|
||||
}
|
||||
}],
|
||||
|
||||
title: 'Unban User',
|
||||
|
||||
tooltip(data) {
|
||||
return this.i18n.t('chat.actions.unban', 'Unban %{user.login}', {user: data.user});
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
this.sendMessage(data.room.login, `/unban ${data.user.login}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const untimeout = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-ok'
|
||||
},
|
||||
|
||||
display: {
|
||||
mod: true,
|
||||
mod_icons: true
|
||||
}
|
||||
}],
|
||||
|
||||
title: 'Untimeout User',
|
||||
|
||||
tooltip(data) {
|
||||
return this.i18n.t('chat.actions.untimeout', 'Untimeout %{user.login}', {user: data.user});
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
this.sendMessage(data.room.login, `/untimeout ${data.user.login}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Whisper
|
||||
// ============================================================================
|
||||
|
||||
export const whisper = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'text',
|
||||
text: 'W'
|
||||
}
|
||||
}],
|
||||
|
||||
title: 'Whisper User',
|
||||
|
||||
tooltip(data) {
|
||||
return this.i18n.t('chat.actions.whisper', 'Whisper %{user.login}', data);
|
||||
},
|
||||
|
||||
click(event, data) {
|
||||
const site = this.resolve('site'),
|
||||
me = site && site.getUser(),
|
||||
store = site && site.store;
|
||||
|
||||
if ( ! me || ! store || ! data.user.id || me.id == data.user.id )
|
||||
return;
|
||||
|
||||
const id_1 = parseInt(me.id, 10),
|
||||
id_2 = parseInt(data.user.id, 10),
|
||||
thread = id_1 < id_2 ? `${id_1}_${id_2}` : `${id_2}_${id_1}`;
|
||||
|
||||
store.dispatch({
|
||||
type: 'whispers.THREAD_OPENED',
|
||||
data: {
|
||||
threadID: thread,
|
||||
collapsed: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Gift Subscription
|
||||
// ============================================================================
|
||||
|
||||
/*export const gift_sub = {
|
||||
presets: [{
|
||||
appearance: {
|
||||
type: 'icon',
|
||||
icon: 'ffz-i-gift'
|
||||
}
|
||||
}],
|
||||
|
||||
title: 'Gift Subscription',
|
||||
|
||||
tooltip(data) {
|
||||
return this.i18n.t('chat.actions.gift_sub', 'Gift a Sub to %{user.login}', data);
|
||||
},
|
||||
|
||||
context() {
|
||||
return (<div class="tw-border">
|
||||
Woop woop.
|
||||
</div>);
|
||||
}
|
||||
}*/
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import Module from 'utilities/module';
|
||||
import {SERVER} from 'utilities/constants';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
import splitter from 'emoji-regex/es2015/index';
|
||||
|
||||
|
@ -140,7 +141,7 @@ export default class Emoji extends Module {
|
|||
if ( ! style )
|
||||
style = this.parent.context.get('chat.emoji.style');
|
||||
|
||||
if ( style === 0 )
|
||||
if ( ! has(SIZES, style) )
|
||||
style = 'twitter';
|
||||
|
||||
return `${SERVER}/static/emoji/img-${style}-${SIZES[style][0]}/${image}`;
|
||||
|
@ -150,7 +151,7 @@ export default class Emoji extends Module {
|
|||
if ( ! style )
|
||||
style = this.parent.context.get('chat.emoji.style');
|
||||
|
||||
if ( style === 0 )
|
||||
if ( ! has(SIZES, style) )
|
||||
style = 'twitter';
|
||||
|
||||
return SIZES[style].map(w =>
|
||||
|
|
|
@ -17,6 +17,8 @@ import User from './user';
|
|||
import * as TOKENIZERS from './tokenizers';
|
||||
import * as RICH_PROVIDERS from './rich_providers';
|
||||
|
||||
import Actions from './actions';
|
||||
|
||||
|
||||
export default class Chat extends Module {
|
||||
constructor(...args) {
|
||||
|
@ -33,6 +35,7 @@ export default class Chat extends Module {
|
|||
this.inject(Badges);
|
||||
this.inject(Emotes);
|
||||
this.inject(Emoji);
|
||||
this.inject(Actions);
|
||||
|
||||
this._link_info = {};
|
||||
|
||||
|
|
|
@ -206,7 +206,7 @@ export const Mentions = {
|
|||
let regex, login, display;
|
||||
if ( user && user.login ) {
|
||||
login = user.login.toLowerCase();
|
||||
display = user.display && user.display.toLowerCase();
|
||||
display = user.displayName && user.displayName.toLowerCase();
|
||||
if ( display === login )
|
||||
display = null;
|
||||
|
||||
|
|
382
src/modules/main_menu/components/action-editor.vue
Normal file
382
src/modules/main_menu/components/action-editor.vue
Normal file
|
@ -0,0 +1,382 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--action tw-elevation-1 tw-c-background tw-border tw-pd-y-05 tw-pd-r-1 tw-mg-y-05 tw-flex tw-flex-nowrap">
|
||||
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start handle tw-pd-x-05 tw-pd-t-1 tw-pd-b-05">
|
||||
<span class="ffz-i-ellipsis-vert" />
|
||||
</div>
|
||||
|
||||
<div class="tw-flex-grow-1">
|
||||
<template v-if="! editing">
|
||||
<h4>{{ title }}</h4>
|
||||
<div class="description">{{ description }}</div>
|
||||
<div v-if="canEdit" class="visibility tw-c-text-alt">
|
||||
{{ t('setting.actions.visible', 'visible: %{list}', {list: visibility}) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<section>
|
||||
<h5>{{ t('setting.actions.appearance', 'Appearance') }}</h5>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="tooltip">
|
||||
{{ t('setting.actions.tooltip', 'Custom Tooltip') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
v-model="edit_data.appearance.tooltip"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="renderer_type">
|
||||
{{ t('setting.actions.type', 'Type') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="renderer_type"
|
||||
ref="renderer_type"
|
||||
v-model="edit_data.appearance.type"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option
|
||||
v-for="(r, key) in data.renderers"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ r.title_i18n ? t(r.title_i18n, r.title, r) : r.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="renderer && renderer.colored" class="tw-flex tw-align-items-start">
|
||||
<label for="color" class="tw-mg-y-05">
|
||||
{{ t('setting.actions.color', 'Color') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-full-width">
|
||||
<color-picker v-model="edit_data.appearance.color" :nullable="true" />
|
||||
|
||||
<div class="tw-c-text-alt-2 tw-mg-b-1">
|
||||
{{ t('setting.actions.color.warn', 'This value will be automatically adjusted for visibility based on your chat color settings.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component
|
||||
v-if="renderer"
|
||||
:is="renderer.editor"
|
||||
v-model="edit_data.appearance"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="tw-mg-t-1 tw-border-t tw-pd-t-1">
|
||||
<h5>{{ t('setting.actions.visibility', 'Visibility') }}</h5>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="vis_mod">
|
||||
{{ t('setting.actions.edit-visible.mod', 'Moderator') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="vis_mod"
|
||||
v-model="edit_data.display.mod"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option :value="undefined" selected>{{ t('setting.unset', 'Unset') }}</option>
|
||||
<option :value="true">{{ t('setting.true', 'True') }}</option>
|
||||
<option :value="false">{{ t('setting.false', 'False') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="vis_mod_icons">
|
||||
{{ t('setting.actions.edit-visible.mod-icons', 'Mod Icons') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="vis_mod_icons"
|
||||
v-model="edit_data.display.mod_icons"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option :value="undefined" selected>{{ t('setting.unset', 'Unset') }}</option>
|
||||
<option :value="true">{{ t('setting.visible', 'Visible') }}</option>
|
||||
<option :value="false">{{ t('setting.hidden', 'Hidden') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="vis_deleted">
|
||||
{{ t('setting.actions.edit-visible.deleted', 'Message Deleted') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="vis_deleted"
|
||||
v-model="edit_data.display.deleted"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option :value="undefined" selected>{{ t('setting.unset', 'Unset') }}</option>
|
||||
<option :value="true">{{ t('setting.true', 'True') }}</option>
|
||||
<option :value="false">{{ t('setting.false', 'False') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tw-mg-t-1 tw-border-t tw-pd-t-1">
|
||||
<h5>{{ t('setting.actions.action', 'Action') }}</h5>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="action_type">
|
||||
{{ t('setting.actions.type', 'Type') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="action_type"
|
||||
v-model="edit_data.action"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option
|
||||
v-for="(a, key) in data.actions"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ a.title_i18n ? t(a.title_i18n, a.title, a) : a.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<component
|
||||
v-if="action_def && action_def.editor"
|
||||
:is="action_def.editor"
|
||||
:value="edit_data.options"
|
||||
:defaults="action_def.defaults"
|
||||
:vars="vars"
|
||||
@input="onChangeAction($event)"
|
||||
/>
|
||||
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<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)"
|
||||
:renderers="data.renderers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-l-1 tw-border-l tw-pd-l-1 tw-flex tw-flex-wrap tw-flex-column tw-justify-content-start tw-align-items-start">
|
||||
<template v-if="editing">
|
||||
<button class="tw-button tw-button--text" @click="save">
|
||||
<span class="tw-button__text ffz-i-floppy">
|
||||
{{ t('setting.save', 'Save') }}
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text" @click="cancel">
|
||||
<span class="tw-button__text ffz-i-cancel">
|
||||
{{ t('setting.cancel', 'Cancel') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="canEdit"
|
||||
class="tw-button tw-button--text"
|
||||
@click="edit"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-cog">
|
||||
{{ t('setting.edit', 'Edit') }}
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text" @click="$emit('remove', action)">
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {has, maybe_call, deep_copy} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
props: ['action', 'data', 'inline'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
edit_data: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
display() {
|
||||
if ( this.editing )
|
||||
return this.edit_data;
|
||||
|
||||
return this.action.v;
|
||||
},
|
||||
|
||||
vars() {
|
||||
const out = ['user.login', 'user.displayName', 'user.id', 'user.type'];
|
||||
|
||||
out.push('room.login')
|
||||
out.push('room.id');
|
||||
|
||||
if ( this.inline )
|
||||
out.push('message_id');
|
||||
|
||||
return out.map(x => `{{${x}}}`).join(', ');
|
||||
},
|
||||
|
||||
renderer() {
|
||||
return this.canPreview && this.data.renderers[this.display.appearance.type];
|
||||
},
|
||||
|
||||
action_def() {
|
||||
return this.display && this.data.actions[this.display.action];
|
||||
},
|
||||
|
||||
canEdit() {
|
||||
return this.action.v != null;
|
||||
},
|
||||
|
||||
canPreview() {
|
||||
return this.display && this.display.appearance;
|
||||
},
|
||||
|
||||
title() {
|
||||
if ( this.action.t === 'inherit' )
|
||||
return this.t('setting.inheritance', 'Inheritance Point');
|
||||
|
||||
else if ( ! this.display )
|
||||
return this.t('setting.unknown', 'Unknown Value');
|
||||
|
||||
const def = this.data.actions[this.display.action];
|
||||
if ( ! def )
|
||||
return this.t('setting.actions.unknown', 'Unknown Action Type: %{action}', this.display);
|
||||
|
||||
if ( def.title ) {
|
||||
const data = this.getData(),
|
||||
out = maybe_call(def.title, this, data, def),
|
||||
i18n = def.title_i18n || `chat.actions.${this.display.action}`;
|
||||
|
||||
if ( out )
|
||||
return this.t(i18n, out, data);
|
||||
}
|
||||
|
||||
return this.t('setting.actions.untitled', 'Action: %{action}', this.display);
|
||||
|
||||
},
|
||||
|
||||
description() {
|
||||
if ( this.action.t === 'inherit' )
|
||||
return this.t('setting.inheritance.desc', 'Inherit values from lower priority profiles at this position.');
|
||||
|
||||
const def = this.display && this.data.actions[this.display.action];
|
||||
if ( ! def || ! def.description )
|
||||
return;
|
||||
|
||||
const data = this.getData(),
|
||||
out = maybe_call(def.description, this, data, def),
|
||||
i18n = def.description_i18n || `chat.actions.${this.display.action}.desc`;
|
||||
|
||||
if ( out )
|
||||
return this.t(i18n, out, data);
|
||||
},
|
||||
|
||||
visibility() {
|
||||
if ( ! this.display || ! this.display.appearance )
|
||||
return;
|
||||
|
||||
const disp = this.display.display, out = [];
|
||||
if ( ! disp )
|
||||
return this.t('setting.actions.visible.always', 'always');
|
||||
|
||||
if ( disp.disable )
|
||||
return this.t('setting.actions.visible.never', 'never');
|
||||
|
||||
if ( disp.mod === true )
|
||||
out.push(this.t('setting.actions.visible.mod', 'when moderator'));
|
||||
|
||||
else if ( disp.mod === false )
|
||||
out.push(this.t('setting.actions.visible.unmod', 'when not moderator'));
|
||||
|
||||
if ( disp.mod_icons === true )
|
||||
out.push(this.t('setting.actions.visible.mod_icons', 'when mod icons are shown'));
|
||||
|
||||
else if ( disp.mod_icons === false )
|
||||
out.push(this.t('setting.actions.visible.mod_icons_off', 'when mod icons are hidden'));
|
||||
|
||||
if ( disp.staff === true )
|
||||
out.push(this.t('setting.actions.visible.staff', 'when staff'));
|
||||
|
||||
else if ( disp.staff === false )
|
||||
out.push(this.t('setting.actions.visible.unstaff', 'when not staff'));
|
||||
|
||||
if ( disp.deleted === true )
|
||||
out.push(this.t('setting.actions.visible.deleted', 'if message deleted'));
|
||||
|
||||
else if ( disp.deleted === false )
|
||||
out.push(this.t('setting.actions.visible.undeleted', 'if message not deleted'));
|
||||
|
||||
return out.join(', ');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChangeAction(val) {
|
||||
for(const key in val)
|
||||
if ( has(val, key) ) {
|
||||
const v = val[key];
|
||||
if ( typeof v === 'string' && ! v.length )
|
||||
delete val[key];
|
||||
}
|
||||
|
||||
this.edit_data.options = val;
|
||||
},
|
||||
|
||||
edit() {
|
||||
if ( ! this.canEdit )
|
||||
return;
|
||||
|
||||
this.editing = true;
|
||||
this.edit_data = deep_copy(this.action.v);
|
||||
|
||||
if ( ! this.edit_data.options )
|
||||
this.edit_data.options = {};
|
||||
|
||||
if ( ! this.edit_data.display )
|
||||
this.edit_data.display = {};
|
||||
|
||||
if ( ! this.edit_data.appearance )
|
||||
this.edit_data.appearance = {};
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$emit('save', this.edit_data);
|
||||
this.cancel();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.editing = false;
|
||||
this.edit_data = null;
|
||||
},
|
||||
|
||||
getData() {
|
||||
const def = this.display && this.data.actions[this.display.action];
|
||||
if ( ! def )
|
||||
return;
|
||||
|
||||
return Object.assign({}, this.display, {
|
||||
options: Object.assign({}, def && def.defaults, this.display.options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
31
src/modules/main_menu/components/action-preview.vue
Normal file
31
src/modules/main_menu/components/action-preview.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template lang="html">
|
||||
<div
|
||||
:data-action="act.action"
|
||||
:data-options="act.options ? JSON.stringify(act.options) : null"
|
||||
:data-tip="act.appearance.tooltip"
|
||||
:class="{'ffz-tooltip': tooltip, 'tw-pd-05': pad, 'colored': color && color.length > 0}"
|
||||
data-tooltip-type="action"
|
||||
class="ffz-mod-icon mod-icon tw-c-text-alt-2 tw-font-size-4"
|
||||
>
|
||||
<component
|
||||
v-if="renderer && renderer.preview"
|
||||
:is="renderer.preview"
|
||||
:data="act.appearance"
|
||||
:color="color"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['act', 'color', 'tooltip', 'pad', 'renderers'],
|
||||
|
||||
computed: {
|
||||
renderer() {
|
||||
return this.renderers[this.act.appearance.type]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -5,7 +5,7 @@
|
|||
class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"
|
||||
>
|
||||
<span class="ffz-i-info" />
|
||||
{{ t('setting.badge-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-b-2 tw-align-right">
|
||||
|
|
428
src/modules/main_menu/components/chat-actions.vue
Normal file
428
src/modules/main_menu/components/chat-actions.vue
Normal file
|
@ -0,0 +1,428 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--chat-actions tw-border-t tw-pd-y-1">
|
||||
<div
|
||||
v-if="source && source !== profile"
|
||||
class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"
|
||||
>
|
||||
<span class="ffz-i-info" />
|
||||
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-b-1 tw-border-b tw-mg-b-1">
|
||||
<div class="tw-flex tw-align-items-center ffz--inline">
|
||||
{{ t('setting.actions.preview', 'Preview:') }}
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="as_mod"
|
||||
ref="as_mod"
|
||||
:checked="is_moderator"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="as_mod" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.mod', 'As Moderator') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="is_deleted"
|
||||
ref="is_deleted"
|
||||
:checked="is_deleted"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="is_deleted" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.deleted', 'Deleted Message') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="with_mod_icons"
|
||||
ref="with_mod_icons"
|
||||
:checked="with_mod_icons"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="with_mod_icons" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.mod_icons', 'With Mod Icons') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="show_all"
|
||||
ref="show_all"
|
||||
:checked="show_all"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="show_all" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.all', 'Show All') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:data-user="JSON.stringify(sample_user)"
|
||||
:data-room="JSON.stringify(sample_room)"
|
||||
class="tw-flex tw-align-items-center tw-justify-content-center tw-pd-t-1"
|
||||
data-msg-id="1234-5678"
|
||||
>
|
||||
<div
|
||||
v-if="! display.length"
|
||||
class="tw-c-text-alt-2 tw-pd-05 tw-font-size-4"
|
||||
>
|
||||
{{ t('setting.actions.no-visible', 'no visible actions') }}
|
||||
</div>
|
||||
|
||||
<action-preview
|
||||
v-for="act in display"
|
||||
:key="act.id"
|
||||
:act="act.v"
|
||||
:color="color(act.v.appearance.color)"
|
||||
:renderers="data.renderers"
|
||||
tooltip="true"
|
||||
pad="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center tw-pd-b-05">
|
||||
<div class="tw-flex-grow-1">
|
||||
{{ t('setting.actions.drag', 'Drag actions to re-order them.') }}
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="closeAdd"
|
||||
class="tw-relative"
|
||||
>
|
||||
<button
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="toggleAdd"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-plus">
|
||||
{{ t('setting.actions.new', 'New...') }}
|
||||
</span>
|
||||
<span class="tw-button__icon tw-button__icon--right">
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>
|
||||
</button>
|
||||
<balloon
|
||||
v-if="add_open"
|
||||
color="background-alt-2"
|
||||
dir="down-right"
|
||||
size="sm"
|
||||
>
|
||||
<simplebar classes="language-select-menu__balloon">
|
||||
<div class="tw-pd-y-1">
|
||||
<template v-for="(preset, idx) in presets">
|
||||
<div
|
||||
v-if="preset.divider"
|
||||
:key="idx"
|
||||
class="tw-mg-1 tw-border-b"
|
||||
/>
|
||||
<button
|
||||
v-else
|
||||
:key="idx"
|
||||
:disabled="preset.disabled"
|
||||
class="tw-interactable"
|
||||
@click="add(preset.value)"
|
||||
>
|
||||
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
|
||||
<div class="tw-flex-grow-1 tw-mg-r-1">
|
||||
{{ t(preset.title_i18n, preset.title, preset) }}
|
||||
</div>
|
||||
<action-preview v-if="preset.appearance" :act="preset" :renderers="data.renderers" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</simplebar>
|
||||
</balloon>
|
||||
</div>
|
||||
<button
|
||||
v-if="val.length"
|
||||
class="tw-mg-l-1 tw-button tw-button--text tw-tooltip-wrapper"
|
||||
@click="clear"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete-all', 'Delete All') }}
|
||||
</span>
|
||||
<span class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.actions.delete-all', "Delete all of this profile's actions.") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="! val.length"
|
||||
class="tw-mg-l-1 tw-button tw-button--text tw-tooltip-wrapper"
|
||||
@click="populate"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.actions.add-default', 'Add Defaults') }}
|
||||
</span>
|
||||
<span class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.actions.add-default-tip', 'Add all of the default actions to this profile.') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<section v-for="act in val" :key="act.id">
|
||||
<action-editor
|
||||
:action="act"
|
||||
:data="data"
|
||||
:inline="item.inline"
|
||||
@remove="remove(act)"
|
||||
@save="save(act, $event)"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
import Sortable from 'sortablejs';
|
||||
import {deep_copy} from 'utilities/object';
|
||||
import {mixin as clickaway} from 'vue-clickaway';
|
||||
|
||||
let last_id = 0;
|
||||
|
||||
|
||||
export default {
|
||||
mixins: [clickaway, SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_moderator: true,
|
||||
with_mod_icons: true,
|
||||
is_staff: false,
|
||||
is_deleted: false,
|
||||
show_all: false,
|
||||
|
||||
add_open: false,
|
||||
|
||||
sample_user: {
|
||||
displayName: 'SirStendec',
|
||||
login: 'sirstendec',
|
||||
id: 49399878
|
||||
},
|
||||
|
||||
sample_room: {
|
||||
displayName: 'FrankerFaceZ',
|
||||
login: 'frankerfacez',
|
||||
id: 46622312
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasInheritance() {
|
||||
for(const val of this.val)
|
||||
if ( val.t === 'inherit' )
|
||||
return true;
|
||||
},
|
||||
|
||||
presets() {
|
||||
const out = [];
|
||||
|
||||
out.push({
|
||||
disabled: this.hasInheritance,
|
||||
|
||||
title: 'Inheritance Point',
|
||||
title_i18n: 'setting.inheritance',
|
||||
value: {t: 'inherit'}
|
||||
});
|
||||
|
||||
if ( ! this.item.inline ) {
|
||||
out.push({
|
||||
title: 'New Line',
|
||||
value: {
|
||||
v: {type: 'new-line'}
|
||||
}
|
||||
});
|
||||
|
||||
out.push({
|
||||
title: 'Space (Small)',
|
||||
value: {
|
||||
v: {type: 'space-small'}
|
||||
}
|
||||
});
|
||||
|
||||
out.push({
|
||||
title: 'Space (Expanding)',
|
||||
value: {
|
||||
v: {type: 'space'}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
out.push({divider: true});
|
||||
|
||||
for(const key in this.data.actions) { // eslint-disable-line guard-for-in
|
||||
const act = this.data.actions[key];
|
||||
if ( act && act.presets )
|
||||
for(const preset of act.presets) {
|
||||
if ( typeof act.title !== 'string' && ! preset.title )
|
||||
continue;
|
||||
|
||||
out.push(Object.assign({
|
||||
action: key,
|
||||
title: act.title,
|
||||
title_i18n: act.title_i18n || `chat.actions.${key}`,
|
||||
value: {
|
||||
v: Object.assign({
|
||||
action: key
|
||||
}, preset)
|
||||
}
|
||||
}, preset));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
display() {
|
||||
const out = [];
|
||||
|
||||
if ( this.val )
|
||||
for(const val of this.val) {
|
||||
if ( val.v && this.displayAction(val.v) )
|
||||
out.push(val);
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
val() {
|
||||
if ( ! this.has_value )
|
||||
return [];
|
||||
|
||||
return this.value.map(x => {
|
||||
x.id = x.id || `${Date.now()}-${Math.random()}-${last_id++}`;
|
||||
return x;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this._sortable = Sortable.create(this.$refs.list, {
|
||||
draggable: 'section',
|
||||
filter: 'button',
|
||||
|
||||
onUpdate: event => {
|
||||
if ( event.newIndex === event.oldIndex )
|
||||
return;
|
||||
|
||||
const new_val = Array.from(this.val);
|
||||
new_val.splice(event.newIndex, 0, ...new_val.splice(event.oldIndex, 1));
|
||||
|
||||
this.set(new_val);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if ( this._sortable )
|
||||
this._sortable.destroy();
|
||||
|
||||
this._sortable = null;
|
||||
},
|
||||
|
||||
methods: {
|
||||
closeAdd() {
|
||||
this.add_open = false;
|
||||
},
|
||||
|
||||
toggleAdd() {
|
||||
this.add_open = ! this.add_open;
|
||||
},
|
||||
|
||||
populate() {
|
||||
this.set(deep_copy(this.default_value));
|
||||
},
|
||||
|
||||
add(val) {
|
||||
const vals = Array.from(this.val);
|
||||
vals.push(val);
|
||||
this.set(deep_copy(vals));
|
||||
this.add_open = false;
|
||||
},
|
||||
|
||||
remove(val) {
|
||||
const vals = Array.from(this.val),
|
||||
idx = vals.indexOf(val);
|
||||
|
||||
if ( idx !== -1 ) {
|
||||
vals.splice(idx, 1);
|
||||
if ( vals.length )
|
||||
this.set(vals);
|
||||
else
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
|
||||
save(val, new_val) {
|
||||
val.v = new_val;
|
||||
this.set(deep_copy(this.val));
|
||||
},
|
||||
|
||||
onPreview() {
|
||||
this.show_all = this.$refs.show_all.checked;
|
||||
this.is_moderator = this.$refs.as_mod.checked;
|
||||
this.is_staff = false; //this.$refs.as_staff.checked;
|
||||
this.with_mod_icons = this.$refs.with_mod_icons.checked;
|
||||
this.is_deleted = this.$refs.is_deleted.checked;
|
||||
},
|
||||
|
||||
displayAction(action) {
|
||||
if ( ! action.appearance )
|
||||
return false;
|
||||
|
||||
const disp = action.display;
|
||||
if ( ! disp || this.show_all )
|
||||
return true;
|
||||
|
||||
if ( disp.disable )
|
||||
return false;
|
||||
|
||||
if ( disp.mod != null && disp.mod !== this.is_moderator )
|
||||
return false;
|
||||
|
||||
if ( disp.mod_icons != null && disp.mod !== this.with_mod_icons )
|
||||
return false;
|
||||
|
||||
if ( disp.staff != null && disp.mod !== this.is_staff )
|
||||
return false;
|
||||
|
||||
if ( disp.deleted != null && disp.deleted !== this.is_deleted )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
color(input) {
|
||||
if ( ! input )
|
||||
return input;
|
||||
|
||||
return this.data.color(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
106
src/modules/main_menu/components/color-picker.vue
Normal file
106
src/modules/main_menu/components/color-picker.vue
Normal file
|
@ -0,0 +1,106 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--color-widget tw-relative tw-full-width tw-mg-y-05">
|
||||
<input
|
||||
ref="input"
|
||||
v-bind="$attrs"
|
||||
v-model="color"
|
||||
type="text"
|
||||
class="tw-input tw-pd-r-3"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
@input="onChange"
|
||||
>
|
||||
|
||||
<button
|
||||
class="ffz-color-preview tw-absolute tw-top-0 tw-bottom-0 tw-right-0 tw-border-l tw-z-default"
|
||||
@click="togglePicker"
|
||||
>
|
||||
<figure v-if="color" :style="`background-color: ${color}`" />
|
||||
<figure v-else class="ffz-i-eyedropper" />
|
||||
</button>
|
||||
<div v-on-clickaway="closePicker" v-if="open" class="tw-absolute tw-z-default tw-right-0">
|
||||
<chrome-picker :value="colors" @input="onPick" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {Color} from 'utilities/color';
|
||||
|
||||
import {Chrome} from 'vue-color';
|
||||
import {mixin as clickaway} from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'chrome-picker': Chrome
|
||||
},
|
||||
|
||||
mixins: [clickaway],
|
||||
|
||||
props: {
|
||||
value: String,
|
||||
default: {
|
||||
type: String,
|
||||
default: '#000'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
color: '',
|
||||
open: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
colors() {
|
||||
return Color.RGBA.fromCSS(this.color || this.default)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val) {
|
||||
this.color = val;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.color = this.value;
|
||||
},
|
||||
|
||||
methods: {
|
||||
openPicker() {
|
||||
this.open = true;
|
||||
},
|
||||
|
||||
closePicker() {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
togglePicker() {
|
||||
this.open = ! this.open;
|
||||
},
|
||||
|
||||
onPick(color) {
|
||||
const old_val = this.color;
|
||||
|
||||
if ( color.rgba.a == 1 )
|
||||
this.color = color.hex;
|
||||
else {
|
||||
const c = color.rgba;
|
||||
this.color = `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
|
||||
}
|
||||
|
||||
if ( old_val !== this.color )
|
||||
this.$emit('input', this.color);
|
||||
},
|
||||
|
||||
onChange() {
|
||||
this.$emit('input', this.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,25 +1,25 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--experiments tw-pd-t-05">
|
||||
<div class="tw-pd-b-1 tw-mg-b-1 tw-border-b">
|
||||
{{ t('settings.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
|
||||
{{ t('setting.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
|
||||
<div class="tw-flex-grow-1">
|
||||
{{ t('settings.experiments.unique-id', 'Unique ID: %{id}', {id: unique_id}) }}
|
||||
{{ t('setting.experiments.unique-id', 'Unique ID: %{id}', {id: unique_id}) }}
|
||||
</div>
|
||||
<select
|
||||
ref="sort_select"
|
||||
class="tw-mg-x-05 tw-select tw-display-line tw-width-auto"
|
||||
@change="onSort"
|
||||
>
|
||||
<option :selected="sort_by === 0">{{ t('settings.experiments.sort-name', 'Sort By: Name') }}</option>
|
||||
<option :selected="sort_by === 1">{{ t('settings.experiments.sort-rarity', 'Sort By: Rarity') }}</option>
|
||||
<option :selected="sort_by === 0">{{ t('setting.experiments.sort-name', 'Sort By: Name') }}</option>
|
||||
<option :selected="sort_by === 1">{{ t('setting.experiments.sort-rarity', 'Sort By: Rarity') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h3 class="tw-mg-b-1">
|
||||
{{ t('settings.experiments.ffz', 'FrankerFaceZ Experiments') }}
|
||||
{{ t('setting.experiments.ffz', 'FrankerFaceZ Experiments') }}
|
||||
</h3>
|
||||
|
||||
<div class="ffz--experiment-list">
|
||||
|
@ -48,7 +48,7 @@
|
|||
:key="idx"
|
||||
:selected="i.value === exp.value"
|
||||
>
|
||||
{{ t('settings.experiments.entry', '%{value} (weight: %{weight})', i) }}
|
||||
{{ t('setting.experiments.entry', '%{value} (weight: %{weight})', i) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
|
@ -67,12 +67,12 @@
|
|||
</div>
|
||||
</section>
|
||||
<div v-if="! Object.keys(ffz_data).length">
|
||||
{{ t('settings.experiments.none', 'There are no current experiments.') }}
|
||||
{{ t('setting.experiments.none', 'There are no current experiments.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="tw-mg-t-5 tw-mg-b-1">
|
||||
{{ t('settings.experiments.twitch', 'Twitch Experiments') }}
|
||||
{{ t('setting.experiments.twitch', 'Twitch Experiments') }}
|
||||
</h3>
|
||||
|
||||
<div class="ffz--experiment-list">
|
||||
|
@ -115,7 +115,7 @@
|
|||
v-if="exp.in_use === false"
|
||||
:selected="exp.default"
|
||||
>
|
||||
{{ t('settings.experiments.unset', 'unset') }}
|
||||
{{ t('setting.experiments.unset', 'unset') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(i, idx) in exp.groups"
|
||||
|
@ -141,7 +141,7 @@
|
|||
</div>
|
||||
</section>
|
||||
<div v-if="! Object.keys(twitch_data).length">
|
||||
{{ t('settings.experiments.none', 'There are no current experiments.') }}
|
||||
{{ t('setting.experiments.none', 'There are no current experiments.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -38,18 +38,14 @@
|
|||
/>
|
||||
</header>
|
||||
<div class="tw-full-width tw-full-height tw-overflow-hidden tw-flex tw-flex-nowrap tw-relative">
|
||||
<div class="ffz-vertical-nav__items tw-full-width tw-flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-tree
|
||||
:current-item="currentItem"
|
||||
:modal="nav"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<simplebar classes="ffz-vertical-nav__items tw-full-width tw-flex-grow-1">
|
||||
<menu-tree
|
||||
:current-item="currentItem"
|
||||
:modal="nav"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</simplebar>
|
||||
</div>
|
||||
<footer class="tw-c-text-alt tw-border-t tw-pd-1">
|
||||
<div>
|
||||
|
@ -60,20 +56,16 @@
|
|||
</div>
|
||||
</footer>
|
||||
</nav>
|
||||
<main class="tw-flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-page
|
||||
v-if="currentItem"
|
||||
ref="page"
|
||||
:context="context"
|
||||
:item="currentItem"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<simplebar classes="tw-flex-grow-1">
|
||||
<menu-page
|
||||
v-if="currentItem"
|
||||
ref="page"
|
||||
:context="context"
|
||||
:item="currentItem"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</simplebar>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@click="save"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-floppy">
|
||||
{{ t('settings.profiles.save', 'Save') }}
|
||||
{{ t('setting.save', 'Save') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
@ -17,24 +17,24 @@
|
|||
@click="del"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.profiles.delete', 'Delete') }}
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
<!--button class="tw-mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-download">
|
||||
{{ t('setting.profiles.export', 'Export') }}
|
||||
{{ t('setting.export', 'Export') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container tw-border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.general', 'General') }}
|
||||
{{ t('setting.data_management.profiles.edit.general', 'General') }}
|
||||
</header>
|
||||
|
||||
<div class="ffz--widget tw-flex tw-flex-nowrap">
|
||||
<label for="ffz:editor:name">
|
||||
{{ t('settings.data_management.profiles.edit.name', 'Name') }}
|
||||
{{ t('setting.data_management.profiles.edit.name', 'Name') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
|
@ -47,7 +47,7 @@
|
|||
|
||||
<div class="ffz--widget tw-flex tw-flex-nowrap">
|
||||
<label for="ffz:editor:description">
|
||||
{{ t('settings.data_management.profiles.edit.desc', 'Description') }}
|
||||
{{ t('setting.data_management.profiles.edit.desc', 'Description') }}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
|
@ -61,10 +61,10 @@
|
|||
|
||||
<div class="ffz--menu-container tw-border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.rules', 'Rules') }}
|
||||
{{ t('setting.data_management.profiles.edit.rules', 'Rules') }}
|
||||
</header>
|
||||
<section class="tw-pd-b-1">
|
||||
{{ t('settings.data_management.profiles.edit.rules.description',
|
||||
{{ t('setting.data_management.profiles.edit.rules.description',
|
||||
'Rules allows you to define a series of conditions under which this profile will be active.')
|
||||
}}
|
||||
</section>
|
||||
|
@ -144,7 +144,7 @@ export default {
|
|||
del() {
|
||||
if ( this.item.profile || this.unsaved ) {
|
||||
if ( ! confirm(this.t( // eslint-disable-line no-alert
|
||||
'settings.profiles.warn-delete',
|
||||
'setting.profiles.warn-delete',
|
||||
'Are you sure you wish to delete this profile? It cannot be undone.'
|
||||
)) )
|
||||
return
|
||||
|
@ -192,7 +192,7 @@ export default {
|
|||
if ( this.unsaved )
|
||||
return confirm( // eslint-disable-line no-alert
|
||||
this.t(
|
||||
'settings.warn-unsaved',
|
||||
'setting.warn-unsaved',
|
||||
'You have unsaved changes. Are you sure you want to leave the editor?'
|
||||
));
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</button>
|
||||
<!--button class="tw-mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-upload">
|
||||
{{ t('setting.profiles.import', 'Import…') }}
|
||||
{{ t('setting.import', 'Import…') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
@ -48,7 +48,7 @@
|
|||
<div class="tw-flex tw-flex-shrink-0 tw-align-items-center">
|
||||
<button class="tw-button tw-button--text" disabled @notclick="edit(p)">
|
||||
<span class="tw-button__text ffz-i-cog">
|
||||
{{ t('setting.profiles.configure', 'Configure') }}
|
||||
{{ t('setting.configure', 'Configure') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -346,7 +346,8 @@ export default class Metadata extends Module {
|
|||
|
||||
tooltipClass: 'ffz-metadata-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background',
|
||||
// Hide the arrow for now, until we re-do our CSS to make it render correctly.
|
||||
arrowClass: 'tw-balloon__tail tw-overflow-hidden tw-absolute tw-hidden',
|
||||
arrowClass: 'tw-balloon__tail tw-overflow-hidden tw-absolute',
|
||||
arrowInner: 'tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background tw-absolute',
|
||||
innerClass: 'tw-pd-1',
|
||||
|
||||
popper: {
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class TooltipProvider extends Module {
|
|||
const container = document.querySelector('.twilight-root,.twilight-minimal-root') || document.body,
|
||||
is_minimal = container && container.classList.contains('twilight-minimal-root');
|
||||
|
||||
this.tips = new Tooltip(is_minimal ? '.twilight-minimal-root' : 'body #root', 'ffz-tooltip', {
|
||||
this.tips = new Tooltip(is_minimal ? '.twilight-minimal-root,body' : 'body #root,body', 'ffz-tooltip', {
|
||||
html: true,
|
||||
delayHide: this.checkDelayHide.bind(this),
|
||||
delayShow: this.checkDelayShow.bind(this),
|
||||
|
|
|
@ -33,7 +33,7 @@ export const object_merge = {
|
|||
values.unshift(val);
|
||||
}
|
||||
|
||||
if ( values.length )
|
||||
if ( sources.length )
|
||||
return [
|
||||
Object.assign({}, ...values),
|
||||
sources
|
||||
|
@ -57,6 +57,8 @@ export const array_merge = {
|
|||
trailing = [],
|
||||
sources = [];
|
||||
|
||||
let had_value = false;
|
||||
|
||||
for(const profile of profiles)
|
||||
if ( profile.has(key) ) {
|
||||
const value = profile.get(key);
|
||||
|
@ -68,6 +70,7 @@ export const array_merge = {
|
|||
sources.push(profile.id);
|
||||
let is_trailing = false;
|
||||
for(const val of value) {
|
||||
had_value = true;
|
||||
if ( val.t === 'inherit' )
|
||||
is_trailing = true;
|
||||
else if ( is_trailing )
|
||||
|
@ -81,7 +84,7 @@ export const array_merge = {
|
|||
break;
|
||||
}
|
||||
|
||||
if ( values.length || trailing.length )
|
||||
if ( had_value )
|
||||
return [
|
||||
values.concat(trailing),
|
||||
sources
|
||||
|
|
|
@ -12,6 +12,7 @@ import FineRouter from 'utilities/compat/fine-router';
|
|||
import Apollo from 'utilities/compat/apollo';
|
||||
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
import MAIN_URL from 'site/styles/main.scss';
|
||||
|
||||
|
@ -59,7 +60,8 @@ export default class Twilight extends BaseSite {
|
|||
document.head.appendChild(createElement('link', {
|
||||
href: MAIN_URL,
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css'
|
||||
type: 'text/css',
|
||||
crossOrigin: 'anonymouse'
|
||||
}));
|
||||
|
||||
// Check for ?ffz-settings in page and open the
|
||||
|
@ -93,8 +95,11 @@ export default class Twilight extends BaseSite {
|
|||
}
|
||||
|
||||
getUser() {
|
||||
if ( this._user )
|
||||
return this._user;
|
||||
|
||||
const session = this.getSession();
|
||||
return session && session.user;
|
||||
return this._user = session && session.user;
|
||||
}
|
||||
|
||||
getCore() {
|
||||
|
@ -119,7 +124,7 @@ Twilight.KNOWN_MODULES = {
|
|||
'core-2': n => n.p && n.p.experiments,
|
||||
cookie: n => n && n.set && n.get && n.getJSON && n.withConverter,
|
||||
'extension-service': n => n.extensionService,
|
||||
'chat-types': n => n.a && n.a.PostWithMention,
|
||||
'chat-types': n => n.b && has(n.b, 'Message') && has(n.b, 'RoomMods'),
|
||||
'gql-printer': n => n !== window && n.print
|
||||
}
|
||||
|
||||
|
|
81
src/sites/twitch-twilight/modules/channel.js
Normal file
81
src/sites/twitch-twilight/modules/channel.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Channel
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
|
||||
export default class Channel extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('site.fine');
|
||||
|
||||
this.settings.add('channel.hosting.enable', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Channel > Behavior >> Hosting',
|
||||
title: 'Enable Channel Hosting',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.updateChannelHosting(val)
|
||||
});
|
||||
|
||||
this.ChannelPage = this.fine.define(
|
||||
'channel-page',
|
||||
n => n.handleHostingChange,
|
||||
['user']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
this.ChannelPage.on('mount', this.wrapChannelPage, this);
|
||||
|
||||
this.ChannelPage.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.wrapChannelPage(inst);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
wrapChannelPage(inst) {
|
||||
if ( inst._ffz_hosting_wrapped )
|
||||
return;
|
||||
|
||||
const t = this;
|
||||
|
||||
inst._ffz_hosting_wrapped = true;
|
||||
|
||||
inst.ffzOldHostHandler = inst.handleHostingChange;
|
||||
inst.handleHostingChange = function(channel) {
|
||||
inst.ffzExpectedHost = channel;
|
||||
|
||||
if ( t.settings.get('channel.hosting.enable') )
|
||||
return inst.ffzOldHostHandler(channel);
|
||||
}
|
||||
|
||||
// Store the current state and disable the current host if needed.
|
||||
inst.ffzExpectedHost = inst.state.isHosting ? inst.state.videoPlayerSource : null;
|
||||
if ( ! this.settings.get('channel.hosting.enable') )
|
||||
inst.ffzOldHostHandler(null);
|
||||
|
||||
// Finally, we force an update so that any child components
|
||||
// receive our updated handler.
|
||||
inst.forceUpdate();
|
||||
}
|
||||
|
||||
|
||||
updateChannelHosting(val) {
|
||||
if ( val === undefined )
|
||||
val = this.settings.get('channel.hosting.enable');
|
||||
|
||||
for(const inst of this.ChannelPage.instances)
|
||||
inst.ffzOldHostHandler(val ? inst.ffzExpectedHost : null);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
// Chat Hooks
|
||||
// ============================================================================
|
||||
|
||||
import {ColorAdjuster} from 'utilities/color';
|
||||
import {ColorAdjuster, Color} from 'utilities/color';
|
||||
import {setChildren} from 'utilities/dom';
|
||||
import {has, split_chars} from 'utilities/object';
|
||||
|
||||
|
@ -19,75 +19,79 @@ import EmoteMenu from './emote_menu';
|
|||
import TabCompletion from './tab_completion';
|
||||
|
||||
|
||||
const CHAT_TYPES = (e => {
|
||||
const MESSAGE_TYPES = ((e = {}) => {
|
||||
e[e.Post = 0] = 'Post';
|
||||
e[e.Action = 1] = 'Action';
|
||||
e[e.PostWithMention = 2] = 'PostWithMention';
|
||||
e[e.Ban = 3] = 'Ban';
|
||||
e[e.Timeout = 4] = 'Timeout';
|
||||
e[e.AutoModRejectedPrompt = 5] = 'AutoModRejectedPrompt';
|
||||
e[e.AutoModMessageRejected = 6] = 'AutoModMessageRejected';
|
||||
e[e.AutoModMessageAllowed = 7] = 'AutoModMessageAllowed';
|
||||
e[e.AutoModMessageDenied = 8] = 'AutoModMessageDenied';
|
||||
e[e.Connected = 9] = 'Connected';
|
||||
e[e.Disconnected = 10] = 'Disconnected';
|
||||
e[e.Reconnect = 11] = 'Reconnect';
|
||||
e[e.Hosting = 12] = 'Hosting';
|
||||
e[e.Unhost = 13] = 'Unhost';
|
||||
e[e.Subscription = 14] = 'Subscription';
|
||||
e[e.Resubscription = 15] = 'Resubscription';
|
||||
e[e.SubGift = 16] = 'SubGift';
|
||||
e[e.Clear = 17] = 'Clear';
|
||||
e[e.SubscriberOnlyMode = 18] = 'SubscriberOnlyMode';
|
||||
e[e.FollowerOnlyMode = 19] = 'FollowerOnlyMode';
|
||||
e[e.SlowMode = 20] = 'SlowMode';
|
||||
e[e.RoomMods = 21] = 'RoomMods';
|
||||
e[e.RoomState = 22] = 'RoomState';
|
||||
e[e.Raid = 23] = 'Raid';
|
||||
e[e.Unraid = 24] = 'Unraid';
|
||||
e[e.Notice = 25] = 'Notice';
|
||||
e[e.Info = 26] = 'Info';
|
||||
e[e.BadgesUpdated = 27] = 'BadgesUpdated';
|
||||
e[e.Purchase = 28] = 'Purchase';
|
||||
return e;
|
||||
})({});
|
||||
})();
|
||||
|
||||
|
||||
const MOD_TYPES = ((e = {}) => {
|
||||
e[e.Ban = 0] = 'Ban';
|
||||
e[e.Timeout = 1] = 'Timeout';
|
||||
return e;
|
||||
})();
|
||||
|
||||
|
||||
const AUTOMOD_TYPES = ((e = {}) => {
|
||||
e[e.MessageRejectedPrompt = 0] = 'MessageRejectedPrompt';
|
||||
e[e.MessageRejected = 1] = 'MessageRejected';
|
||||
e[e.MessageAllowed = 2] = 'MessageAllowed';
|
||||
e[e.MessageDenied = 3] = 'MessageDenied';
|
||||
return e;
|
||||
})();
|
||||
|
||||
|
||||
const CHAT_TYPES = ((e = {}) => {
|
||||
e[e.Message = 0] = 'Message';
|
||||
e[e.Moderation = 1] = 'Moderation';
|
||||
e[e.ModerationAction = 2] = 'ModerationAction';
|
||||
e[e.TargetedModerationAction = 3] = 'TargetedModerationAction';
|
||||
e[e.AutoMod = 4] = 'AutoMod';
|
||||
e[e.Connected = 5] = 'Connected';
|
||||
e[e.Disconnected = 6] = 'Disconnected';
|
||||
e[e.Reconnect = 7] = 'Reconnect';
|
||||
e[e.Hosting = 8] = 'Hosting';
|
||||
e[e.Unhost = 9] = 'Unhost';
|
||||
e[e.Hosted = 10] = 'Hosted';
|
||||
e[e.Subscription = 11] = 'Subscription';
|
||||
e[e.Resubscription = 12] = 'Resubscription';
|
||||
e[e.SubGift = 13] = 'SubGift';
|
||||
e[e.Clear = 14] = 'Clear';
|
||||
e[e.SubscriberOnlyMode = 15] = 'SubscriberOnlyMode';
|
||||
e[e.FollowerOnlyMode = 16] = 'FollowerOnlyMode';
|
||||
e[e.SlowMode = 17] = 'SlowMode';
|
||||
e[e.EmoteOnlyMode = 18] = 'EmoteOnlyMode';
|
||||
e[e.RoomMods = 19] = 'RoomMods';
|
||||
e[e.RoomState = 20] = 'RoomState';
|
||||
e[e.Raid = 21] = 'Raid';
|
||||
e[e.Unraid = 22] = 'Unraid';
|
||||
e[e.Ritual = 23] = 'Ritual';
|
||||
e[e.Notice = 24] = 'Notice';
|
||||
e[e.Info = 25] = 'Info';
|
||||
e[e.BadgesUpdated = 26] = 'BadgesUpdated';
|
||||
e[e.Purchase = 27] = 'Purchase';
|
||||
e[e.BitsCharity = 28] = 'BitsCharity';
|
||||
e[e.CrateGift = 29] = 'CrateGift'
|
||||
return e;
|
||||
})();
|
||||
|
||||
|
||||
const NULL_TYPES = [
|
||||
'Reconnect',
|
||||
'RoomState',
|
||||
'BadgesUpdated'
|
||||
'BadgesUpdated',
|
||||
'Clear'
|
||||
];
|
||||
|
||||
|
||||
const EVENTS = [
|
||||
'onJoinedEvent',
|
||||
'onDisconnectedEvent',
|
||||
'onReconnectingEvent',
|
||||
'onChatMessageEvent',
|
||||
'onChatNoticeEvent',
|
||||
'onChatActionEvent',
|
||||
const MISBEHAVING_EVENTS = [
|
||||
'onBitsCharityEvent',
|
||||
//'onRitualEvent', -- handled by conversion to Message event
|
||||
'onBadgesUpdatedEvent',
|
||||
'onHostingEvent',
|
||||
'onUnhostEvent',
|
||||
'onPurchaseEvent',
|
||||
'onCrateEvent',
|
||||
//'onRitualEvent',
|
||||
'onSubscriptionEvent',
|
||||
//'onResubscriptionEvent',
|
||||
'onSubscriptionGiftEvent',
|
||||
'onTimeoutEvent',
|
||||
'onBanEvent',
|
||||
'onClearChatEvent',
|
||||
'onRaidEvent',
|
||||
'onUnraidEvent',
|
||||
'onRoomModsEvent',
|
||||
'onRoomStateEvent',
|
||||
'onFollowerOnlyModeEvent',
|
||||
'onSlowModeEvent',
|
||||
'onSubscriberOnlyModeEvent',
|
||||
'onEmoteOnlyModeEvent',
|
||||
'onBitsCharityEvent'
|
||||
'onCrateEvent'
|
||||
];
|
||||
|
||||
|
||||
|
@ -98,6 +102,7 @@ export default class ChatHook extends Module {
|
|||
this.should_enable = true;
|
||||
|
||||
this.colors = new ColorAdjuster;
|
||||
this.inverse_colors = new ColorAdjuster;
|
||||
|
||||
this.inject('settings');
|
||||
|
||||
|
@ -134,6 +139,12 @@ export default class ChatHook extends Module {
|
|||
Twilight.CHAT_ROUTES
|
||||
);
|
||||
|
||||
this.RoomPicker = this.fine.define(
|
||||
'chat-picker',
|
||||
n => n.closeRoomPicker && n.handleRoomSelect,
|
||||
Twilight.CHAT_ROUTES
|
||||
);
|
||||
|
||||
|
||||
// Settings
|
||||
|
||||
|
@ -262,7 +273,8 @@ export default class ChatHook extends Module {
|
|||
const is_dark = this.chat.context.get('theme.is-dark'),
|
||||
mode = this.chat.context.get('chat.adjustment-mode'),
|
||||
contrast = this.chat.context.get('chat.adjustment-contrast'),
|
||||
c = this.colors;
|
||||
c = this.colors,
|
||||
ic = this.inverse_colors;
|
||||
|
||||
// TODO: Get the background color from the theme system.
|
||||
// Updated: Use the lightest/darkest colors from alternating rows for better readibility.
|
||||
|
@ -270,6 +282,10 @@ export default class ChatHook extends Module {
|
|||
c.mode = mode;
|
||||
c.contrast = contrast;
|
||||
|
||||
ic._base = is_dark ? '#dad8de' : '#19171c';
|
||||
ic.mode = mode;
|
||||
ic.contrast = is_dark ? 13 : 16;
|
||||
|
||||
this.updateChatLines();
|
||||
}
|
||||
|
||||
|
@ -313,14 +329,20 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
this.on('site.web_munch:loaded', () => {
|
||||
const ct = this.web_munch.getModule('chat-types');
|
||||
this.chat_types = ct && ct.a || CHAT_TYPES;
|
||||
})
|
||||
|
||||
grabTypes() {
|
||||
const ct = this.web_munch.getModule('chat-types');
|
||||
this.chat_types = ct && ct.a || CHAT_TYPES;
|
||||
if ( ct ) {
|
||||
this.automod_types = ct && ct.a || AUTOMOD_TYPES;
|
||||
this.chat_types = ct && ct.b || CHAT_TYPES;
|
||||
this.message_types = ct && ct.c || MESSAGE_TYPES;
|
||||
this.mod_types = ct && ct.e || MOD_TYPES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
this.on('site.web_munch:loaded', this.grabTypes);
|
||||
this.grabTypes();
|
||||
|
||||
this.chat.context.on('changed:chat.width', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
|
||||
|
@ -447,6 +469,19 @@ export default class ChatHook extends Module {
|
|||
for(const inst of instances)
|
||||
this.fixPinnedCheer(inst);
|
||||
});
|
||||
|
||||
|
||||
this.RoomPicker.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.closeRoomPicker(inst);
|
||||
});
|
||||
|
||||
this.RoomPicker.on('mount', this.closeRoomPicker, this);
|
||||
}
|
||||
|
||||
|
||||
closeRoomPicker(inst) { // eslint-disable-line class-methods-use-this
|
||||
inst.closeRoomPicker();
|
||||
}
|
||||
|
||||
|
||||
|
@ -480,18 +515,52 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
|
||||
|
||||
sendMessage(room, message) {
|
||||
const controller = this.ChatController.first,
|
||||
service = controller && controller.chatService;
|
||||
|
||||
if ( ! service )
|
||||
return null;
|
||||
|
||||
if ( room.startsWith('#') )
|
||||
room = room.slice(1);
|
||||
|
||||
if ( room.toLowerCase() !== service.channelLogin.toLowerCase() )
|
||||
return null;
|
||||
|
||||
service.sendMessage(message);
|
||||
}
|
||||
|
||||
|
||||
wrapChatService(cls) {
|
||||
const t = this,
|
||||
old_handler = cls.prototype.connectHandlers;
|
||||
old_handler = cls.prototype.connectHandlers,
|
||||
old_send = cls.prototype.sendMessage;
|
||||
|
||||
cls.prototype._ffz_was_here = true;
|
||||
|
||||
|
||||
cls.prototype.sendMessage = function(raw_msg) {
|
||||
const msg = raw_msg.replace(/\n/g, '');
|
||||
|
||||
if ( msg.startsWith('/ffz') ) {
|
||||
this.postMessage({
|
||||
type: t.chat_types.Notice,
|
||||
message: 'The /ffz command is not yet re-implemented.'
|
||||
})
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return old_send.call(this, msg);
|
||||
}
|
||||
|
||||
|
||||
cls.prototype.connectHandlers = function(...args) {
|
||||
if ( ! this._ffz_init ) {
|
||||
const i = this,
|
||||
pm = this.postMessage;
|
||||
const i = this;
|
||||
|
||||
for(const key of EVENTS) {
|
||||
for(const key of MISBEHAVING_EVENTS) {
|
||||
const original = this[key];
|
||||
if ( original )
|
||||
this[key] = function(e, t) {
|
||||
|
@ -510,10 +579,7 @@ export default class ChatHook extends Module {
|
|||
out.sub_months = e.months;
|
||||
out.sub_plan = e.methods;
|
||||
|
||||
i._wrapped = e;
|
||||
const ret = i.postMessage(out);
|
||||
i._wrapped = null;
|
||||
return ret;
|
||||
return i.postMessageToCurrentChannel(e, out);
|
||||
|
||||
} catch(err) {
|
||||
t.log.capture(err, {extra: e});
|
||||
|
@ -528,10 +594,7 @@ export default class ChatHook extends Module {
|
|||
out.ffz_type = 'ritual';
|
||||
out.ritual = e.type;
|
||||
|
||||
i._wrapped = e;
|
||||
const ret = i.postMessage(out);
|
||||
i._wrapped = null;
|
||||
return ret;
|
||||
return i.postMessageToCurrentChannel(e, out);
|
||||
|
||||
} catch(err) {
|
||||
t.log.capture(err, {extra: e});
|
||||
|
@ -551,38 +614,13 @@ export default class ChatHook extends Module {
|
|||
return old_unhost.call(i, e, _t);
|
||||
}
|
||||
|
||||
const old_post = this.postMessage;
|
||||
this.postMessage = function(e) {
|
||||
const original = this._wrapped;
|
||||
if ( original ) {
|
||||
// Check that the message is relevant to this channel.
|
||||
if ( original.channel && this.channelLogin && original.channel.slice(1) !== this.channelLogin.toLowerCase() )
|
||||
return;
|
||||
if ( original && ! e._ffz_checked )
|
||||
return this.postMessageToCurrentChannel(original, e);
|
||||
|
||||
const c = e.channel = original.channel;
|
||||
if ( c )
|
||||
e.roomLogin = c.charAt(0) === '#' ? c.slice(1) : c;
|
||||
|
||||
if ( original.message ) {
|
||||
const u = original.message.user;
|
||||
if ( u )
|
||||
e.emotes = u.emotes;
|
||||
|
||||
if ( typeof original.action === 'string' )
|
||||
e.message = original.action;
|
||||
else
|
||||
e.message = original.message.body;
|
||||
|
||||
// Twitch doesn't generate a proper emote tag for echoed back
|
||||
// actions, so we have to regenerate it. Fun. :D
|
||||
if ( u && u.username === i.userLogin )
|
||||
e.emotes = findEmotes(e.message, i.selfEmotes);
|
||||
}
|
||||
|
||||
//e.original = original;
|
||||
}
|
||||
|
||||
//t.log.info('postMessage', e);
|
||||
return pm.call(this, e);
|
||||
return old_post.call(this, e);
|
||||
}
|
||||
|
||||
this._ffz_init = true;
|
||||
|
@ -590,6 +628,39 @@ export default class ChatHook extends Module {
|
|||
|
||||
return old_handler.apply(this, ...args);
|
||||
}
|
||||
|
||||
cls.prototype.postMessageToCurrentChannel = function(original, message) {
|
||||
message._ffz_checked = true;
|
||||
|
||||
if ( original.channel ) {
|
||||
let chan = message.channel = original.channel.toLowerCase();
|
||||
if ( chan.startsWith('#') )
|
||||
chan = chan.slice(1);
|
||||
|
||||
if ( chan !== this.channelLogin.toLowerCase() )
|
||||
return;
|
||||
|
||||
message.roomLogin = chan;
|
||||
}
|
||||
|
||||
if ( original.message ) {
|
||||
const user = original.message.user;
|
||||
if ( user )
|
||||
message.emotes = user.emotes;
|
||||
|
||||
if ( typeof original.action === 'string' )
|
||||
message.message = original.action;
|
||||
else
|
||||
message.message = original.message.body;
|
||||
|
||||
// Twitch doesn't generate a proper emote tag for echoed back
|
||||
// actions, so we have to regenerate it. Fun. :D
|
||||
if ( user && user.username === this.userLogin )
|
||||
message.emotes = findEmotes(message.message, this.selfEmotes);
|
||||
}
|
||||
|
||||
this.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import Twilight from 'site';
|
||||
import Module from 'utilities/module';
|
||||
//import {Color} from 'utilities/color';
|
||||
|
||||
import RichContent from './rich_content';
|
||||
|
||||
|
@ -23,10 +22,13 @@ export default class ChatLine extends Module {
|
|||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
this.inject('chat');
|
||||
this.inject('site');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.web_munch');
|
||||
this.inject(RichContent);
|
||||
|
||||
this.inject('chat.actions');
|
||||
|
||||
this.ChatLine = this.fine.define(
|
||||
'chat-line',
|
||||
n => n.renderMessageBody && ! n.getMessageParts,
|
||||
|
@ -49,6 +51,7 @@ export default class ChatLine extends Module {
|
|||
this.chat.context.on('changed:chat.rituals.show', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.actions.inline', this.updateLines, this);
|
||||
|
||||
const t = this,
|
||||
React = this.web_munch.getModule('react');
|
||||
|
@ -77,7 +80,7 @@ export default class ChatLine extends Module {
|
|||
}
|
||||
|
||||
cls.prototype.render = function() {
|
||||
const types = t.parent.chat_types || {},
|
||||
const types = t.parent.message_types || {},
|
||||
|
||||
msg = this.props.message,
|
||||
is_action = msg.messageType === types.Action;
|
||||
|
@ -98,21 +101,23 @@ export default class ChatLine extends Module {
|
|||
|
||||
const user = msg.user,
|
||||
color = t.parent.colors.process(user.color),
|
||||
/*bg_rgb = Color.RGBA.fromHex(user.color),
|
||||
bg_color = bg_rgb.luminance() < .005 ? bg_rgb : bg_rgb.toHSLA().targetLuminance(0.005).toRGBA(),
|
||||
bg_css = bg_color.toCSS(),*/
|
||||
room = msg.channel ? msg.channel.slice(1) : undefined,
|
||||
bg_css = null, //Math.random() > .7 ? t.parent.inverse_colors.process(user.color) : null,
|
||||
room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined,
|
||||
|
||||
show = this._ffz_show = this.state.alwaysShowMessage || ! this.props.message.deleted;
|
||||
|
||||
if ( ! msg.message && msg.messageParts )
|
||||
detokenizeMessage(msg);
|
||||
|
||||
const u = {
|
||||
login: this.props.currentUserLogin,
|
||||
display: this.props.currentUserDisplayName
|
||||
},
|
||||
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u),
|
||||
const u = t.site.getUser(),
|
||||
r = {id: this.props.channelID, login: room};
|
||||
|
||||
if ( u ) {
|
||||
u.moderator = this.props.isCurrentUserModerator;
|
||||
u.staff = this.props.isCurrentUserStaff;
|
||||
}
|
||||
|
||||
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u, r),
|
||||
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
|
||||
|
||||
let cls = 'chat-line__message',
|
||||
|
@ -120,7 +125,7 @@ export default class ChatLine extends Module {
|
|||
this.props.showTimestamps && e('span', {
|
||||
className: 'chat-line__timestamp'
|
||||
}, t.chat.formatTime(msg.timestamp)),
|
||||
this.renderModerationIcons(),
|
||||
t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e),
|
||||
e('span', {
|
||||
className: 'chat-line__message--badges'
|
||||
}, t.chat.badges.render(msg, e)),
|
||||
|
@ -225,6 +230,7 @@ export default class ChatLine extends Module {
|
|||
|
||||
return e('div', {
|
||||
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}`,
|
||||
style: {backgroundColor: bg_css},
|
||||
'data-room-id': this.props.channelID,
|
||||
'data-room': room,
|
||||
'data-user-id': user.userID,
|
||||
|
|
|
@ -44,6 +44,11 @@ export default class SettingsMenu extends Module {
|
|||
<button onClick={this.ffzSettingsClick}>
|
||||
{t.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
|
||||
</button>
|
||||
{t.cant_window && <div class="tw-mg-t-05 tw-c-text-alt-2">
|
||||
<span class="ffz-i-attention">
|
||||
{t.i18n.t('popup.error', 'We tried opening a pop-up window and could not. Make sure to allow pop-ups from Twitch.')}
|
||||
</span>
|
||||
</div>}
|
||||
</div>);
|
||||
|
||||
return val;
|
||||
|
@ -73,7 +78,14 @@ export default class SettingsMenu extends Module {
|
|||
'_blank',
|
||||
'resizable=yes,scrollbars=yes,width=850,height=600'
|
||||
);
|
||||
win.focus();
|
||||
if ( win )
|
||||
win.focus();
|
||||
else {
|
||||
this.cant_window = true;
|
||||
this.SettingsMenu.forceUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
this.emit('site.menu_button:clicked');
|
||||
}
|
||||
|
|
|
@ -53,6 +53,17 @@ export default class CSSTweaks extends Module {
|
|||
|
||||
// Layout
|
||||
|
||||
/*this.settings.add('layout.portrait', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Channel',
|
||||
title: 'Enable Portrait Mode',
|
||||
description: 'In Portrait Mode, chat will be displayed beneath the player when the window is taller than it is wide.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggle('portrait', val)
|
||||
});*/
|
||||
|
||||
this.settings.add('layout.side-nav.show', {
|
||||
default: true,
|
||||
ui: {
|
||||
|
@ -194,6 +205,7 @@ export default class CSSTweaks extends Module {
|
|||
this.toggle('swap-sidebars', this.settings.get('layout.swap-sidebars'));
|
||||
this.toggle('minimal-navigation', this.settings.get('layout.minimal-navigation'));
|
||||
this.toggle('theatre-nav', this.settings.get('layout.theatre-navigation'));
|
||||
//this.toggle('portrait', this.settings.get('layout.portrait'));
|
||||
|
||||
this.toggleHide('side-nav', !this.settings.get('layout.side-nav.show'));
|
||||
this.toggleHide('side-rec-friends', !this.settings.get('layout.side-nav.show-rec-friends'));
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.message > .chat-line__message--emoji {
|
||||
.message > .chat-line__message--emote.ffz-emoji {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.tw-button-icon[data-a-target="emote-picker-button"] {
|
||||
.tw-button-icon__icon {
|
||||
padding: .4rem .2rem;
|
||||
padding: .5rem .2rem .3rem;
|
||||
|
||||
figure {
|
||||
&:before {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// ============================================================================
|
||||
|
||||
import {SiteModule} from 'utilities/module';
|
||||
import {createElement, setChildren} from 'utilities/dom';
|
||||
import {createElement} from 'utilities/dom';
|
||||
|
||||
export default class MenuButton extends SiteModule {
|
||||
constructor(...args) {
|
||||
|
|
|
@ -57,6 +57,55 @@
|
|||
}
|
||||
|
||||
|
||||
.ffz-mod-icon {
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
|
||||
& + .ffz-mod-icon {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
span, img, figure {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
span, img,
|
||||
figure:before {
|
||||
margin: 0 0.05rem !important
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
min-width: 1.6rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mod-icon__image img {
|
||||
max-height: 1.6rem;
|
||||
}
|
||||
|
||||
&.colored,
|
||||
.mod-icon__image img {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.ffz--action &,
|
||||
.tw-interactable:hover &,
|
||||
&:hover {
|
||||
.tw-theme--dark &, & {
|
||||
&.tw-c-text-alt-2 {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.colored,
|
||||
.mod-icon__image img {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ffz--emote-picker {
|
||||
section:not(.filtered) heading {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -190,6 +190,11 @@ export default class SocketClient extends Module {
|
|||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
if ( this._socket !== ws ) {
|
||||
this.log.warn('A socket connected that is not our primary socket.');
|
||||
return ws.close();
|
||||
}
|
||||
|
||||
this._state = State.CONNECTED;
|
||||
this._sent_user = false;
|
||||
|
||||
|
@ -249,11 +254,17 @@ export default class SocketClient extends Module {
|
|||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
if ( ws !== this._socket )
|
||||
return;
|
||||
|
||||
if ( ! this._offline_time )
|
||||
this._offline_time = Date.now();
|
||||
}
|
||||
|
||||
ws.onclose = event => {
|
||||
if ( ws !== this._socket )
|
||||
return;
|
||||
|
||||
const old_state = this._state;
|
||||
this.log.info(`Disconnected. (${event.code}:${event.reason})`);
|
||||
|
||||
|
@ -290,6 +301,9 @@ export default class SocketClient extends Module {
|
|||
|
||||
|
||||
ws.onmessage = event => {
|
||||
if ( ws !== this._socket )
|
||||
return;
|
||||
|
||||
// Format:
|
||||
// -1 <cmd_name>[ <json_data>]
|
||||
// <reply-id> <ok/err>[ <json_data>]
|
||||
|
|
38
src/std-components/balloon.vue
Normal file
38
src/std-components/balloon.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template lang="html">
|
||||
<div
|
||||
:class="classes"
|
||||
class="tw-balloon tw-block tw-absolute"
|
||||
>
|
||||
<div class="tw-balloon__tail tw-overflow-hidden tw-absolute">
|
||||
<div
|
||||
:class="`tw-c-${color}`"
|
||||
class="tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-absolute"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-border-t tw-border-r tw-border-b tw-border-l tw-elevation-1 tw-border-radius-small">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: 'background'
|
||||
},
|
||||
|
||||
size: String,
|
||||
dir: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes() {
|
||||
return `tw-c-${this.color} ${this.size ? `tw-balloon--${this.size}` : ''} ${this.dir ? this.dir.split('-').map(x => `tw-balloon--${x}`).join(' ') : ''}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
33
src/std-components/simplebar.vue
Normal file
33
src/std-components/simplebar.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<template lang="html">
|
||||
<div
|
||||
:class="classes"
|
||||
:data-simplebar-auto-hide="autoHide"
|
||||
:data-simplebar-scrollbar-min-size="scrollbarMinSize"
|
||||
data-simplebar
|
||||
class="scrollable-area"
|
||||
>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
classes: String,
|
||||
autoHide: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
scrollbarMinSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -15,6 +15,7 @@ const BAD_ERRORS = [
|
|||
'error internal',
|
||||
'context deadline exceeded',
|
||||
'unexpected service response',
|
||||
'service unavailable',
|
||||
'404',
|
||||
'500',
|
||||
'501',
|
||||
|
|
|
@ -123,13 +123,21 @@ export default class WebMunch extends Module {
|
|||
// Grabbing Require
|
||||
// ========================================================================
|
||||
|
||||
getRequire() {
|
||||
getRequire(limit = 0) {
|
||||
if ( this._require )
|
||||
return Promise.resolve(this._require);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const fn = this._original_loader || window.webpackJsonp;
|
||||
if ( ! fn ) {
|
||||
if ( limit > 100 )
|
||||
throw new Error('unable to find webpackJsonp');
|
||||
|
||||
return setTimeout(() => this.getRequire(limit++).then(resolve), 100);
|
||||
}
|
||||
|
||||
const id = `${this._id}$${this._rid++}`;
|
||||
(this._original_loader || window.webpackJsonp)(
|
||||
fn(
|
||||
[],
|
||||
{
|
||||
[id]: (module, exports, __webpack_require__) => {
|
||||
|
|
|
@ -91,14 +91,25 @@ export function createElement(tag, props, ...children) {
|
|||
return el;
|
||||
}
|
||||
|
||||
export function setChildren(el, children, no_sanitize) {
|
||||
export function setChildren(el, children, no_sanitize, no_empty) {
|
||||
if ( typeof children === 'string' ) {
|
||||
if ( no_sanitize )
|
||||
el.innerHTML = children;
|
||||
else
|
||||
el.textContent = children;
|
||||
if ( no_empty ) {
|
||||
el.appendChild(no_sanitize ?
|
||||
range.createContextualFragment(children) :
|
||||
document.createTextNode(children)
|
||||
)
|
||||
|
||||
} else {
|
||||
if ( no_sanitize )
|
||||
el.innerHTML = children;
|
||||
else
|
||||
el.textContent = children;
|
||||
}
|
||||
|
||||
} else if ( Array.isArray(children) ) {
|
||||
if ( ! no_empty )
|
||||
el.innerHTML = '';
|
||||
|
||||
for(const child of children)
|
||||
if ( typeof child === 'string' )
|
||||
el.appendChild(no_sanitize ?
|
||||
|
@ -106,11 +117,18 @@ export function setChildren(el, children, no_sanitize) {
|
|||
document.createTextNode(child)
|
||||
);
|
||||
|
||||
else if ( Array.isArray(child) )
|
||||
setChildren(el, child, no_sanitize, true);
|
||||
|
||||
else if ( child )
|
||||
el.appendChild(child);
|
||||
|
||||
} else if ( children )
|
||||
} else if ( children ) {
|
||||
if ( ! no_empty )
|
||||
el.innerHTML = '';
|
||||
|
||||
el.appendChild(children);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
236
src/utilities/font-awesome.js
Normal file
236
src/utilities/font-awesome.js
Normal file
|
@ -0,0 +1,236 @@
|
|||
'use strict';
|
||||
|
||||
import {createElement} from 'utilities/dom';
|
||||
|
||||
// ============================================================================
|
||||
// Font Awesome Data
|
||||
// ============================================================================
|
||||
|
||||
export const ALIASES = {
|
||||
'ban': ['ban', 'block'],
|
||||
'ok': ['ok', 'unban', 'untimeout'],
|
||||
'clock-o': ['clock-o', 'clock', 'timeout'],
|
||||
'times': ['remove','close','times'],
|
||||
'cog': ['gear','cog'],
|
||||
'repeat': ['rotate-right','repeat'],
|
||||
'outdent': ['dedent','outdent'],
|
||||
'picture-o': ['photo','image','picture-o'],
|
||||
'pencil-square-o': ['edit','pencil-square-o'],
|
||||
'share': ['mail-forward','share'],
|
||||
'exclamation-triangle': ['warning','exclamation-triangle'],
|
||||
'bar-chart': ['bar-chart-o','bar-chart'],
|
||||
'cogs': ['gears','cogs'],
|
||||
'facebook': ['facebook-f','facebook'],
|
||||
'rss': ['feed','rss'],
|
||||
'users': ['group','users'],
|
||||
'link': ['chain','link'],
|
||||
'scissors': ['cut','scissors'],
|
||||
'files-o': ['copy','files-o'],
|
||||
'floppy-o': ['save','floppy-o'],
|
||||
'bars': ['navicon','reorder','bars'],
|
||||
'sort': ['unsorted','sort'],
|
||||
'sort-desc': ['sort-down','sort-desc'],
|
||||
'sort-asc': ['sort-up','sort-asc'],
|
||||
'undo': ['rotate-left','undo'],
|
||||
'gavel': ['legal','gavel'],
|
||||
'tachometer': ['dashboard','tachometer'],
|
||||
'bolt': ['flash','bolt'],
|
||||
'clipboard': ['paste','clipboard'],
|
||||
'mobile': ['mobile-phone','mobile'],
|
||||
'reply': ['mail-reply','reply'],
|
||||
'reply-all': ['mail-reply-all','reply-all'],
|
||||
'star-half-o': ['star-half-empty','star-half-full','star-half-o'],
|
||||
'chain-broken': ['unlink','chain-broken'],
|
||||
'caret-square-o-down': ['toggle-down','caret-square-o-down'],
|
||||
'caret-square-o-up': ['toggle-up','caret-square-o-up'],
|
||||
'caret-square-o-right': ['toggle-right','caret-square-o-right'],
|
||||
'eur': ['euro','eur'],
|
||||
'usd': ['dollar','usd'],
|
||||
'inr': ['rupee','inr'],
|
||||
'jpy': ['cny','rmb','yen','jpy'],
|
||||
'rub': ['ruble','rouble','rub'],
|
||||
'krw': ['won','krw'],
|
||||
'btc': ['bitcoin','btc'],
|
||||
'gratipay': ['gittip','gratipay'],
|
||||
'caret-square-o-left': ['toggle-left','caret-square-o-left'],
|
||||
'try': ['turkish-lira','try'],
|
||||
'university': ['institution','bank','university'],
|
||||
'graduation-cap': ['mortar-board','graduation-cap'],
|
||||
'car': ['automobile','car'],
|
||||
'taxi': ['cab','taxi'],
|
||||
'file-image-o': ['file-photo-o','file-picture-o','file-image-o'],
|
||||
'file-archive-o': ['file-zip-o','file-archive-o'],
|
||||
'file-audio-o': ['file-sound-o','file-audio-o'],
|
||||
'file-video-o': ['file-movie-o','file-video-o'],
|
||||
'life-ring': ['life-bouy','life-buoy','life-saver','support','life-ring'],
|
||||
'rebel': ['ra','resistance','rebel'],
|
||||
'empire': ['ge','empire'],
|
||||
'hacker-news': ['y-combinator-square','yc-square','hacker-news'],
|
||||
'weixin': ['wechat','weixin'],
|
||||
'paper-plane': ['send','paper-plane'],
|
||||
'paper-plane-o': ['send-o','paper-plane-o'],
|
||||
'futbol-o': ['soccer-ball-o','futbol-o'],
|
||||
'ils': ['shekel','sheqel','ils'],
|
||||
'transgender': ['intersex','transgender'],
|
||||
'bed': ['hotel','bed'],
|
||||
'y-combinator': ['yc','y-combinator'],
|
||||
'battery-full': ['battery-4','battery','battery-full'],
|
||||
'battery-three-quarters': ['battery-3','battery-three-quarters'],
|
||||
'battery-half': ['battery-2','battery-half'],
|
||||
'battery-quarter': ['battery-1','battery-quarter'],
|
||||
'battery-empty': ['battery-0','battery-empty'],
|
||||
'hourglass-start': ['hourglass-1','hourglass-start'],
|
||||
'hourglass-half': ['hourglass-2','hourglass-half'],
|
||||
'hourglass-end': ['hourglass-3','hourglass-end'],
|
||||
'hand-rock-o': ['hand-grab-o','hand-rock-o'],
|
||||
'hand-paper-o': ['hand-stop-o','hand-paper-o'],
|
||||
'television': ['tv','television'],
|
||||
'american-sign-language-interpreting': ['asl-interpreting','american-sign-language-interpreting'],
|
||||
'deaf': ['deafness','hard-of-hearing','deaf'],
|
||||
'sign-language': ['signing','sign-language'],
|
||||
'google-plus-official': ['google-plus-circle','google-plus-official'],
|
||||
'font-awesome': ['fa','font-awesome'],
|
||||
'address-card': ['vcard','address-card'],
|
||||
'address-card-o': ['vcard-o','address-card-o'],
|
||||
'id-card': ['drivers-license','id-card'],
|
||||
'id-card-o': ['drivers-license-o','id-card-o'],
|
||||
'thermometer-full': ['thermometer-4','thermometer','thermometer-full'],
|
||||
'thermometer-three-quarters': ['thermometer-3','thermometer-three-quarters'],
|
||||
'thermometer-half': ['thermometer-2','thermometer-half'],
|
||||
'thermometer-quarter': ['thermometer-1','thermometer-quarter'],
|
||||
'thermometer-empty': ['thermometer-0','thermometer-empty'],
|
||||
'bath': ['bathtub','s15','bath'],
|
||||
'window-close': ['times-rectangle','window-close'],
|
||||
'window-close-o': ['times-rectangle-o','window-close-o']
|
||||
};
|
||||
|
||||
export const ICONS = [
|
||||
'glass','music','search','envelope-o','heart','star','star-o','user',
|
||||
'film','th-large','th','th-list','check','times','search-plus',
|
||||
'search-minus','power-off','signal','cog','trash-o','home','file-o',
|
||||
'clock-o','road','download','arrow-circle-o-down','arrow-circle-o-up',
|
||||
'inbox','play-circle-o','repeat','refresh','list-alt','lock','flag',
|
||||
'headphones','volume-off','volume-down','volume-up','qrcode','barcode',
|
||||
'tag','tags','book','bookmark','print','camera','font','bold','italic',
|
||||
'text-height','text-width','align-left','align-center','align-right',
|
||||
'align-justify','list','outdent','indent','video-camera','picture-o',
|
||||
'pencil','map-marker','adjust','tint','pencil-square-o','share-square-o',
|
||||
'check-square-o','arrows','step-backward','fast-backward','backward',
|
||||
'play','pause','stop','forward','fast-forward','step-forward','eject',
|
||||
'chevron-left','chevron-right','plus-circle','minus-circle','times-circle',
|
||||
'check-circle','question-circle','info-circle','crosshairs',
|
||||
'times-circle-o','check-circle-o','ban','arrow-left','arrow-right',
|
||||
'arrow-up','arrow-down','share','expand','compress','plus','minus',
|
||||
'asterisk','exclamation-circle','gift','leaf','fire','eye','eye-slash',
|
||||
'exclamation-triangle','plane','calendar','random','comment','magnet',
|
||||
'chevron-up','chevron-down','retweet','shopping-cart','folder','folder-open',
|
||||
'arrows-v','arrows-h','bar-chart','twitter-square','facebook-square',
|
||||
'camera-retro','key','cogs','comments','thumbs-o-up','thumbs-o-down',
|
||||
'star-half','heart-o','sign-out','linkedin-square','thumb-tack','external-link',
|
||||
'sign-in','trophy','github-square','upload','lemon-o','phone','square-o',
|
||||
'bookmark-o','phone-square','twitter','facebook','github','unlock',
|
||||
'credit-card','rss','hdd-o','bullhorn','bell','certificate','hand-o-right',
|
||||
'hand-o-left','hand-o-up','hand-o-down','arrow-circle-left','arrow-circle-right',
|
||||
'arrow-circle-up','arrow-circle-down','globe','wrench','tasks','filter',
|
||||
'briefcase','arrows-alt','users','link','cloud','flask','scissors','files-o',
|
||||
'paperclip','floppy-o','square','bars','list-ul','list-ol','strikethrough',
|
||||
'underline','table','magic','truck','pinterest','pinterest-square',
|
||||
'google-plus-square','google-plus','money','caret-down','caret-up',
|
||||
'caret-left','caret-right','columns','sort','sort-desc','sort-asc',
|
||||
'envelope','linkedin','undo','gavel','tachometer','comment-o','comments-o',
|
||||
'bolt','sitemap','umbrella','clipboard','lightbulb-o','exchange',
|
||||
'cloud-download','cloud-upload','user-md','stethoscope','suitcase','bell-o',
|
||||
'coffee','cutlery','file-text-o','building-o','hospital-o','ambulance',
|
||||
'medkit','fighter-jet','beer','h-square','plus-square','angle-double-left',
|
||||
'angle-double-right','angle-double-up','angle-double-down','angle-left',
|
||||
'angle-right','angle-up','angle-down','desktop','laptop','tablet','mobile',
|
||||
'circle-o','quote-left','quote-right','spinner','circle','reply','github-alt',
|
||||
'folder-o','folder-open-o','smile-o','frown-o','meh-o','gamepad','keyboard-o',
|
||||
'flag-o','flag-checkered','terminal','code','reply-all','star-half-o',
|
||||
'location-arrow','crop','code-fork','chain-broken','question','info',
|
||||
'exclamation','superscript','subscript','eraser','puzzle-piece','microphone',
|
||||
'microphone-slash','shield','calendar-o','fire-extinguisher','rocket',
|
||||
'maxcdn','chevron-circle-left','chevron-circle-right','chevron-circle-up',
|
||||
'chevron-circle-down','html5','css3','anchor','unlock-alt','bullseye',
|
||||
'ellipsis-h','ellipsis-v','rss-square','play-circle','ticket','minus-square',
|
||||
'minus-square-o','level-up','level-down','check-square','pencil-square',
|
||||
'external-link-square','share-square','compass','caret-square-o-down',
|
||||
'caret-square-o-up','caret-square-o-right','eur','gbp','usd','inr','jpy','rub',
|
||||
'krw','btc','file','file-text','sort-alpha-asc','sort-alpha-desc',
|
||||
'sort-amount-asc','sort-amount-desc','sort-numeric-asc','sort-numeric-desc',
|
||||
'thumbs-up','thumbs-down','youtube-square','youtube','xing','xing-square',
|
||||
'youtube-play','dropbox','stack-overflow','instagram','flickr','adn','bitbucket',
|
||||
'bitbucket-square','tumblr','tumblr-square','long-arrow-down','long-arrow-up',
|
||||
'long-arrow-left','long-arrow-right','apple','windows','android','linux',
|
||||
'dribbble','skype','foursquare','trello','female','male','gratipay','sun-o',
|
||||
'moon-o','archive','bug','vk','weibo','renren','pagelines','stack-exchange',
|
||||
'arrow-circle-o-right','arrow-circle-o-left','caret-square-o-left','dot-circle-o',
|
||||
'wheelchair','vimeo-square','try','plus-square-o','space-shuttle','slack',
|
||||
'envelope-square','wordpress','openid','university','graduation-cap','yahoo',
|
||||
'google','reddit','reddit-square','stumbleupon-circle','stumbleupon','delicious',
|
||||
'digg','pied-piper-pp','pied-piper-alt','drupal','joomla','language','fax',
|
||||
'building','child','paw','spoon','cube','cubes','behance','behance-square',
|
||||
'steam','steam-square','recycle','car','taxi','tree','spotify','deviantart',
|
||||
'soundcloud','database','file-pdf-o','file-word-o','file-excel-o',
|
||||
'file-powerpoint-o','file-image-o','file-archive-o','file-audio-o','file-video-o',
|
||||
'file-code-o','vine','codepen','jsfiddle','life-ring','circle-o-notch','rebel',
|
||||
'empire','git-square','git','hacker-news','tencent-weibo','qq','weixin',
|
||||
'paper-plane','paper-plane-o','history','circle-thin','header','paragraph',
|
||||
'sliders','share-alt','share-alt-square','bomb','futbol-o','tty','binoculars',
|
||||
'plug','slideshare','twitch','yelp','newspaper-o','wifi','calculator','paypal',
|
||||
'google-wallet','cc-visa','cc-mastercard','cc-discover','cc-amex','cc-paypal',
|
||||
'cc-stripe','bell-slash','bell-slash-o','trash','copyright','at','eyedropper',
|
||||
'paint-brush','birthday-cake','area-chart','pie-chart','line-chart','lastfm',
|
||||
'lastfm-square','toggle-off','toggle-on','bicycle','bus','ioxhost','angellist',
|
||||
'cc','ils','meanpath','buysellads','connectdevelop','dashcube','forumbee',
|
||||
'leanpub','sellsy','shirtsinbulk','simplybuilt','skyatlas','cart-plus',
|
||||
'cart-arrow-down','diamond','ship','user-secret','motorcycle','street-view',
|
||||
'heartbeat','venus','mars','mercury','transgender','transgender-alt','venus-double',
|
||||
'mars-double','venus-mars','mars-stroke','mars-stroke-v','mars-stroke-h','neuter',
|
||||
'genderless','facebook-official','pinterest-p','whatsapp','server','user-plus',
|
||||
'user-times','bed','viacoin','train','subway','medium','y-combinator','optin-monster',
|
||||
'opencart','expeditedssl','battery-full','battery-three-quarters','battery-half',
|
||||
'battery-quarter','battery-empty','mouse-pointer','i-cursor','object-group',
|
||||
'object-ungroup','sticky-note','sticky-note-o','cc-jcb','cc-diners-club','clone',
|
||||
'balance-scale','hourglass-o','hourglass-start','hourglass-half','hourglass-end',
|
||||
'hourglass','hand-rock-o','hand-paper-o','hand-scissors-o','hand-lizard-o',
|
||||
'hand-spock-o','hand-pointer-o','hand-peace-o','trademark','registered',
|
||||
'creative-commons','gg','gg-circle','tripadvisor','odnoklassniki',
|
||||
'odnoklassniki-square','get-pocket','wikipedia-w','safari','chrome','firefox',
|
||||
'opera','internet-explorer','television','contao','500px','amazon',
|
||||
'calendar-plus-o','calendar-minus-o','calendar-times-o','calendar-check-o',
|
||||
'industry','map-pin','map-signs','map-o','map','commenting','commenting-o',
|
||||
'houzz','vimeo','black-tie','fonticons','reddit-alien','edge','credit-card-alt',
|
||||
'codiepie','modx','fort-awesome','usb','product-hunt','mixcloud','scribd',
|
||||
'pause-circle','pause-circle-o','stop-circle','stop-circle-o','shopping-bag',
|
||||
'shopping-basket','hashtag','bluetooth','bluetooth-b','percent','gitlab',
|
||||
'wpbeginner','wpforms','envira','universal-access','wheelchair-alt',
|
||||
'question-circle-o','blind','audio-description','volume-control-phone','braille',
|
||||
'assistive-listening-systems','american-sign-language-interpreting','deaf',
|
||||
'glide','glide-g','sign-language','low-vision','viadeo','viadeo-square',
|
||||
'snapchat','snapchat-ghost','snapchat-square','pied-piper','first-order',
|
||||
'yoast','themeisle','google-plus-official','font-awesome','handshake-o',
|
||||
'envelope-open','envelope-open-o','linode','address-book','address-book-o',
|
||||
'address-card','address-card-o','user-circle','user-circle-o','user-o',
|
||||
'id-badge','id-card','id-card-o','quora','free-code-camp','telegram',
|
||||
'thermometer-full','thermometer-three-quarters','thermometer-half',
|
||||
'thermometer-quarter','thermometer-empty','shower','bath','podcast',
|
||||
'window-maximize','window-minimize','window-restore','window-close',
|
||||
'window-close-o','bandcamp','grav','etsy','imdb','ravelry','eercast',
|
||||
'microchip','snowflake-o','superpowers','wpexplorer','meetup'];
|
||||
|
||||
let loaded = false;
|
||||
|
||||
import FA_URL from 'styles/font-awesome.scss';
|
||||
|
||||
export const load = () => {
|
||||
if ( loaded )
|
||||
return;
|
||||
|
||||
document.head.appendChild(createElement('link', {
|
||||
href: FA_URL,
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
crossOrigin: 'anonymouse'
|
||||
}));
|
||||
}
|
|
@ -246,6 +246,11 @@ export function pick_random(obj) {
|
|||
}
|
||||
|
||||
|
||||
export const escape_regex = RegExp.escape || function escape_regex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
|
||||
export class SourcedSet {
|
||||
constructor() {
|
||||
this._cache = [];
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
}
|
||||
|
||||
|
||||
.ffz-i-lock {
|
||||
.ffz__tooltip .ffz-i-lock {
|
||||
position: absolute;
|
||||
bottom: 0; right: 0;
|
||||
border-radius: .2rem;
|
||||
|
|
2187
styles/font-awesome.scss
vendored
Normal file
2187
styles/font-awesome.scss
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -101,6 +101,8 @@
|
|||
.ffz-i-block:before { content: '\e824'; } /* '' */
|
||||
.ffz-i-pin:before { content: '\e825'; } /* '' */
|
||||
.ffz-i-pin-outline:before { content: '\e826'; } /* '' */
|
||||
.ffz-i-gift:before { content: '\e827'; } /* '' */
|
||||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
|
||||
.ffz-i-download-cloud:before { content: '\f0ed'; } /* '' */
|
||||
|
@ -111,6 +113,7 @@
|
|||
.ffz-i-twitch:before { content: '\f1e8'; } /* '' */
|
||||
.ffz-i-bell-off:before { content: '\f1f7'; } /* '' */
|
||||
.ffz-i-trash:before { content: '\f1f8'; } /* '' */
|
||||
.ffz-i-eyedropper:before { content: '\f1fb'; } /* '' */
|
||||
.ffz-i-window-maximize:before { content: '\f2d0'; } /* '' */
|
||||
.ffz-i-window-minimize:before { content: '\f2d1'; } /* '' */
|
||||
.ffz-i-window-restore:before { content: '\f2d2'; } /* '' */
|
||||
|
|
|
@ -5,60 +5,53 @@ body {
|
|||
.ffz-metadata-balloon {
|
||||
z-index: 999999999 !important;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
|
||||
.tw-balloon {
|
||||
&[x-placement^="bottom"] > .tw-balloon__tail {
|
||||
top: -3px;
|
||||
box-shadow: -1px -1px 0 #dad8de;
|
||||
bottom: 100%;
|
||||
|
||||
.tw-theme--dark & {
|
||||
box-shadow: -1px -1px 0 #2c2541;
|
||||
}
|
||||
|
||||
.tw-theme--ffz & {
|
||||
box-shadow: -1px -1px 0 var(--ffz-color-20);
|
||||
.tw-balloon__tail-symbol {
|
||||
top: auto;
|
||||
bottom: -8px;
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="top"] > .tw-balloon__tail {
|
||||
bottom: -3px;
|
||||
box-shadow: 1px 1px 0 #dad8de;
|
||||
top: 100%;
|
||||
|
||||
.tw-theme--dark & {
|
||||
box-shadow: 1px 1px 0 #2c2541;
|
||||
}
|
||||
|
||||
.tw-theme--ffz & {
|
||||
box-shadow: 1px 1px 0 var(--ffz-color-20);
|
||||
.tw-balloon__tail-symbol {
|
||||
top: auto;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="right"] > .tw-balloon__tail {
|
||||
left: -3px;
|
||||
box-shadow: -1px 1px 0 #dad8de;
|
||||
right: 100%;
|
||||
|
||||
.tw-theme--dark & {
|
||||
box-shadow: -1px 1px 0 #2c2541;
|
||||
}
|
||||
|
||||
.tw-theme--ffz & {
|
||||
box-shadow: -1px 1px 0 var(--ffz-color-20);
|
||||
.tw-balloon__tail-symbol {
|
||||
left: auto;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="left"] > .tw-balloon__tail {
|
||||
right: -3px;
|
||||
box-shadow: 1px -1px 0 #dad8de;
|
||||
left: 100%;
|
||||
|
||||
.tw-theme--dark & {
|
||||
box-shadow: 1px -1px 0 #2c2541;
|
||||
}
|
||||
|
||||
.tw-theme--ffz & {
|
||||
box-shadow: 1px -1px 0 var(--ffz-color-20);
|
||||
.tw-balloon__tail-symbol {
|
||||
left: auto;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ffz-metadata-balloon,
|
||||
.ffz__tooltip {
|
||||
.loader {
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
.tw-width-auto { width: auto !important }
|
||||
|
||||
.ffz--widget {
|
||||
|
||||
input, select {
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
@ -23,6 +22,34 @@
|
|||
}
|
||||
|
||||
|
||||
.ffz--color-widget input,
|
||||
.ffz--inline label {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
|
||||
.ffz-color-preview {
|
||||
margin: 1px;
|
||||
|
||||
figure.ffz-i-eyedropper {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
figure {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
figure {
|
||||
width: 3rem;
|
||||
margin: .4rem;
|
||||
|
||||
height: calc(100% - .8rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ffz--menu-page {
|
||||
padding: 1rem;
|
||||
|
||||
|
@ -67,7 +94,9 @@
|
|||
.ffz--profile__icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ffz--profile {
|
||||
.ffz-i-ok { color: green }
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue