1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-03 08:28:31 +00:00
This commit is contained in:
SirStendec 2016-09-30 13:09:03 -04:00
parent 8db999a8a8
commit 8280b93c97
28 changed files with 2140 additions and 603 deletions

View file

@ -1,3 +1,123 @@
<div class="list-header">3.5.302</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Bug causing chat pausing by scrolling up / moving the mouse / pressing a key to stop working.</li>
</ul>
<div class="list-header">3.5.301</div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Channel Bar on Bottom, Channel Title on Top, and Minimal Channel Bar settings to reflow the new channel design as the user wishes.</li>
<li>Fixed: Layout spacing tweaks.</li>
</ul>
<div class="list-header">3.5.300</div>
<ul class="chat-menu-content menu-side-padding">
<li>This... is... <em>breaking layout changes</em>!</li>
<li>&nbsp;</li>
<li>Fixed: Sidebar Width, Swap Sidebars, and Portrait Mode were all doing bad things with theater mode. They've been whipped into shape.</li>
<li>Fixed: That one lighter-than-it-should-be bar separating channel title and info if you had dark theme.</li>
<li>Fixed: Host mode's ugly dark borders were not just ugly, but too purpley. Fixed.</li>
<li>&nbsp;</li>
<li>Still Broken: Some minor spacing issues, Auto-Theater Mode, disabling Hosting, extra following buttons, SRL races. Not looking like this.</li>
</ul>
<div class="list-header">3.5.299</div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Command Aliases! Define custom commands for chat that are shortcuts for other commands! Madness! (pls no recursion)</li>
<li>API Added: <code>hidden</code> property for emote sets to keep them out of the emote menu.</li>
<li>API Fixed: API instances were storing incorrect emote set IDs (the global FFZ IDs instead of the per-API IDs specifically).</li>
</ul>
<div class="list-header">3.5.298</div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: <code>/rules</code> chat command to show the chat rules for a room.</li>
<li>Added: <code>/open_link</code> chat command for use with moderation actions.</li>
<li>Added: <code>/fetch_link</code> chat command for user with moderation actions.</li>
<li>Changed: Completely redo the UI for setting in-line mod icons and additional mod card buttons.</li>
<li>Changed: Add extra variables to custom in-line mod icons and additional mod card buttons.</li>
<li>Changed: Allow the use of emoji for in-line mod icons.</li>
<li>Fixed: Ensure moderation logs are compared in a case-sensitive manner.</li>
<li>Fixed: Don't add a message to a chat room if that message ID already exists.</li>
</ul>
<div class="list-header">3.5.297</div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Stuff.</li>
<li>Fixed: Append historical messages to chatter history in the correct order.</li>
<li>Fixed: Bug with the rendering of chat room states, specifically regarding Delay.</li>
<li>Fixed: Treat the front page of Twitch as a standard part of the Ember app to avoid only partially loading FFZ elsewhere.</li>
</ul>
<div class="list-header">3.5.296</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Bug with a straight timeout not working with the next context menu.</li>
</ul>
<div class="list-header">3.5.295</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Accidental debugging code made the new context menu always use the last option. I'm a dumb.</li>
</ul>
<div class="list-header">3.5.294</div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Context menu for in-line moderation icons to let moderators easily apply a reason to their ban or timeout.</li>
</ul>
<div class="list-header">3.5.293</div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: CBenni memes.</li>
<li>Changed: The Delay badge will be faded to indicate that it's disabled if you are a moderator who doesn't experience chat delay in a room.</li>
<li>Fixed: The Slow Mode status indicator would not display in some situations.</li>
<li>Fixed: Badges not rendering correctly in the chat menu's preview.</li>
</ul>
<div class="list-header">3.5.292</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Timestamps rendering as <code>undefined</code> in chat under certain situations.</li>
<li>Fixed: Message history showing in reverse order.</li>
<li>Removed: FFZ's experimental chat history that had very limited server-side support.</li>
</ul>
<div class="list-header">3.5.291</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Enhanced channel information, including clickable links in titles, stream uptime, stream latency, the Host button, and chatter count should all be working now. The channel model was reworked and I had to update my code to match.</li>
<li>Fixed: FFZ settings weren't appearing in the chat settings menu.</li>
</ul>
<div class="list-header">3.5.290</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Unable to tab complete usernames. It works now. (For some reason, the suggestions property is now a function I have to call? Okay Twitch.)</li>
</ul>
<div class="list-header">3.5.289</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Username colors were not being calculated properly. (Twitch moved the settings controller and so FFZ didn't realize chat was dark.)</li>
<li>Fixed: Mod icons could potentially be a bit screwy for the same reason.</li>
</ul>
<div class="list-header">3.5.288</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Proper fix for chat messages not appearing. Rewrote the message queue.</li>
</ul>
<div class="list-header">3.5.287</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Temporary fix for chat messages not appearing as they should.</li>
</ul>
<div class="list-header">3.5.286</div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Disabled two debugging logging statements that were erroneously left intact.</li>
</ul>
<div class="list-header">3.5.285</div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Show FFZ badges in the Name Display section of the Twitch chat settings menu.</li>
<li>Added: Basic regular expression support for highlighting and banning words.</li>
<li>Fixed: Update the pubsub patch for chat for the new topics.</li>
<li>Changed: Logic tweaks to make the Show Moderation Actions setting in the Twitch chat settings menu more effectual with FFZ present.</li>
<li>Changed: Dark CSS tweaks.</li>
</ul>
<div class="list-header">3.5.284</div> <div class="list-header">3.5.284</div>
<ul class="chat-menu-content menu-side-padding"> <ul class="chat-menu-content menu-side-padding">
<li>Changed: Inject FFZ emotes into the BetterTTV tab completion list.</li> <li>Changed: Inject FFZ emotes into the BetterTTV tab completion list.</li>

17
credits.html Normal file
View file

@ -0,0 +1,17 @@
<div class="list-header">Awesome People</div>
<ul class="chat-menu-content menu-side-padding">
<li><a href="//twitch.tv/fugi" target="_blank">Fugi</a> - Wrote the Chat Delay feature.</li>
<li><a href="//twitch.tv/daxterspeed" target="_blank">DaxterSpeed</a> - Wrote a method for adjusting username colors.</li>
<li><a href="//twitch.tv/riking27" target="_blank">Riking</a> - Wrote a large part of the socket server system.</li>
</ul>
<div class="list-header">Awesome Resources</div>
<ul class="chat-menu-content menu-side-padding">
<li><a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> - Free and Automated SSL for Everyone</li>
<li><a href="https://github.com/eligrey/FileSaver.js" target="_blank">FileSaver.js</a> - saveAs() implementation</li>
<li><a href="http://objectpath.org/" target="_blank">ObjectPath</a> - Lightweight query language for JSON</li>
<li><a href="https://github.com/twitter/twemoji" target="_blank">Twitter Emoji for Everyone</a> - Twitter's Emoji</li>
<li><a href="https://github.com/googlei18n/noto-emoji" target="_blank">Noto Emoji</a> - Google's Noto Emoji</li>
<li><a href="http://emojione.com/" target="_blank">EmojiOne</a> - An open source emoji project</li>
</ul>

View file

@ -200,9 +200,15 @@ body.ffz-dark:not([data-page="teams#show"]),
box-shadow: rgba(255,255,255,0.2) 0 0 0 1px inset; box-shadow: rgba(255,255,255,0.2) 0 0 0 1px inset;
} }
.ffz-dark .balloon--tooltip:after,
.ffz-dark .balloon--tooltip {
background-color: #000;
}
.ffz-dark #flyout .content, .ffz-dark #flyout .content,
.ffz-dark .ffz-ui-popup.emoticon-selector .emoticon-selector-box { border: none } .ffz-dark .ffz-ui-popup.emoticon-selector .emoticon-selector-box { border: none }
.ffz-dark .balloon--right.balloon--up:after,
.ffz-dark .balloon--up:after { box-shadow: 1px 1px 0 rgba(255,255,255,0.2) } .ffz-dark .balloon--up:after { box-shadow: 1px 1px 0 rgba(255,255,255,0.2) }
.ffz-dark .balloon--right.balloon--down:after, .ffz-dark .balloon--right.balloon--down:after,
.ffz-dark .balloon--down:after { box-shadow: -1px -1px 0 rgba(255,255,255,0.2) } .ffz-dark .balloon--down:after { box-shadow: -1px -1px 0 rgba(255,255,255,0.2) }
@ -317,6 +323,7 @@ body.ffz-dark:not([data-page="teams#show"]),
.ffz-dark .ember-chat .chat-room-list .room:not(:hover) p.room-title, .ffz-dark .ember-chat .chat-room-list .room:not(:hover) p.room-title,
.ffz-dark .dropmenu_action:not(:hover), .ffz-dark .dropmenu_action:not(:hover),
.ffz-dark .dropmenu_action:not(:hover) span, .ffz-dark .dropmenu_action:not(:hover) span,
.ffz-dark .chat-menu-content button.font-color-purple,
.ffz-dark a { .ffz-dark a {
/*.ffz-dark a:not(.profile-card__content):not(.filter-item):not(.ui-state-focus):not(.button):not(.switch):not(.follow):not(.fb_button):not(.what) {*/ /*.ffz-dark a:not(.profile-card__content):not(.filter-item):not(.ui-state-focus):not(.button):not(.switch):not(.follow):not(.fb_button):not(.what) {*/
color: #a68ed2; color: #a68ed2;
@ -365,6 +372,7 @@ body.ffz-dark:not([data-page="teams#show"]),
color: #a68ed2; color: #a68ed2;
} }
.ffz-dark .button--icon.button--hollow figure svg,
.ffz-dark .button.button--icon-only svg { .ffz-dark .button.button--icon-only svg {
fill: #a68ed2; fill: #a68ed2;
} }
@ -534,6 +542,7 @@ body.ffz-dark:not([data-page="teams#show"]),
background-color: rgb(16,16,16); background-color: rgb(16,16,16);
} }
.ffz-dark .card__title,
.ffz-dark .card .card__title a, .ffz-dark .card .card__title a,
.ffz-dark .card .card__info a, .ffz-dark .card .card__info a,
.ffz-dark .items-grid .meta .title, .ffz-dark .items-grid .meta .title,
@ -552,6 +561,17 @@ body.ffz-dark:not([data-page="teams#show"]),
color: #a68ed2; color: #a68ed2;
} }
.ffz-dark .tabs { box-shadow: 0 -1px 0 rgba(255,255,255,0.2) inset; }
.ffz-dark .tabs>.tab:hover,
.ffz-dark .tabs>li:hover,
.ffz-dark .tabs__item:hover,
.ffz-dark .tabs__item--active,
.ffz-dark .tabs__item.active,
.ffz-dark .tabs__item.selected,
.ffz-dark .tabs>li.selected,
.ffz-dark .tabs>li>a.active { box-shadow: 0 -1px 0 #a68cd4 inset }
.ffz-dark .mininav, .ffz-dark .mininav,
.ffz-dark ul.tabs:before, .ffz-dark ul.tabs:before,
.ffz-dark .direcotry_header .nav:before, .ffz-dark .direcotry_header .nav:before,
@ -856,7 +876,7 @@ body.ffz-dark:not([data-page="teams#show"]),
} }
.ffz-dark .js-show-stream-key { background-color: #804400; } .ffz-dark .js-show-stream-key { background-color: #804400; }
.ffz-dark .js-reset-stream-key { r-color: rgb(128,0,0); } .ffz-dark .js-reset-stream-key { border-color: rgb(128,0,0); }
.ffz-dark .js-show-stream-key:hover, .ffz-dark .js-show-stream-key:hover,
.ffz-dark .js-show-stream-key:focus, .ffz-dark .js-show-stream-key:focus,
@ -1417,3 +1437,48 @@ body.ffz-dark:not([data-page="teams#show"]),
.ffz-dark .resultView__titlesep { .ffz-dark .resultView__titlesep {
background-color: #090909; background-color: #090909;
} }
/* Channel Redesign */
.ffz-dark .cn-bar {
background-color: #101010;
box-shadow: inset 0 -1px 0 #161616, -1px 1px rgba(255,255,255,0.065);
}
.ffz-dark .cn-metabar__more {
border-color: rgba(255,255,255,0.2) !important;
}
.ffz-dark .button--icon.cn-bar__button--subscribed {
background-color: #161616;
box-shadow: 0 0 0 1px rgba(255,255,255,0.065) inset;
}
.ffz-dark .cn-bar__avatar-wrap {
background-color: #101010;
border-color: #101010;
}
.ffz-dark .cn-bar__displayname {
color: #c3c3c3;
}
.ffz-dark .cn-tabs__item--withseparator:after {
border-color: rgba(255,255,255,0.2);
}
.ffz-dark .cn-tabs__item > a {
color: #999;
}
.ffz-dark .cn-tabs__item > a.active,
.ffz-dark .cn-tabs__item > a:hover {
color: #a68ed2;
}
.ffz-dark .cn-tabs__item > a:hover:after {
border-color: #a68ed2;
}

21
src/ObjectPath.js Normal file
View file

@ -0,0 +1,21 @@
String.prototype.tokens=function(h){h=h?h:!1;var a,g,b=0,k=this.length,f,c,e=[],l="and;or;not;in;not in;is;is not;".split(";"),d=function(a,c,d){if(!d)return{type:a,value:c,position:[g,b]}};if(this){for(a=this.charAt(b);a;)if(g=b," ">=a)b+=1,a=this.charAt(b);else if("a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"==a){c=a;for(b+=1;;)if(a=this.charAt(b),"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"0"<=a&&"9">=a||"_"===a)c+=a,b+=1;else break;0<=l.indexOf(c)?(f=e[e.length-1],"not"===c&&f&&"is"===f.value?(c=d("op","is not"),
c.position[0]=f.position[0],e[e.length-1]=c):"in"===c&&f&&"not"===e[e.length-1].value?(c=d("op","not in"),c.position[0]=f.position[0],e[e.length-1]=c):e.push(d("op",c))):e.push(d("name",c))}else if("0"<=a&&"9">=a){c=a;for(b+=1;;){a=this.charAt(b);if("0">a||"9"<a)break;b+=1;c+=a}if("."===a)for(b+=1,c+=a;;){a=this.charAt(b);if("0">a||"9"<a)break;b+=1;c+=a}if("e"===a||"E"===a){b+=1;c+=a;a=this.charAt(b);if("-"===a||"+"===a)b+=1,c+=a,a=this.charAt(b);("0">a||"9"<a)&&d("number",c,"Bad exponent");do b+=
1,c+=a,a=this.charAt(b);while("0"<=a&&"9">=a)}"a"<=a&&"z">=a&&(c+=a,b+=1,d("number",c,"Bad number"));f=+c;isFinite(f)?e.push(d("number",f)):d("number",c,"Bad number")}else if("'"===a||'"'===a){c="";f=a;for(b+=1;;){a=this.charAt(b);" ">a&&d("str",c,"\n"===a||"\r"===a||""===a?"Unterminated string.":"Control character in string.",d("",c));if(a===f)break;if("\\"===a)switch(b+=1,b>=k&&d("str",c,"Unterminated string"),a=this.charAt(b),a){case "b":a="\b";break;case "f":a="\f";break;case "n":a="\n";break;
case "r":a="\r";break;case "t":a="\t";break;case "u":b>=k&&d("str",c,"Unterminated string"),a=parseInt(this.substr(b+1,4),16),(!isFinite(a)||0>a)&&d("str",c,"Unterminated string"),a=String.fromCharCode(a),b+=4}c+=a;b+=1}b+=1;e.push(d("str",c));a=this.charAt(b)}else 0<="<>.".indexOf(a)?(c=a,b+=1,a=this.charAt(b),"<"===c&&"="===a?c="<=":">"===c&&"="===a?c=">=":"."===c&&"."===a&&(c=".."),1<c.length&&(b+=1,a=this.charAt(b)),e.push(d("op",c))):(b+=1,"$"===a?e.push(d("(root)",a)):"@"===a?e.push(d("(current)",
a)):"!"===a?e.push(d("(context)",a)):0<="+-*/%<>:[,]{}:()".indexOf(a)?e.push(d("op",a)):d("op",a,a+" is not an ObjectPath operator!"),a=this.charAt(b));h&&clog("}tokens with",e);return e}};
var makeTree=function(){var h={},e,n,l,u=["true","t"],v=["false","f"],w=["none","null","n","nil"],m=function(){return this},f=function(a){var b,c,d;a&&e.id!==a&&e.error("Expected '"+a+"', got '"+e.id+"'.");if(l>=n.length)e=h["(end)"];else return c=n[l],l+=1,d=c.value,a=c.type,"name"===a?(0<=v.indexOf(d.toLowerCase())&&(d="false"),0<=u.indexOf(d.toLowerCase())&&(d="true"),0<=w.indexOf(d.toLowerCase())&&(d="null"),(b=h[d])||(b=h["(name)"])):"(root)"===a?b=h["(root)"]:"(current)"===a?b=h["(current)"]:
"(context)"===a?b=h["(context)"]:"op"===a?(b=h[d])||c.error("Unknown operator."):"str"===a||"number"===a?(b=h["(literal)"],a="literal"):c.error("Unexpected token."),e=Object.create(b),e.position=c.position,e.value=d,e.arity=a,e.error=function(a){},e},k=function(a){var b,c=e;a="undefined"===typeof a?0:a;f();for(b=c.nud();a<e.lbp;)c=e,f(),b=c.led(b);return b},x={nud:function(){this.error("Undefined nud().")},led:function(a){this.error("Missing operator.")}},d=function(a,b){var c=h[a];b=b||0;c?b>=c.lbp&&
(c.lbp=b):(c=Object.create(x),c.id=c.value=a,c.lbp=b,h[a]=c);return c},p=function(a,b){var c=d(a);c.nud=function(){this.value=h[this.id].value;this.arity="literal";this.id="(literal)";return this};c.value=b;return c},g=function(a,b,c){a=d(a,b);a.led=c||function(a){this.first=a;this.second=k(b);this.arity="binary";return this};return a},r=function(a,b,c){a=d(a,b);a.led=c||function(a){this.first=a;this.second=k(b-1);this.arity="binary";return this};return a},q=function(a,b,c){a=d(a);a.nud=c||function(){this.first=
k(b);this.arity="unary";return this};return a},t=function(a){this.first=a;0>["(name)","*"].indexOf(e.id)&&SyntaxError("Expected an attribute name.");"*"===e.id&&(e.arity="wildcard");this.second=e;f();return this};d("(end)");d("(name)").nud=m;d("(literal)").nud=m;d("(root)").nud=m;d("(current)").nud=m;d("(context)").nud=m;d(":");d(",");p("true",!0);p("false",!1);p("null",null);r("or",30);r("and",40);q("not",50);g(":",120);g("in",60);g("not in",60);g("is",60);g("is not",60);g("<",60);g("<=",60);g(">",
60);g(">=",60);g("+",110);g("-",110);g("*",120);g("/",120);g("%",120);q("-",130);q("+",130);d(".",150).led=t;d("..",150).led=t;d("]");g("[",150,function(a){this.first=a;this.second=k(0);this.arity="binary";f("]");return this});d("[").led=function(a){this.first=a;this.second=k();f("]");return this};d("[").nud=function(){var a=[];if("]"!==e.id)for(;;){a.push(k());if(","!==e.id)break;f(",")}f("]");this.first=a;this.arity="unary";return this};d(")");d("(",150).led=function(a){var b=[];this.arity="binary";
this.id="fn";this.first=a.value;if(")"!==e.id)for(;;){b.push(k());if(","!==e.id)break;f(",")}f(")");this.second=b;return this};d("(",150).nud=function(){var a=k();f(")");return a};d("}");d("{").nud=function(){var a=[],b,c;if("}"!==e.id)for(;;){b=k();f(":");c=k();c.key=b;a.push(c);if(","!==e.id)break;f(",")}f("}");this.first=a;this.arity="unary";return this};return function(a,b){var c=this.D=b&&b.debug||!1;n="string"===typeof a?a.tokens(c):a;l=0;f();c=k();f("(end)");return c}},parse=makeTree();
module.exports = ObjectPath=function(a,h){this.exprCache=[];this._init_(a,h);return this};
ObjectPath.prototype={D:!1,current:null,data:null,SELECTOR_OPS:"is;>;<;is not;>=;<=;in;not in;:;and;or".split(";"),simpleTypes:["string","number"],_init_:function(a,h){this.setData(a);h&&this.setDebug(h.debug||this.D)},setCurrent:function(a){this.current=a},resetCurrent:function(){this.current=null},setContext:function(a){this.context=a},compile:function(a){return this.exprCache.hasOwnProperty(a)?this.exprCache[a]:this.exprCache[a]=parse.call(this,a,{debug:this.D})},setData:function(a){return 0>["object",
"array"].indexOf(typeof a)?(this.log(a+" is not object nor array! Data not changed."),a):this.data=a},setDebug:function(a){this.D=a},flatten:function(a){var h=[],l=function(a){if(Array.isArray(a))for(var g=0;g<a.length;g++)l(a[g]);else if("object"===typeof a)for(g in h.push(a),a)l(a[g])};l(a);return h},execute:function(a){var h=this,l=this.flatten,m=this.simpleTypes,g,f=function(b){if(Array.isArray(b)){for(var c=[],e=0;e<b.length;e++)c.push(f(b[e]));return c}var a=b.id;if(0<="+;-;*;%;/;>;>=;<;<=;in;not in;is;is not".split(";").indexOf(b.id))var c=
"object"===typeof b.first&&b.first.id?f(b.first):null,d="object"===typeof b.second&&b.second.id?f(b.second):null,g=typeof c,k=typeof d;switch(a){case "(literal)":return b.value;case "+":if("number"===g&&"number"!==k)d=parseInt(d);else if(Array.isArray(c)){if(Array.isArray(d))return c.concat(d);c.push(d)}return c+d;case "-":return d?c-d:-c;case "*":return"wildcard"!=b.arity?c*d:null;case "%":return c%d;case "/":return c/d;case ">":return c>d;case "<":return c<d;case ">=":return c>=d;case "<=":return c<=
d;case "not":return!f(b.first);case "or":return f(b.first)||f(b.second);case "and":return f(b.first)&&f(b.second);case "in":case "not in":return e=null,"string"===k?e=0<=d.search(c.toString()):Array.isArray(d)?e=0<=d.indexOf(c):"object"===k&&(e=d.hasOwnProperty(c)),"in"===a?e:!e;case "is":case "is not":e=null;if(0<=m.indexOf(g))e=c==d;else if("array"===g||"object"===g)try{throw e=JSON.stringify(c)==JSON.stringify(d),{name:"comparison Error",message:"JSONStringifyNotAvailable. Your web browser doesn't support JSON.stringify(). Vote for support at"};
}catch(n){}return"is"===a?e:!e;case "(root)":return h.data;case "(current)":return h.current;case "(context)":return h.context;case ":":return b;case "(name)":return b.value;case "[":if("unary"===b.arity){a=[];for(e in b.first)a.push(f(b.first[e]));return a}if("op"===b.arity){c=f(b.first);if(!c)return c;if("string"===typeof c||Array.isArray(c)){d=f(b.second);k=typeof d;if(d&&":"===d.id)return c.slice(f(d.first),f(d.second));if("number"===k)return-1===d?c.slice(-1)[0]:0>d?c.slice(d,d+1)[0]:c[d];if("string"===
k){a=[];for(e=0;e<c.length;e++)c[e][d]&&a.push(c[e][d]);return a}if("object"===typeof b.second&&0<=h.SELECTOR_OPS.indexOf(b.second.id)){b=b.second;a=[];b=Object.create(b);for(e in c)d=c[e],h.current=d,b.first=d,f(b)&&a.push(d);return a}programmingError("left is array and right is not number")}else if("object"===g&&("(name)"===b.second.id||"string"===k))return c[d];return 1}return null;case "(":switch(b.first.value){case "str":return e=f(b.second),"object"===typeof e?JSON.stringify(e):e.toString}return null;
case "{":case "":throw{error:"NotImplementedYet",message:a+" is not implemented yet!",data:b};case "..":c=l(f(b.first));case ".":c=c||f(b.first);if("*"===b.second.id)return c;if(c){if(Array.isArray(c)){a=[];d=f(b.second);for(e=0;e<c.length;e++)c[e][d]&&a.push(c[e][d]);return a}return c[b.second.value]}return null;case "fn":switch(b.first){case "float":case "int":return parseFloat(f(b.second));case "str":return f(b.second).toString();case "array":return e=f(b.second),Array.isArray(e[0])?e[0]:"string"===
typeof e[0]?e[0].split(""):[];case "replace":return a=f(b.second),a[0]?a[0].replace(new RegExp(a[1],"g"),a[2]):"";case "join":a=f(b.second);try{return a[0].join(a[1])}catch(p){return null}case "split":return a=f(b.second),a[0]?a[0].split(a[1]):"";case "max":return Math.max.apply(null,f(b.second)[0]);case "min":return Math.min.apply(null,f(b.second)[0])}throw{error:"WrongFunction",message:"Function "+a+" is not proper ObjectPath function.",data:b};}throw{error:"WrongOperator",message:"Operator "+a+
" is not proper ObjectPath operator.",data:b};};if(!a)return a;"string"===typeof a&&(g=this.compile(a));return f(g)}};

View file

@ -158,15 +158,15 @@ FFZ.prototype.setup_colors = function() {
// Events for rebuilding colors. // Events for rebuilding colors.
var Layout = utils.ember_lookup('service:layout'), var Layout = utils.ember_lookup('service:layout'),
Settings = utils.ember_lookup('controller:settings'); Settings = utils.ember_settings();
if ( Layout ) if ( Layout )
Layout.addObserver("isTheatreMode", this._update_colors.bind(this, true)); Layout.addObserver("isTheatreMode", this._update_colors.bind(this, true));
if ( Settings ) if ( Settings )
Settings.addObserver("settings.darkMode", this._update_colors.bind(this, true)) Settings.addObserver("darkMode", this._update_colors.bind(this, true))
this._color_old_darkness = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')); this._color_old_darkness = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('darkMode'));
} }
@ -685,9 +685,8 @@ FFZ.prototype._rebuild_colors = function() {
FFZ.prototype._update_colors = function(darkness_only) { FFZ.prototype._update_colors = function(darkness_only) {
// Update the lines. ALL of them. // Update the lines. ALL of them.
var Layout = utils.ember_lookup('service:layout'), var Layout = utils.ember_lookup('service:layout'),
Settings = utils.ember_lookup('controller:settings'),
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')), is_dark = (Layout && Layout.get('isTheatreMode')) || this.settings.get_twitch("darkMode"),
cr_dark = this.settings.dark_twitch || (Layout && Layout.get('isTheatreMode')); cr_dark = this.settings.dark_twitch || (Layout && Layout.get('isTheatreMode'));
if ( darkness_only && this._color_old_darkness === is_dark ) if ( darkness_only && this._color_old_darkness === is_dark )

View file

@ -1,5 +1,132 @@
var FFZ = window.FrankerFaceZ, var FFZ = window.FrankerFaceZ,
utils = require('./utils'); constants = require('./constants'),
utils = require('./utils'),
KNOWN_COMMANDS = ['ffz', 'unban', 'ban', 'timeout', 'r9kbeta', 'r9kbetaoff', 'slow', 'slowoff', 'subscribers', 'subscribersoff', 'mod', 'unmod', 'me', 'emotesonly', 'emotesonlyoff', 'host', 'unhost', 'commercial'],
STATUS_CODES = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
500: "Internal Server Error"
},
format_result = function(response) {
if ( typeof response === "string" )
return response;
else if ( Array.isArray(response) )
return _.map(response, format_result).join(", ");
return JSON.stringify(response);
},
ObjectPath = require('./ObjectPath');
FFZ.ObjectPath = ObjectPath;
// -----------------
// Settings
// -----------------
FFZ.settings_info.command_aliases = {
type: "button",
value: [],
category: "Chat Moderation",
no_bttv: true,
name: "Command Aliases",
help: "Define custom commands for chat that are shortcuts for other commands or messages to send in chat.",
on_update: function() {
this.cache_command_aliases();
},
method: function() {
var f = this,
old_val = [],
input = utils.createElement('textarea');
input.style.marginBottom = '20px';
for(var i=0; i < this.settings.command_aliases.length; i++) {
var pair = this.settings.command_aliases[i],
name = pair[0],
command = pair[1];
old_val.push(name + '=' + command);
}
utils.prompt(
"Command Aliases",
"Please enter a list of custom commands that you would like to use in Twitch chat. " +
"One item per line. To send multiple commands, separate them with <code>&lt;LINE&gt;</code>. " +
"Variables, such as arguments you provide running the custom command, can be inserted into the output.<hr>" +
"All custom commands require names. Names go at the start of each line, and are separated from " +
" the actual command by an equals sign. Do not include the leading slash or dot. Those are automatically included.<br>" +
"<strong>Example:</strong> <code>boop=/timeout {0} 15 Boop!</code><hr>" +
"<code>{0}</code>, <code>{1}</code>, <code>{2}</code>, etc. will be replaced with any arguments you've supplied. " +
"Follow an argument index with a <code>$</code> to also include all remaining arguments.<br>" +
"<strong>Example:</strong> <code>boop=/timeout {0} 15 {1$}</code><hr>" +
"<strong>Allowed Variables</strong><br><table><tbody>" +
"<tr><td><code>{room}</code></td><td>chat room's name</td>" +
"<td><code>{room_name}</code></td><td>chat room's name</td></tr>" +
"<tr><td><code>{room_display_name}</code></td><td>chat room's display name</td>" +
"<td><code>{room_id}</code></td><td>chat room's numeric ID</td></tr>" +
"</tbody></table>",
old_val.join("\n"),
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
var vals = new_val.trim().split(/\s*\n\s*/g),
output = [];
for(var i=0; i < vals.length; i++) {
var cmd = vals[i],
name,
name_match = /^([^=]+)=/.exec(cmd);
if ( ! cmd || ! cmd.length )
continue;
if ( name_match ) {
name = name_match[1].toLowerCase();
cmd = cmd.substr(name_match[0].length);
}
output.push([name, cmd]);
}
f.settings.set("command_aliases", output);
}, 600, input);
}
};
FFZ.prototype._command_aliases = {};
FFZ.prototype.cache_command_aliases = function() {
var aliases = this._command_aliases = {};
for(var i=0; i < this.settings.command_aliases.length; i++) {
var pair = this.settings.command_aliases[i],
name = pair[0],
command = pair[1];
// Skip taken/invalid names.
if ( ! name || ! name.length || aliases[name] || KNOWN_COMMANDS.indexOf(name) !== -1 )
continue;
aliases[name] = command;
}
}
// ----------------- // -----------------
@ -117,6 +244,82 @@ FFZ.chat_commands.card = function(room, args) {
} }
FFZ.chat_commands.rules = function(room, args) {
var f = this,
r = room.room;
r.waitForRoomProperties().then(function() {
var rules = r.get("roomProperties.chat_rules");
if ( ! rules || ! rules.length )
return f.room_message(room, "This chat room does not have rules set.");
r.set("chatRules", rules);
r.set("shouldDisplayChatRules", true);
});
}
FFZ.chat_commands.open_link = function(room, args) {
if ( ! args || ! args.length )
return "Usage: /open_link <url>";
var wnd = window.open(args.join(" "), "_blank");
wnd.opener = null;
}
FFZ.chat_commands.fetch_link = function(room, args) {
if ( ! args || ! args.length )
return "Usage: /fetch_link <url> [template]\nTemplates use http://objectpath.org/ to format data. Default Template is \"Response: #$#\"";
var f = this,
url = args.shift(),
headers = {};
if ( /https?:\/\/[^.]+\.twitch\.tv\//.test(url) )
headers['Client-ID'] = constants.CLIENT_ID;
jQuery.ajax({
url: url,
headers: headers,
success: function(data) {
f.log("Response Received", data);
args = (args && args.length) ? args.join(" ").split(/#/g) : ["Response: ", "$"];
if ( typeof data === "string" )
data = [data];
var is_special = true,
output = [],
op = new ObjectPath(data);
for(var i=0; i < args.length; i++) {
var segment = args[i];
is_special = !is_special;
if ( ! is_special )
output.push(segment);
else
try {
output.push(format_result(op.execute(segment)));
} catch(err) {
f.log("Error", err);
output.push("[Error: " + (err.message || err) + "]");
}
}
f.room_message(room, output.join(''));
},
error: function(xhr) {
f.log("Request Error", xhr);
f.room_message(room, "Request Failed: " + (xhr.status === 0 ? 'Unknown Error. ' + (url.indexOf('https') === -1 ? 'Please make sure you\'re making HTTPS requests.' : 'Likely a CORS problem. Check your browser\'s Networking console for more.') : xhr.status + ' ' + (STATUS_CODES[xhr.status] || '' )));
}
});
}
// ----------------- // -----------------
// Mass Moderation // Mass Moderation
// ----------------- // -----------------

View file

@ -17,9 +17,16 @@ FFZ.prototype.setup_channel = function() {
// Settings stuff! // Settings stuff!
document.body.classList.toggle("ffz-hide-view-count", !this.settings.channel_views); document.body.classList.toggle("ffz-hide-view-count", !this.settings.channel_views);
document.body.classList.toggle('ffz-theater-stats', this.settings.theater_stats); document.body.classList.toggle('ffz-theater-stats', this.settings.theater_stats);
document.body.classList.toggle('ffz-channel-bar-bottom', this.settings.channel_bar_bottom);
utils.toggle_cls('ffz-minimal-channel-title')(this.settings.channel_title_top === 2);
utils.toggle_cls('ffz-channel-title-top')(this.settings.channel_title_top > 0);
utils.toggle_cls('ffz-minimal-channel-bar')(this.settings.channel_bar_collapse);
this.log("Hooking the Ember Channel Index view."); this.log("Hooking the Ember Channel Index redesign.");
if ( ! this.update_views('view:channel/index', this.modify_channel_index) ) this.update_views('component:channel-redesign', this.modify_channel_redesign);
this.log("Hooking the Ember Channel Index component.");
if ( ! this.update_views('component:legacy-channel', this.modify_channel_index) )
return; return;
this.log("Hooking the Ember Channel model."); this.log("Hooking the Ember Channel model.");
@ -71,52 +78,52 @@ FFZ.prototype.setup_channel = function() {
}.property('content.id', 'login.userData', 'login.userData.login'),*/ }.property('content.id', 'login.userData', 'login.userData.login'),*/
ffzUpdateUptime: function() { /*ffzUpdateUptime: function() {
if ( f._cindex ) if ( f._cindex )
f._cindex.ffzUpdateUptime(); f._cindex.ffzUpdateUptime();
}.observes("isLive", "content.id"), }.observes("isLive", "channel.id"),*/
ffzUpdateInfo: function() { ffzUpdateInfo: function() {
if ( this._ffz_update_timer ) if ( this._ffz_update_timer )
clearTimeout(this._ffz_update_timer); clearTimeout(this._ffz_update_timer);
if ( ! this.get('content.id') ) if ( ! this.get('channel.id') )
return; return;
this._ffz_update_timer = setTimeout(this.ffzCheckUpdate.bind(this), 55000 + (Math.random() * 10000)); this._ffz_update_timer = setTimeout(this.ffzCheckUpdate.bind(this), 55000 + (Math.random() * 10000));
}.observes("content.id"), }.observes("channel.id"),
ffzCheckUpdate: function() { ffzCheckUpdate: function() {
var t = this, var t = this,
id = t.get('content.id'); id = t.get('channel.id');
id && utils.api.get("streams/" + id, {}, {version:3}) id && utils.api.get("streams/" + id, {}, {version:3})
.done(function(data) { .done(function(data) {
if ( ! data || ! data.stream ) { if ( ! data || ! data.stream ) {
// If the stream is offline, clear its created_at time and set it to zero viewers. // If the stream is offline, clear its created_at time and set it to zero viewers.
t.set('content.stream.created_at', null); t.set('channel.stream.createdAt', null);
t.set('content.stream.viewers', 0); t.set('channel.stream.viewers', 0);
return; return;
} }
t.set('content.stream.created_at', data.stream.created_at || null); t.set('channel.stream.createdAt', utils.parse_date(data.stream.created_at) || null);
t.set('content.stream.viewers', data.stream.viewers || 0); t.set('channel.stream.viewers', data.stream.viewers || 0);
var game = data.stream.game || (data.stream.channel && data.stream.channel.game); var game = data.stream.game || (data.stream.channel && data.stream.channel.game);
if ( game ) { if ( game ) {
t.set('content.game', game); t.set('channel.game', game);
} }
if ( data.stream.channel ) { if ( data.stream.channel ) {
if ( data.stream.channel.status ) if ( data.stream.channel.status )
t.set('content.status', data.stream.channel.status); t.set('channel.status', data.stream.channel.status);
if ( data.stream.channel.views ) if ( data.stream.channel.views )
t.set('content.views', data.stream.channel.views); t.set('channel.views', data.stream.channel.views);
if ( data.stream.channel.followers && t.get('content.followers.isLoaded') ) if ( data.stream.channel.followers && t.get('channel.followers.isLoaded') )
t.set('content.followers.total', data.stream.channel.followers); t.set('channel.followers.total', data.stream.channel.followers);
} }
}) })
@ -125,19 +132,19 @@ FFZ.prototype.setup_channel = function() {
}); });
}, },
ffzUpdateTitle: function() { /*ffzUpdateTitle: function() {
var name = this.get('content.name'), var name = this.get('channel.name'),
display_name = this.get('content.display_name'); display_name = this.get('channel.display_name');
if ( display_name ) if ( display_name )
FFZ.capitalization[name] = [display_name, Date.now()]; FFZ.capitalization[name] = [display_name, Date.now()];
if ( f._cindex ) if ( f._cindex )
f._cindex.ffzFixTitle(); f._cindex.ffzFixTitle();
}.observes("content.status", "content.game", "content.id", "hostModeTarget.status", "hostModeTarget.id", "hostModeTarget.game"), }.observes("channel.status", "channel.game", "channel.id", "channel.hostModeTarget.status", "channel.hostModeTarget.id", "channel.hostModeTarget.game"),*/
ffzHostTarget: function() { ffzHostTarget: function() {
var target = this.get('content.hostModeTarget'), var target = this.get('channel.hostModeTarget'),
name = target && target.get('name'), name = target && target.get('name'),
id = target && target.get('id'), id = target && target.get('id'),
display_name = target && target.get('display_name'); display_name = target && target.get('display_name');
@ -165,10 +172,9 @@ FFZ.prototype.setup_channel = function() {
if ( f.settings.srl_races ) if ( f.settings.srl_races )
f.rebuild_race_ui(); f.rebuild_race_ui();
}.observes("content.hostModeTarget") }.observes("channel.hostModeTarget")
}); });
Ember.propertyDidChange(Channel, 'isEditable');
Channel.ffzUpdateInfo(); Channel.ffzUpdateInfo();
} }
@ -191,17 +197,345 @@ FFZ.prototype._modify_cmodel = function(model) {
} }
FFZ.prototype.modify_channel_redesign = function(view) {
var f = this;
utils.ember_reopen_view(view, {
ffz_init: function() {
var channel_id = this.get("channel.id"),
el = this.get("element");
f._cindex = this;
f.ws_send("sub", "channel." + channel_id);
el.setAttribute('data-channel', channel_id);
el.classList.add('ffz-channel');
this.ffzFixTitle();
this.ffzUpdateUptime();
this.ffzUpdateChatters();
this.ffzUpdateHostButton();
this.ffzUpdatePlayerStats();
this.ffzUpdateCoverHeight();
if ( f.settings.auto_theater ) {
var player = f.players && f.players[channel_id] && f.players[channel_id].get('player');
if ( player )
player.setTheatre(true);
}
this.$().on("click", ".ffz-creative-tag-link", function(e) {
if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey )
return;
utils.ember_lookup("router:main").transitionTo('creative.hashtag.index', this.getAttribute('data-tag'));
e.preventDefault();
return false;
});
},
ffzUpdateCoverHeight: function() {
var old_height = this.get('channelCoverHeight'),
new_height = f.settings.channel_bar_bottom ? 0 : 380;
this.set('channelCoverHeight', new_height);
this.$("#channel").toggleClass('ffz-bar-fixed', this.get('isFixed'));
if ( old_height !== new_height )
this.scrollTo(this.$scrollContainer.scrollTop() + (new_height - old_height));
}.observes('isFixed'),
ffzFixTitle: function() {
if ( ! f.settings.stream_title )
return;
var channel_id = this.get("channel.id"),
status = this.get("channel.status"),
game = this.get("channel.game"),
tokens = f.tokenize_line(channel_id, channel_id, status, true);
if ( game === 'Creative' )
tokens = f.tokenize_ctags(tokens);
var el = this.$(".cn-metabar__title .card__title");
el && el.html(f.render_tokens(tokens));
}.observes('channel.id', 'channel.status', 'channel.game'),
ffzUpdateUptime: function() {
if ( this._ffz_update_uptime ) {
clearTimeout(this._ffz_update_uptime);
delete this._ffz_update_uptime;
}
var container = this.get('element');
if ( this.isDestroyed || ! container || ! f.settings.stream_uptime || ! this.get('isLiveAccordingToKraken') )
return container && this.$("#ffz-uptime-display").remove();
// Schedule an update.
this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000);
// Determine when the channel last went live.
var online = this.get("channel.stream.createdAt"),
now = Date.now() - (f._ws_server_offset || 0);
var uptime = online && Math.floor((now - online.getTime()) / 1000) || -1;
if ( uptime < 0 )
return this.$("#ffz-uptime-display").remove();
var el = container.querySelector('#ffz-uptime-display span');
if ( ! el ) {
var cont = container.querySelector('.cn-metabar__more');
if ( ! cont )
return;
var stat = utils.createElement('span'),
figure = utils.createElement('figure', 'icon cn-metabar__icon', constants.CLOCK + ' '),
balloon = utils.createElement('div', 'balloon balloon--tooltip balloon--down balloon--center'),
balloon_wrapper = utils.createElement('div', 'balloon-wrapper', figure),
stat_wrapper = utils.createElement('div', 'cn-metabar__ffz flex__item mg-l-1', balloon_wrapper);
balloon_wrapper.appendChild(stat);
balloon_wrapper.appendChild(balloon);
stat_wrapper.id = 'ffz-uptime-display';
balloon.innerHTML = 'Stream Uptime <nobr>(since ' + online.toLocaleString() + ')</nobr>';
var viewers = cont.querySelector(".cn-metabar__livecount");
if ( viewers )
cont.insertBefore(stat_wrapper, viewers.nextSibling);
else
cont.appendChild(stat_wrapper);
el = stat;
}
el.innerHTML = utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1 || f.settings.stream_uptime === 3);
}.observes('channel.stream.createdAt', 'isLiveAccordingToKraken'),
ffzUpdatePlayerStats: function() {
if ( this._ffz_update_stats ) {
clearTimeout(this._ffz_update_stats);
this._ffz_update_stats = null;
}
// Stop scheduling this so it can die.
if ( this.isDestroyed )
return;
// Schedule an update.
if ( f.settings.player_stats )
this._ffz_update_stats = setTimeout(this.ffzUpdatePlayerStats.bind(this), 1000);
var channel_id = this.get("channel.id"),
container = this.get("element"),
player_cont = f.players && f.players[channel_id],
player, stats;
try {
player = player_cont && player_cont.get('player');
stats = player && player.getVideoInfo();
} catch(err) {
f.error("Channel ffzUpdatePlayerStats: player.getVideoInfo", err);
}
if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hls_latency_broadcaster )
return container && this.$("#ffz-player-stats").remove();
var el = container.querySelector("#ffz-player-stats");
if ( ! el ) {
var cont = container.querySelector('.cn-metabar__more');
if ( ! cont )
return;
var stat = utils.createElement('span'),
figure = utils.createElement('figure', 'icon cn-metabar__icon', constants.GRAPH + ' '),
balloon = utils.createElement('div', 'balloon balloon--tooltip balloon--up balloon--center'),
balloon_wrapper = utils.createElement('div', 'balloon-wrapper', figure);
el = utils.createElement('div', 'cn-metabar__ffz flex__item mg-l-1', balloon_wrapper);
balloon_wrapper.appendChild(stat);
balloon_wrapper.appendChild(balloon);
el.id = 'ffz-player-stats';
var viewers = cont.querySelector('#ffz-uptime-display') || cont.querySelector(".cn-metabar__livecount");
if ( viewers )
cont.insertBefore(el, viewers.nextSibling);
else
cont.appendChild(el);
}
var stat = el.querySelector('span'),
balloon = el.querySelector('.balloon');
var delay = Math.round(stats.hls_latency_broadcaster / 10) / 100,
dropped = utils.number_commas(stats.dropped_frames || 0),
bitrate;
if ( stats.playback_bytes_per_second )
bitrate = Math.round(stats.playback_bytes_per_second * 8 / 10.24) / 100;
else
bitrate = Math.round(stats.current_bitrate * 100) / 100;
var is_old = delay > 180;
if ( is_old ) {
delay = Math.floor(delay);
stat.textContent = utils.time_to_string(delay, true, delay > 172800) + ' old';
} else {
delay = delay.toString();
var ind = delay.indexOf('.');
delay += (ind === -1 ? '.00' : (ind >= delay.length - 2 ? '0' : '')) + 's';
stat.textContent = delay;
}
balloon.innerHTML = (is_old ? 'Video Information<br>' +
'Broadcast ' + utils.time_to_string(delay, true) + ' Ago<br><br>' : 'Stream Latency<br>') +
'Video: ' + stats.vid_width + 'x' + stats.vid_height + 'p ' + stats.current_fps + ' fps<br>' +
'Playback Rate: ' + bitrate + ' Kbps<br>' +
'Dropped Frames: ' + dropped;
},
ffzUpdateChatters: function() {
var channel_id = this.get("channel.id"),
room = f.rooms && f.rooms[channel_id],
container = this.get('element');
if ( ! container || ! room || ! f.settings.chatter_count )
return container && this.$("#ffz-chatter-display").remove();
var chatter_count = Object.keys(room.room.get('ffz_chatters') || {}).length,
el = container.querySelector('#ffz-chatter-display span');
if ( ! el ) {
var cont = container.querySelector('.cn-metabar__more');
if ( ! cont )
return;
var stat = utils.createElement('span'),
figure = utils.createElement('figure', 'icon cn-metabar__icon', constants.ROOMS + ' '),
balloon = utils.createElement('div', 'balloon balloon--tooltip balloon--down balloon--center', 'Currently in Chat'),
balloon_wrapper = utils.createElement('div', 'balloon-wrapper', figure),
stat_wrapper = utils.createElement('div', 'cn-metabar__ffz flex__item mg-l-1', balloon_wrapper);
balloon_wrapper.appendChild(stat);
balloon_wrapper.appendChild(balloon);
stat_wrapper.id = 'ffz-chatter-display';
var viewers = cont.querySelector('#ffz-player-stats') || cont.querySelector('#ffz-uptime-display') || cont.querySelector(".cn-metabar__livecount") || cont.querySelector(".cn-metabar__viewcount");
if ( viewers )
cont.insertBefore(stat_wrapper, viewers.nextSibling);
else
cont.appendChild(stat_wrapper);
el = stat;
}
el.innerHTML = utils.number_commas(chatter_count);
}.observes('channel.id'),
ffzUpdateHostButton: function() {
var t = this,
channel_id = this.get("channel.id"),
hosted_id = this.get("channel.hostModeTarget.id"),
user = f.get_user(),
room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room,
now_hosting = room && room.ffz_host_target,
hosts_left = room && room.ffz_hosts_left,
el = this.get("element"),
update_button = function(channel, container, before) {
if ( ! container )
return;
var btn = container.querySelector('#ffz-ui-host-button');
if ( ! f.settings.stream_host_button || ! user || user.login === channel ) {
if ( btn )
btn.parentElement.removeChild(btn);
return;
}
if ( ! btn ) {
btn = utils.createElement('button', 'button button--hollow mg-l-1'),
btn.id = 'ffz-ui-host-button';
btn.addEventListener('click', t.ffzClickHost.bind(t, channel !== channel_id));
if ( before )
container.insertBefore(btn, before);
else
container.appendChild(btn);
jQuery(btn).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')});
}
btn.classList.remove('disabled');
btn.innerHTML = channel === now_hosting ? 'Unhost' : 'Host';
if ( now_hosting ) {
var name = FFZ.get_capitalization(now_hosting);
btn.title = 'You are now hosting ' + f.format_display_name(name, now_hosting, true)[0] + '.';
} else
btn.title = 'You are not hosting any channel.';
if ( typeof hosts_left === 'number' )
btn.title += ' You have ' + hosts_left + ' host command' + utils.pluralize(hosts_left) + ' remaining this half hour.';
};
if ( ! el )
return;
this.set("ffz_host_updating", false);
if ( channel_id ) {
var container = el.querySelector('.cn-metabar__more'),
share = container && container.querySelector('.js-share-box');
update_button(channel_id, container, share ? share.parentElement : null);
}
if ( hosted_id )
update_button(hosted_id, el.querySelector('.cn-hosting--bottom'));
}.observes('channel.id', 'channel.hostModeTarget.id'),
ffzClickHost: function(is_host, e) {
var btn = e.target,
target = this.get(is_host ? 'channel.hostModeTarget.id' : 'channel.id'),
user = f.get_user(),
room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room,
now_hosting = room && room.ffz_host_target;
if ( ! room || this.get('ffz_host_updating') )
return;
btn.classList.add('disabled');
btn.title = 'Updating...';
this.set('ffz_host_updating', true);
if ( now_hosting === target )
room.send('/unhost', true);
else
room.send('/host ' + target, true);
}
})
}
FFZ.prototype.modify_channel_index = function(view) { FFZ.prototype.modify_channel_index = function(view) {
var f = this; var f = this;
utils.ember_reopen_view(view, { utils.ember_reopen_view(view, {
ffz_init: function() { ffz_init: function() {
var id = this.get('controller.content.id') || this.get('controller.id'), var channel_id = this.get('model.id'),
el = this.get('element'); el = this.get('element');
f._cindex = this; f._cindex = this;
f.ws_send("sub", "channel." + id); f.ws_send("sub", "channel." + channel_id);
el.setAttribute('data-channel', id); el.setAttribute('data-channel', channel_id);
el.classList.add('ffz-channel'); el.classList.add('ffz-channel');
this.ffzFixTitle(); this.ffzFixTitle();
@ -225,7 +559,7 @@ FFZ.prototype.modify_channel_index = function(view) {
f.rebuild_race_ui(); f.rebuild_race_ui();
if ( f.settings.auto_theater ) { if ( f.settings.auto_theater ) {
var player = f.players && f.players[id] && f.players[id].get('player'); var player = f.players && f.players[channel_id] && f.players[channel_id].get('player');
if ( player ) if ( player )
player.setTheatre(true); player.setTheatre(true);
} }
@ -241,12 +575,15 @@ FFZ.prototype.modify_channel_index = function(view) {
}, },
ffz_destroy: function() { ffz_destroy: function() {
var id = this.get('controller.content.id') || this.get('controller.id'); var channel_id = this.get('model.id');
if ( id ) if ( channel_id )
f.ws_send("unsub", "channel." + id); f.ws_send("unsub", "channel." + channel_id);
this.get('element').setAttribute('data-channel', ''); this.get('element').setAttribute('data-channel', '');
f._cindex = undefined;
if ( f._cindex === this )
f._cindex = null;
if ( this._ffz_update_uptime ) if ( this._ffz_update_uptime )
clearTimeout(this._ffz_update_uptime); clearTimeout(this._ffz_update_uptime);
@ -259,7 +596,7 @@ FFZ.prototype.modify_channel_index = function(view) {
} }
document.body.classList.remove('ffz-small-player'); document.body.classList.remove('ffz-small-player');
utils.update_css(f._channel_style, id, null); utils.update_css(f._channel_style, channel_id, null);
}, },
@ -279,23 +616,23 @@ FFZ.prototype.modify_channel_index = function(view) {
if ( f.has_bttv || ! f.settings.stream_title ) if ( f.has_bttv || ! f.settings.stream_title )
return; return;
var status = this.get("controller.content.status"), var channel_id = this.get('model.id'),
channel = this.get("controller.content.id"), status = this.get('model.status'),
game = this.get("controller.content.game"), game = this.get('model.game'),
tokens = f.tokenize_line(channel, channel, status, true); tokens = f.tokenize_line(channel_id, channel_id, status, true);
if ( game === 'Creative' ) if ( game === 'Creative' )
tokens = f.tokenize_ctags(tokens); tokens = f.tokenize_ctags(tokens);
this.$("#broadcast-meta .title").html(f.render_tokens(tokens)); this.$("#broadcast-meta .title").html(f.render_tokens(tokens));
status = this.get('controller.hostModeTarget.status'); status = this.get('hostModeTarget.status');
channel = this.get('controller.hostModeTarget.id'); channel_id = this.get('hostModeTarget.id');
game = this.get('controller.hostModeTarget.game'); game = this.get('hostModeTarget.game');
if ( channel ) { if ( channel_id ) {
tokens = f.tokenize_line(channel, channel, status, true); tokens = f.tokenize_line(channel_id, channel_id, status, true);
if ( game === 'Creative' ) if ( game === 'Creative' )
tokens = f.tokenize_ctags(tokens); tokens = f.tokenize_ctags(tokens);
@ -305,8 +642,8 @@ FFZ.prototype.modify_channel_index = function(view) {
ffzUpdateHostButton: function() { ffzUpdateHostButton: function() {
var channel_id = this.get('controller.content.id') || this.get('controller.id'), var channel_id = this.get('model.id'),
hosted_id = this.get('controller.hostModeTarget.id'), hosted_id = this.get('hostModeTarget.id'),
user = f.get_user(), user = f.get_user(),
room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room, room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room,
@ -399,7 +736,7 @@ FFZ.prototype.modify_channel_index = function(view) {
ffzClickHost: function(is_host, e) { ffzClickHost: function(is_host, e) {
var btn = e.target, var btn = e.target,
target = is_host ? this.get('controller.hostModeTarget.id') : (this.get('controller.content.id') || this.get('controller.id')), target = is_host ? this.get('hostModeTarget.id') : this.get('model.id'),
user = f.get_user(), user = f.get_user(),
room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room, room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room,
now_hosting = room && room.ffz_host_target; now_hosting = room && room.ffz_host_target;
@ -420,7 +757,7 @@ FFZ.prototype.modify_channel_index = function(view) {
ffzUpdateChatters: function() { ffzUpdateChatters: function() {
// Get the counts. // Get the counts.
var room_id = this.get('controller.content.id') || this.get('controller.id'), var room_id = this.get('model.id'),
room = f.rooms && f.rooms[room_id]; room = f.rooms && f.rooms[room_id];
if ( ! room || ! f.settings.chatter_count ) { if ( ! room || ! f.settings.chatter_count ) {
@ -505,8 +842,8 @@ FFZ.prototype.modify_channel_index = function(view) {
if ( f.settings.player_stats ) if ( f.settings.player_stats )
this._ffz_update_stats = setTimeout(this.ffzUpdatePlayerStats.bind(this), 1000); this._ffz_update_stats = setTimeout(this.ffzUpdatePlayerStats.bind(this), 1000);
var channel_id = this.get('controller.content.id') || this.get('controller.id'), var channel_id = this.get('model.id'),
hosted_id = this.get('controller.hostModeTarget.id'), hosted_id = this.get('hostModeTarget.id'),
el = this.get('element'); el = this.get('element');
@ -667,7 +1004,8 @@ FFZ.prototype.modify_channel_index = function(view) {
delete this._ffz_update_uptime; delete this._ffz_update_uptime;
} }
if ( ! f.settings.stream_uptime || ! this.get("controller.isLiveAccordingToKraken") ) { var controller = utils.ember_lookup('controller:channel');
if ( ! f.settings.stream_uptime || ! (controller && controller.get('isLiveAccordingToKraken')) ) {
var el = this.get('element').querySelector('#ffz-uptime-display'); var el = this.get('element').querySelector('#ffz-uptime-display');
if ( el ) if ( el )
el.parentElement.removeChild(el); el.parentElement.removeChild(el);
@ -678,7 +1016,7 @@ FFZ.prototype.modify_channel_index = function(view) {
this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000); this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000);
// Determine when the channel last went live. // Determine when the channel last went live.
var online = this.get("controller.content.stream.created_at"), var online = this.get("model.stream.created_at"),
now = Date.now() - (f._ws_server_offset || 0); now = Date.now() - (f._ws_server_offset || 0);
online = online && utils.parse_date(online); online = online && utils.parse_date(online);
@ -894,6 +1232,93 @@ FFZ.settings_info.stream_title = {
}; };
FFZ.settings_info.channel_bar_bottom = {
type: "boolean",
value: false,
no_bttv: true,
no_mobile: true,
category: "Appearance",
name: "Channel Bar on Bottom",
help: "Hide the profile banner and position the channel bar at the bottom of the screen.",
on_update: function(val) {
if ( this.has_bttv )
return;
utils.toggle_cls('ffz-channel-bar-bottom')(val);
if ( this._cindex )
this._cindex.ffzUpdateCoverHeight();
var Layout = utils.ember_lookup('service:layout');
if ( Layout )
Ember.propertyDidChange(Layout, 'windowHeight');
}
}
FFZ.settings_info.channel_bar_collapse = {
type: "boolean",
value: false,
no_bttv: true,
no_mobile: true,
category: "Appearance",
name: "Minimal Channel Bar",
help: "Slide the channel bar mostly out of view when it's not being used.",
on_update: function(val) {
if ( this.has_bttv )
return;
utils.toggle_cls('ffz-minimal-channel-bar')(val);
var Layout = utils.ember_lookup('service:layout');
if ( Layout )
Ember.propertyDidChange(Layout, 'windowHeight');
}
}
FFZ.settings_info.channel_title_top = {
type: "select",
options: {
0: "Disabled",
1: "On Top",
2: "On Top, Minimal"
},
value: 0,
process_value: function(val) {
if ( typeof val === "string" ) {
val = parseInt(val);
if ( isNaN(val) || ! isFinite(val) )
val = 0;
}
return val;
},
no_bttv: true,
no_mobile: true,
category: "Appearance",
name: "Channel Title on Top",
help: "Display the channel title and game above the player rather than below.",
on_update: function(val) {
if ( this.has_bttv )
return;
document.body.classList.toggle('ffz-minimal-channel-title', val === 2);
document.body.classList.toggle('ffz-channel-title-top', val > 0);
var Layout = utils.ember_lookup('service:layout');
if ( Layout )
Ember.propertyDidChange(Layout, 'windowHeight');
}
}
FFZ.settings_info.theater_stats = { FFZ.settings_info.theater_stats = {
type: "boolean", type: "boolean",
value: true, value: true,

View file

@ -100,7 +100,7 @@ FFZ.settings_info.input_complete_emotes = {
2: "All Emoticons" 2: "All Emoticons"
}, },
value: 0, value: 1,
process_value: function(val) { process_value: function(val) {
if ( typeof val === 'string' ) if ( typeof val === 'string' )
@ -111,7 +111,7 @@ FFZ.settings_info.input_complete_emotes = {
category: "Chat Input", category: "Chat Input",
no_bttv: true, no_bttv: true,
name: "Tab-Complete Emoticons <span>Beta</span>", name: "Tab-Complete Emoticons",
help: "Use tab completion to complete emoticon names in chat.", help: "Use tab completion to complete emoticon names in chat.",
on_update: function(val) { on_update: function(val) {
@ -503,7 +503,7 @@ FFZ.prototype.modify_chat_input = function(component) {
ffzFetchNameSuggestions: function() { ffzFetchNameSuggestions: function() {
if ( ! this.get('ffz_suggestions_visible') ) if ( ! this.get('ffz_suggestions_visible') )
this.set('ffz_name_suggestions', this.get('suggestions')); this.set('ffz_name_suggestions', this.get('suggestions')());
}.observes('suggestions'), }.observes('suggestions'),

View file

@ -138,16 +138,17 @@ FFZ.settings_info.chat_batching = {
FFZ.settings_info.chat_delay = { FFZ.settings_info.chat_delay = {
type: "select", type: "select",
options: { options: {
"-1": "Default Delay (Room Specific; Non-Mod Only)", "-1": ["Default Delay (Room Specific; Non-Mod Only)", 0],
0: "No Delay", 0: ["No Delay", 1],
300: "Minor (Bot Moderation; 0.3s)", 300: ["Minor (Bot Moderation; 0.3s)", 2],
1200: "Normal (Human Moderation; 1.2s)", 1200: ["Normal (Human Moderation; 1.2s)", 3],
5000: "Large (Spoiler Removal / Really Slow Mods; 5s)", 5000: ["Large (Spoiler Removal / Really Slow Mods; 5s)", 4],
10000: "Extra Large (10s)", 10000: ["Extra Large (10s)", 5],
15000: "Extremely Large (15s)", 15000: ["Extremely Large (15s)", 6],
20000: "Mods Asleep; Delay Chat (20s)", 20000: ["Mods Asleep; Delay Chat (20s)", 7],
30000: "Half a Minute (30s)", 30000: ["Half a Minute (30s)", 8],
60000: "Why??? (1m)" 60000: ["Why??? (1m)", 9],
788400000000: ["The CBenni Option (Literally 25 Years)", 10]
}, },
value: -1, value: -1,
@ -484,9 +485,83 @@ FFZ.prototype.setup_chatview = function() {
this.log("Hooking the Ember Chat view."); this.log("Hooking the Ember Chat view.");
this.update_views('view:chat', this.modify_chat_view); this.update_views('view:chat', this.modify_chat_view);
this.log("Hooking the Ember from-display-preview component.");
this.update_views('component:chat/from-display-preview', this.modify_from_display_preview);
} }
// ----------------------------
// Modify From Display Preview
// ----------------------------
FFZ.prototype.modify_from_display_preview = function(view) {
var f = this;
utils.ember_reopen_view(view, {
attributeBindings: ["chatUser.id:data-room"],
ffz_init: function() {
var el = this.get('element');
if ( el )
el.classList.add('from-display-preview');
//this.ffzUpdateChatColor();
this.ffzRenderBadges();
},
ffz_badges: function() {
var badges = f.get_twitch_badges(this.get('chatUser.chatBadges'));
return f.get_badges(this.get('userData.login'), this.get('chatUser.id'), badges);
}.property('chatUser.chatBadges', 'userData.login', 'chatUser.id'),
/*ffzUpdateChatColor: function() {
var el = this.get('element'),
name = el && el.querySelector('span.strong');
if ( ! name )
return;
name.classList.add('has-color');
name.classList.add('replay-color');
name.setAttribute('data-color', this.get('room.model.chatColor'));
}.observes('room.model.chatColor'),*/
ffzRenderBadges: function() {
var badges = this.get('ffz_badges'),
el = this.get('element'),
badge_container = el && el.querySelector('.ffz_badges');
if ( ! badge_container ) {
var old_container = el && el.querySelector('.badges');
if ( ! old_container )
return;
old_container.classList.add('hidden');
badge_container = utils.createElement('div', 'badges ffz_badges');
old_container.parentElement.insertBefore(badge_container, old_container);
}
badge_container.innerHTML = f.render_badges(badges);
}.observes('ffz_badges'),
/*colorStyle: function(e) {
var base_color = this.get('room.model.chatColor'),
colors = f._handle_color(base_color);
if ( ! colors )
return "";
return "color:" + ( f.settings.dark_twitch ? colors[1] : colors[0] );
}*/
});
}
// -------------------- // --------------------
// Modify Chat View // Modify Chat View
// -------------------- // --------------------
@ -662,7 +737,7 @@ FFZ.prototype.modify_chat_view = function(view) {
ffzUpdateHost: function() { ffzUpdateHost: function() {
var Channel = utils.ember_lookup('controller:channel'), var Channel = utils.ember_lookup('controller:channel'),
Room = utils.ember_resolve('model:room'), Room = utils.ember_resolve('model:room'),
target = Room && Channel && Channel.get('hostModeTarget'), target = Room && Channel && Channel.get('channelModel.hostModeTarget'),
updated = false; updated = false;

View file

@ -98,6 +98,83 @@ FFZ.prototype.setup_profile_following = function() {
// Modify followed items. // Modify followed items.
this.update_views('component:display-followed-item', this.modify_display_followed_item); this.update_views('component:display-followed-item', this.modify_display_followed_item);
this.update_views('component:twitch-profile-card', this.modify_twitch_profile_card);
}
FFZ.prototype.modify_twitch_profile_card = function(component) {
var f = this;
utils.ember_reopen_view(component, {
ffzParentModel: function() {
var x = this.get('parentView');
while(x) {
var model = x.get('model');
if ( model )
return model;
x = x.get('parentView');
}
}.property('parentView'),
ffz_init: function() {
var el = this.get('element');
el.classList.add('ffz-processed');
jQuery('.aspect', el).tipsy();
if ( ! f.settings.enhance_profile_following )
return;
this.ffzUpdate();
},
ffzUpdate: function() {
var el = this.get('element'),
t_el = el.querySelector('.ffz-followed-since'),
channel_id = this.get('ffzParentModel.model.id'),
is_following = this.get('ffzParentModel.relationshipName') === 'following',
user = f.get_user(),
mine = user && user.login && user.login === channel_id,
big_cache = is_following ? f._following_cache : f._follower_cache,
user_cache = big_cache[channel_id] = big_cache[channel_id] || {},
user_id = this.get('channelInfo.id'),
data = user_cache[user_id];
f.log("Profile Card [" + channel_id + "] " + user_id + " <" + JSON.stringify(data) + ">", this);
if ( ! data || ! el ) {
if ( t_el )
t_el.parentElement.removeChild(t_el);
return false;
}
var now = Date.now() - (f._ws_server_offset || 0),
age = data[0] ? Math.floor((now - data[0].getTime()) / 1000) : 0,
t_el = el.querySelector('.ffz-followed-since')
update_time = function() {
var now = Date.now() - (f._ws_server_offset || 0),
age = data && data[0] ? Math.floor((now - data[0].getTime()) / 1000) : undefined;
if ( age !== undefined ) {
t_el.innerHTML = constants.CLOCK + ' ' + (age < 60 ? 'now' : utils.human_time(age, 10));
t_el.title = 'Follow' + (is_following ? 'ed by ' : 'er of ') + channel_id + ' since: <nobr>' + data[0].toLocaleString() + '</nobr>';
t_el.style.display = '';
} else
t_el.style.display = 'none';
};
if ( ! t_el ) {
t_el = createElement('div', 'overlay_info length html-tooltip ffz-followed-since');
el.appendChild(t_el);
}
update_time();
}.observes('channelInfo')
});
} }

View file

@ -136,31 +136,6 @@ FFZ.settings_info.right_column_width = {
}; };
FFZ.settings_info.minimal_channel_title = {
type: "boolean",
value: false,
category: "Appearance",
no_mobile: true,
no_bttv: true,
name: "Minimal Channel Title",
help: "Hide the channel's name and current game when viewing a channel to maximize player size.",
on_update: function(val) {
if ( this.has_bttv )
return;
var Layout = utils.ember_lookup('service:layout');
if ( ! Layout )
return;
document.body.classList.toggle('ffz-minimal-channel-title', val);
Ember.propertyDidChange(Layout, 'windowHeight');
}
}
// -------------------- // --------------------
// Initialization // Initialization
// -------------------- // --------------------
@ -170,7 +145,6 @@ FFZ.prototype.setup_layout = function() {
return; return;
document.body.classList.toggle("ffz-sidebar-swap", this.settings.swap_sidebars); document.body.classList.toggle("ffz-sidebar-swap", this.settings.swap_sidebars);
document.body.classList.toggle('ffz-minimal-channel-title', this.settings.minimal_channel_title);
this.log("Creating layout style element."); this.log("Creating layout style element.");
var s = this._layout_style = document.createElement('style'); var s = this._layout_style = document.createElement('style');
@ -232,8 +206,13 @@ FFZ.prototype.setup_layout = function() {
c = this.get('PLAYER_CONTROLS_HEIGHT'), c = this.get('PLAYER_CONTROLS_HEIGHT'),
r = this.get('contentWidth'), r = this.get('contentWidth'),
extra_height =
(f.settings.channel_bar_collapse ? 10 : 60) + 15 +
(f.settings.channel_title_top === 2 ? 20 : f.settings.channel_title_top > 0 ? 55 : 0) +
(f.settings.channel_title_top ? 70 : 80),
i = (9 * r / 16) + c, i = (9 * r / 16) + c,
d = h - (f.settings.minimal_channel_title ? 75 : 120) - 60, d = h - extra_height,
c = h - 94 - 185, c = h - 94 - 185,
l = Math.floor(r), l = Math.floor(r),
@ -268,6 +247,7 @@ FFZ.prototype.setup_layout = function() {
ffzUpdateCss: function() { ffzUpdateCss: function() {
var window_height = this.get('windowHeight'), var window_height = this.get('windowHeight'),
window_width = this.get('windowWidth'), window_width = this.get('windowWidth'),
width = this.get('rightColumnWidth'),
out = 'body.ffz-small-player #player .dynamic-player {' + out = 'body.ffz-small-player #player .dynamic-player {' +
'position: fixed;' + 'position: fixed;' +
'z-index: 9;' + 'z-index: 9;' +
@ -318,16 +298,29 @@ FFZ.prototype.setup_layout = function() {
'height:' + chat_height + 'px}' + 'height:' + chat_height + 'px}' +
'body[data-current-path^="user."] .app-main.theatre #left_col .warp,' + 'body[data-current-path^="user."] .app-main.theatre #left_col .warp,' +
'body[data-current-path^="user."] .app-main.theatre #left_col,' + 'body[data-current-path^="user."] .app-main.theatre #left_col,' +
'body[data-current-path^="user."] .app-main.theatre .cn-content #player,' +
'body[data-current-path^="user."] .app-main.theatre #main_col{' + 'body[data-current-path^="user."] .app-main.theatre #main_col{' +
'top:' + theatre_video_top + 'px;' + 'top:' + theatre_video_top + 'px;' +
'height:' + theatre_video_height + 'px}' + 'height:' + theatre_video_height + 'px !important}' +
'body[data-current-path^="user."] .app-main.theatre #right_col{' + 'body[data-current-path^="user."] .app-main.theatre #right_col{' +
'top:' + theatre_chat_top + 'px;' + 'top:' + theatre_chat_top + 'px;' +
'height:' + theatre_chat_height + 'px}'; 'height:' + theatre_chat_height + 'px}' +
'.app-main.theatre .cn-content #player {' +
'right: 0 !important}' +
'body.ffz-minimal-channel-bar:not(.ffz-channel-bar-bottom) .cn-bar-fixed {' +
'top: ' + (video_top - 40) + 'px}' +
'body.ffz-minimal-channel-bar:not(.ffz-channel-bar-bottom) .cn-bar-fixed:hover,' +
'body:not(.ffz-channel-bar-bottom) .cn-bar-fixed {' +
'top: ' + video_top + 'px}' +
'.ffz-minimal-channel-bar.ffz-channel-bar-bottom .cn-bar {' +
'bottom: ' + (chat_top - 40) + 'px}' +
'.ffz-minimal-channel-bar.ffz-channel-bar-bottom .cn-bar:hover,' +
'.ffz-channel-bar-bottom .cn-bar {' +
'bottom: ' + chat_top + 'px}' +
'body:not(.ffz-sidebar-swap) .cn-bar-fixed { right: 0 !important }' +
'body.ffz-sidebar-swap .cn-bar-fixed { left: 0 !important }';
} else { } else {
var width = this.get('rightColumnWidth');
out += 'top: 0; right: ' + width + 'px}' + out += 'top: 0; right: ' + width + 'px}' +
'#main_col.expandRight #right_close{left: none !important}' + '#main_col.expandRight #right_close{left: none !important}' +
'#right_col{width:' + width + 'px}' + '#right_col{width:' + width + 'px}' +
@ -335,7 +328,16 @@ FFZ.prototype.setup_layout = function() {
'margin-right:' + width + 'px}' + 'margin-right:' + width + 'px}' +
'body.ffz-sidebar-swap .theatre #main_col:not(.expandRight),' + 'body.ffz-sidebar-swap .theatre #main_col:not(.expandRight),' +
'body.ffz-sidebar-swap #main_col:not(.expandRight){' + 'body.ffz-sidebar-swap #main_col:not(.expandRight){' +
'margin-left:' + width + 'px}'; 'margin-left:' + width + 'px}' +
'body:not(.ffz-sidebar-swap) .app-main.theatre #main_col:not(.expandRight) .cn-content #player {' +
'right: ' + width + 'px !important}' +
'body.ffz-sidebar-swap .app-main.theatre #main_col:not(.expandRight) .cn-content #player {' +
'right: 0 !important;' +
'left:' + width + 'px !important}' +
'body:not(.ffz-sidebar-swap) #main_col:not(.expandRight) .cn-bar-fixed {' +
'right: ' + width + 'px}' +
'body.ffz-sidebar-swap #main_col:not(.expandRight) .cn-bar-fixed {' +
'left: ' + width + 'px !important}';
} }
} }

View file

@ -1,6 +1,8 @@
var FFZ = window.FrankerFaceZ, var FFZ = window.FrankerFaceZ,
utils = require("../utils"), utils = require("../utils"),
constants = require("../constants"); constants = require("../constants"),
BAN_SPLIT = /[/\.](?:ban ([^ ]+)|timeout ([^ ]+)(?: (\d+))?)(?: (.*))?$/;
// --------------------- // ---------------------
@ -211,29 +213,31 @@ FFZ.settings_info.banned_words = {
name: "Banned Words", name: "Banned Words",
help: "Set a list of words that will be locally removed from chat messages.", help: "Set a list of words that will be locally removed from chat messages.",
method: function() { method: function(e, from_basic) {
var f = this, var f = this,
old_val = this.settings.banned_words.join(", "); old_val = this.settings.banned_words.join("\n"),
input = utils.createElement('textarea');
input.style.marginBottom = "20px";
utils.prompt( utils.prompt(
"Banned Words", "Banned Words",
"Please enter a comma-separated list of words that you would like to have removed from chat messages.", "Please enter a list of words or phrases that you would like to have removed from chat messages. One item per line." + (from_basic ? "" : "<hr><strong>Advanced Stuff:</strong> If you know regex, you can use regular expressions to match too! Start a line with <code>regex:</code> to trigger that behavior.<br><div class=\"small\">(Note: Your expression is wrapped in a capture group and may be joined with other expressions within that group via <code>|</code>. All regular expressions are executed with the flags <code>ig</code>.)</div>"),
old_val, old_val,
function(new_val) { function(new_val) {
if ( new_val === null || new_val === undefined ) if ( new_val === null || new_val === undefined )
return; return;
new_val = new_val.trim().split(constants.SPLITTER); var vals = new_val.trim().split(/\s*\n\s*/g),
var vals = []; i = vals.length;
for(var i=0; i < new_val.length; i++) while(i--)
new_val[i] && vals.push(new_val[i]); if ( vals[i].length === 0 )
vals.splice(i, 1);
if ( vals.length == 1 && vals[0] == "disable" )
vals = [];
f.settings.set("banned_words", vals); f.settings.set("banned_words", vals);
}); },
600, input);
} }
}; };
@ -249,30 +253,32 @@ FFZ.settings_info.keywords = {
name: "Highlight Keywords", name: "Highlight Keywords",
help: "Set additional keywords that will be highlighted in chat.", help: "Set additional keywords that will be highlighted in chat.",
method: function() { method: function(e, from_basic) {
var f = this, var f = this,
old_val = this.settings.keywords.join(", "); old_val = this.settings.keywords.join("\n"),
input = utils.createElement('textarea');
input.style.marginBottom = "20px";
utils.prompt( utils.prompt(
"Highlight Keywords", "Highlight Keywords",
"Please enter a comma-separated list of words that you would like to be highlighted in chat.", "Please enter a list of words or phrases that you would like to be highlighted in chat. One item per line." + (from_basic ? "" : "<hr><strong>Advanced Stuff:</strong> If you know regex, you can use regular expressions to match too! Start a line with <code>regex:</code> to trigger that behavior.<br><div class=\"small\">(Note: Your expression is wrapped in a capture group and may be joined with other expressions within that group via <code>|</code>. All regular expressions are executed with the flags <code>ig</code>.)</div>"),
old_val, old_val,
function(new_val) { function(new_val) {
if ( new_val === null || new_val === undefined ) if ( new_val === null || new_val === undefined )
return; return;
// Split them up. // Split them up.
new_val = new_val.trim().split(constants.SPLITTER); var vals = new_val.trim().split(/\s*\n\s*/g),
var vals = []; i = vals.length;
for(var i=0; i < new_val.length; i++) while(i--)
new_val[i] && vals.push(new_val[i]); if ( vals[i].length === 0 )
vals.splice(i,1);
if ( vals.length == 1 && vals[0] == "disable" )
vals = [];
f.settings.set("keywords", vals); f.settings.set("keywords", vals);
}); },
600, input);
} }
}; };
@ -720,7 +726,7 @@ FFZ.prototype.save_aliases = function() {
FFZ.prototype._modify_chat_line = function(component, is_vod) { FFZ.prototype._modify_chat_line = function(component, is_vod) {
var f = this, var f = this,
Layout = utils.ember_lookup('service:layout'), Layout = utils.ember_lookup('service:layout'),
Settings = utils.ember_lookup('controller:settings'); Settings = utils.ember_settings();
component.reopen({ component.reopen({
/*tokenizedMessage: function() { /*tokenizedMessage: function() {
@ -784,22 +790,39 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) { for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) {
var pair = f.settings.mod_buttons[i], var pair = f.settings.mod_buttons[i],
prefix = pair[0], btn = pair[1], prefix = pair[0], btn = pair[1], had_label = pair[2], is_emoji = pair[3],
cmd, tip; cmd, tip;
if ( is_emoji ) {
var setting = f.settings.parse_emoji,
token = f.emoji_data[is_emoji],
url = null;
if ( token ) {
if ( setting === 1 && token.tw )
url = token.tw_src;
else if ( setting === 2 && token.noto )
url = token.noto_src;
else if ( setting === 3 && token.one )
url = token.one_src;
if ( url )
prefix = '<img class="mod-icon-emoji" src="' + utils.quote_attr(url) + '">';
}
}
if ( btn === false ) { if ( btn === false ) {
if ( deleted ) if ( deleted )
output += '<a class="mod-icon html-tooltip unban" title="Unban User" href="#">Unban</a>'; output += '<a class="mod-icon html-tooltip unban" title="Unban User" href="#">Unban</a>';
else else
output += '<a class="mod-icon html-tooltip ban" title="Ban User" href="#">Ban</a>'; output += '<a class="mod-icon html-tooltip ban' + (had_label ? ' custom' : '') + '" title="Ban User" href="#">' + (had_label ? prefix : 'Ban') + '</a>';
} else if ( btn === 600 ) } else if ( btn === 600 )
output += '<a class="mod-icon html-tooltip timeout" title="Timeout User (10m)" href="#">Timeout</a>'; output += '<a class="mod-icon html-tooltip timeout' + (had_label ? ' custom' : '') + '" title="Timeout User (10m)" href="#">' + ( had_label ? prefix : 'Timeout') + '</a>';
else { else {
if ( typeof btn === "string" ) { if ( typeof btn === "string" ) {
cmd = btn.replace(/{user}/g, user).replace(/{id}/g, this.get('msgObject.tags.id')).replace(/ *<LINE> */, "\n"); cmd = utils.replace_cmd_variables(btn, {name: user}, room && room.room, this.get('msgObject')).replace(/\s*<LINE>\s*/g, '\n');
tip = "Custom Command" + (cmd.indexOf("\n") !== -1 ? 's' : '') + '<br>' + utils.quote_san(cmd).replace('\n','<br>'); tip = "Custom Command" + (cmd.indexOf("\n") !== -1 ? 's' : '') + '<br>' + utils.quote_san(cmd).replace('\n','<br>');
} else { } else {
cmd = "/timeout " + user + " " + btn; cmd = "/timeout " + user + " " + btn;
@ -819,7 +842,7 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
raw_color = this.get(is_recipient ? 'msgObject.toColor' : 'msgObject.color'), raw_color = this.get(is_recipient ? 'msgObject.toColor' : 'msgObject.color'),
is_dark = (Layout && Layout.get('isTheatreMode')) || (is_replay ? f.settings.dark_twitch : (Settings && Settings.get('settings.darkMode'))), is_dark = (Layout && Layout.get('isTheatreMode')) || (is_replay ? f.settings.dark_twitch : (Settings && Settings.get('darkMode'))),
is_replay = this.get('ffz_is_replay'), is_replay = this.get('ffz_is_replay'),
colors = raw_color && f._handle_color(raw_color), colors = raw_color && f._handle_color(raw_color),
@ -845,7 +868,9 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
} }
// Timestamp // Timestamp
output += '<span class="timestamp">' + this.get('timestamp') + '</span> '; var timestamp = this.get('timestamp');
if ( timestamp )
output += '<span class="timestamp">' + timestamp + '</span> ';
// Moderator Actions // Moderator Actions
output += this.buildModIconsHTML(); output += this.buildModIconsHTML();
@ -877,7 +902,7 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
var raw_color = this.get('msgObject.color'), var raw_color = this.get('msgObject.color'),
colors = raw_color && f._handle_color(raw_color), colors = raw_color && f._handle_color(raw_color),
is_replay = this.get('ffz_is_replay'), is_replay = this.get('ffz_is_replay'),
is_dark = (Layout && Layout.get('isTheatreMode')) || (is_replay ? f.settings.dark_twitch : (Settings && Settings.get('settings.darkMode'))); is_dark = (Layout && Layout.get('isTheatreMode')) || (is_replay ? f.settings.dark_twitch : (Settings && Settings.get('darkMode')));
if ( raw_color ) if ( raw_color )
output = '<span class="message has-color' + (is_replay ? ' replay-color' : '') + '" style="color:' + (is_dark ? colors[1] : colors[0]) + '" data-color="' + raw_color + '">'; output = '<span class="message has-color' + (is_replay ? ' replay-color' : '') + '" style="color:' + (is_dark ? colors[1] : colors[0]) + '" data-color="' + raw_color + '">';
@ -886,7 +911,11 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
} else } else
output = '<span class="message">'; output = '<span class="message">';
output += f.render_tokens(this.get('ffzTokenizedMessage'), true, is_whisper && f.settings.filter_whispered_links && this.get("ffzUserLevel") < 4, this.get('isBitsEnabled')); var body = f.render_tokens(this.get('ffzTokenizedMessage'), true, is_whisper && f.settings.filter_whispered_links && this.get("ffzUserLevel") < 4, this.get('isBitsEnabled'));
if ( this.get('msgObject.ffz_line_returns') )
body = body.replace(/\n/g, '<br>');
output += body;
var old_messages = this.get('msgObject.ffz_old_messages'); var old_messages = this.get('msgObject.ffz_old_messages');
if ( old_messages && old_messages.length ) if ( old_messages && old_messages.length )
@ -913,9 +942,9 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
return ! this.get('hasSystemMsg') || this.get('hasMessageBody'); return ! this.get('hasSystemMsg') || this.get('hasMessageBody');
}.property('hasSystemMsg', 'hasMessageBody'), }.property('hasSystemMsg', 'hasMessageBody'),
shouldRenderMessageBody: function() { //shouldRenderMessageBody: function() {
return false; // return false;
}.property('hasSystemMsg', 'hasMessageBody'), //}.property('hasSystemMsg', 'hasMessageBody'),
ffzWasDeleted: function() { ffzWasDeleted: function() {
return f.settings.prevent_clear && this.get("msgObject.ffz_deleted") return f.settings.prevent_clear && this.get("msgObject.ffz_deleted")
@ -949,6 +978,135 @@ FFZ.prototype._modify_chat_subline = function(component) {
//didUpdate: function() { this.ffzRender(); }, //didUpdate: function() { this.ffzRender(); },
ffzBuildModMenu: function(el) {
var t = this,
setting = f.settings.mod_button_context,
cl = el.classList,
from = this.get("msgObject.from"),
cmd = el.getAttribute('data-cmd'),
trail = '';
if ( ! cmd && cl.contains('ban') )
cmd = "/ban " + from;
else if ( ! cmd && cl.contains('timeout') )
cmd = "/timeout " + from + " 600";
else if ( ! cmd || cl.contains('unban') )
return; // We can't send mod reasons for unbans and we need a command.
else {
var lines = cmd.split("\n"),
first_line = lines.shift(),
trail = lines.length ? "\n" + lines.join("\n") : "",
match = BAN_SPLIT.exec(first_line);
// If the line didn't match this, it's invalid.
if ( ! match )
return;
cmd = match[1] ? "/ban " + match[1] : "/timeout " + match[2] + " " + (match[3] || "600");
if ( match[4] )
trail = match[4] + trail;
}
var bl = utils.createElement('ul', 'balloon__list'),
balloon = utils.createElement('div', 'balloon balloon--dropmenu ffz-mod-balloon', bl),
bc = utils.createElement('div', 'balloon-wrapper', balloon),
has_items = false,
is_ban = cmd.substr(1, 4) === 'ban ',
title = utils.createElement('li', 'ffz-title');
title.textContent = (is_ban ? 'Ban ' : 'Timeout ') + from + ' for...';
bl.appendChild(title);
bl.appendChild(utils.createElement('li', 'balloon__stroke'));
var btn_click = function(reason, e) {
if ( e.button !== 0 )
return;
var room_id = t.get('msgObject.room'),
room = room_id && f.rooms[room_id] && f.rooms[room_id].room;
if ( room ) {
cmd = cmd + ' ' + reason + (trail ? (trail[0] === '\n' ? '' : ' ') + trail : '');
var lines = cmd.split("\n");
for(var i=0; i < lines.length; i++)
room.send(lines[i], true);
if ( cl.contains('is-timeout') )
room.clearMessages(from, null, true);
}
f.close_popup();
e.stopImmediatePropagation();
e.preventDefault();
return false;
};
if ( setting & 1 )
for(var i=0; i < f.settings.mod_card_reasons.length; i++) {
var btn = utils.createElement('div', 'balloon__link ellipsis'),
line = utils.createElement('li', '', btn),
reason = f.settings.mod_card_reasons[i];
btn.textContent = btn.title = reason;
btn.addEventListener('click', btn_click.bind(btn, reason));
bl.appendChild(line);
has_items = true;
}
if ( setting & 2 ) {
var room_id = t.get('msgObject.room'),
room = room_id && f.rooms[room_id] && f.rooms[room_id].room,
rules = room && room.get('roomProperties.chat_rules');
if ( rules && rules.length ) {
if ( has_items )
bl.appendChild(utils.createElement('li', 'balloon__stroke'));
for(var i=0; i < rules.length; i++) {
var btn = utils.createElement('div', 'balloon__link ellipsis'),
line = utils.createElement('li', '', btn),
reason = rules[i];
btn.textContent = btn.title = reason;
btn.addEventListener('click', btn_click.bind(btn, reason));
bl.appendChild(line);
has_items = true;
}
}
}
if ( ! has_items )
return false;
var rect = el.getBoundingClientRect(),
is_bottom = rect.top > (window.innerHeight / 2),
position = [rect.left, (is_bottom ? rect.top : rect.bottom)];
balloon.classList.add('balloon--' + (is_bottom ? 'up' : 'down'));
f.show_popup(bc, position, utils.find_parent(this.get('element'), 'chat-messages'));
return true;
},
contextMenu: function(e) {
if ( ! e.target )
return;
var cl = e.target.classList,
from = this.get("msgObject.from"),
abort = false;
// We only want to show a context menu for mod icons right now.
if ( cl.contains('mod-icon') )
abort |= this.ffzBuildModMenu(e.target);
if ( abort ) {
e.stopImmediatePropagation();
e.preventDefault();
}
},
click: function(e) { click: function(e) {
if ( ! e.target ) if ( ! e.target )
return; return;
@ -970,7 +1128,16 @@ FFZ.prototype._modify_chat_subline = function(component) {
jQuery(e.target).trigger('mouseout'); jQuery(e.target).trigger('mouseout');
e.preventDefault(); e.preventDefault();
if ( cl.contains('custom') ) { if ( cl.contains('ban') )
this.sendAction("banUser", {user:from});
else if ( cl.contains('unban') )
this.sendAction("unbanUser", {user:from});
else if ( cl.contains('timeout') )
this.sendAction("timeoutUser", {user:from});
else if ( cl.contains('custom') ) {
var room_id = this.get('msgObject.room'), var room_id = this.get('msgObject.room'),
room = room_id && f.rooms[room_id] && f.rooms[room_id].room, room = room_id && f.rooms[room_id] && f.rooms[room_id].room,
cmd = e.target.getAttribute('data-cmd'); cmd = e.target.getAttribute('data-cmd');
@ -985,14 +1152,7 @@ FFZ.prototype._modify_chat_subline = function(component) {
} }
return; return;
} else if ( cl.contains('ban') ) }
this.sendAction("banUser", {user:from});
else if ( cl.contains('unban') )
this.sendAction("unbanUser", {user:from});
else if ( cl.contains('timeout') )
this.sendAction("timeoutUser", {user:from});
} else if ( cl.contains('badge') ) { } else if ( cl.contains('badge') ) {
if ( cl.contains('click_url') ) if ( cl.contains('click_url') )

View file

@ -6,7 +6,6 @@ var FFZ = window.FrankerFaceZ,
TO_REG = /^\/t(?:imeout)? +([^ ]+)(?: +(\d+)(?: +(.+))?)?$/, TO_REG = /^\/t(?:imeout)? +([^ ]+)(?: +(\d+)(?: +(.+))?)?$/,
BAN_REG = /^\/b(?:an)? +([^ ]+)(?: +(.+))?$/, BAN_REG = /^\/b(?:an)? +([^ ]+)(?: +(.+))?$/,
USER_REG = /\{user\}/g,
keycodes = { keycodes = {
ESC: 27, ESC: 27,
@ -102,8 +101,7 @@ FFZ.settings_info.chat_mod_icon_visibility = {
}, },
value: function() { value: function() {
var settings = utils.ember_lookup('controller:settings'); return this.settings.get_twitch("showModIcons") ? 1 : 0;
return (settings && settings.get('settings.showModIcons')) ? 1 : 0;
}, },
process_value: function(val) { process_value: function(val) {
@ -119,9 +117,9 @@ FFZ.settings_info.chat_mod_icon_visibility = {
help: "Choose when you should see in-line moderation icons in chat.", help: "Choose when you should see in-line moderation icons in chat.",
on_update: function(val) { on_update: function(val) {
var settings = utils.ember_lookup('controller:settings'); var settings = utils.ember_settings();
if ( settings ) if ( settings )
settings.set('settings.showModIcons', val === 1); settings.set('showModIcons', val === 1);
} }
} }
@ -260,6 +258,33 @@ FFZ.settings_info.mod_card_history = {
}; };
FFZ.settings_info.mod_button_context = {
type: "select",
options: {
0: "Disabled",
1: "Show Ban Reasons Only",
2: "Show Chat Rules Only",
3: "Ban Reasons + Chat Rules"
},
value: 3,
process_value: function(val) {
if ( typeof val === "string" ) {
val = parseInt(val);
if ( isNaN(val) || ! isFinite(val) )
val = 3;
}
return val;
},
category: "Chat Moderation",
no_bttv: true,
name: "Mod Icon Context Menus",
help: "Choose the available options when right-clicking an in-line moderation icon."
}
FFZ.settings_info.mod_card_reasons = { FFZ.settings_info.mod_card_reasons = {
type: "button", type: "button",
value: [ value: [
@ -274,8 +299,8 @@ FFZ.settings_info.mod_card_reasons = {
category: "Chat Moderation", category: "Chat Moderation",
no_bttv: true, no_bttv: true,
name: "Moderation Card Ban Reasons", name: "Ban / Timeout Reasons",
help: "Change the available options in the chat moderation card ban reasons list.", help: "Change the available options in the chat ban reasons list shown in moderation cards and when right-clicking an in-line ban or timeout button.",
method: function() { method: function() {
var f = this, var f = this,
@ -286,7 +311,7 @@ FFZ.settings_info.mod_card_reasons = {
utils.prompt( utils.prompt(
"Moderation Card Ban Reasons", "Moderation Card Ban Reasons",
"Please enter a list of ban reasons to select from in chat moderation cards. One item per line.", "Please enter a list of ban reasons to select from. One item per line.",
old_val, old_val,
function(new_val) { function(new_val) {
if ( new_val === null || new_val === undefined ) if ( new_val === null || new_val === undefined )
@ -323,127 +348,124 @@ FFZ.settings_info.mod_buttons = {
method: function() { method: function() {
var f = this, var f = this,
old_val = ""; old_val = "",
input = utils.createElement('textarea');
input.style.marginBottom = '20px';
input.placeholder = '/ban\n600';
for(var i=0; i < this.settings.mod_buttons.length; i++) { for(var i=0; i < this.settings.mod_buttons.length; i++) {
var pair = this.settings.mod_buttons[i], var pair = this.settings.mod_buttons[i],
prefix = pair[0], cmd = pair[1], had_prefix = pair[2]; prefix = pair[0], cmd = pair[1], had_prefix = pair[2];
if ( cmd === false ) if ( cmd === false )
cmd = "<BAN>"; cmd = "/ban";
else if ( cmd === 600 )
cmd = "/timeout";
else if ( typeof cmd !== "string" ) else if ( typeof cmd !== "string" )
cmd = '' + cmd; cmd = '' + cmd;
if ( ! had_prefix ) prefix = had_prefix ? 'name:' + prefix + '=' : '';
prefix = ''; old_val += (old_val.length ? '\n' : '') + prefix + cmd;
else
prefix += '=';
if ( cmd.substr(cmd.length - 7) === ' {user}' )
cmd = cmd.substr(0, cmd.length - 7);
if ( cmd.indexOf(' ') !== -1 )
old_val += ' ' + prefix + '"' + cmd + '"';
else
old_val += ' ' + prefix + cmd;
} }
utils.prompt( utils.prompt(
"Custom In-Line Moderation Icons", "Custom In-Line Moderation Icons",
"Please enter a list of commands to be made available as mod icons within chat lines. Commands are separated by spaces. " + "Please enter a list of commands to be displayed as moderation buttons within chat lines. " +
"To include spaces in a command, surround the command with double quotes (\"). Use <code>{user}</code> to insert the user's name " + "One item per line. As a shortcut for specific duration timeouts, you can enter the number of seconds by itself. " +
"into the command, otherwise it will be appended to the end. Use <code>{id}</code> to insert the unique message ID into the command.</p>" + " To send multiple commands, separate them with <code>&lt;LINE&gt;</code>. " +
"<p><b>Example:</b> <code>!permit \"!reg add {user}\" \"/timeout {user} 1 {id}\"</code></p><p>To " + "Variables, such as the target user's name, can be inserted into your commands. If no variables are detected " +
"send multiple commands, separate them with <code>&lt;LINE&gt;</code>.</p><p>Numeric values will become timeout buttons for " + "in a line, <code>{user}</code> will be added to the end of the first command.<hr>" +
"that number of seconds. The text <code>&lt;BAN&gt;</code> is a special value that will act like the normal Ban button in chat.</p><p>" +
"To assign a specific letter for use as the icon, specify it at the start of the command followed by an equals sign.</p><p>" + "To set a custom label for the button, start your line with <code>name:</code> followed by the " +
"<b>Example:</b> <code>A=\"!reg add\"</code></p><p><b>Default:</b> <code>&lt;BAN&gt; 600</code>", "name of the button. End the name with an equals sign. Only the first character will be displayed.<br>" +
old_val.substr(1), "<strong>Example:</strong> <code>name:B=/ban {user}</code><hr>" +
"<strong>Allowed Variables</strong><br><table><tbody>" +
"<tr><td><code>{user}</code></td><td>target user's name</td>" +
"<td><code>{user_name}</code></td><td>target user's name</td></tr>" +
"<tr><td><code>{user_display_name}</code></td><td>target user's display name</td>" +
"<td><code>{user_id}</code></td><td>target user's numeric ID</td></tr>" +
"<tr><td><code>{room}</code></td><td>chat room's name</td>" +
"<td><code>{room_name}</code></td><td>chat room's name</td></tr>" +
"<tr><td><code>{room_display_name}</code></td><td>chat room's display name</td>" +
"<td><code>{room_id}</code></td><td>chat room's numeric ID</td></tr>" +
"<tr><td><code>{id}</code></td><td>message's UUID</td></tr>" +
"</tbody></table>",
old_val,
function(new_val) { function(new_val) {
if ( new_val === null || new_val === undefined ) if ( new_val === null || new_val === undefined )
return; return;
var vals = [], prefix = ''; var vals = new_val.trim().split(/\s*\n\s*/g),
new_val = new_val.trim(); output = [];
while(new_val) {
if ( new_val.charAt(1) === '=' ) {
prefix = new_val.charAt(0);
new_val = new_val.substr(2);
continue;
}
if ( new_val.charAt(0) === '"' ) {
var end = new_val.indexOf('"', 1);
if ( end === -1 )
end = new_val.length;
var segment = new_val.substr(1, end - 1);
if ( segment ) {
vals.push([prefix, segment]);
prefix = '';
}
new_val = new_val.substr(end + 1);
} else {
var ind = new_val.indexOf(' ');
if ( ind === -1 ) {
if ( new_val ) {
vals.push([prefix, new_val]);
prefix = '';
}
new_val = '';
} else {
var segment = new_val.substr(0, ind);
if ( segment ) {
vals.push([prefix, segment]);
prefix = '';
}
new_val = new_val.substr(ind + 1);
}
}
}
var final = [];
for(var i=0; i < vals.length; i++) { for(var i=0; i < vals.length; i++) {
var had_prefix = false, prefix = vals[i][0], val = vals[i][1]; var cmd = vals[i],
if ( val === "<BAN>" ) prefix,
val = false; is_emoji = false,
name_match = /^name:([^=]+)=/.exec(cmd);
var num = parseInt(val); if ( ! cmd || ! cmd.length )
if ( num > 0 && ! Number.isNaN(num) ) continue;
val = num;
if ( ! prefix ) { if ( name_match ) {
var tmp; label = name_match[1];
if ( typeof val === "string" )
tmp = /\w/.exec(val);
else
tmp = utils.duration_string(val);
prefix = tmp && tmp.length ? tmp[0].toUpperCase() : "C"; if ( window.punycode && punycode.ucs2 )
label = punycode.ucs2.encode([punycode.ucs2.decode(label)[0]]);
// Check for an emoji
var tokens = f.tokenize_emoji(label);
if ( tokens && tokens[0] && tokens[0].ffzEmoji )
is_emoji = tokens[0].ffzEmoji;
cmd = cmd.substr(name_match[0].length).trim();
} else } else
had_prefix = true; label = undefined;
if ( typeof val === "string" ) { // Check for a plain ban.
// Split it up for this step. if ( /^\/b(?:an)?(?:\s+{user(?:_name)?})?\s*$/.test(cmd) )
var lines = val.split(/ *<LINE> */); cmd = false;
for(var x=0; x < lines.length; x++) {
if ( lines[x].indexOf('{user}') === -1 ) // Numeric Timeout
lines[x] += ' {user}'; else if ( /^\d+$/.test(cmd) )
} cmd = parseInt(cmd);
val = lines.join("<LINE>");
// Command Timeout
else if ( /^\/t(?:imeout)?(?:\s+{user(?:_name)?}(?:\s+(\d+))?)?\s*$/.test(cmd) ) {
cmd = parseInt(/^\/t(?:imeout)?(?:\s+{user(?:_name)?}(?:\s+(\d+))?)?\s*$/.exec(cmd)[1]);
if ( isNaN(cmd) || ! isFinite(cmd) )
cmd = 600;
} }
final.push([prefix, val, had_prefix]);
// Okay. Do we still need a prefix?
if ( label === undefined ) {
var tmp;
if ( typeof cmd === "string" )
tmp = /\w/.exec(cmd);
else
tmp = utils.duration_string(cmd);
label = tmp && tmp.length ? tmp[0].toUpperCase() : 'C';
} }
f.settings.set('mod_buttons', final); // Add {user} to the first command if it's a custom command and missing.
if ( typeof cmd === "string" ) {
utils.CMD_VAR_REGEX.lastIndex = 0;
if ( ! utils.CMD_VAR_REGEX.test(cmd) ) {
var lines = cmd.split(/\s*<LINE>\s*/g);
lines[0] += ' {user}';
cmd = lines.join("<LINE>");
}
}
output.push([label, cmd, name_match != null, is_emoji]);
}
f.settings.set('mod_buttons', output);
// Update existing chat lines. // Update existing chat lines.
var CL = utils.ember_resolve('component:chat/chat-line'), var CL = utils.ember_resolve('component:chat/chat-line'),
@ -455,7 +477,7 @@ FFZ.settings_info.mod_buttons = {
view.$('.mod-icons').replaceWith(view.buildModIconsHTML()); view.$('.mod-icons').replaceWith(view.buildModIconsHTML());
} }
}, 600); }, 600, input);
} }
}; };
@ -472,60 +494,74 @@ FFZ.settings_info.mod_card_buttons = {
method: function() { method: function() {
var f = this, var f = this,
old_val = ""; old_val = "",
input = utils.createElement('textarea');
input.style.marginBottom = '20px';
for(var i=0; i < this.settings.mod_card_buttons.length; i++) { for(var i=0; i < this.settings.mod_card_buttons.length; i++) {
var cmd = this.settings.mod_card_buttons[i]; var label, cmd, had_label, pair = this.settings.mod_card_buttons[i];
if ( cmd.indexOf(' ') !== -1 ) if ( Array.isArray(pair) ) {
old_val += ' "' + cmd + '"'; label = pair[0];
else cmd = pair[1];
old_val += ' ' + cmd; had_label = pair[2];
} else {
cmd = pair;
had_label = false;
}
label = had_label ? 'name:' + label + '=' : '';
old_val += (old_val.length ? '\n' : '') + label + cmd;
} }
utils.prompt( utils.prompt(
"Moderation Card Additional Buttons", "Moderation Card Additional Buttons",
"Please enter a list of additional commands to display buttons for on moderation cards. Commands are separated by spaces. " + "Please enter a list of additional commands to display buttons for on moderation cards. " +
"To include spaces in a command, surround the command with double quotes (\"). Use <code>{user}</code> to insert the " + "One item per line. To send multiple commands, separate them with <code>&lt;LINE&gt;</code>. " +
"user's name into the command, otherwise it will be appended to the end.</p><p><b>Example:</b> !permit \"!reg add {user}\"", "Variables, such as the target user's name, can be inserted into your commands. If no variables are detected " +
old_val.substr(1), "in a line, <code>{user}</code> will be added to the end of the first command.<hr>" +
"To set a custom label for the button, start your line with <code>name:</code> followed by the name of the button. " +
"End the name with an equals sign.<br>" +
"<strong>Example:</strong> <code>name:Boop=/timeout {user} 15 Boop!</code><hr>" +
"<strong>Allowed Variables</strong><br><table><tbody>" +
"<tr><td><code>{user}</code></td><td>target user's name</td>" +
"<td><code>{user_name}</code></td><td>target user's name</td></tr>" +
"<tr><td><code>{user_display_name}</code></td><td>target user's display name</td>" +
"<td><code>{user_id}</code></td><td>target user's numeric ID</td></tr>" +
"<tr><td><code>{room}</code></td><td>chat room's name</td>" +
"<td><code>{room_name}</code></td><td>chat room's name</td></tr>" +
"<tr><td><code>{room_display_name}</code></td><td>chat room's display name</td>" +
"<td><code>{room_id}</code></td><td>chat room's numeric ID</td></tr>" +
"</tbody></table>",
old_val,
function(new_val) { function(new_val) {
if ( new_val === null || new_val === undefined ) if ( new_val === null || new_val === undefined )
return; return;
var vals = []; var vals = new_val.trim().split(/\s*\n\s*/g),
new_val = new_val.trim(); output = [];
while(new_val) { for(var i=0; i < vals.length; i++) {
if ( new_val.charAt(0) === '"' ) { var cmd = vals[i],
var end = new_val.indexOf('"', 1); label,
if ( end === -1 ) name_match = /^name:([^=]+)=/.exec(cmd);
end = new_val.length;
var segment = new_val.substr(1, end - 1); if ( ! cmd || ! cmd.length )
if ( segment ) continue;
vals.push(segment);
new_val = new_val.substr(end + 1); if ( name_match ) {
label = name_match[1];
cmd = cmd.substr(name_match[0].length);
} else
label = cmd.split(' ', 1)[0]
} else { output.push([label, cmd, name_match != null]);
var ind = new_val.indexOf(' ');
if ( ind === -1 ) {
if ( new_val )
vals.push(new_val);
new_val = '';
} else {
var segment = new_val.substr(0, ind);
if ( segment )
vals.push(segment);
new_val = new_val.substr(ind + 1);
}
}
} }
f.settings.set("mod_card_buttons", vals); f.settings.set("mod_card_buttons", output);
}, 600); }, 600, input);
} }
}; };
@ -586,11 +622,11 @@ FFZ.prototype.setup_mod_card = function() {
this.log("Listening to the Settings controller to catch mod icon state changes."); this.log("Listening to the Settings controller to catch mod icon state changes.");
var f = this, var f = this,
Settings = utils.ember_lookup('controller:settings'); Settings = utils.ember_settings();
if ( Settings ) if ( Settings )
Settings.addObserver('settings.showModIcons', function() { Settings.addObserver('showModIcons', function() {
if ( Settings.get('settings.showModIcons') ) if ( Settings.get('showModIcons') )
f.settings.set('chat_mod_icon_visibility', 1); f.settings.set('chat_mod_icon_visibility', 1);
}); });
@ -684,7 +720,8 @@ FFZ.prototype.modify_moderation_card = function(component) {
chat = utils.ember_lookup('controller:chat'), chat = utils.ember_lookup('controller:chat'),
user = f.get_user(), user = f.get_user(),
room_id = chat && chat.get('currentRoom.id'), room = chat && chat.get('currentRoom'),
room_id = room && room.get('id'),
is_broadcaster = user && room_id === user.login, is_broadcaster = user && room_id === user.login,
user_id = controller.get('cardInfo.user.id'), user_id = controller.get('cardInfo.user.id'),
@ -747,13 +784,12 @@ FFZ.prototype.modify_moderation_card = function(component) {
if ( is_mod && f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) { if ( is_mod && f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) {
line = utils.createElement('div', 'extra-interface interface clearfix'); line = utils.createElement('div', 'extra-interface interface clearfix');
var cmds = {}, var add_btn_click = function(cmd) {
add_btn_click = function(cmd) { var user = controller.get('cardInfo.user'),
var user_id = controller.get('cardInfo.user.id'), chat_controller = utils.ember_lookup('controller:chat'),
cont = utils.ember_lookup('controller:chat'), room = chat_controller && chat_controller.get('currentRoom'),
room = cont && cont.get('currentRoom'),
cm = cmd.replace(USER_REG, user_id), cm = utils.replace_cmd_variables(cmd, user, room),
reason = ban_reason(); reason = ban_reason();
if ( reason ) { if ( reason ) {
@ -775,36 +811,49 @@ FFZ.prototype.modify_moderation_card = function(component) {
room && room.send(cm, true); room && room.send(cm, true);
}, },
add_btn_make = function(cmd) { add_btn_make = function(label, cmd) {
var btn = utils.createElement('button', 'button ffz-no-bg'), var btn = utils.createElement('button', 'button ffz-no-bg', utils.sanitize(label));
segment = cmd.split(' ', 1)[0],
title = cmds[segment] > 1 ? cmd.split(' ', cmds[segment]) : [segment];
if ( /^[!~./]/.test(title[0]) ) jQuery(btn).tipsy({
title[0] = title[0].substr(1); html: true,
gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n'),
title: function() {
var user = controller.get('cardInfo.user'),
chat_controller = utils.ember_lookup('controller:chat'),
room = chat_controller && chat_controller.get('currentRoom');
title = _.map(title, function(s){ return s.capitalize() }).join(' '); title = utils.replace_cmd_variables(cmd, user, room);
btn.innerHTML = utils.sanitize(title); title = _.map(title.split(/\s*<LINE>\s*/g, utils.sanitize).join("<br>"));
btn.title = utils.sanitize(cmd.replace(/{user}/g, controller.get('cardInfo.user.id') || '{user}'));
return "Custom Command" + (title.indexOf('<br>') !== -1 ? 's' : '') +
"<br>" + title;
}
});
jQuery(btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')});
btn.addEventListener('click', add_btn_click.bind(this, cmd)); btn.addEventListener('click', add_btn_click.bind(this, cmd));
return btn; return btn;
}; };
var cmds = {};
for(var i=0; i < f.settings.mod_card_buttons.length; i++)
cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] = (cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] || 0) + 1;
for(var i=0; i < f.settings.mod_card_buttons.length; i++) { for(var i=0; i < f.settings.mod_card_buttons.length; i++) {
var cmd = f.settings.mod_card_buttons[i], var label, cmd, pair = f.settings.mod_card_buttons[i];
ind = cmd.indexOf('{user}'); if ( ! Array.isArray(pair) ) {
cmd = pair;
label = cmd.split(' ', 1)[0];
} else {
label = pair[0];
cmd = pair[1];
}
if ( ind === -1 ) utils.CMD_VAR_REGEX.lastIndex = 0;
cmd += ' {user}'; if ( ! utils.CMD_VAR_REGEX.test(cmd) ) {
var lines = cmd.split(/\s*<LINE>\s*/g);
lines[0] += ' {user}';
cmd = lines.join("<LINE>");
}
line.appendChild(add_btn_make(cmd)) line.appendChild(add_btn_make(label, cmd));
} }
el.appendChild(line); el.appendChild(line);
@ -1094,7 +1143,7 @@ FFZ.prototype.modify_moderation_card = function(component) {
} }
if ( user_history.length < 50 ) { if ( user_history.length < 50 ) {
var before = (user_history.length > 0 ? user_history[0].date.getTime() : Date.now()) - (f._ws_server_offset || 0); var before = (user_history.length > 0 && user_history[0].date ? user_history[0].date.getTime() : Date.now()) - (f._ws_server_offset || 0);
f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) { f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) {
if ( ! success ) if ( ! success )
return; return;
@ -1271,9 +1320,9 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) {
colors = raw_color && this._handle_color(raw_color), colors = raw_color && this._handle_color(raw_color),
Layout = utils.ember_lookup('service:layout'), Layout = utils.ember_lookup('service:layout'),
Settings = utils.ember_lookup('controller:settings'), Settings = utils.ember_settings(),
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')); is_dark = (Layout && Layout.get('isTheatreMode')) || this.settings.get_twitch("darkMode");
// Styling // Styling

View file

@ -12,17 +12,28 @@ var FFZ = window.FrankerFaceZ,
'subscribers': 'subs_on', 'subscribers': 'subs_on',
'subscribersoff': 'subs_off', 'subscribersoff': 'subs_off',
'emoteonly': 'emote_only_on', 'emoteonly': 'emote_only_on',
'emoteonlyoff': 'emote_only_off' 'emoteonlyoff': 'emote_only_off',
'host': 'host_on',
'unhost': 'host_off'
}, },
STATUS_BADGES = [ STATUS_BADGES = [
["r9k", "r9k", "This room is in R9K-mode."], ["r9k", "r9k", "This room is in R9K-mode."],
["emote", "emoteOnly", "This room is in Twitch emoticons only mode. Emoticons added by extensions are not available in this mode."], ["emote", "emoteOnly", "This room is in Twitch emoticons only mode. Emoticons added by extensions are not available in this mode."],
["sub", "subsOnly", "This room is in subscribers-only mode."], ["sub", "subsOnly", "This room is in subscribers-only mode."],
["slow", "slow", function(room) { return "This room is in slow mode. You may send messages every " + utils.number_commas(room && room.get('slow') || 120) + " seconds." }], ["slow", "slow", function(room) { return "This room is in slow mode. You may send messages every <nobr>" + utils.number_commas(room && room.get('slow') || 120) + " seconds</nobr>." }],
["ban", "ffz_banned", "You have been banned from talking in this room."], ["ban", "ffz_banned", "You have been banned from talking in this room."],
["delay", function(room) { return room && room.get('ffz_chat_delay') !== 0 }, function(room) { return "Artificial chat delay is enabled. Messages are displayed after " + (room ? room.get('ffz_chat_delay')/1000 : 0) + " seconds." }], ["delay", function(room) {
["batch", function() { return this.settings.chat_batching !== 0 }, function() { return "You have enabled chat message batching. Messages are displayed in " + (this.settings.chat_batching/1000) + " second increments." }] return room && (this.settings.chat_delay === -1 ?
room.get('roomProperties.chat_delay_duration')
: room.get('ffz_chat_delay'))
}, function(room) {
var is_mod = this.settings.chat_delay === -1;
return "Artificial chat delay is enabled" + (is_mod ? " for this channel" : "") + ". Messages are displayed after " + (room ? (is_mod ? room.get('roomProperties.chat_delay_duration') : room.get('ffz_chat_delay')/1000) : 0) + " seconds" + (is_mod ? " for <nobr>non-moderators</nobr>." : ".");
}, null, function(room) {
return room && this.settings.chat_delay === -1 && room.get('isModeratorOrHigher') || false;
}],
["batch", function() { return this.settings.chat_batching !== 0 }, function() { return "You have enabled chat message batching. Messages are displayed in <nobr>" + (this.settings.chat_batching/1000) + " second</nobr> increments." }]
], ],
// StrimBagZ Support // StrimBagZ Support
@ -32,7 +43,7 @@ var FFZ = window.FrankerFaceZ,
if ( ! room.moderator_badge ) if ( ! room.moderator_badge )
return ""; return "";
return '.chat-line[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement) { background-repeat: no-repeat; background-size: initial; background-position: center; background-image:url("' + room.moderator_badge + '") !important; }'; return '.from-display-preview[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement),.chat-line[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement) { background-repeat: no-repeat; background-size: initial; background-position: center; background-image:url("' + room.moderator_badge + '") !important; }';
}; };
@ -156,14 +167,14 @@ FFZ.prototype._modify_chat_pubsub = function(pubsub) {
token = user.chat_oauth_token, token = user.chat_oauth_token,
new_topics = [ new_topics = [
"chat_message_updated." + room_id, "chat_message_updated." + room_id,
"chat_moderator_actions." + room_id]; "chat_moderator_actions." + user.id + "." + room_id];
for(var i=0; i < new_topics.length; i++) for(var i=0; i < new_topics.length; i++)
ps.Listen({ ps.Listen({
topic: new_topics[i], topic: new_topics[i],
auth: token, auth: token,
success: function() {}, success: function() {},
failure: function() {}, failure: function(t) { f.log("[PubSub] Failed to listen to topic: " + new_topics[i], t); },
message: Ember.run.bind(n, n._onPubsubMessage, new_topics[i]) message: Ember.run.bind(n, n._onPubsubMessage, new_topics[i])
}); });
@ -199,7 +210,7 @@ FFZ.prototype._modify_chat_pubsub = function(pubsub) {
ps.Unlisten({ ps.Unlisten({
topic: old_topics[i], topic: old_topics[i],
success: function() {}, success: function() {},
failure: function() {} failure: function(t) { f.log("[PubSub] Failed to unlisten to topic: " + old_topics[i], t); }
}); });
this.chatTopics.removeObject(old_topics[i]); this.chatTopics.removeObject(old_topics[i]);
} }
@ -238,14 +249,14 @@ FFZ.prototype._modify_chat_pubsub = function(pubsub) {
ps.Unlisten({ ps.Unlisten({
topic: pubsub.chatTopics[i], topic: pubsub.chatTopics[i],
success: function() {}, success: function() {},
failure: function() {} failure: function(t) { f.log("[PubSub] Failed to unlisten to topic: " + old_topics[i], t); }
}); });
ps.Listen({ ps.Listen({
topic: pubsub.chatTopics[i], topic: pubsub.chatTopics[i],
auth: token, auth: token,
success: function() {}, success: function() {},
failure: function() {}, failure: function(t) { f.log("[PubSub] Failed to listen to topic: " + new_topics[i], t); },
message: Ember.run.bind(pubsub, pubsub._onPubsubMessage, pubsub.chatTopics[i]) message: Ember.run.bind(pubsub, pubsub._onPubsubMessage, pubsub.chatTopics[i])
}); });
} }
@ -424,7 +435,7 @@ FFZ.prototype.modify_room_view = function(view) {
if ( ! badge ) { if ( ! badge ) {
badge = utils.createElement('span', 'ffz room-state stat float-right', (label || info[0]).charAt(0).toUpperCase() + '<span>' + (label || info[0]).substr(1).toUpperCase() + '</span>'); badge = utils.createElement('span', 'ffz room-state stat float-right', (label || info[0]).charAt(0).toUpperCase() + '<span>' + (label || info[0]).substr(1).toUpperCase() + '</span>');
badge.id = id; badge.id = id;
jQuery(badge).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'se')}); jQuery(badge).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'se')});
cont.appendChild(badge); cont.appendChild(badge);
} }
@ -433,6 +444,7 @@ FFZ.prototype.modify_room_view = function(view) {
badge.title = typeof info[2] === "function" ? info[2].call(f, room) : info[2]; badge.title = typeof info[2] === "function" ? info[2].call(f, room) : info[2];
badge.classList.toggle('hidden', ! visible); badge.classList.toggle('hidden', ! visible);
badge.classList.toggle('faded', info[4] !== undefined ? typeof info[4] === "function" ? info[4].call(f, room) : info[4] : false);
if ( visible ) if ( visible )
vis_count++; vis_count++;
} }
@ -576,6 +588,7 @@ FFZ.prototype.modify_room_view = function(view) {
var e = this, var e = this,
s = this._$chatMessagesScroller; s = this._$chatMessagesScroller;
//this.runTask(function() {
Ember.run.next(function(){ Ember.run.next(function(){
// Trying random performance tweaks for fun and profit! // Trying random performance tweaks for fun and profit!
(window.requestAnimationFrame||setTimeout)(function(){ (window.requestAnimationFrame||setTimeout)(function(){
@ -584,13 +597,16 @@ FFZ.prototype.modify_room_view = function(view) {
s[0].scrollTop = s[0].scrollHeight; s[0].scrollTop = s[0].scrollHeight;
e._setStuckToBottom(true); e._setStuckToBottom(true);
e._tagVisibleMessages();
}) })
}) })
}, 200), }, 200),
_setStuckToBottom: function(val) { _setStuckToBottom: function(val) {
this.set("stuckToBottom", val); this.set("stuckToBottom", val);
this.get("controller.model") && this.set("controller.model.messageBufferSize", f.settings.scrollback_length + (val ? 0 : 150)); var model = this.get("controller.model");
if ( model )
model.messageBufferSize = f.settings.scrollback_length + (val ? 0 : 150);
if ( ! val ) if ( ! val )
this.ffzUnfreeze(true); this.ffzUnfreeze(true);
}, },
@ -649,15 +665,13 @@ FFZ.ffz_commands = {};
FFZ.prototype.room_message = function(room, text) { FFZ.prototype.room_message = function(room, text) {
var lines = text.split("\n");
if ( this.has_bttv ) { if ( this.has_bttv ) {
var lines = text.split("\n");
for(var i=0; i < lines.length; i++) for(var i=0; i < lines.length; i++)
BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]});
} else { } else
for(var i=0; i < lines.length; i++) room.room.addMessage({ffz_line_returns: true, style: 'ffz admin', date: new Date(), from: 'FFZ', message: text});
room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]});
}
} }
@ -821,10 +835,10 @@ FFZ.prototype.add_room = function(id, room) {
this.ws_sub("room." + id); this.ws_sub("room." + id);
// Do we want history? // Do we want history?
if ( ! this.has_bttv && this.settings.chat_history && room && (room.get('messages.length') || 0) < 10 ) { /*if ( ! this.has_bttv && this.settings.chat_history && room && (room.get('messages.length') || 0) < 10 ) {
if ( ! this.ws_send("chat_history", [id,25], this._load_history.bind(this, id)) ) if ( ! this.ws_send("chat_history", [id,25], this._load_history.bind(this, id)) )
data.needs_history = true; data.needs_history = true;
} }*/
} }
@ -874,7 +888,7 @@ FFZ.prototype.remove_room = function(id) {
// Chat History // Chat History
// -------------------- // --------------------
FFZ.prototype._load_history = function(room_id, success, data) { /*FFZ.prototype._load_history = function(room_id, success, data) {
var room = this.rooms[room_id]; var room = this.rooms[room_id];
if ( ! room || ! room.room ) if ( ! room || ! room.room )
return; return;
@ -888,7 +902,7 @@ FFZ.prototype._load_history = function(room_id, success, data) {
return; return;
return this._insert_history(room_id, data, true); return this._insert_history(room_id, data, true);
} }*/
FFZ.prototype._show_deleted = function(room_id) { FFZ.prototype._show_deleted = function(room_id) {
@ -1224,6 +1238,9 @@ FFZ.prototype._modify_room = function(room) {
if ( event.topic && event.topic.substr(-room_id.length) !== room_id || event.created_by === this.get("session.userData.login") ) if ( event.topic && event.topic.substr(-room_id.length) !== room_id || event.created_by === this.get("session.userData.login") )
return; return;
if ( ! f.settings.get_twitch('showModerationActions') )
return;
var target_notice = NOTICE_MAPPING[event.moderation_action]; var target_notice = NOTICE_MAPPING[event.moderation_action];
if ( target_notice ) { if ( target_notice ) {
var last_notice = this.ffz_last_notices && this.ffz_last_notices[target_notice]; var last_notice = this.ffz_last_notices && this.ffz_last_notices[target_notice];
@ -1233,13 +1250,13 @@ FFZ.prototype._modify_room = function(room) {
last_notice.has_owner = true; last_notice.has_owner = true;
last_notice.cachedTokens = undefined; last_notice.cachedTokens = undefined;
if ( last_notice._line ) if ( last_notice._line )
last_notice._line.ffzRender(); Ember.propertyDidChange(last_notice._line, 'ffzTokenizedMessage');
} else { } else {
var waiting = this.ffz_waiting_notices = this.ffz_waiting_notices || {}; var waiting = this.ffz_waiting_notices = this.ffz_waiting_notices || {};
waiting[target_notice] = event.created_by; waiting[target_notice] = event.created_by;
} }
} else if ( f.settings.get_twitch('showModerationActions') ) } else
this._super(event); this._super(event);
}, },
@ -1249,6 +1266,8 @@ FFZ.prototype._modify_room = function(room) {
if ( event.topic && event.topic.substr(-room_id.length) !== room_id || event.created_by === this.get("session.userData.login") ) if ( event.topic && event.topic.substr(-room_id.length) !== room_id || event.created_by === this.get("session.userData.login") )
return; return;
// f.log("Login Moderation", event);
// In case we get unexpected input, do the other thing. // In case we get unexpected input, do the other thing.
if ( ["ban", "unban", "timeout"].indexOf(event.moderation_action) === -1 ) if ( ["ban", "unban", "timeout"].indexOf(event.moderation_action) === -1 )
return this._super(event); return this._super(event);
@ -1259,7 +1278,7 @@ FFZ.prototype._modify_room = function(room) {
'ban-moderator': event.created_by 'ban-moderator': event.created_by
}; };
this.clearMessages(event.args[0], tags, false, event.moderation_action !== 'unban'); this.clearMessages(event.args[0].toLowerCase(), tags, false, event.moderation_action !== 'unban');
}, },
clearMessages: function(user, tags, disable_log, report_only) { clearMessages: function(user, tags, disable_log, report_only) {
@ -1424,6 +1443,7 @@ FFZ.prototype._modify_room = function(room) {
// Look up the user's last ban. // Look up the user's last ban.
var show_notice = is_me || this.ffzShouldDisplayNotice(), var show_notice = is_me || this.ffzShouldDisplayNotice(),
show_reason = is_me || this.get('isModeratorOrHigher'), show_reason = is_me || this.get('isModeratorOrHigher'),
show_moderator = f.settings.get_twitch('showModerationActions'),
room = f.rooms && f.rooms[t.get('id')], room = f.rooms && f.rooms[t.get('id')],
now = new Date, now = new Date,
end_time = now + (duration * 1000), end_time = now + (duration * 1000),
@ -1457,7 +1477,7 @@ FFZ.prototype._modify_room = function(room) {
durations: [duration], durations: [duration],
end_time: end_time, end_time: end_time,
timeouts: report_only ? 0 : 1, timeouts: report_only ? 0 : 1,
message: message + (show_reason && moderator ? ' by ' + moderator : '') + (show_reason && reason ? ' with reason: ' + reason : '.') message: message + (show_reason && show_moderator && moderator ? ' by ' + moderator : '') + (show_reason && reason ? ' with reason: ' + reason : '.')
}; };
if ( ban_history ) if ( ban_history )
@ -1485,7 +1505,7 @@ FFZ.prototype._modify_room = function(room) {
last_ban.message = message + last_ban.message = message +
(last_ban.timeouts > 1 ? ' (' + utils.number_commas(last_ban.timeouts) + ' times)' : '') + (last_ban.timeouts > 1 ? ' (' + utils.number_commas(last_ban.timeouts) + ' times)' : '') +
(!show_reason || last_ban.moderators.length === 0 ? '' : ' by ' + last_ban.moderators.join(', ') ) + (!show_reason || !show_moderator || last_ban.moderators.length === 0 ? '' : ' by ' + last_ban.moderators.join(', ') ) +
(!show_reason || last_ban.reasons.length === 0 ? '.' : ' with reason' + utils.pluralize(last_ban.reasons.length) + ': ' + last_ban.reasons.join(', ')); (!show_reason || last_ban.reasons.length === 0 ? '.' : ' with reason' + utils.pluralize(last_ban.reasons.length) + ': ' + last_ban.reasons.join(', '));
last_ban.cachedTokens = [{type: "text", text: last_ban.message}]; last_ban.cachedTokens = [{type: "text", text: last_ban.message}];
@ -1556,7 +1576,7 @@ FFZ.prototype._modify_room = function(room) {
} }
}, },
trimMessages: function() { /*trimMessages: function() {
var messages = this.get("messages"), var messages = this.get("messages"),
len = messages.get("length"), len = messages.get("length"),
limit = this.get("messageBufferSize"); limit = this.get("messageBufferSize");
@ -1578,7 +1598,7 @@ FFZ.prototype._modify_room = function(room) {
messages.removeAt(0, to_remove); messages.removeAt(0, to_remove);
} }
}, },*/
// Artificial chat delay // Artificial chat delay
ffz_chat_delay: function() { ffz_chat_delay: function() {
@ -1608,14 +1628,81 @@ FFZ.prototype._modify_room = function(room) {
this.ffzSchedulePendingFlush(now); this.ffzSchedulePendingFlush(now);
} else { } else {
this.ffzActualPushMessage(msg); this.ffzPushMessages([msg]);
} }
}, },
ffzActualPushMessage: function (msg) { ffzPushMessages: function(messages) {
var new_messages = [],
new_unread = 0;
for(var i=0; i < messages.length; i++) {
var msg = messages[i];
if ( this.shouldShowMessage(msg) && this.ffzShouldShowMessage(msg) ) {
new_messages.push(msg);
if ( ! (msg.tags && msg.tags.historical) && msg.style !== "admin" && msg.style !== "whisper" ) {
if ( msg.ffz_has_mention )
this.ffz_last_mention = Date.now();
new_unread++;
}
}
}
if ( ! new_messages.length )
return;
var room_messages = this.get("messages"),
trimmed = room_messages.length + new_messages.length > this.messageBufferSize ?
room_messages.slice(Math.max(0, (room_messages.length - this.messageBufferSize)) + new_messages.length, room_messages.length) :
room_messages.slice(0, room_messages.length);
var earliest_message;
for(var i=0; i < trimmed.length; i++)
if ( trimmed[i].date ) {
earliest_message = trimmed[i].date;
break;
}
for(var i = 0; i < new_messages.length; i++) {
var msg = new_messages[i];
if ( msg.tags && msg.tags.historical ) {
// Add a warning about really old messages.
if ( earliest_message ) {
var age = earliest_message - msg.date;
if ( age > 300000 )
trimmed.unshift({
color: '#755000',
date: msg.date,
from: 'frankerfacez_admin',
style: 'admin',
message: '(Last message is ' + utils.human_time(age/1000) + ' old.)',
room: msg.room,
from_server: true
});
}
trimmed.unshift(msg);
earliest_message = null;
} else
trimmed.push(msg);
}
this.set("messages", trimmed);
if ( new_unread ) {
this.incrementProperty("unreadCount", new_unread);
this.ffz_last_activity = Date.now();
}
},
/*ffzActualPushMessage: function (msg) {
if ( this.shouldShowMessage(msg) && this.ffzShouldShowMessage(msg) ) { if ( this.shouldShowMessage(msg) && this.ffzShouldShowMessage(msg) ) {
this.get("messages").pushObject(msg); this.get("messages").pushObject(msg);
this.trimMessages(); this.trimMessages();
Ember.propertyDidChange(this, "messages");
if ( msg.style !== "admin" && msg.style !== "whisper" ) { if ( msg.style !== "admin" && msg.style !== "whisper" ) {
if ( msg.ffz_has_mention ) { if ( msg.ffz_has_mention ) {
@ -1626,7 +1713,7 @@ FFZ.prototype._modify_room = function(room) {
this.incrementProperty("unreadCount", 1); this.incrementProperty("unreadCount", 1);
} }
} }
}, },*/
ffzSchedulePendingFlush: function(now) { ffzSchedulePendingFlush: function(now) {
// Instead of just blindly looping every x seconds, we want to calculate the time until // Instead of just blindly looping every x seconds, we want to calculate the time until
@ -1655,7 +1742,8 @@ FFZ.prototype._modify_room = function(room) {
this._ffz_pending_flush = null; this._ffz_pending_flush = null;
var now = this._ffz_last_batch = Date.now(), var now = this._ffz_last_batch = Date.now(),
chat_delay = this.get('ffz_chat_delay'); chat_delay = this.get('ffz_chat_delay'),
to_display = [];
for (var i = 0, l = this.ffzPending.length; i < l; i++) { for (var i = 0, l = this.ffzPending.length; i < l; i++) {
var msg = this.ffzPending[i]; var msg = this.ffzPending[i];
@ -1677,9 +1765,11 @@ FFZ.prototype._modify_room = function(room) {
break; break;
msg.pending = false; msg.pending = false;
this.ffzActualPushMessage(msg); to_display.push(msg);
} }
this.ffzPushMessages(to_display);
this.ffzPending = this.ffzPending.slice(i); this.ffzPending = this.ffzPending.slice(i);
this.ffzSchedulePendingFlush(now); this.ffzSchedulePendingFlush(now);
}, },
@ -1714,7 +1804,7 @@ FFZ.prototype._modify_room = function(room) {
if ( (msg.msgId === 'timeout_success' || msg.msgId === 'ban_success') && this.ffzShouldDisplayNotice() ) if ( (msg.msgId === 'timeout_success' || msg.msgId === 'ban_success') && this.ffzShouldDisplayNotice() )
return; return;
f.log("Notification", msg); // f.log("Notification", msg);
if ( ! msg.tags ) if ( ! msg.tags )
msg.tags = {}; msg.tags = {};
@ -1740,7 +1830,7 @@ FFZ.prototype._modify_room = function(room) {
this.addMessage(msg); this.addMessage(msg);
}, },
addMessage: function(msg) { ffzProcessMessage: function(msg) {
if ( msg ) { if ( msg ) {
var notice_type = msg.tags && msg.tags['msg-id'], var notice_type = msg.tags && msg.tags['msg-id'],
is_resub = notice_type === 'resub', is_resub = notice_type === 'resub',
@ -1769,6 +1859,13 @@ FFZ.prototype._modify_room = function(room) {
msg.tags['system-msg'] = ''; msg.tags['system-msg'] = '';
} }
// Fix dates for historical messages.
if ( ! msg.date && msg.tags && msg.tags['tmi-sent-ts'] ) {
var sent = parseInt(msg.tags['tmi-sent-ts']);
if ( sent && ! isNaN(sent) && isFinite(sent) )
msg.date = new Date(sent);
}
var is_whisper = msg.style === 'whisper'; var is_whisper = msg.style === 'whisper';
// Ignore whispers if conversations are enabled. // Ignore whispers if conversations are enabled.
@ -1792,7 +1889,7 @@ FFZ.prototype._modify_room = function(room) {
// Tokenization // Tokenization
f.tokenize_chat_line(msg, false, this.get('roomProperties.hide_chat_links')); f.tokenize_chat_line(msg, false, this.get('roomProperties.hide_chat_links'));
// If it's from Twitch notify, and it's directly related to // Check for a new subscription line that would need a chat badge.
if ( msg.from === 'twitchnotify' && msg.message.indexOf('subscribed to') === -1 && msg.message.indexOf('subscribed') !== -1 ) { if ( msg.from === 'twitchnotify' && msg.message.indexOf('subscribed to') === -1 && msg.message.indexOf('subscribed') !== -1 ) {
if ( ! msg.tags ) if ( ! msg.tags )
msg.tags = {}; msg.tags = {};
@ -1804,6 +1901,29 @@ FFZ.prototype._modify_room = function(room) {
msg.labels.push("subscriber"); msg.labels.push("subscriber");
} }
// Color processing.
if ( msg.color )
f._handle_color(msg.color);
return msg;
}
},
addMessage: function(msg) {
msg = this.ffzProcessMessage(msg);
if ( ! msg )
return;
var msg_id = msg.tags && msg.tags.id,
notice_type = msg.tags && msg.tags['msg-id'],
is_whisper = msg.style === 'whisper';
// If this message is already in the room, discard the duplicate.
if ( msg_id && this.ffz_ids && this.ffz_ids[msg_id] )
return;
// Keep the history. // Keep the history.
if ( ! is_whisper && msg.from && msg.from !== 'jtv' && msg.from !== 'twitchnotify' && f.settings.mod_card_history ) { if ( ! is_whisper && msg.from && msg.from !== 'jtv' && msg.from !== 'twitchnotify' && f.settings.mod_card_history ) {
var room = f.rooms && f.rooms[msg.room]; var room = f.rooms && f.rooms[msg.room];
@ -1824,6 +1944,12 @@ FFZ.prototype._modify_room = function(room) {
date: msg.date date: msg.date
}; };
if ( msg.tags && msg.tags.historical ) {
// If it's historical, insert it at the beginning. And stuff.
if ( user_history.length < 20 )
user_history.unshift(new_msg);
} else {
// Preserve message order if we *just* received a ban. // Preserve message order if we *just* received a ban.
if ( last_history && last_history.is_delete && (msg.date - last_history.date) <= 200 ) { if ( last_history && last_history.is_delete && (msg.date - last_history.date) <= 200 ) {
user_history.splice(user_history.length - 1, 0, new_msg); user_history.splice(user_history.length - 1, 0, new_msg);
@ -1832,6 +1958,7 @@ FFZ.prototype._modify_room = function(room) {
if ( user_history.length > 20 ) if ( user_history.length > 20 )
user_history.shift(); user_history.shift();
}
if ( f._mod_card && f._mod_card.ffz_room_id === msg.room && f._mod_card.get('cardInfo.user.id') === msg.from ) { if ( f._mod_card && f._mod_card.ffz_room_id === msg.room && f._mod_card.get('cardInfo.user.id') === msg.from ) {
var el = f._mod_card.get('element'), var el = f._mod_card.get('element'),
@ -1839,7 +1966,11 @@ FFZ.prototype._modify_room = function(room) {
was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight); was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight);
if ( history ) { if ( history ) {
history.appendChild(f._build_mod_card_history(msg, f._mod_card)); var el = f._build_mod_card_history(msg, f._mod_card);
if ( msg.tags && msg.tags.historical )
history.insertBefore(el, history.firstElementChild);
else
history.appendChild(el);
if ( was_at_top ) if ( was_at_top )
setTimeout(function() { history.scrollTop = history.scrollHeight; }) setTimeout(function() { history.scrollTop = history.scrollHeight; })
@ -1881,14 +2012,6 @@ FFZ.prototype._modify_room = function(room) {
} }
} }
// Also update chatters.
if ( ! is_whisper && this.chatters && ! this.chatters[msg.from] && msg.from !== 'twitchnotify' && msg.from !== 'jtv' )
this.ffzUpdateChatters(msg.from);
}
// Color processing.
if ( msg.color )
f._handle_color(msg.color);
// Message Filtering // Message Filtering
var i = f._chat_filters.length; var i = f._chat_filters.length;
@ -1896,6 +2019,11 @@ FFZ.prototype._modify_room = function(room) {
if ( f._chat_filters[i](msg) === false ) if ( f._chat_filters[i](msg) === false )
return; return;
// Also update chatters.
if ( ! is_whisper && this.chatters && ! this.chatters[msg.from] && msg.from !== 'twitchnotify' && msg.from !== 'jtv' )
this.ffzUpdateChatters(msg.from);
// We're past the last return, so store the message // We're past the last return, so store the message
// now that we know we're keeping it. // now that we know we're keeping it.
if ( msg_id ) { if ( msg_id ) {
@ -1913,8 +2041,15 @@ FFZ.prototype._modify_room = function(room) {
if ( window !== window.parent && parent.postMessage && msg.from && msg.from !== "jtv" && msg.from !== "twitchnotify" ) if ( window !== window.parent && parent.postMessage && msg.from && msg.from !== "jtv" && msg.from !== "twitchnotify" )
parent.postMessage({from_ffz: true, command: 'chat_message', data: {from: msg.from, room: msg.room}}, "*"); //location.protocol + "//www.twitch.tv/"); parent.postMessage({from_ffz: true, command: 'chat_message', data: {from: msg.from, room: msg.room}}, "*"); //location.protocol + "//www.twitch.tv/");
// Add the message. // Flagging for review.
return this._super(msg); if ( msg.tags && msg.tags.risk === "high" )
msg.flaggedForReview = true;
// Add the message. We don't do super anymore because it does stupid stuff.'
this.pushMessage(msg);
msg.from && msg.style !== "admin" && msg.style !== "notification" && msg.tags && this.addChatter(msg);
this.trackLatency(msg);
//return this._super(msg);
}, },
ffzChatFilters: function(msg) { ffzChatFilters: function(msg) {
@ -1934,7 +2069,7 @@ FFZ.prototype._modify_room = function(room) {
return this._super(e); return this._super(e);
}, },
send: function(text, ignore_history) { send: function(text, ignore_history, used_aliases) {
try { try {
this.ffz_last_input = Date.now(); this.ffz_last_input = Date.now();
@ -1958,16 +2093,42 @@ FFZ.prototype._modify_room = function(room) {
mru.unshift(text); mru.unshift(text);
} }
var cmd = text.split(' ', 1)[0].toLowerCase(); if ( is_cmd ) {
if ( cmd === "/ffz" ) { var cmd = text.substr(1).split(' ', 1)[0].toLowerCase(),
this.set("messageToSend", ""); was_handled = false;
f.run_ffz_command(text.substr(5), this.get('id'));
return;
} else if ( cmd.charAt(0) === "/" && f.run_command(text, this.get('id')) ) { if ( cmd === "ffz" ) {
f.run_ffz_command(text.substr(5), this.get('id'));
was_handled = true;
} else if ( f._command_aliases[cmd] ) {
used_aliases = used_aliases || [];
if ( used_aliases.indexOf(cmd) !== -1 ) {
f.room_message(f.rooms[this.get('id')], "Error: Your command aliases are recursing. [Path: " + used_aliases.join(", ") + "]");
was_handled = true;
} else {
var alias = f._command_aliases[cmd],
args = text.substr(1 + cmd.length).trimLeft().split(/\s+/g),
output = utils.replace_cmd_variables(alias, null, this, null, args);
used_aliases.push(cmd);
this.set("messageToSend", "");
var lines = output.split(/\s*<LINE>\s*/g);
for(var i=0; i < lines.length; i++)
this.send(lines[i], true, used_aliases);
return;
}
} else if ( f.run_command(text, this.get('id')) )
was_handled = true;
if ( was_handled ) {
this.set("messageToSend", ""); this.set("messageToSend", "");
return; return;
} }
}
} catch(err) { } catch(err) {
f.error("send: " + err); f.error("send: " + err);
@ -2130,10 +2291,6 @@ FFZ.prototype._modify_room = function(room) {
// Room State Stuff // Room State Stuff
slowMode: function() {
return this.get('slow') > 0;
}.property('slow'),
onSlowOff: function() { onSlowOff: function() {
if ( ! this.get('slowMode') ) if ( ! this.get('slowMode') )
this.updateWait(0); this.updateWait(0);

View file

@ -45,12 +45,12 @@ FFZ.prototype.setup_vod_chat = function() {
f.error("Unable to locate VOD Chat Service."); f.error("Unable to locate VOD Chat Service.");
this.update_views('component:vod-right-column', this.modify_vod_right_column); this.update_views('component:vod-right-column', this.modify_vod_right_column);
this.update_views('view:vod', this.modify_vod_view); //this.update_views('view:vod', this.modify_vod_view);
this.update_views('component:vod-chat-display', this.modify_vod_chat_display); this.update_views('component:vod-chat-display', this.modify_vod_chat_display);
} }
FFZ.prototype.modify_vod_view = function(view) { /*FFZ.prototype.modify_vod_view = function(view) {
var f = this; var f = this;
utils.ember_reopen_view(view, { utils.ember_reopen_view(view, {
ffz_init: function() { ffz_init: function() {
@ -90,7 +90,7 @@ FFZ.prototype.modify_vod_view = function(view) {
document.body.classList.toggle('ffz-small-player', f.settings.small_player && top >= height); document.body.classList.toggle('ffz-small-player', f.settings.small_player && top >= height);
} }
}); });
} }*/
FFZ.prototype.modify_vod_right_column = function(component) { FFZ.prototype.modify_vod_right_column = function(component) {

View file

@ -92,7 +92,7 @@ FFZ.prototype.api = function(name, icon, version) {
try { try {
this._known_apis = JSON.parse(localStorage.ffz_known_apis); this._known_apis = JSON.parse(localStorage.ffz_known_apis);
} catch(err) { } catch(err) {
this.log("Error loading Known APIs: " + err); this.error("Error loading Known APIs", err);
} }
} }
@ -105,6 +105,11 @@ API.prototype.log = function(msg, data, to_json, log_json) {
} }
API.prototype.error = function(msg, error, to_json, log_json) {
this.ffz.error('Ext "' + this.name + '": ' + msg, data, to_json, log_json);
}
// --------------------- // ---------------------
// Set Loading // Set Loading
// --------------------- // ---------------------
@ -115,8 +120,8 @@ API.prototype._load_set = function(real_id, set_id, data) {
// Check for an existing set to copy the users. // Check for an existing set to copy the users.
var users = []; var users = [];
if ( this.emote_sets[real_id] && this.emote_sets[real_id].users ) if ( this.emote_sets[set_id] && this.emote_sets[set_id].users )
users = this.emote_sets[real_id].users; users = this.emote_sets[set_id].users;
var emote_set = { var emote_set = {
source: this.name, source: this.name,
@ -127,14 +132,16 @@ API.prototype._load_set = function(real_id, set_id, data) {
emoticons: {}, emoticons: {},
_type: data._type || 0, _type: data._type || 0,
css: data.css || null, css: data.css || null,
hidden: data.hidden || false,
description: data.description || null, description: data.description || null,
icon: data.icon || this.icon || null, icon: data.icon || this.icon || null,
id: real_id, id: real_id,
title: data.title || "Global Emoticons", title: data.title || "Global Emoticons",
}; };
this.emote_sets[real_id] = emote_set; this.emote_sets[set_id] = emote_set;
// Use the real ID for FFZ's own tracking.
if ( this.ffz.emote_sets ) if ( this.ffz.emote_sets )
this.ffz.emote_sets[real_id] = emote_set; this.ffz.emote_sets[real_id] = emote_set;
@ -202,6 +209,7 @@ API.prototype._load_set = function(real_id, set_id, data) {
emoticons[id] = new_emote; emoticons[id] = new_emote;
} }
// Use the real ID for building CSS.
utils.update_css(this.ffz._emote_style, real_id, output_css + (emote_set.css || "")); utils.update_css(this.ffz._emote_style, real_id, output_css + (emote_set.css || ""));
if ( this.ffz._cindex ) if ( this.ffz._cindex )
@ -231,21 +239,21 @@ API.prototype.load_set = function(id, emote_set) {
} }
API.prototype.unload_set = function(id) { API.prototype.unload_set = function(set_id) {
var exact_id = this.id + '-' + id, var exact_id = this.id + '-' + set_id,
emote_set = this.emote_sets[exact_id]; emote_set = this.emote_sets[set_id];
if ( ! emote_set ) if ( ! emote_set )
return; return;
// First, let's unregister it as a global. // First, let's unregister it as a global.
this.unregister_global_set(id); this.unregister_global_set(set_id);
// Now, remove the set data. // Now, remove the set data.
utils.update_css(this.ffz._emote_style, exact_id, null); utils.update_css(this.ffz._emote_style, exact_id, null);
this.emote_sets[exact_id] = undefined; this.emote_sets[set_id] = undefined;
if ( this.ffz.emote_sets ) if ( this.ffz.emote_sets )
this.ffz.emote_sets[exact_id] = undefined; this.ffz.emote_sets[exact_id] = undefined;
@ -272,9 +280,8 @@ API.prototype.unload_set = function(id) {
} }
API.prototype.get_set = function(id) { API.prototype.get_set = function(set_id) {
var exact_id = this.id + '-' + id; return this.emote_sets[set_id];
return this.emote_sets[exact_id];
} }
@ -282,14 +289,14 @@ API.prototype.get_set = function(id) {
// Global Emote Sets // Global Emote Sets
// --------------------- // ---------------------
API.prototype.register_global_set = function(id, emote_set) { API.prototype.register_global_set = function(set_id, emote_set) {
var exact_id = this.id + '-' + id; var exact_id = this.id + '-' + set_id;
if ( emote_set ) { if ( emote_set ) {
// If a set was provided, load it. // If a set was provided, load it.
emote_set = this.load_set(id, emote_set); emote_set = this.load_set(set_id, emote_set);
} else } else
emote_set = this.emote_sets[exact_id]; emote_set = this.emote_sets[set_id];
if ( ! emote_set ) if ( ! emote_set )
throw new Error("Invalid set ID"); throw new Error("Invalid set ID");
@ -301,11 +308,11 @@ API.prototype.register_global_set = function(id, emote_set) {
// It's a valid set if we get here, so make it global. // It's a valid set if we get here, so make it global.
if ( this.global_sets.indexOf(exact_id) === -1 ) if ( this.global_sets.indexOf(set_id) === -1 )
this.global_sets.push(exact_id); this.global_sets.push(set_id);
if ( this.default_sets.indexOf(exact_id) === -1 ) if ( this.default_sets.indexOf(set_id) === -1 )
this.default_sets.push(exact_id); this.default_sets.push(set_id);
if ( this.ffz.global_sets && this.ffz.global_sets.indexOf(exact_id) === -1 ) if ( this.ffz.global_sets && this.ffz.global_sets.indexOf(exact_id) === -1 )
this.ffz.global_sets.push(exact_id); this.ffz.global_sets.push(exact_id);
@ -319,19 +326,19 @@ API.prototype.register_global_set = function(id, emote_set) {
}; };
API.prototype.unregister_global_set = function(id) { API.prototype.unregister_global_set = function(set_id) {
var exact_id = this.id + '-' + id, var exact_id = this.id + '-' + set_id,
emote_set = this.emote_sets[exact_id]; emote_set = this.emote_sets[set_id];
if ( ! emote_set ) if ( ! emote_set )
return; return;
// Remove the set from global sets. // Remove the set from global sets.
var ind = this.global_sets.indexOf(exact_id); var ind = this.global_sets.indexOf(set_id);
if ( ind !== -1 ) if ( ind !== -1 )
this.global_sets.splice(ind,1); this.global_sets.splice(ind,1);
ind = this.default_sets.indexOf(exact_id); ind = this.default_sets.indexOf(set_id);
if ( ind !== -1 ) if ( ind !== -1 )
this.default_sets.splice(ind,1); this.default_sets.splice(ind,1);
@ -353,8 +360,8 @@ API.prototype.unregister_global_set = function(id) {
// Per-Channel Emote Sets // Per-Channel Emote Sets
// ----------------------- // -----------------------
API.prototype.register_room_set = function(room_id, id, emote_set) { API.prototype.register_room_set = function(room_id, set_id, emote_set) {
var exact_id = this.id + '-' + id, var exact_id = this.id + '-' + set_id,
room = this.ffz.rooms && this.ffz.rooms[room_id]; room = this.ffz.rooms && this.ffz.rooms[room_id];
if ( ! room ) if ( ! room )
@ -365,9 +372,9 @@ API.prototype.register_room_set = function(room_id, id, emote_set) {
emote_set.title = emote_set.title || "Channel: " + (room.display_name || room_id); emote_set.title = emote_set.title || "Channel: " + (room.display_name || room_id);
emote_set._type = emote_set._type || 1; emote_set._type = emote_set._type || 1;
emote_set = this.load_set(id, emote_set); emote_set = this.load_set(set_id, emote_set);
} else } else
emote_set = this.emote_sets[exact_id]; emote_set = this.emote_sets[set_id];
if ( ! emote_set ) if ( ! emote_set )
throw new Error("Invalid set ID"); throw new Error("Invalid set ID");
@ -386,9 +393,9 @@ API.prototype.register_room_set = function(room_id, id, emote_set) {
} }
API.prototype.unregister_room_set = function(room_id, id) { API.prototype.unregister_room_set = function(room_id, set_id) {
var exact_id = this.id + '-' + id, var exact_id = this.id + '-' + set_id,
emote_set = this.emote_sets[exact_id], emote_set = this.emote_sets[set_id],
room = this.ffz.rooms && this.ffz.rooms[room_id]; room = this.ffz.rooms && this.ffz.rooms[room_id];
if ( ! emote_set || ! room ) if ( ! emote_set || ! room )
@ -483,8 +490,8 @@ API.prototype.user_add_set = function(username, set_id) {
exact_id = this.id + '-' + set_id; exact_id = this.id + '-' + set_id;
if ( emote_sets.indexOf(exact_id) === -1 ) if ( emote_sets.indexOf(set_id) === -1 )
emote_sets.push(exact_id); emote_sets.push(set_id);
if ( ffz_sets.indexOf(exact_id) === -1 ) if ( ffz_sets.indexOf(exact_id) === -1 )
ffz_sets.push(exact_id); ffz_sets.push(exact_id);
@ -505,7 +512,7 @@ API.prototype.user_remove_set = function(username, set_id) {
exact_id = this.id + '-' + set_id; exact_id = this.id + '-' + set_id;
var ind = emote_sets ? emote_sets.indexOf(exact_id) : -1; var ind = emote_sets ? emote_sets.indexOf(set_id) : -1;
if ( ind !== -1 ) if ( ind !== -1 )
emote_sets.splice(ind, 1); emote_sets.splice(ind, 1);
@ -550,7 +557,7 @@ API.prototype._room_callbacks = function(room_id, room, specific_func) {
try { try {
specific_func(room_id, callback); specific_func(room_id, callback);
} catch(err) { } catch(err) {
this.log("Error in On-Room Callback: " + err); this.error("Error in On-Room Callback", err);
} }
} else { } else {
@ -559,7 +566,7 @@ API.prototype._room_callbacks = function(room_id, room, specific_func) {
try { try {
cb(room_id, callback); cb(room_id, callback);
} catch(err) { } catch(err) {
this.log("Error in On-Room Callback: " + err); this.error("Error in On-Room Callback", err);
} }
} }
} }
@ -568,11 +575,16 @@ API.prototype._room_callbacks = function(room_id, room, specific_func) {
API.prototype.register_on_room_callback = function(callback, dont_iterate) { API.prototype.register_on_room_callback = function(callback, dont_iterate) {
this.on_room_callbacks.push(callback); this.on_room_callbacks.push(callback);
var register_callback = this.register_room_set.bind(this, room_id);
// Call this for all current rooms. // Call this for all current rooms.
if ( ! dont_iterate && this.ffz.rooms ) { if ( ! dont_iterate && this.ffz.rooms ) {
for(var room_id in this.ffz.rooms) for(var room_id in this.ffz.rooms)
this._room_callbacks(room_id, this.ffz.rooms[room_id], callback); try {
callback(room_id, register_callback);
} catch(err) {
this.error("Error in On-Room Callback", err);
}
} }
} }

View file

@ -109,6 +109,9 @@ FFZ.prototype.setup_bttv = function(delay) {
cl.remove("ffz-portrait"); cl.remove("ffz-portrait");
cl.remove("ffz-minimal-channel-title"); cl.remove("ffz-minimal-channel-title");
cl.remove("ffz-flip-dashboard"); cl.remove("ffz-flip-dashboard");
cl.remove('ffz-minimal-channel-bar');
cl.remove('ffz-channel-bar-bottom');
cl.remove('ffz-channel-title-top');
// Remove Following Count // Remove Following Count
if ( this.settings.following_count ) { if ( this.settings.following_count ) {
@ -270,8 +273,6 @@ FFZ.prototype.setup_bttv = function(delay) {
} }
} }
f.log("BTTV Emotes", output);
return output; return output;
} }

View file

@ -34,7 +34,7 @@ FFZ.msg_commands = {};
// Version // Version
var VER = FFZ.version_info = { var VER = FFZ.version_info = {
major: 3, minor: 5, revision: 283, major: 3, minor: 5, revision: 302,
toString: function() { toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
} }
@ -249,7 +249,7 @@ FFZ.prototype.initialize = function(increment, delay) {
return this.init_clips(delay); return this.init_clips(delay);
// Check for special non-ember pages. // Check for special non-ember pages.
if ( /^\/(?:$|search$|team\/|user\/|p\/|settings|m\/|messages?\/)/.test(location.pathname) ) if ( /^\/(?:team\/|user\/|p\/|settings|m\/|messages?\/)/.test(location.pathname) )
return this.init_normal(delay); return this.init_normal(delay);
// Check for the dashboard. // Check for the dashboard.
@ -410,6 +410,7 @@ FFZ.prototype.init_dashboard = function(delay) {
// Set up the FFZ message passer. // Set up the FFZ message passer.
this.setup_message_event(); this.setup_message_event();
this.cache_command_aliases();
this.fix_tooltips(); this.fix_tooltips();
this.find_bttv(10); this.find_bttv(10);
@ -433,12 +434,6 @@ FFZ.prototype.init_ember = function(delay) {
} catch(err) { this.embed_in_dash = false; } } catch(err) { this.embed_in_dash = false; }
// Make an alias so they STOP RENAMING THIS ON ME
var Settings = FFZ.utils.ember_lookup('controller:settings');
if ( Settings && Settings.get('settings') === undefined )
Settings.reopen({settings: Ember.computed.alias('model')});
// Settings are important. // Settings are important.
this.load_settings(); this.load_settings();
@ -448,10 +443,18 @@ FFZ.prototype.init_ember = function(delay) {
var f = this; var f = this;
if ( Ember.RSVP && Ember.RSVP.on ) if ( Ember.RSVP && Ember.RSVP.on )
Ember.RSVP.on('error', function(error) { Ember.RSVP.on('error', function(error) {
// We want to ignore errors that are just 4xx HTTP responses.
if ( error && error.responseJSON && typeof error.responseJSON.status === "number" && error.responseJSON.status >= 400 )
return;
f.error("There was an error within an Ember RSVP.", error); f.error("There was an error within an Ember RSVP.", error);
}); });
Ember.onerror = function(error) { Ember.onerror = function(error) {
// We want to ignore errors that are just 4xx HTTP responses.
if ( error && error.responseJSON && typeof error.responseJSON.status === "number" && error.responseJSON.status >= 400 )
return;
f.error("There was an unknown error within Ember.", error); f.error("There was an unknown error within Ember.", error);
} }
} }
@ -505,6 +508,7 @@ FFZ.prototype.init_ember = function(delay) {
// Do all Ember modification before this point. // Do all Ember modification before this point.
this.finalize_ember_wrapper(); this.finalize_ember_wrapper();
this.cache_command_aliases();
this.fix_tooltips(); this.fix_tooltips();
this.connect_extra_chat(); this.connect_extra_chat();

View file

@ -815,8 +815,8 @@ FFZ.prototype._setting_get = function(key) {
} }
FFZ.prototype._setting_get_twitch = function(key) { FFZ.prototype._setting_get_twitch = function(key) {
var Settings = utils.ember_lookup('controller:settings'); var Settings = utils.ember_settings();
return Settings && Settings.get('settings.' + key); return Settings && Settings.get(key);
} }

View file

@ -192,18 +192,18 @@ FFZ.prototype.ws_create = function() {
if ( room.important ) { if ( room.important ) {
f.ws_sub("room." + room_id); f.ws_sub("room." + room_id);
if ( room.needs_history ) { /*if ( room.needs_history ) {
room.needs_history = false; room.needs_history = false;
if ( ! f.has_bttv && f.settings.chat_history ) if ( ! f.has_bttv && f.settings.chat_history )
f.ws_send("chat_history", [room_id,25], f._load_history.bind(f, room_id)); f.ws_send("chat_history", [room_id,25], f._load_history.bind(f, room_id));
} }*/
} }
} }
// Send the channel(s). // Send the channel(s).
if ( f._cindex ) { if ( f._cindex ) {
var channel_id = f._cindex.get('controller.model.id'), var channel_id = f._cindex.get('channel.id'),
hosted_id = f._cindex.get('controller.hostModeTarget.id'); hosted_id = f._cindex.get('channel.hostModeTarget.id');
if ( channel_id ) if ( channel_id )
f.ws_sub("channel." + channel_id); f.ws_sub("channel." + channel_id);
@ -463,20 +463,18 @@ FFZ.ws_commands.do_authorize = function(data) {
if ( ! this.rooms.hasOwnProperty(room_id) ) if ( ! this.rooms.hasOwnProperty(room_id) )
continue; continue;
var r = this.rooms[room_id]; var r = this.rooms[room_id],
c = r && r.room && r.room.tmiRoom && r.room.tmiRoom._getConnection();
if ( r && r.room && !r.room.get('roomProperties.eventchat') && !r.room.get('isGroupRoom') && r.room.tmiRoom ) {
var c = r.room.tmiRoom._getConnection();
if ( c.isConnected ) { if ( c.isConnected ) {
conn = c; conn = c;
break; break;
} }
} }
}
if ( conn ) if ( conn )
conn._send("PRIVMSG #frankerfacezauthorizer :AUTH " + data); conn._send("PRIVMSG #frankerfacezauthorizer :AUTH " + data);
else else
// Try again shortly. // Try again shortly.
setTimeout(FFZ.ws_commands.do_authorize.bind(this, data), 5000); setTimeout(FFZ.ws_commands.do_authorize.bind(this, data), 1000);
} }

View file

@ -728,6 +728,10 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del
// We have a mention! // We have a mention!
msgObject.ffz_has_mention = true; msgObject.ffz_has_mention = true;
// If it's a historical message we don't want to update any other UI.
if ( msg.tags && msg.tags.historical )
continue;
// If we have chat tabs/rows, update the status. // If we have chat tabs/rows, update the status.
if ( room_id && ! this.has_bttv && this._chatv ) { if ( room_id && ! this.has_bttv && this._chatv ) {
var room = this.rooms[room_id] && this.rooms[room_id].room; var room = this.rooms[room_id] && this.rooms[room_id].room;
@ -1227,7 +1231,7 @@ FFZ._words_to_regex = function(list) {
if ( ! list[i] ) if ( ! list[i] )
continue; continue;
reg += (reg ? "|" : "") + reg_escape(list[i]); reg += (reg ? "|" : "") + (list[i].substr(0,6) === "regex:" ? list[i].substr(6) : reg_escape(list[i]));
} }
regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + constants.SEPARATORS + ")(" + reg + ")(?=$|" + constants.SEPARATORS + ")", "ig"); regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + constants.SEPARATORS + ")(" + reg + ")(?=$|" + constants.SEPARATORS + ")", "ig");

View file

@ -89,7 +89,7 @@ FFZ.basic_settings.keywords = {
help: "Set additional keywords that will be highlighted in chat.", help: "Set additional keywords that will be highlighted in chat.",
method: function() { method: function() {
FFZ.settings_info.keywords.method.call(this); FFZ.settings_info.keywords.method.call(this, null, true);
} }
}; };
@ -103,7 +103,7 @@ FFZ.basic_settings.banned_words = {
help: "Set a list of words that will be removed from chat messages, locally.", help: "Set a list of words that will be removed from chat messages, locally.",
method: function() { method: function() {
FFZ.settings_info.banned_words.method.call(this); FFZ.settings_info.banned_words.method.call(this, null, true);
} }
}; };
@ -137,15 +137,14 @@ FFZ.settings_info.dark_twitch = {
(this.is_clips ? document.querySelector('html') : document.body).classList.toggle("ffz-dark", val); (this.is_clips ? document.querySelector('html') : document.body).classList.toggle("ffz-dark", val);
var Settings = utils.ember_lookup('controller:settings'), var Settings = utils.ember_settings();
settings = Settings && Settings.get('settings');
if ( val ) { if ( val ) {
this._load_dark_css(); this._load_dark_css();
settings && this.settings.set('twitch_chat_dark', settings.get('darkMode')); Settings && this.settings.set('twitch_chat_dark', Settings.get('darkMode'));
settings && settings.set('darkMode', true); Settings && Settings.set('darkMode', true);
} else } else
settings && settings.set('darkMode', this.settings.twitch_chat_dark); Settings && Settings.set('darkMode', this.settings.twitch_chat_dark);
// Try coloring chat replay // Try coloring chat replay
window.jQuery && jQuery('.chatReplay').toggleClass('dark', val || false); window.jQuery && jQuery('.chatReplay').toggleClass('dark', val || false);
@ -203,10 +202,10 @@ FFZ.prototype.setup_dark = function() {
if ( ! this.settings.dark_twitch ) if ( ! this.settings.dark_twitch )
return; return;
var Settings = utils.ember_lookup('controller:settings'); var Settings = utils.ember_settings();
if ( Settings ) { if ( Settings ) {
try { try {
Settings.set('settings.darkMode', true); Settings.set('darkMode', true);
} catch(err) { } catch(err) {
this.error("Unable to set the darkMode setting because it isn't named what we expect. WTF?"); this.error("Unable to set the darkMode setting because it isn't named what we expect. WTF?");
} }

View file

@ -76,8 +76,8 @@ FFZ.ffz_commands.following = function(room, args) {
FFZ.ws_on_close.push(function() { FFZ.ws_on_close.push(function() {
var controller = utils.ember_lookup('controller:channel'), var controller = utils.ember_lookup('controller:channel'),
current_id = controller && controller.get('content.id'), current_id = controller && controller.get('channelModel.id'),
current_host = controller && controller.get('hostModeTarget.id'), current_host = controller && controller.get('channelModel.hostModeTarget.id'),
need_update = false; need_update = false;
this.follow_sets = {}; this.follow_sets = {};
@ -112,8 +112,8 @@ FFZ.ws_on_close.push(function() {
FFZ.ws_commands.follow_buttons = function(data) { FFZ.ws_commands.follow_buttons = function(data) {
var controller = utils.ember_lookup('controller:channel'), var controller = utils.ember_lookup('controller:channel'),
current_id = controller && controller.get('content.id'), current_id = controller && controller.get('channelModel.id'),
current_host = controller && controller.get('hostModeTarget.id'), current_host = controller && controller.get('channelModel.hostModeTarget.id'),
need_update = false; need_update = false;
this.follow_data = this.follow_data || {}; this.follow_data = this.follow_data || {};
@ -131,8 +131,8 @@ FFZ.ws_commands.follow_buttons = function(data) {
FFZ.ws_commands.follow_sets = function(data) { FFZ.ws_commands.follow_sets = function(data) {
var controller = utils.ember_lookup('controller:channel'), var controller = utils.ember_lookup('controller:channel'),
current_id = controller && controller.get('content.id'), current_id = controller && controller.get('channelModel.id'),
current_host = controller && controller.get('hostModeTarget.id'), current_host = controller && controller.get('channelModel.hostModeTarget.id'),
need_update = false, need_update = false,
f = this; f = this;
@ -187,13 +187,12 @@ FFZ.ws_commands.follow_sets = function(data) {
// --------------- // ---------------
FFZ.prototype.rebuild_following_ui = function() { FFZ.prototype.rebuild_following_ui = function() {
var controller = utils.ember_lookup('controller:channel'),
channel_id = controller && controller.get('content.id'),
hosted_id = controller && controller.get('hostModeTarget.id');
if ( ! this._cindex ) if ( ! this._cindex )
return; return;
var channel_id = this._cindex.get('channel.id'),
hosted_id = this._cindex.get('channel.hostModeTarget.id');
if ( channel_id ) { if ( channel_id ) {
var data = this.follow_data && this.follow_data[channel_id], var data = this.follow_data && this.follow_data[channel_id],

View file

@ -45,42 +45,20 @@ FFZ.prototype.setup_menu = function() {
this.log("Hooking the Ember Chat Settings view."); this.log("Hooking the Ember Chat Settings view.");
var Settings = utils.ember_resolve('view:settings'), var Settings = utils.ember_resolve('component:chat/chat-settings-menu'),
Layout = utils.ember_lookup('service:layout'), Layout = utils.ember_lookup('service:layout'),
f = this; f = this;
if ( ! Settings ) if ( ! Settings )
return; return;
Settings.reopen({ utils.ember_reopen_view(Settings, {
didInsertElement: function() { ffz_init: function() {
this._super();
try {
this.ffzInit();
} catch(err) {
f.error("ChatSettings didInsertElement: " + err);
}
},
willClearRender: function() {
try {
this.ffzTeardown();
} catch(err) {
f.error("ChatSettings willClearRender: " + err);
}
this._super();
},
ffzInit: function() {
var view = this, var view = this,
el = this.get('element'), el = this.get('element');
menu = el && el.querySelector('.dropmenu');
if ( ! menu ) var container = utils.createElement('div', ''),
return; header = utils.createElement('div', 'list-header', 'FrankerFaceZ'),
var header = utils.createElement('div', 'list-header', 'FrankerFaceZ'),
content = utils.createElement('div', 'chat-menu-content'), content = utils.createElement('div', 'chat-menu-content'),
p, cb, a; p, cb, a;
@ -125,25 +103,35 @@ FFZ.prototype.setup_menu = function() {
content.appendChild(p); content.appendChild(p);
a.addEventListener('click', function(e) { a.addEventListener('click', function(e) {
view.set('controller.settings.hidden', true); view.set('isHidden', true);
f._last_page = 'settings'; f._last_page = 'settings';
f.build_ui_popup(f._chatv); f.build_ui_popup(f._chatv);
e.stopPropagation(); e.stopPropagation();
return false; return false;
}); });
menu.appendChild(header); container.appendChild(header);
menu.appendChild(content); container.appendChild(content);
container.classList.toggle('hidden', this.get('showDisplaySettings'));
el.appendChild(container);
this.ffz_menu = container;
// Maximum Height // Maximum Height
var e = el.querySelector('.chat-settings'); if ( Layout && el )
if ( Layout && e ) el.style.maxHeight = (Layout.get('windowHeight') - 90) + 'px';
e.style.maxHeight = (Layout.get('windowHeight') - 90) + 'px';
}, },
ffzTeardown: function() { ffz_update_visibility: function() {
// Nothing~! if ( this.ffz_menu )
this.ffz_menu.classList.toggle('hidden', this.get('showDisplaySettings'));
}.observes('showDisplaySettings'),
ffz_destroy: function() {
if ( this.ffz_menu ) {
this.ffz_menu.parentElement.removeChild(this.ffz_menu);
this.ffz_menu = null;
}
} }
}); });
@ -648,6 +636,9 @@ FFZ.menu_pages.channel = {
var set = this.emote_sets[extra_sets[i]], var set = this.emote_sets[extra_sets[i]],
name = set ? (set.hasOwnProperty('source_ext') ? "" : "Featured ") + set.title : "Featured Channel"; name = set ? (set.hasOwnProperty('source_ext') ? "" : "Featured ") + set.title : "Featured Channel";
if ( ! set || ! set.count || set.hidden )
continue;
this._emotes_for_sets(inner, view, [extra_sets[i]], name, set.icon || "//cdn.frankerfacez.com/script/devicon.png", set.source || "FrankerFaceZ"); this._emotes_for_sets(inner, view, [extra_sets[i]], name, set.icon || "//cdn.frankerfacez.com/script/devicon.png", set.source || "FrankerFaceZ");
} }

View file

@ -224,7 +224,7 @@ FFZ.menu_pages.myemotes = {
if ( favorites_only && (! favorites_list || ! favorites_list.length) ) if ( favorites_only && (! favorites_list || ! favorites_list.length) )
continue; continue;
if ( ! set || ! set.count || ( ! this.settings.global_emotes_in_menu && this.default_sets.indexOf(set_id) !== -1 ) ) if ( ! set || ! set.count || set.hidden || ( ! this.settings.global_emotes_in_menu && this.default_sets.indexOf(set_id) !== -1 ) )
continue; continue;
var menu = FFZ.menu_pages.myemotes.draw_ffz_set.call(this, view, set, favorites_only); var menu = FFZ.menu_pages.myemotes.draw_ffz_set.call(this, view, set, favorites_only);

View file

@ -36,8 +36,8 @@ FFZ.settings_info.srl_races = {
FFZ.ws_on_close.push(function() { FFZ.ws_on_close.push(function() {
var controller = utils.ember_lookup('controller:channel'), var controller = utils.ember_lookup('controller:channel'),
current_id = controller && controller.get('content.id'), current_id = controller && controller.get('channelModel.id'),
current_host = controller && controller.get('hostModeTarget.id'), current_host = controller && controller.get('channelModel.hostModeTarget.id'),
need_update = false; need_update = false;
if ( ! controller ) if ( ! controller )
@ -56,8 +56,8 @@ FFZ.ws_on_close.push(function() {
FFZ.ws_commands.srl_race = function(data) { FFZ.ws_commands.srl_race = function(data) {
var controller = utils.ember_lookup('controller:channel'), var controller = utils.ember_lookup('controller:channel'),
current_id = controller && controller.get('content.id'), current_id = controller && controller.get('channelModel.id'),
current_host = controller && controller.get('hostModeTarget.id'), current_host = controller && controller.get('channelModel.hostModeTarget.id'),
need_update = false; need_update = false;
this.srl_races = this.srl_races || {}; this.srl_races = this.srl_races || {};
@ -91,13 +91,12 @@ FFZ.ws_commands.srl_race = function(data) {
// --------------- // ---------------
FFZ.prototype.rebuild_race_ui = function() { FFZ.prototype.rebuild_race_ui = function() {
var controller = utils.ember_lookup('controller:channel'),
channel_id = controller && controller.get('content.id'),
hosted_id = controller && controller.get('hostModeTarget.id');
if ( ! this._cindex ) if ( ! this._cindex )
return; return;
var channel_id = this._cindex.get('channel.id'),
hosted_id = this._cindex.get('channel.hostModeTarget.id');
if ( channel_id ) { if ( channel_id ) {
var race = this.srl_races && this.srl_races[channel_id], var race = this.srl_races && this.srl_races[channel_id],

View file

@ -267,7 +267,20 @@ var createElement = function(tag, className, content) {
FrankerFaceZ.get().error("There was an error looking up an Ember instance: " + thing, err); FrankerFaceZ.get().error("There was an error looking up an Ember instance: " + thing, err);
return null; return null;
} }
}; },
ember_resolve = function(thing) {
if ( ! window.App )
return;
if ( App.__deprecatedInstance__ && App.__deprecatedInstance__.registry && App.__deprecatedInstance__.registry.resolve )
return App.__deprecatedInstance__.registry.resolve(thing);
if ( App.__container__ && App.__container__.resolve )
return App.__container__.resolve(thing);
},
CMD_VAR_REGEX = /{(\d+(?:\$(?:\d+)?)?|id|msg_id|message_id|(?:user|room)(?:_id|_name|_display_name)?)}/g;
module.exports = FFZ.utils = { module.exports = FFZ.utils = {
@ -277,15 +290,10 @@ module.exports = FFZ.utils = {
}, },
ember_lookup: ember_lookup, ember_lookup: ember_lookup,
ember_resolve: ember_resolve,
ember_resolve: function(thing) { ember_settings: function() {
if ( ! window.App ) var settings = ember_resolve('model:settings');
return; return settings && settings.findOne();
if ( App.__deprecatedInstance__ && App.__deprecatedInstance__.registry && App.__deprecatedInstance__.registry.resolve )
return App.__deprecatedInstance__.registry.resolve(thing);
if ( App.__container__ && App.__container__.resolve )
return App.__container__.resolve(thing);
}, },
ember_reopen_view: function(component, data) { ember_reopen_view: function(component, data) {
@ -332,6 +340,62 @@ module.exports = FFZ.utils = {
}, },
find_parent: function(el, klass) {
while (el && el.parentNode) {
el = el.parentNode;
if ( el.classList.contains(klass) )
return el;
}
return null;
},
CMD_VAR_REGEX: CMD_VAR_REGEX,
replace_cmd_variables: function(command, user, room, message, args) {
user = user || {};
room = room || {};
message = message || {};
message.tags = message.tags || {};
var msg_id = message.tags.id,
replacements = {
user: user.name,
user_name: user.name,
user_display_name: user.display_name || message.tags['display-name'],
user_id: user._id || message.tags['user-id'],
room: room.id,
room_name: room.id,
room_display_name: room.get && (room.get('tmiRoom.displayName') || room.get('channel.displayName')),
room_id: room.get && room.get('roomProperties._id') || message.tags['room-id'],
id: msg_id,
message_id: msg_id,
msg_id: msg_id
};
CMD_VAR_REGEX.lastIndex = 0;
return command.replace(CMD_VAR_REGEX, function(match, variable) {
if ( replacements[variable] )
return replacements[variable];
if ( args ) {
var match = /(\d+)(?:(\$)(\d+)?)?/.exec(variable);
if ( match ) {
var num = parseInt(match[1]),
second_num = match[3] ? parseInt(match[3]) : undefined;
return match[2] === '$' ? args.slice(num, second_num).join(" ") : args[num];
}
}
return '{' + variable + '}';
});
},
show_modal: show_modal, show_modal: show_modal,
confirm: function(title, description, callback) { confirm: function(title, description, callback) {
var contents = createElement('div', 'text-content'), var contents = createElement('div', 'text-content'),

138
style.css
View file

@ -18,6 +18,10 @@ body > div.tipsy .tipsy-arrow { opacity: 0.8; }
cursor: pointer; cursor: pointer;
} }
.ffz-minimal-channel-title .cn-metabar__title:not(:hover) .card__info,
.ffz-minimal-channel-bar .cn-bar-spacer,
.ffz-channel-bar-bottom .cn-bar-spacer,
.ffz-channel-bar-bottom .cn-cover-wrap,
.app-main .ad_leader:empty, .app-main .ad_leader:empty,
body:not(.ffz-show-bits-tags) .bits-tag--container, body:not(.ffz-show-bits-tags) .bits-tag--container,
.ffz-hide-friends nav .friend-list, .ffz-hide-friends nav .friend-list,
@ -28,12 +32,37 @@ body:not(.ffz-show-bits-tags) .bits-tag--container,
.ffz-hide-promoted-games .promotedGames, .ffz-hide-promoted-games .promotedGames,
.ffz-hide-recent-past-broadcast .recent-past-broadcast, .ffz-hide-recent-past-broadcast .recent-past-broadcast,
.ffz-hide-view-count .stat.twitch-channel-views, .ffz-hide-view-count .stat.twitch-channel-views,
.ffz-hide-view-count .cn-metabar__viewcount,
.ffz-minimal-chat-input .chat-interface .emoticon-selector-toggle, .ffz-minimal-chat-input .chat-interface .emoticon-selector-toggle,
.ffz-menu-replace .chat-interface .ember-emoticon-selector, .ffz-menu-replace .chat-interface .ember-emoticon-selector,
.ffz-menu-replace .chat-interface .emoticon-selector-toggle { .ffz-menu-replace .chat-interface .emoticon-selector-toggle {
display: none !important; display: none !important;
} }
.ffz-channel-bar-bottom #channel {
margin-bottom: 60px;
}
.ffz-channel-bar-bottom .notification-controls .balloon {
top: auto;
bottom: 50px;
}
.ffz-channel-bar-bottom .js-username-hover-tip .balloon {
top: auto;
bottom: 100px;
}
.ffz-channel-bar-bottom .cn-bar {
top: auto;
bottom: 0;
box-shadow: inset 0 1px 0 #e5e3e8,0 -1px -1px rgba(0,0,0,.065);
}
.ffz-dark.ffz-channel-bar-bottom .cn-bar {
box-shadow: inset 0 1px 0 #161616, -1px -1px rgba(255,255,255,0.065);
}
body:not(.ffz-show-bits-tags) .ember-chat .chat-messages.bits-tags__offset { body:not(.ffz-show-bits-tags) .ember-chat .chat-messages.bits-tags__offset {
top: 0; top: 0;
} }
@ -223,31 +252,18 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
/* Minimal Channel Title */ /* Minimal Channel Title */
.ffz-minimal-channel-title #broadcast-meta { .ffz-minimal-channel-title .cn-metabar__title:hover {
height: 25px; background-color: #fff;
margin: 20px 0; padding: 1rem;
margin-left: -1rem;
} }
.ffz-dark.ffz-minimal-channel-title .cn-metabar__title:hover { background-color: #101010 }
.ffz-minimal-channel-title #channel #broadcast-meta .profile-link .profile-photo img { .ffz-minimal-channel-title .cn-metabar__title:not(:hover) .card__img--boxart {
width: 25px; height: 25px; height: 20px !important;
width: 14px !important;
} }
.ffz-minimal-channel-title #channel #broadcast-meta .info {
padding-top: 25px;
padding-left: 50px;
}
.ffz-minimal-channel-title #channel #broadcast-meta .info .title {
padding-top: 0;
margin: 0;
left: calc(25px + 1.5rem);
height: 25px;
min-height: 25px;
}
.ffz-minimal-channel-title #broadcast-meta .profile-photo .goto,
.ffz-minimal-channel-title #broadcast-meta .channel { display: none }
/* Theater Mode hover bar */ /* Theater Mode hover bar */
@ -1404,6 +1420,7 @@ img.channel_background[src="null"] { display: none; }
background-color: #232329; background-color: #232329;
} }
.ffz-no-blue .cn-hosting,
.ffz-no-blue .dark .ember-chat .moderation-card .interface, .ffz-no-blue .dark .ember-chat .moderation-card .interface,
.ffz-no-blue .force-dark .ember-chat .moderation-card .interface, .ffz-no-blue .force-dark .ember-chat .moderation-card .interface,
.ffz-no-blue .theatre .ember-chat .moderation-card .interface { .ffz-no-blue .theatre .ember-chat .moderation-card .interface {
@ -1428,6 +1445,11 @@ img.channel_background[src="null"] { display: none; }
font-weight: bold; font-weight: bold;
margin-top: -1px !important; margin-top: -1px !important;
color: #888 !important; color: #888 !important;
background: none !important;
}
.mod-icons .mod-icon img {
pointer-events: none;
} }
@ -1550,6 +1572,8 @@ body:not(.ffz-bttv) .chat-container:not(.chatReplay) .more-messages-indicator {
/* Menu About Page */ /* Menu About Page */
#ffz-chat-menu .center { text-align: center }
.ffz-about-table { .ffz-about-table {
width: 100%; width: 100%;
} }
@ -2039,8 +2063,11 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar
line-height: 30px; line-height: 30px;
margin-left: -10px; margin-left: -10px;
margin-right: 15px; margin-right: 15px;
cursor: default;
} }
.ffz.room-state.faded { opacity: 0.5 }
.ffz.room-state.truncated span { font-size: 8px; } .ffz.room-state.truncated span { font-size: 8px; }
.button.primary.ffz-waiting:not(:hover) { .button.primary.ffz-waiting:not(:hover) {
@ -2082,6 +2109,14 @@ body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-inter
left: 20px; left: 20px;
} }
.ffz-sidebar-swap #main_col .cn-bar-fixed {
right: 50px;
}
.ffz-sidebar-swap #left_col.open ~ #main_col .cn-bar-fixed {
right: 240px;
}
.ffz-sidebar-swap #left_col { .ffz-sidebar-swap #left_col {
left: auto; left: auto;
right: 0; right: 0;
@ -3235,3 +3270,64 @@ body.ffz-bttv #ffz-feed-tabs .tabs { margin-bottom: 0 }
color: #ccc; color: #ccc;
border-color: rgba(255,255,255,0.2); border-color: rgba(255,255,255,0.2);
} }
.ffz-mod-balloon {
display: block;
max-width: 200px;
}
.ffz-mod-balloon .ffz-title {
padding: 0 1rem;
}
.cn-metabar__ffz svg { fill: #898395; }
.cn-metabar__ffz {
cursor: default;
color: #706a7c;
font-size: 1.4rem;
}
.cn-metabar__ffz figure {
margin-right: 5px;
}
.ffz-channel-title-top .cn-metabar__more { width: 100% }
.ffz-channel-title-top #channel {
position: relative;
padding-top: 80px;
}
.ffz-minimal-channel-title.ffz-channel-title-top #channel {
padding-top: 50px;
}
.ffz-channel-title-top .cn-metabar__title {
position: absolute;
top: 5px;
}
.ffz-channel-title-top:not(.ffz-channel-bar-bottom) #channel.ffz-bar-fixed .cn-metabar__title {
margin-top: 50px;
}
.ffz-channel-title-top .cn-metabar__more > div { order: 999; }
.ffz-channel-title-top .cn-metabar__more > span:last-of-type { flex-grow: 1 }
.ffz-channel-title-top .cn-metabar__more > span:last-of-type > div { float: left }
.ffz-channel-title-top .cn-metabar__more > div:last-of-type { margin-right: 0 !important }
.ffz-channel-title-top .cn-metabar__more > div + button:first-of-type,
.ffz-channel-title-top .cn-metabar__more > div + span:first-of-type button.mg-l-1 { margin-left: 0 !important }
.ffz-channel-title-top .cn-metabar__viewcount { flex-grow: 0 !important; }
.ffz-minimal-channel-bar .cn-bar-fixed {
top: -40px;
transition: top ease-in-out 75ms, bottom ease-in-out 75ms;
}
.ffz-minimal-channel-bar .cn-bar-fixed:hover { top: 0 }
.ffz-minimal-channel-bar.ffz-channel-bar-bottom .cn-bar-fixed { top: auto; bottom: -40px }
.ffz-minimal-channel-bar.ffz-channel-bar-bottom .cn-bar-fixed:hover { bottom: 0 }
.ffz-minimal-channel-title .cn-metabar,
.ffz-channel-title-top .cn-metabar {
min-height: 70px;
}