diff --git a/dark.css b/dark.css index 891cc5b6..dd858c3b 100644 --- a/dark.css +++ b/dark.css @@ -87,6 +87,11 @@ /* stats */ +.ffz-dark .stats-and-actions, +.ffz-dark #main_col .content #stats_and_actions { + border-bottom-color: rgba(255,255,255,0.2); +} + .ffz-dark #channel .player-column .stats-and-actions .channel-stats .stat svg:not(.svg-glyph_live) path{ fill:rgba(255,255,255,0.35)!important; } @@ -100,7 +105,7 @@ /* main column */ .ffz-dark div#main_col { - background:rgb(16,16,16)!important; + background:rgb(16,16,16); color:rgb(195,195,195)!important; border-right:1px solid rgb(0,0,0)!important; } @@ -119,7 +124,7 @@ .ffz-dark div.title > span.real, .ffz-dark div.title > span.over{ color:rgb(222,222,222)!important; - background-color:rgb(16,16,16)!important; + background-color:rgba(16,16,16,0.3)!important; } .ffz-dark div.title > span.over:hover, @@ -165,6 +170,18 @@ border-right-color: #32323e; } +.ffz-dark .notification-controls .toggle-notification-menu { + background-color: #19191f; +} + +.ffz-dark .notification-controls .toggle-notification-menu:before { + border-top-color: #8c8c9c; +} + +.ffz-dark .notification-controls .toggle-notification-menu:hover { + background-color: #101014; +} + .ffz-dark .change-banner .banner-preview, .ffz-dark form.js-new_panel_form input, .ffz-dark form.js-new_panel_form textarea, @@ -206,6 +223,7 @@ color: #a68ed2; } +.ffz-dark .follow-button a, .ffz-dark a.dropmenu_action:hover { color: #fff !important; } @@ -248,16 +266,67 @@ } /* Upsell banner */ +.ffz-dark .bookmark-meta, .ffz-dark .upsell-banner { - background-color: rgb(25,25,31); + background-color: rgba(25,25,31, 0.7); } +.ffz-dark .bookmark-meta .bookmark-title, .ffz-dark .upsell-banner .message .title { color: #ccc; } +.ffz-dark .bookmark-meta { + box-shadow: none; +} + +/* VoD Description */ + +.ffz-dark .archives-contain .videos { + padding: 0 0; +} + +.ffz-dark #right_col a.bottom, +.ffz-dark .archives-contain .list-video { + padding: 10px 20px; + border-color: rgba(255,255,255,0.2); +} + +.ffz-dark #right_col a.bottom { + padding: 0 20px; + margin: 0 !important; +} + +.ffz-dark .archives-contain .list-video .meta .title { + color: rgb(195,195,195); +} + +.ffz-dark #right_col #archives { background-color: transparent; } + +.ffz-dark .archives-contain .list-video .meta .info p.broadcaster, +.ffz-dark #right_col #archives .more_archives { + color: #a68ed2; + box-shadow: none; +} + +.ffz-dark #right_col a.bottom:hover, +.ffz-dark .archives-contain .list-video:hover, +.ffz-dark #right_col #archives .more_archives:hover { + background-color: rgba(255,255,255,0.1); +} + +.ffz-dark #main_col .content .archive_info p, +.ffz-dark #main_col .content .archive_info time { + color: #888; +} + + /* Video Manager */ +.ffz-dark ul.tabs:before, +.ffz-dark .directory_header .nav:before, +.ffz-dark ul.tabs_fake:before, +.ffz-dark #right_col #archives .more_archives, .ffz-dark .manager .videos-grid .video .meta .actions { border-top-color: rgba(255,255,255,0.25); } @@ -704,4 +773,16 @@ .ffz-dark .dash-chat-column { border-color: rgba(255,255,255,0.2); -} \ No newline at end of file +} + + +/* TEST +.ffz-dark div#main_col .tse-scroll-content { + background-image: + linear-gradient(rgba(16,16,16,0.75), rgba(16,16,16,1)), + url("http://i.imgur.com/DzCLo3d.jpg"); + + background-attachment: fixed; + background-size: 100% auto; + background-repeat: no-repeat; +}*/ \ No newline at end of file diff --git a/script.js b/script.js index 56771da2..eabb19d5 100644 --- a/script.js +++ b/script.js @@ -8,13 +8,13 @@ var FFZ = window.FrankerFaceZ, // Settings // -------------------- -FFZ.settings_info.bot_badges = { +FFZ.settings_info.show_badges = { type: "boolean", value: true, category: "Chat", - name: "Bot Badges", - help: "Give special badges to known bots." + name: "Additional Badges", + help: "Show additional badges for bots, FrankerFaceZ donors, and other special users." }; @@ -75,6 +75,9 @@ var badge_css = function(badge) { // -------------------- FFZ.prototype.bttv_badges = function(data) { + if ( ! this.settings.show_badges ) + return; + var user_id = data.sender, user = this.users[user_id], badges_out = [], @@ -155,10 +158,13 @@ FFZ.prototype.bttv_badges = function(data) { } -FFZ.prototype.render_badge = function(view) { - var user = view.get('context.model.from'), - room_id = view.get('context.parentController.content.id'), - badges = view.$('.badges'); +FFZ.prototype.render_badge = function(component) { + if ( ! this.settings.show_badges ) + return; + + var user = component.get('msgObject.from'), + room_id = App.__container__.lookup('controller:chat').get('currentRoom.id'), + badges = component.$('.badges'); var data = this.users[user]; if ( ! data || ! data.badges ) @@ -234,17 +240,17 @@ FFZ.bttv_known_bots = ["nightbot","moobot","sourbot","xanbot","manabot","mtgbot" FFZ.prototype._legacy_add_donors = function() { // Developer Badge - this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "//cdn.frankerfacez.com/channel/global/devicon.png"}; + this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "//cdn.frankerfacez.com/script/devicon.png"}; utils.update_css(this._badge_style, 0, badge_css(this.badges[0])); // Donor Badge - this.badges[1] = {id: 1, title: "FFZ Donor", color: "#755000", image: "//cdn.frankerfacez.com/channel/global/donoricon.png"}; + this.badges[1] = {id: 1, title: "FFZ Donor", color: "#755000", image: "//cdn.frankerfacez.com/script/donoricon.png"}; utils.update_css(this._badge_style, 1, badge_css(this.badges[1])); // Bot Badge - this.badges[2] = {id: 2, title: "Bot", color: "#595959", image: "//cdn.frankerfacez.com/channel/global/boticon.png", + this.badges[2] = {id: 2, title: "Bot", color: "#595959", image: "//cdn.frankerfacez.com/script/boticon.png", replaces: 'moderator', - visible: function(r,user) { return this.settings.bot_badges && !(this.has_bttv && FFZ.bttv_known_bots.indexOf(user)!==-1); }}; + visible: function(r,user) { return !(this.has_bttv && FFZ.bttv_known_bots.indexOf(user)!==-1); }}; utils.update_css(this._badge_style, 2, badge_css(this.badges[2])); // Load BTTV Bots @@ -258,10 +264,8 @@ FFZ.prototype._legacy_add_donors = function() { } // Special Badges - this.users.sirstendec = {badges: {1: {id:0}}}; - this.users.zenwan = {badges: {0: {id:2, image: "//cdn.frankerfacez.com/channel/global/momiglee_badge.png", title: "WAN"}}}; - - this.load_set(".donor"); + this.users.sirstendec = {badges: {1: {id:0}}, sets: [4330]}; + this.users.zenwan = {badges: {0: {id:2, image: "//cdn.frankerfacez.com/script/momiglee_badge.png", title: "WAN"}}}; this._legacy_load_bots(); this._legacy_load_donors(); @@ -301,7 +305,7 @@ FFZ.prototype._legacy_load_donors = function(tries) { FFZ.prototype._legacy_parse_badges = function(data, slot, badge_id) { var title = this.badges[badge_id].title, count = 0; - ds = badge_id == 1 ? ".donor" : ""; + ds = null; if ( data != null ) { var lines = data.trim().split(/\W+/); @@ -311,7 +315,7 @@ FFZ.prototype._legacy_parse_badges = function(data, slot, badge_id) { badges = user.badges = user.badges || {}, sets = user.sets = user.sets || []; - if ( sets.indexOf(ds) === -1 ) + if ( ds !== null && sets.indexOf(ds) === -1 ) sets.push(ds); if ( badges[slot] ) @@ -324,7 +328,7 @@ FFZ.prototype._legacy_parse_badges = function(data, slot, badge_id) { this.log('Added "' + title + '" badge to ' + utils.number_commas(count) + " users."); } -},{"./constants":3,"./utils":28}],2:[function(require,module,exports){ +},{"./constants":3,"./utils":29}],2:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -405,15 +409,22 @@ var SVGPATH = '' + SVGPATH + '', + ZREKNARF: '' + SVGPATH + '', CHAT_BUTTON: '' + SVGPATH + '', - CLOCK: '', + ROOMS: '', + CAMERA: '', + INVITE: '', + + EYE: '', + CLOCK: '', GEAR: '', HEART: '', - EMOTE: '' + EMOTE: '', + STAR: '' } },{}],4:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -466,35 +477,277 @@ var FFZ = window.FrankerFaceZ, // -------------------- FFZ.prototype.setup_channel = function() { - this.channels = {}; + // Settings stuff! + document.body.classList.toggle("ffz-hide-view-count", !this.settings.channel_views); this.log("Creating channel style element."); var s = this._channel_style = document.createElement('style'); s.id = "ffz-channel-css"; document.head.appendChild(s); - this.log("Hooking the Ember Channel view."); - - var Channel = App.__container__.lookup('controller:channel'), + this.log("Hooking the Ember Channel Index view."); + var Channel = App.__container__.resolve('view:channel/index'), f = this; if ( ! Channel ) return; + this._modify_cindex(Channel); + + // The Stupid View Fix. Is this necessary still? + try { + Channel.create().destroy(); + } catch(err) { } + + // Update Existing + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Channel) ) + continue; + + this.log("Manually updating Channel Index view.", view); + this._modify_cindex(view); + view.ffzInit(); + }; + + + this.log("Hooking the Ember Channel controller."); + + Channel = App.__container__.lookup('controller:channel'); + if ( ! Channel ) + return; + Channel.reopen({ ffzUpdateUptime: function() { - f.update_uptime(); - }.observes("isLive", "content.id").on("init") + if ( f._cindex ) + f._cindex.ffzUpdateUptime(); - /*ffzUpdateInfo: function() { - f.log("Updated! ID: " + this.get("content.id")); - f.update_stream_info(true); - }.observes("content.id").on("init")*/ + }.observes("isLive", "content.id"), + + ffzUpdateTitle: function() { + var name = this.get('content.name'), + display_name = this.get('content.display_name'); + + if ( display_name ) + FFZ.capitalization[name] = [display_name, Date.now()]; + + if ( f._cindex ) + f._cindex.ffzFixTitle(); + }.observes("content.status", "content.id") + + /*ffzHostTarget: function() { + var target = this.get('content.hostModeTarget'), + name = target && target.get('name'), + display_name = target && target.get('display_name'); + + if ( display_name ) + FFZ.capitalization[name] = [display_name, Date.now()]; + + if ( f.settings.group_tabs && f._chatv ) + f._chatv.ffzRebuildTabs(); + }.observes("content.hostModeTarget")*/ }); +} - // Do uptime the first time. - this.update_uptime(); - //this.update_stream_info(true); + +FFZ.prototype._modify_cindex = function(view) { + var f = this; + + view.reopen({ + didInsertElement: function() { + this._super(); + try { + this.ffzInit(); + } catch(err) { + f.error("CIndex didInsertElement: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("CIndex willClearRender: " + err); + } + return this._super(); + }, + + ffzInit: function() { + f._cindex = this; + this.get('element').setAttribute('data-channel', this.get('controller.id')); + this.ffzFixTitle(); + this.ffzUpdateUptime(); + this.ffzUpdateChatters(); + + var el = this.get('element').querySelector('.svg-glyph_views:not(.ffz-svg)') + if ( el ) + el.parentNode.classList.add('twitch-channel-views'); + }, + + ffzFixTitle: function() { + if ( f.has_bttv || ! f.settings.stream_title ) + return; + + var status = this.get("controller.status"), + channel = this.get("controller.id"); + + status = f.render_tokens(f.tokenize_line(channel, channel, status, true)); + + this.$(".title span").each(function(i, el) { + var scripts = el.querySelectorAll("script"); + el.innerHTML = scripts[0].outerHTML + status + scripts[1].outerHTML; + }); + }, + + + ffzUpdateChatters: function() { + // Get the counts. + var room_id = this.get('controller.id'), + room = f.rooms && f.rooms[room_id]; + + if ( ! room || ! f.settings.chatter_count ) { + var el = this.get('element').querySelector('#ffz-chatter-display'); + el && el.parentElement.removeChild(el); + el = this.get('element').querySelector('#ffz-ffzchatter-display'); + el && el.parentElement.removeChild(el); + return; + } + + var chatter_count = Object.keys(room.room.get('ffz_chatters') || {}).length, + ffz_chatters = room.ffz_chatters || 0; + + var el = this.get('element').querySelector('#ffz-chatter-display span'); + if ( ! el ) { + var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-chatter-display'; + stat.title = "Current Chatters"; + + stat.innerHTML = constants.ROOMS + " "; + el = document.createElement("span"); + stat.appendChild(el); + + var other = cont.querySelector("#ffz-ffzchatter-display"); + if ( other ) + cont.insertBefore(stat, other); + else + cont.appendChild(stat); + + jQuery(stat).tipsy(); + } + + el.innerHTML = utils.number_commas(chatter_count); + + if ( ! ffz_chatters ) { + el = this.get('element').querySelector('#ffz-ffzchatter-display'); + el && el.parentNode.removeChild(el); + return; + } + + el = this.get('element').querySelector('#ffz-ffzchatter-display span'); + if ( ! el ) { + var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-ffzchatter-display'; + stat.title = "Chatters with FrankerFaceZ"; + + stat.innerHTML = constants.ZREKNARF + " "; + el = document.createElement("span"); + stat.appendChild(el); + + var other = cont.querySelector("#ffz-chatter-display"); + if ( other ) + cont.insertBefore(stat, other.nextSibling); + else + cont.appendChild(stat); + + jQuery(stat).tipsy(); + } + + el.innerHTML = utils.number_commas(ffz_chatters); + }, + + + ffzUpdateUptime: function() { + if ( this._ffz_update_uptime ) { + clearTimeout(this._ffz_update_uptime); + delete this._ffz_update_uptime; + } + + if ( ! f.settings.stream_uptime || ! this.get("controller.isLiveAccordingToKraken") ) { + var el = this.get('element').querySelector('#ffz-uptime-display'); + if ( el ) + el.parentElement.removeChild(el); + return; + } + + // Schedule an update. + this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000); + + // Determine when the channel last went live. + var online = this.get("controller.content.stream.created_at"); + if ( ! online ) + return; + + online = utils.parse_date(online); + if ( ! online ) + return; + + var uptime = Math.floor((Date.now() - online.getTime()) / 1000); + if ( uptime < 0 ) + return; + + var el = this.get('element').querySelector('#ffz-uptime-display span'); + if ( ! el ) { + var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-uptime-display'; + stat.title = "Stream Uptime (since " + online.toLocaleString() + ")"; + + stat.innerHTML = constants.CLOCK + " "; + el = document.createElement("span"); + stat.appendChild(el); + + var viewers = cont.querySelector(".live-count"); + if ( viewers ) + cont.insertBefore(stat, viewers.nextSibling); + else { + try { + viewers = cont.querySelector("script:nth-child(0n+2)"); + cont.insertBefore(stat, viewers.nextSibling); + } catch(err) { + cont.insertBefore(stat, cont.childNodes[0]); + } + } + + jQuery(stat).tipsy({html: true}); + } + + el.innerHTML = utils.time_to_string(uptime); + }, + + ffzTeardown: function() { + this.get('element').setAttribute('data-channel', ''); + f._cindex = undefined; + if ( this._ffz_update_uptime ) + clearTimeout(this._ffz_update_uptime); + } + }); } @@ -502,6 +755,42 @@ FFZ.prototype.setup_channel = function() { // Settings // --------------- +FFZ.settings_info.chatter_count = { + type: "boolean", + value: false, + + category: "Channel Metadata", + + name: "Chatter Count", + help: "Display the current number of users connected to chat beneath the channel.", + + on_update: function(val) { + if ( this._cindex ) + this._cindex.ffzUpdateChatters(); + + if ( ! val || ! this.rooms ) + return; + + // Refresh the data. + for(var room_id in this.rooms) + this.rooms.hasOwnProperty(room_id) && this.rooms[room_id].room && this.rooms[room_id].room.ffzInitChatterCount(); + } + }; + + +FFZ.settings_info.channel_views = { + type: "boolean", + value: true, + + category: "Channel Metadata", + name: "Channel Views", + help: 'Display the number of times the channel has been viewed beneath the stream.', + on_update: function(val) { + document.body.classList.toggle("ffz-hide-view-count", !val); + } + }; + + FFZ.settings_info.stream_uptime = { type: "boolean", value: false, @@ -510,125 +799,78 @@ FFZ.settings_info.stream_uptime = { name: "Stream Uptime", help: 'Display the stream uptime under a channel by the viewer count.', on_update: function(val) { - this.update_uptime(); + if ( this._cindex ) + this._cindex.ffzUpdateUptime(); } }; +FFZ.settings_info.stream_title = { + type: "boolean", + value: true, + no_bttv: true, + + category: "Channel Metadata", + name: "Title Links", + help: "Make links in stream titles clickable.", + on_update: function(val) { + if ( this._cindex ) + this._cindex.ffzFixTitle(); + } + }; +},{"../constants":3,"../utils":29}],6:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + constants = require('../constants'), + + format_unread = function(count) { + if ( count < 1 ) + return ""; + + else if ( count >= 99 ) + return "99+"; + + return "" + count; + }; + + // -------------------- -// Stream Data Update +// Settings // -------------------- -/*FFZ.prototype.update_stream_info = function(just_schedule) { - if ( this._stream_info_update ) { - clearTimeout(this._stream_info_update); - delete this._stream_info_update; - } +FFZ.settings_info.group_tabs = { + type: "boolean", + value: false, - this._stream_info_update = setTimeout(this.update_stream_info.bind(this), 90000); + no_bttv: true, - if ( just_schedule ) - return; + category: "Chat", + name: "Chat Room Tabs Beta", + help: "Enhanced UI for switching the current chat room and noticing new messages.", - var Channel = App.__container__.lookup('controller:channel'), - channel_id = Channel ? Channel.get('content.id') : undefined, - f = this; - if ( ! channel_id ) - return; - - Twitch.api.get("streams/" + channel_id, {}, {version: 3}) - .done(function(data) { - var channel_id = Channel.get('content.id'), d = data.stream; - if ( ! data.stream || d.channel.name != channel_id ) + on_update: function(val) { + var enabled = !this.has_bttv && val; + if ( ! this._chatv || enabled === this._group_tabs_state ) return; - // Override the data in Twitch. We can't just .load() the stream - // because that resets the whole channel layout, resetting the - // video player. Twitch pls fix - var old_created = Channel.get('content.stream.created_at'); - - Channel.set('content.stream.created_at', d.created_at); - Channel.set('content.stream.average_fps', d.average_fps); - Channel.set('content.stream.viewers', d.viewers); - Channel.set('content.stream.video_height', d.video_height); - Channel.set('content.stream.csGoSkill', Twitch.uri.csGoSkillImg(("0" + d.skill).slice(-2))); - - Channel.set('content.stream.game', d.game); - Channel.set('content.stream.gameUrl', Twitch.uri.game(d.game)); - Channel.set('content.stream.gameBoxart', Twitch.uri.gameBoxArtJpg(d.game)); - - - // Update the uptime display. - if ( f.settings.stream_uptime && old_created != d.created_at ) - f.update_uptime(true) && f.update_uptime(); - }); -}*/ - - -// -------------------- -// Uptime Display -// -------------------- - -FFZ.prototype.update_uptime = function(destroy) { - if ( this._uptime_update ) { - clearTimeout(this._uptime_update); - delete this._uptime_update; - } - - var Channel = App.__container__.lookup('controller:channel'); - if ( destroy || ! this.settings.stream_uptime || ! Channel || ! Channel.get('isLiveAccordingToKraken') ) { - var el = document.querySelector("#ffz-uptime-display"); - if ( el ) - el.parentElement.removeChild(el); - return; - } - - // Schedule an update. - this._update_uptime = setTimeout(this.update_uptime.bind(this), 1000); - - // Determine when the channel last went live. - var online = Channel.get('content.stream.created_at'); - if ( ! online ) return; - - online = utils.parse_date(online); - if ( ! online ) return; - - var uptime = Math.floor((Date.now() - online.getTime()) / 1000); - if ( uptime < 0 ) return; - - var el = document.querySelector("#ffz-uptime-display span"); - if ( ! el ) { - var cont = document.querySelector("#channel .stats-and-actions .channel-stats"); - if ( ! cont ) return; - - var stat = document.createElement("span"); - stat.className = "ffz stat"; - stat.id = "ffz-uptime-display"; - stat.title = "Stream Uptime (since " + online.toLocaleString() + ")"; - - stat.innerHTML = constants.CLOCK + " "; - el = document.createElement("span"); - stat.appendChild(el); - - var viewers = cont.querySelector(".live-count"); - if ( viewers ) - cont.insertBefore(stat, viewers.nextSibling); - else { - try { - viewers = cont.querySelector("script:nth-child(0n+2)"); - cont.insertBefore(stat, viewers.nextSibling); - } catch(err) { - cont.insertBefore(stat, cont.childNodes[0]); - } + if ( enabled ) + this._chatv.ffzEnableTabs(); + else + this._chatv.ffzDisableTabs(); } + }; - jQuery(stat).tipsy({html:true}); - } - el.innerHTML = utils.time_to_string(uptime); -} -},{"../constants":3,"../utils":28}],6:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ; +FFZ.settings_info.pinned_rooms = { + type: "button", + value: [], + + category: "Chat", + visible: false, + + name: "Pinned Chat Rooms", + help: "Set a list of channels that should always be available in chat." + }; // -------------------- @@ -636,6 +878,21 @@ var FFZ = window.FrankerFaceZ; // -------------------- FFZ.prototype.setup_chatview = function() { + this.log("Hooking the Ember Chat controller."); + + var Chat = App.__container__.lookup('controller:chat'), + f = this; + + if ( Chat ) { + Chat.reopen({ + ffzUpdateChannels: function() { + if ( f.settings.group_tabs && f._chatv ) + f._chatv.ffzRebuildTabs(); + }.observes("currentChannelRoom", "connectedPrivateGroupRooms") + }); + } + + this.log("Hooking the Ember Chat view."); var Chat = App.__container__.resolve('view:chat'); @@ -647,7 +904,6 @@ FFZ.prototype.setup_chatview = function() { Chat.create().destroy(); } catch(err) { } - // Modify all existing Chat views. for(var key in Ember.View.views) { if ( ! Ember.View.views.hasOwnProperty(key) ) @@ -657,13 +913,45 @@ FFZ.prototype.setup_chatview = function() { if ( !(view instanceof Chat) ) continue; - this.log("Adding UI link manually to Chat view.", view); + this.log("Manually updating existing Chat view.", view); try { - view.$('.textarea-contain').append(this.build_ui_link(view)); + view.ffzInit(); } catch(err) { this.error("setup: build_ui_link: " + err); } } + + + this.log("Hooking the Ember Layout controller."); + var Layout = App.__container__.lookup('controller:layout'); + if ( ! Layout ) + return; + + Layout.reopen({ + ffzFixTabs: function() { + if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { + setTimeout(function() { + f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + },0); + } + }.observes("isRightColumnClosed") + }); + + + this.log("Hooking the Ember 'Right Column' controller. Seriously..."); + var Column = App.__container__.lookup('controller:right-column'); + if ( ! Column ) + return; + + Column.reopen({ + ffzFixTabs: function() { + if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { + setTimeout(function() { + f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + },0); + } + }.observes("firstTabSelected") + }); } @@ -677,39 +965,398 @@ FFZ.prototype._modify_cview = function(view) { view.reopen({ didInsertElement: function() { this._super(); + try { - this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + this.ffzInit(); } catch(err) { - f.error("didInsertElement: build_ui_link: " + err); + f.error("ChatView didInsertElement: " + err); } }, willClearRender: function() { - this._super(); try { - this.$(".ffz-ui-toggle").remove(); + this.ffzTeardown(); } catch(err) { - f.error("willClearRender: remove ui link: " + err); + f.error("ChatView willClearRender: " + err); } + this._super(); }, - ffzUpdateLink: Ember.observer('controller.currentRoom', function() { + ffzInit: function() { + f._chatv = this; + this.$('.textarea-contain').append(f.build_ui_link(this)); + + if ( !f.has_bttv && f.settings.group_tabs ) + this.ffzEnableTabs(); + + setTimeout(function() { + if ( f.settings.group_tabs && f._chatv._ffz_tabs ) + f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + + var controller = f._chatv.get('controller'); + controller && controller.set('showList', false); + }, 1000); + }, + + ffzTeardown: function() { + if ( f._chatv === this ) + f._chatv = null; + + this.$('.textarea-contain .ffz-ui-toggle').remove(); + + if ( f.settings.group_tabs ) + this.ffzDisableTabs(); + }, + + ffzChangeRoom: Ember.observer('controller.currentRoom', function() { try { f.update_ui_link(); + + if ( !f.has_bttv && f.settings.group_tabs && this._ffz_tabs ) { + var room = this.get('controller.currentRoom'); + room && room.resetUnreadCount(); + + var tabs = jQuery(this._ffz_tabs); + tabs.children('.ffz-chat-tab').removeClass('active'); + if ( room ) + tabs.children('.ffz-chat-tab[data-room="' + room.get('id') + '"]').removeClass('tab-mentioned').addClass('active').children('span').text(''); + + // Invite Link + var can_invite = room && room.get('canInvite'); + this._ffz_invite && this._ffz_invite.classList.toggle('hidden', !can_invite); + this.set('controller.showInviteUser', can_invite && this.get('controller.showInviteUser')) + + // Now, adjust the chat-room. + this.$('.chat-room').css('top', this._ffz_tabs.offsetHeight + "px"); + } + } catch(err) { - f.error("ffzUpdateLink: update_ui_link: " + err); + f.error("ChatView ffzUpdateLink: " + err); } - }) + }), + + // Group Tabs~! + + ffzEnableTabs: function() { + if ( f.has_bttv || ! f.settings.group_tabs ) + return; + + // Hide the existing chat UI. + this.$(".chat-header").addClass("hidden"); + + // Create our own UI. + var tabs = this._ffz_tabs = document.createElement("div"); + tabs.id = "ffz-group-tabs"; + this.$(".chat-header").after(tabs); + + // List the Rooms + this.ffzRebuildTabs(); + }, + + ffzRebuildTabs: function() { + if ( f.has_bttv || ! f.settings.group_tabs ) + return; + + var tabs = this._ffz_tabs || this.get('element').querySelector('#ffz-group-tabs'); + if ( ! tabs ) + return; + + tabs.innerHTML = ""; + + var link = document.createElement('a'), + view = this; + + link.className = 'button glyph-only tooltip'; + link.title = "Chat Room Management"; + link.innerHTML = constants.ROOMS; + + link.addEventListener('click', function() { + var controller = view.get('controller'); + controller && controller.set('showList', !controller.get('showList')); + }); + + tabs.appendChild(link); + + + link = document.createElement('a'), + link.className = 'button glyph-only tooltip invite'; + link.title = "Invite a User"; + link.innerHTML = constants.INVITE; + + link.addEventListener('click', function() { + var controller = view.get('controller'); + controller && controller.set('showInviteUser', controller.get('currentRoom.canInvite') && !controller.get('showInviteUser')); + }); + + link.classList.toggle('hidden', !this.get("controller.currentRoom.canInvite")); + view._ffz_invite = link; + tabs.appendChild(link); + + var room = this.get('controller.currentChannelRoom'), tab; + if ( room ) { + tab = this.ffzBuildTab(view, room, true); + tab && tabs.appendChild(tab); + } + + // Check Host Target + var Channel = App.__container__.lookup('controller:channel'), + Room = App.__container__.resolve('model:room'); + target = Channel && Channel.get('hostModeTarget'); + + if ( target && Room ) { + var target_id = target.get('id'); + if ( this._ffz_host !== target_id ) { + if ( this._ffz_host_room ) { + if ( this.get('controller.currentRoom') === this._ffz_host_room ) + this.get('controller').blurRoom(); + this._ffz_host_room.destroy(); + } + + this._ffz_host = target_id; + this._ffz_host_room = Room.findOne(target_id); + } + } else if ( this._ffz_host ) { + if ( this._ffz_host_room ) { + if ( this.get('controller.currentRoom') === this._ffz_host_room ) + this.get('controller').blurRoom(); + this._ffz_host_room.destroy(); + } + + delete this._ffz_host; + delete this._ffz_host_room; + } + + if ( this._ffz_host_room ) { + tab = view.ffzBuildTab(view, this._ffz_host_room, false, true); + tab && tabs.appendChild(tab); + } + + // Pinned Rooms + for(var i=0; i < f.settings.pinned_rooms.length; i++) { + var room_id = f.settings.pinned_rooms[i]; + if ( room && room.get('id') !== room_id && this._ffz_host !== room_id && f.rooms[room_id] && f.rooms[room_id].room ) { + var tab = view.ffzBuildTab(view, f.rooms[room_id].room, false, false); + tab && tabs.appendChild(tab); + } + } + + _.each(this.get('controller.connectedPrivateGroupRooms'), function(room) { + var tab = view.ffzBuildTab(view, room); + tab && tabs.appendChild(tab); + }); + + // Now, adjust the chat-room. + this.$('.chat-room').css('top', tabs.offsetHeight + "px"); + }, + + ffzTabUnread: function(room_id) { + if ( f.has_bttv || ! f.settings.group_tabs ) + return; + + var tabs = this._ffz_tabs || this.get('element').querySelector('#ffz-group-tabs'), + current_id = this.get('controller.currentRoom.id'); + if ( ! tabs ) + return; + + if ( room_id ) { + var tab = tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'), + room = f.rooms && f.rooms[room_id]; + + if ( tab && room ) { + var unread = format_unread(room_id === current_id ? 0 : room.room.get('unreadCount')); + tab.querySelector('span').innerHTML = unread; + } + + // Now, adjust the chat-room. + return this.$('.chat-room').css('top', tabs.offsetHeight + "px"); + } + + var children = tabs.querySelectorAll('.ffz-chat-tab'); + for(var i=0; i < children.length; i++) { + var tab = children[i], + room_id = tab.getAttribute('data-room'), + room = f.rooms && f.rooms[room_id]; + + if ( ! room ) + continue; + + var unread = format_unread(room_id === current_id ? 0 : room.room.get('unreadCount')); + tab.querySelector('span').innerHTML = unread; + } + + // Now, adjust the chat-room. + this.$('.chat-room').css('top', tabs.offsetHeight + "px"); + }, + + ffzBuildTab: function(view, room, current_channel, host_channel) { + var tab = document.createElement('span'), name, unread, + group = room.get('isGroupRoom'), + current = room === view.get('controller.currentRoom'); + + tab.setAttribute('data-room', room.id); + + tab.className = 'ffz-chat-tab tooltip'; + tab.classList.toggle('current-channel', current_channel); + tab.classList.toggle('host-channel', host_channel); + tab.classList.toggle('group-chat', group); + tab.classList.toggle('active', current); + + name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'))); + unread = format_unread(current ? 0 : room.get('unreadCount')); + + if ( current_channel ) { + tab.innerHTML = constants.CAMERA; + tab.title = "Current Channel"; + } else if ( host_channel ) { + tab.innerHTML = constants.EYE; + tab.title = "Hosted Channel"; + } else if ( group ) + tab.title = "Group Chat"; + else + tab.title = "Pinned Channel"; + + tab.innerHTML += utils.sanitize(name) + '' + unread + ''; + + tab.addEventListener('click', function() { + view.get('controller').focusRoom(room); + }); + + return tab; + }, + + ffzDisableTabs: function() { + if ( this._ffz_tabs ) { + this._ffz_tabs.parentElement.removeChild(this._ffz_tabs); + delete this._ffz_tabs; + delete this._ffz_invite; + } + + if ( this._ffz_host ) { + if ( this._ffz_host_room ) { + if ( this.get('controller.currentRoom') === this._ffz_host_room ) + this.get('controller').blurRoom(); + this._ffz_host_room.destroy(); + } + + delete this._ffz_host; + delete this._ffz_host_room; + } + + // Show the old chat UI. + this.$('.chat-room').css('top', ''); + this.$(".chat-header").removeClass("hidden"); + }, }); } -},{}],7:[function(require,module,exports){ + + +// ---------------------- +// Chat Room Connections +// ---------------------- + +FFZ.prototype.connect_extra_chat = function() { + if ( this.has_bttv ) + return; + + for(var i=0; i < this.settings.pinned_rooms.length; i++) + this._join_room(this.settings.pinned_rooms[i], true); + + if ( ! this.has_bttv && this._chatv && this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); +} + + +FFZ.prototype._join_room = function(room_id, no_rebuild) { + var did_join = false; + if ( this.settings.pinned_rooms.indexOf(room_id) === -1 ) { + this.settings.pinned_rooms.push(room_id); + this.settings.set("pinned_rooms", this.settings.pinned_rooms); + did_join = true; + } + + // Make sure we're not already there. + if ( this.rooms[room_id] && this.rooms[room_id].room ) + return did_join; + + // Okay, fine. Get it. + var Room = App.__container__.resolve('model:room'), + r = Room && Room.findOne(room_id); + + // Finally, rebuild the chat UI. + if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); + + return did_join; +} + + +FFZ.prototype._leave_room = function(room_id, no_rebuild) { + var did_leave = false; + if ( this.settings.pinned_rooms.indexOf(room_id) !== -1 ) { + this.settings.pinned_rooms.removeObject(room_id); + this.settings.set("pinned_rooms", this.settings.pinned_rooms); + did_leave = true; + } + + if ( ! this.rooms[room_id] || ! this.rooms[room_id].room ) + return did_leave; + + var Chat = App.__container__.lookup('controller:chat'), + r = this.rooms[room_id].room; + + if ( ! Chat || Chat.get('currentChannelRoom.id') === room_id || (this._chatv && this._chatv._ffz_host === room_id) ) + return did_leave; + + if ( Chat.get('currentRoom.id') === room_id ) + Chat.blurRoom(); + + r.destroy(); + + if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); + + return did_leave; +} + + +// ---------------------- +// Commands +// ---------------------- + +FFZ.chat_commands.join = function(room, args) { + if ( ! args || ! args.length || args.length > 1 ) + return "Join Usage: /join "; + + var room_id = args[0].toLowerCase(); + if ( room_id.charAt(0) === "#" ) + room_id = room_id.substr(1); + + if ( this._join_room(room_id) ) + return "Joining " + room_id + ". You will always connect to this channel's chat unless you later /part from it."; + else + return "You have already joined " + room_id + ". Please use \"/part " + room_id + "\" to leave it."; +} + + +FFZ.chat_commands.part = function(room, args) { + if ( ! args || ! args.length || args.length > 1 ) + return "Part Usage: /part "; + + var room_id = args[0].toLowerCase(); + if ( room_id.charAt(0) === "#" ) + room_id = room_id.substr(1); + + if ( this._leave_room(room_id) ) + return "Leaving " + room_id + "."; + else if ( this.rooms[room_id] ) + return "You do not have " + room_id + " pinned and you cannot leave the current channel or hosted channels via /part."; + else + return "You are not in " + room_id + "."; +} +},{"../constants":3,"../utils":29}],7:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("../utils"), - reg_escape = function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - SEPARATORS = "[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]", SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"), @@ -731,7 +1378,8 @@ var FFZ = window.FrankerFaceZ, data_to_tooltip = function(data) { var set = data.set, - set_type = data.set_type; + set_type = data.set_type, + owner = data.owner; if ( set_type === undefined ) set_type = "Channel"; @@ -744,7 +1392,7 @@ var FFZ = window.FrankerFaceZ, set_type = null; } - return "Emoticon: " + data.code + "\n" + (set_type ? set_type + ": " : "") + set; + return "Emoticon: " + data.code + "\n" + (set_type ? set_type + ": " : "") + set + (owner ? "\nBy: " + owner.display_name : ""); }, build_tooltip = function(id) { @@ -817,6 +1465,12 @@ var FFZ = window.FrankerFaceZ, tooltip += "Views: " + utils.number_commas(link_data.views) + " | Followers: " + utils.number_commas(link_data.followers) + ""; + } else if ( link_data.type == "twitch_vod" ) { + tooltip = "Twitch " + (link_data.broadcast_type == "highlight" ? "Highlight" : "Broadcast") + ": " + utils.sanitize(link_data.title) + "
"; + tooltip += "By: " + utils.sanitize(link_data.display_name) + (link_data.game ? " | Playing: " + utils.sanitize(link_data.game) : " | Not Playing") + "
"; + tooltip += "Views: " + utils.number_commas(link_data.views) + " | " + utils.time_to_string(link_data.length); + + } else if ( link_data.type == "twitter" ) { tooltip = "Tweet By: " + utils.sanitize(link_data.user) + "
"; tooltip += utils.sanitize(link_data.tweet); @@ -1026,27 +1680,41 @@ FFZ.prototype.setup_line = function() { this.log("Hooking the Ember Line controller."); - var Line = App.__container__.resolve('controller:line'), + var Line = App.__container__.resolve('component:message-line'), f = this; Line.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. - var tokens = this._super(); + var tokens = this.get("msgObject.cachedTokens"); + if ( tokens ) + return tokens; + + tokens = this._super(); try { - var start = performance.now(); + var start = performance.now(), + user = f.get_user(), + from_me = user && this.get("msgObject.from") === user.login; + tokens = f._remove_banned(tokens); tokens = f._emoticonize(this, tokens); - var user = f.get_user(); - - if ( ! user || this.get("model.from") != user.login ) - tokens = f._mentionize(this, tokens); // Store the capitalization. - var display = this.get("model.tags.display-name"); + var display = this.get("msgObject.tags.display-name"); if ( display && display.length ) - FFZ.capitalization[this.get("model.from")] = [display.trim(), Date.now()]; + FFZ.capitalization[this.get("msgObject.from")] = [display.trim(), Date.now()]; + + if ( ! from_me ) + tokens = f.tokenize_mentions(tokens); + + for(var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if ( ! _.isString(token) && token.mentionedUser && ! token.own ) { + this.set('msgObject.ffz_has_mention', true); + break; + } + } var end = performance.now(); if ( end - start > 5 ) @@ -1058,38 +1726,30 @@ FFZ.prototype.setup_line = function() { } catch(err) { } } + this.set("msgObject.cachedTokens", tokens); return tokens; - }.property("model.message", "isModeratorOrHigher") - }); + }.property("msgObject.message", "isChannelLinksDisabled", "currentUserNick", "msgObject.from", "msgObject.tags.emotes"), - - this.log("Hooking the Ember Line view."); - var Line = App.__container__.resolve('view:line'); - - Line.reopen({ didInsertElement: function() { this._super(); try { var start = performance.now(); var el = this.get('element'), - controller = this.get('context'), - user = controller.get('model.from'), - room = controller.get('parentController.content.id'), - color = controller.get('model.color'), - - row_type = controller.get('model.ffz_alternate'); + user = this.get('msgObject.from'), + room = this.get('msgObject.room') || App.__container__.lookup('controller:chat').get('currentRoom.id'), + color = this.get('msgObject.color'), + row_type = this.get('msgObject.ffz_alternate'); // Color Processing if ( color ) f._handle_color(color); - // Row Alternation if ( row_type === undefined ) { row_type = f._last_row[room] = f._last_row.hasOwnProperty(room) ? !f._last_row[room] : false; - this.set("context.model.ffz_alternate", row_type); + this.set("msgObject.ffz_alternate", row_type); } el.classList.toggle('ffz-alternate', row_type); @@ -1098,7 +1758,7 @@ FFZ.prototype.setup_line = function() { // Basic Data el.setAttribute('data-room', room); el.setAttribute('data-sender', user); - el.setAttribute('data-deleted', controller.get('model.deleted')); + el.setAttribute('data-deleted', this.get('deleted')||false); // Badge @@ -1106,36 +1766,9 @@ FFZ.prototype.setup_line = function() { // Mention Highlighting - var mentioned = el.querySelector('span.mentioned'); - if ( mentioned ) { + if ( this.get("msgObject.ffz_has_mention") ) el.classList.add("ffz-mentioned"); - if ( f.settings.highlight_notifications && !document.hasFocus() && !this.get('context.model.ffz_notified') ) { - var cap_room = FFZ.get_capitalization(room), - cap_user = FFZ.get_capitalization(user), - room_name = cap_room, - msg = this.get("context.model.message"); - - if ( this.get("context.parentController.content.isGroupRoom") ) - room_name = this.get("context.parentController.content.tmiRoom.displayName"); - - if ( this.get("context.model.style") == "action" ) - msg = "* " + cap_user + " " + msg; - else - msg = cap_user + ": " + msg; - - f.show_notification( - msg, - "Twitch Chat Mention in " + room_name, - cap_room, - 60000, - window.focus.bind(window) - ); - } - } - - // Mark that we've checked this message for mentions. - this.set('context.model.ffz_notified', true); // Banned Links var bad_links = el.querySelectorAll('a.deleted-link'); @@ -1246,19 +1879,16 @@ FFZ.prototype.setup_line = function() { set_id = data && data[1] || null, set = f.emote_sets[set_id], - emote = set ? set.emotes[id] : null, + emote = set ? set.emoticons[id] : null; - set_name = set ? (set.title || set.id) : "Unknown FFZ Set", - set_type = (set && set.title) ? "FrankerFaceZ" : "FFZ Channel"; + // High-DPI! + if ( emote && emote.srcSet ) + img.setAttribute('srcset', emote.srcSet); if ( set && f.feature_friday && set.id == f.feature_friday.set ) set_name = f.feature_friday.title + " - " + f.feature_friday.display_name; - img.title = data_to_tooltip({ - code: emote ? (emote.hidden ? "???" : emote.name) : name, - set: set_name, - set_type: set_type - }); + img.title = f._emote_tooltip(emote); } } @@ -1386,75 +2016,6 @@ FFZ.get_capitalization = function(name, callback) { } -// --------------------- -// Extra Mentions -// --------------------- - -FFZ._regex_cache = {}; - -FFZ._get_regex = function(word) { - return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig"); -} - -FFZ._words_to_regex = function(list) { - var regex = FFZ._regex_cache[list]; - if ( ! regex ) { - var reg = ""; - for(var i=0; i < list.length; i++) { - if ( ! list[i] ) - continue; - - reg += (reg ? "|" : "") + reg_escape(list[i]); - } - - regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig"); - } - - return regex; -} - - -FFZ.prototype._mentionize = function(controller, tokens) { - var mention_words = this.settings.keywords; - if ( ! mention_words || ! mention_words.length ) - return tokens; - - if ( typeof tokens == "string" ) - tokens = [tokens]; - - var regex = FFZ._words_to_regex(mention_words), - new_tokens = []; - - for(var i=0; i < tokens.length; i++) { - var token = tokens[i]; - if ( ! _.isString(token) ) { - new_tokens.push(token); - continue; - } - - if ( ! token.match(regex) ) { - new_tokens.push(token); - continue; - } - - token = token.replace(regex, function(all, prefix, match) { - new_tokens.push(prefix); - new_tokens.push({ - mentionedUser: match, - own: false - }); - - return ""; - }); - - if ( token ) - new_tokens.push(token); - } - - return new_tokens; -} - - // --------------------- // Banned Words // --------------------- @@ -1495,62 +2056,13 @@ FFZ.prototype._remove_banned = function(tokens) { // Emoticon Replacement // --------------------- -FFZ.prototype._emoticonize = function(controller, tokens) { - var room_id = controller.get("parentController.model.id"), - user_id = controller.get("model.from"), - f = this; +FFZ.prototype._emoticonize = function(component, tokens) { + var room_id = component.get("msgObject.room") || App.__container__.lookup('controller:chat').get('currentRoom.id'), + user_id = component.get("msgObject.from"); - // Get our sets. - var sets = this.getEmotes(user_id, room_id), - emotes = []; - - // Build a list of emotes that match. - _.each(sets, function(set_id) { - var set = f.emote_sets[set_id]; - if ( ! set ) - return; - - _.each(set.emotes, function(emote) { - _.any(tokens, function(token) { - return _.isString(token) && token.match(emote.regex); - }) && emotes.push(emote); - }); - }); - - // Don't bother proceeding if we have no emotes. - if ( ! emotes.length ) - return tokens; - - // Now that we have all the matching tokens, do crazy stuff. - if ( typeof tokens == "string" ) - tokens = [tokens]; - - // This is weird stuff I basically copied from the old Twitch code. - // Here, for each emote, we split apart every text token and we - // put it back together with the matching bits of text replaced - // with an object telling Twitch's line template how to render the - // emoticon. - _.each(emotes, function(emote) { - //var eo = {isEmoticon:true, cls: emote.klass}; - var eo = {isEmoticon:true, cls: emote.klass,srcSet: emote.url + ' 1x', emoticonSrc: emote.url + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), altText: (emote.hidden ? "???" : emote.name)}; - - tokens = _.compact(_.flatten(_.map(tokens, function(token) { - if ( _.isObject(token) ) - return token; - - var tbits = token.split(emote.regex), bits = []; - tbits.forEach(function(val, ind) { - bits.push(val); - if ( ind !== tbits.length - 1 ) - bits.push(eo); - }); - return bits; - }))); - }); - - return tokens; + return this.tokenize_emotes(user_id, room_id, tokens); } -},{"../utils":28}],8:[function(require,module,exports){ +},{"../utils":29}],8:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("../utils"), @@ -1809,7 +2321,7 @@ FFZ.chat_commands.u = function(room, args) { } FFZ.chat_commands.u.enabled = function() { return this.settings.enhanced_moderation; } -},{"../utils":28}],9:[function(require,module,exports){ +},{"../utils":29}],9:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/, @@ -1863,6 +2375,7 @@ FFZ.prototype.setup_room = function() { var inst = instances[key]; this.add_room(inst.id, inst); this._modify_room(inst); + inst.ffzPatchTMI(); } } @@ -2011,7 +2524,7 @@ FFZ.prototype.add_room = function(id, room) { this.ws_send("sub", id); // For now, we use the legacy function to grab the .css file. - this._legacy_add_room(id); + this.load_room(id); } @@ -2031,14 +2544,14 @@ FFZ.prototype.remove_room = function(id) { delete this.rooms[id]; // Clean up sets we aren't using any longer. - for(var i=0; i < room.sets.length; i++) { - var set_id = room.sets[i], set = this.emote_sets[set_id]; - if ( ! set ) - continue; + if ( id.charAt(0) === "_" ) + return; + var set = this.emote_sets[room.set]; + if ( set ) { set.users.removeObject(id); - if ( !set.global && !set.users.length ) - this.unload_set(set_id); + if ( ! this.global_sets.contains(room.set) && ! set.users.length ) + this.unload_set(room.set); } } @@ -2047,12 +2560,36 @@ FFZ.prototype.remove_room = function(id) { // Receiving Set Info // -------------------- -FFZ.prototype.load_room = function(room_id, callback) { - return this._legacy_load_room(room_id, callback); +FFZ.prototype.load_room = function(room_id, callback, tries) { + var f = this; + jQuery.getJSON(constants.API_SERVER + "v1/room/" + room_id) + .done(function(data) { + if ( data.sets ) { + for(var key in data.sets) + data.sets.hasOwnProperty(key) && f._load_set_json(key, undefined, data.sets[key]); + } + + f._load_room_json(room_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return typeof callback == "function" && callback(false); + + tries = (tries || 0) + 1; + if ( tries < 10 ) + return f.load_room(room_id, callback, tries); + + return typeof callback == "function" && callback(false); + }); } FFZ.prototype._load_room_json = function(room_id, callback, data) { + if ( ! data || ! data.room ) + return typeof callback == "function" && callback(false); + + data = data.room; + // Preserve the pointer to the Room instance. if ( this.rooms[room_id] ) data.room = this.rooms[room_id].room; @@ -2062,11 +2599,8 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { if ( data.css || data.moderator_badge ) utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); - for(var i=0; i < data.sets.length; i++) { - var set_id = data.sets[i]; - if ( ! this.emote_sets.hasOwnProperty(set_id) ) - this.load_set(set_id); - } + if ( ! this.emote_sets.hasOwnProperty(data.set) ) + this.load_set(data.set); this.update_ui_link(); @@ -2087,6 +2621,7 @@ FFZ.prototype._modify_room = function(room) { this._super(); try { f.add_room(this.id, this); + this.set("ffz_chatters", {}); } catch(err) { f.error("add_room: " + err); } @@ -2101,20 +2636,25 @@ FFZ.prototype._modify_room = function(room) { } }, - getSuggestions: function() { - // This returns auto-complete suggestions for use in chat. We want - // to apply our capitalizations here. Overriding the - // filteredSuggestions property of the chat-input component would - // be even better, but I was already hooking the room model. - var suggestions = this._super(); - + addMessage: function(msg) { try { - suggestions = _.map(suggestions, FFZ.get_capitalization); + if ( msg ) { + msg.room = this.get('id'); + f.tokenize_chat_line(msg); + } } catch(err) { - f.error("get_suggestions: " + err); + f.error("Room addMessage: " + err); } - return suggestions; + return this._super(msg); + }, + + setHostMode: function(e) { + var Chat = App.__container__.lookup('controller:chat'); + if ( ! Chat || Chat.get('currentChannelRoom') !== this ) + return; + + return this._super(e); }, send: function(text) { @@ -2135,58 +2675,172 @@ FFZ.prototype._modify_room = function(room) { } return this._super(text); - } + }, + + ffzUpdateUnread: function() { + if ( f.settings.group_tabs ) { + var Chat = App.__container__.lookup('controller:chat'); + if ( Chat && Chat.get('currentRoom') === this ) + this.resetUnreadCount(); + else if ( f._chatv ) + f._chatv.ffzTabUnread(this.get('id')); + } + }.observes('unreadCount'), + + + ffzInitChatterCount: function() { + if ( ! this.tmiRoom ) + return; + + var room = this; + this.tmiRoom.list().done(function(data) { + var chatters = {}; + data = data.data.chatters; + for(var i=0; i < data.admins.length; i++) + chatters[data.admins[i]] = true; + for(var i=0; i < data.global_mods.length; i++) + chatters[data.global_mods[i]] = true; + for(var i=0; i < data.moderators.length; i++) + chatters[data.moderators[i]] = true; + for(var i=0; i < data.staff.length; i++) + chatters[data.staff[i]] = true; + for(var i=0; i < data.viewers.length; i++) + chatters[data.viewers[i]] = true; + + room.set("ffz_chatters", chatters); + room.ffzUpdateChatters(); + }); + }, + + ffzUpdateChatters: function(add, remove) { + var chatters = this.get("ffz_chatters") || {}; + if ( add ) + chatters[add] = true; + if ( remove && chatters[remove] ) + delete chatters[remove]; + + if ( ! f.settings.chatter_count ) + return; + + if ( f._cindex ) + f._cindex.ffzUpdateChatters(); + + if ( window.parent && window.parent.postMessage ) + window.parent.postMessage({from_ffz: true, command: 'chatter_count', message: Object.keys(this.get('ffz_chatters') || {}).length}, "http://www.twitch.tv/"); + }, + + + ffzPatchTMI: function() { + if ( this.get('ffz_is_patched') || ! this.get('tmiRoom') ) + return; + + if ( f.settings.chatter_count ) + this.ffzInitChatterCount(); + + var tmi = this.get('tmiRoom'), + room = this; + + // This method is stupid and bad and it leaks between rooms. + if ( ! tmi.ffz_notice_patched ) { + tmi.ffz_notice_patched = true; + + tmi._roomConn.off("notice", tmi._onNotice, tmi); + tmi._roomConn.on("notice", function(ircMsg) { + var target = ircMsg.target || (ircMsg.params && ircMsg.params[0]) || this.ircChannel; + if( target != this.ircChannel ) + return; + + this._trigger("notice", { + msgId: ircMsg.tags['msg-id'], + message: ircMsg.message + }); + }, tmi); + } + + // Let's get chatter information! + var connection = tmi._roomConn._connection; + if ( ! connection.ffz_cap_patched ) { + connection.ffz_cap_patched = true; + connection._send("CAP REQ :twitch.tv/membership"); + + connection.on("opened", function() { + this._send("CAP REQ :twitch.tv/membership"); + }, connection); + + // Since TMI starts sending SPECIALUSER with this, we need to + // ignore that. \ CatBag / + var orig_handle = connection._handleTmiPrivmsg.bind(connection); + connection._handleTmiPrivmsg = function(msg) { + if ( msg.message && msg.message.split(" ",1)[0] === "SPECIALUSER" ) + return; + return orig_handle(msg); + } + } + + + // Check this shit. + tmi._roomConn._connection.off("message", tmi._roomConn._onIrcMessage, tmi._roomConn); + + tmi._roomConn._onIrcMessage = function(ircMsg) { + if ( ircMsg.target != this.ircChannel ) + return; + + switch ( ircMsg.command ) { + case "JOIN": + if ( this._session && this._session.nickname === ircMsg.sender ) { + this._onIrcJoin(ircMsg); + } else + f.settings.chatter_count && room.ffzUpdateChatters(ircMsg.sender); + break; + + case "PART": + if ( this._session && this._session.nickname === ircMsg.sender ) { + this._resetActiveState(); + this._connection._exitedRoomConn(); + this._trigger("exited"); + } else + f.settings.chatter_count && room.ffzUpdateChatters(null, ircMsg.sender); + break; + + default: + break; + } + } + + tmi._roomConn._connection.on("message", tmi._roomConn._onIrcMessage, tmi._roomConn); + + + // Okay, we need to patch the *session's* updateUserState + if ( ! tmi.session.ffz_patched ) { + tmi.session.ffz_patched = true; + var uus = tmi.session._updateUserState.bind(tmi.session); + + tmi.session._updateUserState = function(user, tags) { + try { + if ( tags.color ) + this._onUserColorChanged(user, tags.color); + + if ( tags['display-name'] ) + this._onUserDisplayNameChanged(user, tags['display-name']); + + if ( tags.turbo ) + this._onUserSpecialAdded(user, 'turbo'); + + if ( tags['user_type'] === 'staff' || tags['user_type'] === 'admin' || tags['user_type'] === 'global_mod' ) + this._onUserSpecialAdded(user, tags['user-type']); + + } catch(err) { + f.error("SessionManager _updateUserState: " + err); + } + } + } + + this.set('ffz_is_patched', true); + + }.observes('tmiRoom') }); } - - -// -------------------- -// Legacy Data Support -// -------------------- - -FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_room_css(room_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return this._legacy_load_room_css(room_id, callback, null); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_add_room(room_id, callback, tries); - }); -} - - -FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { - var set_id = room_id, - match = set_id.match(GROUP_CHAT); - - if ( match && match[1] ) - set_id = match[1]; - - var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; - - if ( data ) - data = data.replace(CSS, "").trim(); - - if ( data ) { - data = data.replace(MOD_CSS, function(match, url) { - if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) - return match; - - output.moderator_badge = url; - return ""; - }); - } - - output.css = data || null; - return this._load_room_json(room_id, callback, output); -} -},{"../constants":3,"../utils":28}],10:[function(require,module,exports){ +},{"../constants":3,"../utils":29}],10:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -2292,15 +2946,6 @@ var FFZ = window.FrankerFaceZ, utils = require('./utils'), - loaded_global = function(set_id, success, data) { - if ( ! success ) - return; - - data.global = true; - this.global_sets.push(set_id); - }, - - check_margins = function(margins, height) { var mlist = margins.split(/ +/); if ( mlist.length != 2 ) @@ -2317,18 +2962,29 @@ var FFZ = window.FrankerFaceZ, build_legacy_css = function(emote) { - var margin = emote.margins; + var margin = emote.margins, srcset = ""; if ( ! margin ) margin = ((emote.height - 18) / -2) + "px 0"; - return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; + + if ( emote.urls[2] || emote.urls[4] ) { + srcset = 'url("' + emote.urls[1] + '") 1x'; + if ( emote.urls[2] ) + srcset += ', url("' + emote.urls[2] + '") 2x'; + if ( emote.urls[4] ) + srcset += ', url("' + emote.urls[4] + '") 4x'; + + srcset = '-webkit-image-set(' + srcset + '); image-set(' + srcset + ');'; + } + + return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.urls[1] + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (srcset ? '; ' + srcset : '') + (emote.css ? "; " + emote.css : "") + "}\n"; }, build_new_css = function(emote) { - if ( ! emote.margins && ! emote.extra_css ) + if ( ! emote.margins && ! emote.css ) return build_legacy_css(emote); - return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; + return build_legacy_css(emote) + 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n"; }, @@ -2344,27 +3000,103 @@ FFZ.prototype.setup_emoticons = function() { this.emote_sets = {}; this.global_sets = []; + this.default_sets = []; this._last_emote_id = 0; + // Usage Data + this.emote_usage = {}; + + this.log("Creating emoticon style element."); var s = this._emote_style = document.createElement('style'); s.id = "ffz-emoticon-css"; document.head.appendChild(s); - this.log("Loading global emote set."); - this.load_set("global", loaded_global.bind(this, "global")); + this.log("Loading global emote sets."); + this.load_global_sets(); + + this.log("Watching Twitch emoticon parser to ensure it loads."); + this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); } +// ------------------------ +// Emote Usage +// ------------------------ + +FFZ.prototype.add_usage = function(room_id, emote_id, count) { + var rooms = this.emote_usage[emote_id] = this.emote_usage[emote_id] || {}; + rooms[room_id] = (rooms[room_id] || 0) + (count || 1); + + if ( this._emote_report_scheduled ) + return; + + this._emote_report_scheduled = setTimeout(this._report_emotes.bind(this), 30000); +} + + +FFZ.prototype._report_emotes = function() { + if ( this._emote_report_scheduled ) + delete this._emote_report_scheduled; + + var usage = this.emote_usage; + this.emote_usage = {}; + this.ws_send("emoticon_uses", [usage], function(){}, true); +} + + +// ------------------------ +// Twitch Emoticon Checker +// ------------------------ + +FFZ.prototype.check_twitch_emotes = function() { + if ( this._twitch_emote_check ) { + clearTimeout(this._twitch_emote_check); + delete this._twitch_emote_check; + } + + var room; + if ( this.rooms ) { + for(var key in this.rooms) { + if ( this.rooms.hasOwnProperty(key) ) { + room = this.rooms[key]; + break; + } + } + } + + if ( ! room || ! room.room || ! room.room.tmiSession ) { + this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); + return; + } + + var parser = room.room.tmiSession._emotesParser, + emotes = Object.keys(parser.emoticonRegexToIds).length; + + // If we have emotes, we're done! + if ( emotes > 0 ) + return; + + // No emotes. Try loading them. + var sets = parser.emoticonSetIds; + parser.emoticonSetIds = ""; + parser.updateEmoticons(sets); + + // Check again in a bit to see if we've got them. + this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); +} + + + // --------------------- // Set Management // --------------------- FFZ.prototype.getEmotes = function(user_id, room_id) { - var user = this.users[user_id], - room = this.rooms[room_id]; + var user = this.users && this.users[user_id], + room = this.rooms && this.rooms[room_id]; - return _.union(user && user.sets || [], room && room.sets || [], this.global_sets); + return _.union(user && user.sets || [], room && room.set && [room.set] || [], this.default_sets); } @@ -2373,16 +3105,87 @@ FFZ.prototype.getEmotes = function(user_id, room_id) { // --------------------- FFZ.ws_commands.reload_set = function(set_id) { + if ( this.emote_sets.hasOwnProperty(set_id) ) + this.load_set(set_id); +} + + +FFZ.ws_commands.load_set = function(set_id) { this.load_set(set_id); } +// --------------------- +// Tooltip Powah! +// --------------------- + +FFZ.prototype._emote_tooltip = function(emote) { + if ( ! emote ) + return null; + + if ( emote._tooltip ) + return emote._tooltip; + + var set = this.emote_sets[emote.set_id], + owner = emote.owner, + title = set && set.title || "Global"; + + emote._tooltip = "Emoticon: " + (emote.hidden ? "???" : emote.name) + "\nFFZ " + title + (owner ? "\nBy: " + owner.display_name : ""); + return emote._tooltip; +} + + // --------------------- // Set Loading // --------------------- -FFZ.prototype.load_set = function(set_id, callback) { - return this._legacy_load_set(set_id, callback); +FFZ.prototype.load_global_sets = function(callback, tries) { + var f = this; + jQuery.getJSON(constants.API_SERVER + "v1/set/global") + .done(function(data) { + f.default_sets = data.default_sets; + var gs = f.global_sets = [], + sets = data.sets || {}; + + for(var key in sets) { + if ( ! sets.hasOwnProperty(key) ) + continue; + + var set = sets[key]; + gs.push(key); + f._load_set_json(key, undefined, set); + } + }).fail(function(data) { + if ( data.status == 404 ) + return typeof callback == "function" && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 50 ) + return f.load_global_sets(callback, tries); + + return typeof callback == "function" && callback(false); + }); +} + + +FFZ.prototype.load_set = function(set_id, callback, tries) { + var f = this; + jQuery.getJSON(constants.API_SERVER + "v1/set/" + set_id) + .done(function(data) { + f._load_set_json(set_id, callback, data && data.set); + + }).fail(function(data) { + if ( data.status == 404 ) + return typeof callback == "function" && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return f.load_set(set_id, callback, tries); + + return typeof callback == "function" && callback(false); + }); } @@ -2395,101 +3198,59 @@ FFZ.prototype.unload_set = function(set_id) { utils.update_css(this._emote_style, set_id, null); delete this.emote_sets[set_id]; - - for(var i=0; i < set.users.length; i++) { - var room = this.rooms[set.users[i]]; - if ( room ) - room.sets.removeObject(set_id); - } } FFZ.prototype._load_set_json = function(set_id, callback, data) { + if ( ! data ) + return typeof callback == "function" && callback(false); + // Store our set. this.emote_sets[set_id] = data; data.users = []; - data.global = false; data.count = 0; + // Iterate through all the emoticons, building CSS and regex objects as appropriate. - var output_css = ""; + var output_css = "", + ems = data.emoticons; - for(var key in data.emotes) { - if ( ! data.emotes.hasOwnProperty(key) ) - continue; + data.emoticons = {}; + + for(var i=0; i < ems.length; i++) { + var emote = ems[i]; - var emote = data.emotes[key]; emote.klass = "ffz-emote-" + emote.id; + emote.set_id = set_id; + + emote.srcSet = emote.urls[1] + " 1x"; + if ( emote.urls[2] ) + emote.srcSet += ", " + emote.urls[2] + " 2x"; + if ( emote.urls[4] ) + emote.srcSet += ", " + emote.urls[4] + " 4x"; if ( emote.name[emote.name.length-1] === "!" ) - emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); + emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")(?=\\W|$)", "g"); else - emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); + emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")\\b", "g"); output_css += build_css(emote); data.count++; + data.emoticons[emote.id] = emote; } - utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); - this.log("Updated emoticons for set: " + set_id, data); + utils.update_css(this._emote_style, set_id, output_css + (data.css || "")); + this.log("Updated emoticons for set #" + set_id + ": " + data.title, data); + + if ( this._cindex ) + this._cindex.ffzFixTitle(); + this.update_ui_link(); if ( callback ) callback(true, data); } - - -FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_css(set_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return typeof callback == "function" && callback(false); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_load_set(set_id, callback, tries); - - return typeof callback == "function" && callback(false); - }); -} - - -FFZ.prototype._legacy_load_css = function(set_id, callback, data) { - var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; - - if ( set_id == "global" ) - output.title = "Global"; - else if ( set_id == "globalevent" ) - output.title = "Global Event"; - else if ( set_id == ".donor" ) - output.title = "Donor"; - - data = data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { - height = parseInt(height); width = parseInt(width); - margins = check_margins(margins, height); - var hidden = path.substr(path.lastIndexOf("/") + 1, 1) === ".", - id = ++f._last_emote_id, - emote = {id: id, set_id: set_id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra}; - - emotes[id] = emote; - return ""; - }).trim(); - - if ( data ) - data.replace(MOD_CSS, function(match, url) { - if ( output.icon || url.substr(-11) !== 'modicon.png' ) - return; - - output.icon = url; - }); - - this._load_set_json(set_id, callback, output); -} -},{"./constants":3,"./utils":28}],12:[function(require,module,exports){ +},{"./constants":3,"./utils":29}],12:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/; @@ -2515,8 +3276,6 @@ FFZ.prototype.setup_bttv = function(delay) { this.log("BetterTTV was detected after " + delay + "ms. Hooking."); this.has_bttv = true; - this.log("WOO"); - // this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); // Disable Dark if it's enabled. @@ -2526,10 +3285,19 @@ FFZ.prototype.setup_bttv = function(delay) { delete this._dark_style; } + // Disable Chat Tabs + if ( this.settings.group_tabs && this._chatv ) { + this._chatv.ffzDisableTabs(); + } + // Disable other features too. document.body.classList.remove("ffz-chat-colors"); document.body.classList.remove("ffz-chat-background"); + // Remove Sub Count + if ( this.is_dashboard ) + this._update_subscribers(); + // Send Message Behavior var original_send = BetterTTV.chat.helpers.sendMessage, f = this; @@ -2563,7 +3331,7 @@ FFZ.prototype.setup_bttv = function(delay) { f.bttv_badges(data); // Now, do everything else manually because things are hard-coded. - return '
'+ + return '
'+ BetterTTV.chat.templates.timestamp(data.time)+' '+ (isMod?BetterTTV.chat.templates.modicons():'')+' '+ BetterTTV.chat.templates.badges(data.badges)+ @@ -2586,6 +3354,7 @@ FFZ.prototype.setup_bttv = function(delay) { var rawMessage = encodeURIComponent(message); if(sender !== 'jtv') { + // Hackilly send our state across. received_sender = sender; var tokenizedMessage = BetterTTV.chat.templates.emoticonize(message, emotes); received_sender = null; @@ -2601,7 +3370,7 @@ FFZ.prototype.setup_bttv = function(delay) { message = tokenizedMessage.join(' '); } - return ''+message+''; + return ''+message+''; } catch(err) { f.log("Error: ", err); return original_message(sender, message, emotes, colored); @@ -2613,8 +3382,13 @@ FFZ.prototype.setup_bttv = function(delay) { var original_emoticonize = BetterTTV.chat.templates.emoticonize; BetterTTV.chat.templates.emoticonize = function(message, emotes) { var tokens = original_emoticonize(message, emotes), - sets = f.getEmotes(received_sender, received_room), - emotes = []; + room = (received_room || BetterTTV.getChannel()), + l_room = room && room.toLowerCase(), + l_sender = received_sender && received_sender.toLowerCase(), + sets = f.getEmotes(l_sender, l_room), + emotes = [], + user = f.get_user(), + mine = user && user.login === l_sender; // Build a list of emotes that match. _.each(sets, function(set_id) { @@ -2622,7 +3396,7 @@ FFZ.prototype.setup_bttv = function(delay) { if ( ! set ) return; - _.each(set.emotes, function(emote) { + _.each(set.emoticons, function(emote) { _.any(tokens, function(token) { return _.isString(token) && token.match(emote.regex); }) && emotes.push(emote); @@ -2635,7 +3409,8 @@ FFZ.prototype.setup_bttv = function(delay) { // Why is emote parsing so bad? ;_; _.each(emotes, function(emote) { - var eo = ['' + emote.name + ''], + var tooltip = f._emote_tooltip(emote), + eo = ['' + tooltip + ''], old_tokens = tokens; tokens = []; @@ -2651,13 +3426,22 @@ FFZ.prototype.setup_bttv = function(delay) { } var tbits = token.split(emote.regex); - tbits.forEach(function(val, ind) { - if ( val && val.length ) - tokens.push(val); + while(tbits.length) { + var bit = tbits.shift(); + if ( tbits.length ) { + bit += tbits.shift(); + if ( bit ) + tokens.push(bit); - if ( ind !== tbits.length - 1 ) + tbits.shift(); tokens.push(eo); - }); + + if ( mine && l_room ) + f.add_usage(l_room, emote.id); + + } else + tokens.push(bit); + } } }); @@ -2707,14 +3491,14 @@ FFZ.prototype._emote_menu_enumerator = function() { for(var x = 0; x < sets.length; x++) { var set = this.emote_sets[sets[x]]; - if ( ! set || ! set.emotes ) + if ( ! set || ! set.emoticons ) continue; - for(var emote_id in set.emotes) { - if ( ! set.emotes.hasOwnProperty(emote_id) ) + for(var emote_id in set.emoticons) { + if ( ! set.emoticons.hasOwnProperty(emote_id) ) continue; - var emote = set.emotes[emote_id]; + var emote = set.emoticons[emote_id]; if ( emote.hidden ) continue; @@ -2735,7 +3519,7 @@ FFZ.prototype._emote_menu_enumerator = function() { } else title = "FrankerFaceZ: " + title; - emotes.push({text: emote.name, url: emote.url, + emotes.push({text: emote.name, url: emote.urls[1], hidden: false, channel: title, badge: badge}); } } @@ -2744,8 +3528,7 @@ FFZ.prototype._emote_menu_enumerator = function() { } },{}],14:[function(require,module,exports){ // Modify Array and others. -require('./shims'); - +// require('./shims'); // ---------------- // The Constructor @@ -2767,7 +3550,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 3, revision: 1, + major: 3, minor: 4, revision: 2, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -2858,6 +3641,8 @@ require('./socket'); require('./emoticons'); require('./badges'); +require('./tokenize'); + // Analytics: require('./ember/router'); require('./ember/channel'); @@ -2881,14 +3666,13 @@ require('./ui/styles'); require('./ui/dark'); require('./ui/notifications'); require('./ui/viewer_count'); +require('./ui/sub_count'); require('./ui/menu_button'); require('./ui/races'); require('./ui/my_emotes'); require('./ui/about_page'); -//require('./ui/group_chat'); - require('./commands'); @@ -2901,7 +3685,7 @@ FFZ.prototype.initialize = function(increment, delay) { // Twitch ember application is ready. // Check for special non-ember pages. - if ( /\/(?:settings|messages?\/)/.test(location.pathname) ) { + if ( /^\/(?:settings|m\/|messages?\/)/.test(location.pathname) ) { this.setup_normal(delay); return; } @@ -2948,6 +3732,7 @@ FFZ.prototype.setup_normal = function(delay) { this.setup_notifications(); this.setup_css(); + this.setup_menu(); this.find_bttv(10); @@ -2980,6 +3765,11 @@ FFZ.prototype.setup_dashboard = function(delay) { this.setup_notifications(); this.setup_css(); + this._update_subscribers(); + + // Set up the FFZ message passer. + this.setup_message_event(); + this.find_bttv(10); var end = (window.performance && performance.now) ? performance.now() : Date.now(), @@ -3022,7 +3812,8 @@ FFZ.prototype.setup_ember = function(delay) { this.setup_menu(); this.setup_my_emotes(); this.setup_races(); - //this.setup_group_chat(); + + this.connect_extra_chat(); this.find_bttv(10); this.find_emote_menu(10); @@ -3034,7 +3825,26 @@ FFZ.prototype.setup_ember = function(delay) { this.log("Initialization complete in " + duration + "ms"); } -},{"./badges":1,"./commands":2,"./debug":4,"./ember/channel":5,"./ember/chatview":6,"./ember/line":7,"./ember/moderation-card":8,"./ember/room":9,"./ember/viewers":10,"./emoticons":11,"./ext/betterttv":12,"./ext/emote_menu":13,"./featurefriday":15,"./settings":16,"./shims":17,"./socket":18,"./ui/about_page":19,"./ui/dark":20,"./ui/menu":21,"./ui/menu_button":22,"./ui/my_emotes":23,"./ui/notifications":24,"./ui/races":25,"./ui/styles":26,"./ui/viewer_count":27}],15:[function(require,module,exports){ + + +// ------------------------ +// Dashboard Message Event +// ------------------------ + +FFZ.prototype.setup_message_event = function() { + this.log("Listening for Window Messages."); + window.addEventListener("message", this._on_window_message.bind(this), false); +} + + +FFZ.prototype._on_window_message = function(e) { + if ( ! e.data || ! e.data.from_ffz ) + return; + + var msg = e.data; + this.log("Window Message", msg); +} +},{"./badges":1,"./commands":2,"./debug":4,"./ember/channel":5,"./ember/chatview":6,"./ember/line":7,"./ember/moderation-card":8,"./ember/room":9,"./ember/viewers":10,"./emoticons":11,"./ext/betterttv":12,"./ext/emote_menu":13,"./featurefriday":15,"./settings":16,"./socket":17,"./tokenize":18,"./ui/about_page":19,"./ui/dark":20,"./ui/menu":21,"./ui/menu_button":22,"./ui/my_emotes":23,"./ui/notifications":24,"./ui/races":25,"./ui/styles":26,"./ui/sub_count":27,"./ui/viewer_count":28}],15:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); @@ -3128,10 +3938,7 @@ FFZ.prototype._load_ff = function(data) { if ( this.feature_friday ) { // Remove the global set, delete the data, and reset the UI link. this.global_sets.removeObject(this.feature_friday.set); - - var set = this.emote_sets[this.feature_friday.set]; - if ( set ) - set.global = false; + this.default_sets.removeObject(this.feature_friday.set); this.feature_friday = null; this.update_ui_link(); @@ -3148,7 +3955,8 @@ FFZ.prototype._load_ff = function(data) { // Add the set. this.global_sets.push(data.set); - this.load_set(data.set, this._update_ff_set.bind(this)); + this.default_sets.push(data.set); + this.load_set(data.set); // Check to see if the channel is live. this._update_ff_live(); @@ -3171,13 +3979,6 @@ FFZ.prototype._update_ff_live = function() { } -FFZ.prototype._update_ff_set = function(success, set) { - // Prevent the set from being unloaded. - if ( set ) - set.global = true; -} - - FFZ.prototype._update_ff_name = function(name) { if ( this.feature_friday ) this.feature_friday.display_name = name; @@ -3503,32 +4304,6 @@ FFZ.prototype._setting_del = function(key) { } } },{"./constants":3}],17:[function(require,module,exports){ -Array.prototype.equals = function (array) { - // if the other array is a falsy value, return - if (!array) - return false; - - // compare lengths - can save a lot of time - if (this.length != array.length) - return false; - - for (var i = 0, l=this.length; i < l; i++) { - // Check if we have nested arrays - if (this[i] instanceof Array && array[i] instanceof Array) { - // recurse into the nested arrays - if (!this[i].equals(array[i])) - return false; - } - else if (this[i] != array[i]) { - // Warning - two different object instances will never be equal: {x:20} != {x:20} - return false; - } - } - return true; -} - - -},{}],18:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; FFZ.prototype._ws_open = false; @@ -3563,6 +4338,18 @@ FFZ.prototype.ws_create = function() { f._ws_delay = 0; f.log("Socket connected."); + // Check for incognito. We don't want to do a hello in incognito mode. + var fs = window.RequestFileSystem || window.webkitRequestFileSystem; + if (!fs) + // Assume not. + f.ws_send("hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)); + + else + fs(window.TEMPORARY, 100, + f.ws_send.bind(f, "hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)), + f.log.bind(f, "Operating in Incognito Mode.")); + + var user = f.get_user(); if ( user ) f.ws_send("setuser", user.login); @@ -3670,6 +4457,43 @@ FFZ.prototype.ws_send = function(func, data, callback, can_wait) { return request; } + +// ---------------- +// HELLO Response +// ---------------- + +FFZ.prototype._ws_on_hello = function(success, data) { + if ( ! success ) + return this.log("Error Saying Hello: " + data); + + localStorage.ffzClientId = data; + this.log("Client ID: " + data); + + var survey = {}, + set = survey['settings'] = {}; + + for(var key in FFZ.settings_info) + set[key] = this.settings[key]; + + set["keywords"] = this.settings.keywords.length; + set["banned_words"] = this.settings.banned_words.length; + + + // Detect BTTV. + survey['bttv'] = this.has_bttv || !!document.head.querySelector('script[src*="betterttv"]'); + + + // Client Info + survey['user-agent'] = navigator.userAgent; + survey['screen'] = [screen.width, screen.height]; + survey['language'] = navigator.language; + survey['platform'] = navigator.platform; + + this.ws_send("survey", [survey]); +} + + + // ---------------- // Authorization // ---------------- @@ -3697,7 +4521,378 @@ FFZ.ws_commands.do_authorize = function(data) { // Try again shortly. setTimeout(FFZ.ws_commands.do_authorize.bind(this, data), 5000); } -},{}],19:[function(require,module,exports){ +},{}],18:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + utils = require("./utils"), + TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", + helpers, + + reg_escape = function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + + SEPARATORS = "[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]", + SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"); + +try { + helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers"); +} catch(err) { } + + +// --------------------- +// Tokenization +// --------------------- + +FFZ.prototype.tokenize_chat_line = function(msgObject) { + if ( msgObject.cachedTokens ) + return msgObject.cachedTokens; + + var msg = msgObject.message, + user = this.get_user(), + room_id = msgObject.room, + from_me = user && msgObject.from === user.login, + emotes = msgObject.tags && msgObject.tags.emotes, + + tokens = [msg]; + + // Standard tokenization + tokens = helpers.linkifyMessage(tokens); + if ( user && user.login ) + tokens = helpers.mentionizeMessage(tokens, user.login, from_me); + tokens = helpers.emoticonizeMessage(tokens, emotes); + + // FrankerFaceZ Extras + tokens = this._remove_banned(tokens); + tokens = this.tokenize_emotes(msgObject.from, room_id, tokens, from_me); + + // Capitalization + var display = msgObject.tags && msgObject.tags['display-name']; + if ( display && display.length ) + FFZ.capitalization[msgObject.from] = [display.trim(), Date.now()]; + + + // Mentions! + if ( ! from_me ) { + tokens = this.tokenize_mentions(tokens); + + for(var i=0; i < tokens.length; i++) { + var token = tokens[i]; + if ( _.isString(token) || ! token.mentionedUser || token.own ) + continue; + + // We have a mention! + msgObject.ffz_has_mention = true; + + // If we have chat tabs, update the status. + if ( ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { + var el = this._chatv._ffz_tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'); + if ( el && ! el.classList.contains('active') ) + el.classList.add('tab-mentioned'); + } + + // Display notifications if that setting is enabled. Also make sure + // that we have a chat view because showing a notification when we + // can't actually go to it is a bad thing. + if ( this._chatv && this.settings.highlight_notifications && ! document.hasFocus() ) { + var room = this.rooms[room_id] && this.rooms[room_id].room, + room_name; + + if ( room && room.get('isGroupRoom') ) + room_name = room.get('tmiRoom.displayName'); + else + room_name = FFZ.get_capitalization(room_id); + + display = display || Twitch.display.capitalize(msgObject.from); + + if ( msgObject.style === 'action' ) + msg = '* ' + display + ' ' + msg; + else + msg = display + ': ' + msg; + + var f = this; + this.show_notification( + msg, + "Twitch Chat Mention in " + room_name, + room_id, + 60000, + function() { + window.focus(); + var cont = App.__container__.lookup('controller:chat'); + room && cont && cont.focusRoom(room); + } + ); + } + + break; + } + } + + msgObject.cachedTokens = tokens; + return tokens; +} + + +FFZ.prototype.tokenize_line = function(user, room, message, no_emotes) { + if ( typeof message === "string" ) + message = [message]; + + if ( helpers && helpers.linkifyMessage ) + message = helpers.linkifyMessage(message); + + if ( helpers && helpers.mentionizeMessage ) { + var u = this.get_user(); + if ( u && u.login ) + message = helpers.mentionizeMessage(message, u.login, user === u.login); + } + + if ( ! no_emotes ) + message = this.tokenize_emotes(user, room, message); + + return message; +} + + +FFZ.prototype.render_tokens = function(tokens, render_links) { + return _.map(tokens, function(token) { + if ( token.emoticonSrc ) + return '' + token.altText + ''; + + if ( token.isLink ) { + if ( ! render_links && render_links !== undefined ) + return token.href; + + var s = token.href; + if ( s.indexOf("@") > -1 && (-1 === s.indexOf("/") || s.indexOf("@") < s.indexOf("/")) ) + return '' + s + ''; + + var n = (s.match(/^https?:\/\//) ? "" : "http://") + s; + return '' + s + ''; + } + + if ( token.mentionedUser ) + return '' + token.mentionedUser + ""; + + if ( token.deletedLink ) + return utils.sanitize(token.text); + + return utils.sanitize(token); + }).join(""); +} + + +// --------------------- +// Emoticon Processing +// --------------------- + +FFZ.prototype.tokenize_title_emotes = function(tokens) { + var f = this, + Channel = App.__container__.lookup('controller:channel'), + possible = Channel && Channel.get('product.emoticons'), + emotes = []; + + if ( _.isString(tokens) ) + tokens = [tokens]; + + // Build a list of emotes that match. + _.each(_.union(f.__twitch_global_emotes||[], possible), function(emote) { + if ( ! emote || emote.state === "inactive" ) + return; + + var r = new RegExp("\\b" + emote.regex + "\\b"); + + _.any(tokens, function(token) { + return _.isString(token) && token.match(r); + }) && emotes.push(emote); + }); + + // Include Global Emotes~! + if ( f.__twitch_global_emotes === undefined || f.__twitch_global_emotes === null ) { + f.__twitch_global_emotes = false; + Twitch.api.get("chat/emoticon_images", {emotesets:"0,42"}).done(function(data) { + if ( ! data || ! data.emoticon_sets || ! data.emoticon_sets[0] ) { + f.__twitch_global_emotes = []; + return; + } + + var emotes = f.__twitch_global_emotes = []; + data = data.emoticon_sets[0]; + for(var i=0; i < data.length; i++) { + var em = data[i]; + emotes.push({regex: em.code, url: TWITCH_BASE + em.id + "/1.0"}); + } + + if ( f._cindex ) + f._cindex.ffzFixTitle(); + }).fail(function() { + setTimeout(function(){f.__twitch_global_emotes = null;},5000); + });; + } + + if ( ! emotes.length ) + return tokens; + + if ( typeof tokens === "string" ) + tokens = [tokens]; + + _.each(emotes, function(emote) { + var eo = {isEmoticon:true, srcSet: emote.url + ' 1x', emoticonSrc: emote.url, altText: emote.regex}; + var r = new RegExp("\\b" + emote.regex + "\\b"); + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(r), bits = []; + tbits.forEach(function(val, ind) { + bits.push(val); + if ( ind !== tbits.length - 1 ) + bits.push(eo); + }); + return bits; + }))); + }); + + return tokens; +} + + +FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { + var f = this; + + // Get our sets. + var sets = this.getEmotes(user, room), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emoticons, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Now that we have all the matching tokens, do crazy stuff. + if ( typeof tokens == "string" ) + tokens = [tokens]; + + // This is weird stuff I basically copied from the old Twitch code. + // Here, for each emote, we split apart every text token and we + // put it back together with the matching bits of text replaced + // with an object telling Twitch's line template how to render the + // emoticon. + _.each(emotes, function(emote) { + var eo = { + srcSet: emote.srcSet, + emoticonSrc: emote.urls[1] + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), + altText: (emote.hidden ? "???" : emote.name) + }; + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(emote.regex), bits = []; + while(tbits.length) { + var bit = tbits.shift(); + if ( tbits.length ) { + bit += tbits.shift(); + if ( bit ) + bits.push(bit); + + tbits.shift(); + bits.push(eo); + + if ( do_report && room ) + f.add_usage(room, emote.id); + + } else + bits.push(bit); + } + return bits; + }))); + }); + + return tokens; +} + + +// --------------------- +// Mention Parsing +// --------------------- + +FFZ._regex_cache = {}; + +FFZ._get_regex = function(word) { + return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig"); +} + +FFZ._words_to_regex = function(list) { + var regex = FFZ._regex_cache[list]; + if ( ! regex ) { + var reg = ""; + for(var i=0; i < list.length; i++) { + if ( ! list[i] ) + continue; + + reg += (reg ? "|" : "") + reg_escape(list[i]); + } + + regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig"); + } + + return regex; +} + + +FFZ.prototype.tokenize_mentions = function(tokens) { + var mention_words = this.settings.keywords; + if ( ! mention_words || ! mention_words.length ) + return tokens; + + if ( typeof tokens === "string" ) + tokens = [tokens]; + + var regex = FFZ._words_to_regex(mention_words), + new_tokens = []; + + for(var i=0; i < tokens.length; i++) { + var token = tokens[i]; + if ( ! _.isString(token) ) { + new_tokens.push(token); + continue; + } + + if ( ! token.match(regex) ) { + new_tokens.push(token); + continue; + } + + token = token.replace(regex, function(all, prefix, match) { + new_tokens.push(prefix); + new_tokens.push({ + mentionedUser: match, + own: false + }); + + return ""; + }); + + if ( token ) + new_tokens.push(token); + } + + return new_tokens; +} +},{"./utils":29}],19:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("../constants"); @@ -3716,14 +4911,10 @@ FFZ.menu_pages.about = { has_emotes = false, f = this; // Check for emoticons. - if ( room && room.sets.length ) { - for(var i=0; i < room.sets.length; i++) { - var set = this.emote_sets[room.sets[i]]; - if ( set && set.count > 0 ) { - has_emotes = true; - break; - } - } + if ( room && room.set ) { + var set = this.emote_sets[room.set]; + if ( set && set.count > 0 ) + has_emotes = true; } // Heading @@ -3765,7 +4956,7 @@ FFZ.menu_pages.about = { var donate_button = document.createElement('a'); donate_button.className = 'button ffz-donate'; - donate_button.href = "http://www.frankerfacez.com/donate.html"; + donate_button.href = "https://www.frankerfacez.com/donate"; donate_button.target = "_new"; donate_button.innerHTML = "Donate"; @@ -3858,11 +5049,11 @@ FFZ.prototype.setup_dark = function() { return; document.body.classList.toggle("ffz-dark", this.settings.dark_twitch); - if ( this.settings.dark_twitch ) - window.App && App.__container__.lookup('controller:settings').set('model.darkMode', true); + if ( ! this.settings.dark_twitch ) + return; - if ( this.settings.dark_twitch ) - this._load_dark_css(); + window.App && App.__container__.lookup('controller:settings').set('model.darkMode', true); + this._load_dark_css(); } @@ -4051,7 +5242,8 @@ FFZ.menu_pages.channel = { if ( product && !product.get("error") ) { // We have a product, and no error~! has_product = true; - var is_subscribed = room.room.get("channel.isSubscribed.content"), + var tickets = App.__container__.resolve('model:ticket').find('user', {channel: room_id}), + is_subscribed = tickets ? tickets.get('content') : false, icon = room.room.get("badgeSet.subscriber.image"), grid = document.createElement("div"), @@ -4119,7 +5311,7 @@ FFZ.menu_pages.channel = { inner.appendChild(sub_message); } else { - var last_content = room.room.get("channel.isSubscribed.content"); + var last_content = tickets.get("content"); last_content = last_content.length > 0 ? last_content[last_content.length-1] : undefined; if ( last_content && last_content.purchase_profile && !last_content.purchase_profile.will_renew ) { var ends_at = utils.parse_date(last_content.access_end || ""); @@ -4142,7 +5334,7 @@ FFZ.menu_pages.channel = { } // Basic Emote Sets - this._emotes_for_sets(inner, view, room && room.menu_sets || [], (this.feature_friday || has_product) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/channel/global/devicon.png", "FrankerFaceZ"); + this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); // Feature Friday! this._feature_friday_ui(room_id, inner, view); @@ -4180,31 +5372,62 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub grid.appendChild(el_header); } + var emotes = []; for(var i=0; i < sets.length; i++) { var set = this.emote_sets[sets[i]]; - if ( ! set || ! set.emotes ) + if ( ! set || ! set.emoticons ) continue; - for(var eid in set.emotes) { - if ( ! set.emotes.hasOwnProperty(eid) ) + for(var eid in set.emoticons) { + if ( ! set.emoticons.hasOwnProperty(eid) || set.emoticons[eid].hidden ) continue; - var emote = set.emotes[eid]; - if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) - continue; - - c++; - var s = document.createElement('span'); - s.className = 'emoticon tooltip'; - s.style.backgroundImage = 'url("' + emote.url + '")'; - s.style.width = emote.width + "px"; - s.style.height = emote.height + "px"; - s.title = emote.name; - s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); - grid.appendChild(s); + emotes.push(set.emoticons[eid]); } } + // Sort the emotes! + emotes.sort(function(a,b) { + var an = a.name.toLowerCase(), + bn = b.name.toLowerCase(); + + if ( an < bn ) return -1; + else if ( an > bn ) return 1; + return 0; + }); + + for(var i=0; i < emotes.length; i++) { + var emote = emotes[i], srcset = null; + + if ( emote.urls[2] || emote.urls[4] ) { + srcset = 'url("' + emote.urls[1] + '") 1x'; + if ( emote.urls[2] ) + srcset += ', url("' + emote.urls[2] + '") 2x'; + if ( emote.urls[4] ) + srcset += ', url("' + emote.urls[4] + '") 4x'; + } + + c++; + var s = document.createElement('span'); + s.className = 'emoticon tooltip'; + s.style.backgroundImage = 'url("' + emote.urls[1] + '")'; + + if ( srcset ) { + var img_set = 'image-set(' + srcset + ')'; + s.style.backgroundImage = '-webkit-' + img_set; + s.style.backgroundImage = '-moz-' + img_set; + s.style.backgroundImage = '-ms-' + img_set; + s.style.backgroundImage = img_set; + } + + s.style.width = emote.width + "px"; + s.style.height = emote.height + "px"; + s.title = this._emote_tooltip(emote); + + s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); + grid.appendChild(s); + } + if ( !c ) { grid.innerHTML += "This channel has no emoticons."; grid.className = "emoticon-grid ffz-no-emotes center"; @@ -4233,7 +5456,7 @@ FFZ.prototype._add_emote = function(view, emote) { else room.set('messageToSend', text); } -},{"../constants":3,"../utils":28}],22:[function(require,module,exports){ +},{"../constants":3,"../utils":29}],22:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -4269,14 +5492,10 @@ FFZ.prototype.update_ui_link = function(link) { // Check for emoticons. - if ( room && room.sets.length ) { - for(var i=0; i < room.sets.length; i++) { - var set = this.emote_sets[room.sets[i]]; - if ( set && set.count > 0 ) { - has_emotes = true; - break; - } - } + if ( room && room.set ) { + var set = this.emote_sets[room.set]; + if ( set && set.count > 0 ) + has_emotes = true; } link.classList.toggle('no-emotes', ! has_emotes); @@ -4287,6 +5506,7 @@ FFZ.prototype.update_ui_link = function(link) { },{"../constants":3}],23:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("../constants"), + utils = require("../utils"), TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", BANNED_SETS = {"00000turbo":true}, @@ -4330,7 +5550,7 @@ var FFZ = window.FrankerFaceZ, if ( ffz.settings.global_emotes_in_menu ) { set_ids.push("0"); - user_sets = _.union(user_sets, ffz.global_sets); + user_sets = _.union(user_sets, ffz.default_sets); } return [set_ids, user_sets]; @@ -4351,18 +5571,22 @@ FFZ.settings_info.global_emotes_in_menu = { FFZ.prototype.setup_my_emotes = function() { - this._twitch_emote_sets = {}; this._twitch_set_to_channel = {}; + this._twitch_badges = {}; if ( localStorage.ffzTwitchSets ) { try { this._twitch_set_to_channel = JSON.parse(localStorage.ffzTwitchSets); + this._twitch_badges = JSON.parse(localStorage.ffzTwitchBadges); } catch(err) { } } - this._twitch_set_to_channel[0] = "twitch_global"; - this._twitch_set_to_channel[33] = "twitch_tfaces"; - this._twitch_set_to_channel[42] = "twitch_tfaces"; + this._twitch_set_to_channel[0] = "global"; + this._twitch_set_to_channel[33] = "tfaces"; + this._twitch_set_to_channel[42] = "tfaces"; + + this._twitch_badges["global"] = "//cdn.frankerfacez.com/script/twitch_logo.png"; + this._twitch_badges["tfaces"] = this._twitch_badges["turbo"] = "//cdn.frankerfacez.com/script/turbo_badge.png"; } @@ -4380,329 +5604,216 @@ FFZ.menu_pages.my_emotes = { }, render: function(view, container) { - var emotes = get_emotes(this), f = this; + var tmi = view.get('controller.currentRoom.tmiSession'), + twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets'], + needed_sets = []; - new RSVP.Promise(function(done) { - var needed_sets = []; - for(var i=0; i < emotes[0].length; i++) { - var set_id = emotes[0][i]; - if ( ! f._twitch_emote_sets[set_id] ) - needed_sets.push(set_id); - } + for(var set_id in twitch_sets) + if ( twitch_sets.hasOwnProperty(set_id) && ! this._twitch_set_to_channel.hasOwnProperty(set_id) ) + needed_sets.push(set_id); - RSVP.all([ - new RSVP.Promise(function(d) { - if ( ! needed_sets.length ) - return d(); + if ( ! needed_sets.length ) + return FFZ.menu_pages.my_emotes.draw_menu.bind(this)(view, container, twitch_sets); - Twitch.api.get("chat/emoticon_images", {emotesets: needed_sets.join(",")}, {version: 3}) - .done(function(data) { - if ( data.emoticon_sets ) { - for(var set_id in data.emoticon_sets) { - if ( ! data.emoticon_sets.hasOwnProperty(set_id) ) - continue; + container.innerHTML = JSON.stringify(needed_sets); + }, - var set = f._twitch_emote_sets[set_id] = f._twitch_emote_sets[set_id] || {}, - emotes = data.emoticon_sets[set_id]; + draw_twitch_set: function(view, set_id, set) { + var heading = document.createElement('div'), + menu = document.createElement('div'), - // Sort Emoticons - emotes.sort(function(a,b) { - var a = (KNOWN_CODES[a.code] ? "000" + KNOWN_CODES[a.code] : a.code).toLowerCase(), - b = (KNOWN_CODES[b.code] ? "000" + KNOWN_CODES[b.code] : b.code).toLowerCase(); + channel_id = this._twitch_set_to_channel[set_id], title; - if ( a == "000grayface" ) a = "grayface"; - if ( b == "000grayface" ) b = "grayface"; - - if ( a < b ) return -1; - else if ( a > b ) return 1; - return 0; - }); - - set.emotes = emotes; - set.source = "Twitch"; - } - } - d(); - }).fail(function() { - d(); - }); - }), - new RSVP.Promise(function(d) { - if ( ! needed_sets.length ) - return d(); - - var promises = [], - old_needed = needed_sets, - handle_set = function(id, name) { - var set = f._twitch_emote_sets[id] = f._twitch_emote_sets[id] || {}; - - if ( !name || BANNED_SETS[name] ) - return; - - if ( name == "twitch_global" ) { - FFZ.capitalization["global emoticons"] = ["Global Emoticons", Date.now()]; - set.channel = "Global Emoticons"; - set.badge = "//cdn.frankerfacez.com/channel/global/twitch_logo.png"; - return; - } - - if ( name == "turbo" || name == "twitch_tfaces" ) { - set.channel = "Twitch Turbo"; - set.badge = "//cdn.frankerfacez.com/script/turbo_badge.png"; - return; - } - - // Badge Lookup - promises.push(new RSVP.Promise(function(set, name, dn) { - Twitch.api.get("chat/" + name + "/badges", null, {version: 3}) - .done(function(data) { - if ( data.subscriber && data.subscriber.image ) - set.badge = data.subscriber.image; - dn(); - }).fail(dn)}.bind(this,set,name))); - - // Mess Up Capitalization - var lname = name.toLowerCase(), - old_data = FFZ.capitalization[lname]; - if ( old_data && Date.now() - old_data[1] < 3600000 ) { - set.channel = old_data[0]; - return; - } - - promises.push(new RSVP.Promise(function(set, lname, name, dn) { - if ( ! f.ws_send("get_display_name", lname, function(success, data) { - var cap_name = success ? data : name; - FFZ.capitalization[lname] = [cap_name, Date.now()]; - set.channel = cap_name; - dn(); - }) ) { - // Can't use socket. - set.channel = name; - dn(); - } - - // Timeout - setTimeout(function(set,name,dn) { - if ( ! set.channel ) - set.channel = name; - dn(); - }.bind(this,set,name,dn), 500); - }.bind(this, set, lname, name))); - }, - handle_promises = function() { - if ( promises.length ) - RSVP.all(promises).then(d,d); - else - d(); - }; - - // Process all the sets we already have. - needed_sets = []; - for(var i=0;i 0 ) { - f.ws_send("twitch_sets", needed_sets, function(success, data) { - needed_sets = []; - if ( success ) { - for(var set_id in data) { - if ( ! data.hasOwnProperty(set_id) ) - continue; - - f._twitch_set_to_channel[set_id] = data[set_id]; - handle_set(set_id, data[set_id]); - } - - localStorage.ffzTwitchSets = JSON.stringify(f._twitch_set_to_channel); - } - - handle_promises(); - }); - - // Timeout! - setTimeout(function() { - if ( needed_sets.length ) - handle_promises(); - }, 2000); - - } else - handle_promises(); - }) - ]).then(function() { - var sets = {}; - for(var i=0; i < emotes[0].length; i++) { - var set_id = emotes[0][i]; - if ( f._twitch_emote_sets[set_id] ) - sets[set_id] = f._twitch_emote_sets[set_id]; - } - done(sets); - }, function() { done({}); }) - }).then(function(twitch_sets) { - try { - - // Don't override a different page. We can wait. - if ( container.getAttribute('data-page') != "my_emotes" ) - return; - - container.innerHTML = ""; - - var ffz_sets = emotes[1], - sets = []; - - for(var set_id in twitch_sets) { - if ( ! twitch_sets.hasOwnProperty(set_id) ) - continue; - - var set = twitch_sets[set_id]; - if ( set.channel && set.emotes && set.emotes.length ) - sets.push([1, set.channel, set]); - } - - for(var i=0; i < ffz_sets.length; i++) { - var set_id = ffz_sets[i], - set = f.emote_sets[set_id]; - - if ( f.feature_friday && set_id == f.feature_friday.set ) - continue; - - if ( set.count > 0 ) - sets.push([2, set.id, set]); - } - - sets.sort(function(a,b) { - if ( a[0] < b[0] ) return -1; - else if ( a[0] > b[0] ) return 1; - - var an = a[1].toLowerCase(), - bn = b[1].toLowerCase(); - - if ( an === "twitch turbo" || an === "twitch_tfaces" ) - an = "zza|" + an; - - else if ( an === "global emoticons" ) - an = "zzz|" + an; - - if ( bn === "twitch turbo" || bn === "twitch_tfaces" ) - bn = "zza|" + bn; - else if ( bn === "global emoticons" ) - bn = "zzz|" + bn; - - if ( an < bn ) return -1; - else if ( an > bn ) return 1; - return 0; - }); - - for(var i=0; i < sets.length; i++) { - var ffz_set = sets[i][0] === 2, - set = sets[i][2], - heading = document.createElement('div'), - menu = document.createElement('div'), - - source = ffz_set ? "FrankerFaceZ" : set.source, - badge, title, ems; - - if ( ffz_set ) { - ems = []; - for(var emote_id in set.emotes) { - var emote = set.emotes[emote_id]; - if ( emote.hidden ) - continue; - - ems.push({code: emote.name, url: emote.url, width: emote.width, height: emote.height}); - } - - if ( set.id === "global" ) - title = "Global Emoticons"; - else - title = set.title || set.id; - - badge = set.icon || "http://cdn.frankerfacez.com/channel/global/devicon.png"; - - } else { - ems = set.emotes; - title = set.channel == "Twitch Turbo" ? set.channel : FFZ.get_capitalization(set.channel); - badge = set.badge; - } - - if ( ! ems.length ) - continue; - - heading.className = 'heading'; - heading.innerHTML = '' + source + '' + title; - if ( badge ) - heading.style.backgroundImage = 'url("' + badge + '")'; - - menu.className = 'emoticon-grid'; - menu.appendChild(heading); - - for(var x=0; x < ems.length; x++) { - var emote = ems[x], - code = KNOWN_CODES[emote.code] || emote.code; - - var s = document.createElement('span'); - s.className = 'emoticon tooltip'; - s.style.backgroundImage = 'url("' + (emote.url ? emote.url : (TWITCH_BASE + emote.id + '/1.0')) + '")'; - - if ( emote.height ) - s.style.height = emote.height + "px"; - - if ( emote.width ) - s.style.width = emote.width + "px"; - - if ( ! emote.url ) { - var img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; - s.style.backgroundImage = '-webkit-' + img_set; - s.style.backgroundImage = '-moz-' + img_set; - s.style.backgroundImage = '-ms-' + img_set; - s.style.backgroundImage = img_set; - } - - s.title = code; - s.addEventListener('click', f._add_emote.bind(f, view, code)); - menu.appendChild(s); - } - - container.appendChild(menu); - } - - if ( ! sets.length ) { - var menu = document.createElement('div'); - - menu.className = 'chat-menu-content center'; - menu.innerHTML = "Error Loading Subscriptions"; - - container.appendChild(menu); - } - - } catch(err) { - f.log("My Emotes Menu Error", err); - container.innerHTML = ""; - - var menu = document.createElement('div'), - heading = document.createElement('div'), - p = document.createElement('p'); - - heading.className = 'heading'; - heading.innerHTML = 'Error Loading Menu'; - menu.appendChild(heading); - - p.className = 'clearfix'; - p.textContent = err; - menu.appendChild(p); - - menu.className = 'chat-menu-content'; - container.appendChild(menu); - } + if ( channel_id === "global" ) + title = "Global Emoticons"; + else if ( channel_id === "turbo" ) + title = "Twitch Turbo"; + else + title = FFZ.get_capitalization(channel_id, function(name) { + heading.innerHTML = 'Twitch' + utils.sanitize(name); }); - } - }; -},{"../constants":3}],24:[function(require,module,exports){ + heading.className = 'heading'; + heading.innerHTML = 'Twitch' + utils.sanitize(title); + + if ( this._twitch_badges[channel_id] ) + heading.style.backgroundImage = 'url("' + this._twitch_badges[channel_id] + '")'; + else { + var f = this; + Twitch.api.get("chat/" + channel_id + "/badges", null, {version: 3}) + .done(function(data) { + if ( data.subscriber && data.subscriber.image ) { + f._twitch_badges[channel_id] = data.subscriber.image; + heading.style.backgroundImage = 'url("' + data.subscriber.image + '")'; + } + }); + } + + menu.className = 'emoticon-grid'; + menu.appendChild(heading); + + for(var i=0; i < set.length; i++) { + var emote = set[i], + code = KNOWN_CODES[emote.code] || emote.code, + + em = document.createElement('span'), + img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; + + em.className = 'emoticon tooltip'; + em.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")'; + em.style.backgroundImage = '-webkit-' + img_set; + em.style.backgroundImage = '-moz-' + img_set; + em.style.backgroundImage = '-ms-' + img_set; + em.style.backgroudnImage = img_set; + + em.title = code; + em.addEventListener("click", this._add_emote.bind(this, view, code)); + menu.appendChild(em); + } + + return menu; + }, + + draw_ffz_set: function(view, set) { + var heading = document.createElement('div'), + menu = document.createElement('div'), + emotes = []; + + heading.className = 'heading'; + heading.innerHTML = 'FrankerFaceZ' + set.title; + heading.style.backgroundImage = 'url("' + (set.icon || '//cdn.frankerfacez.com/script/devicon.png') + '")'; + + menu.className = 'emoticon-grid'; + menu.appendChild(heading); + + for(var emote_id in set.emoticons) + set.emoticons.hasOwnProperty(emote_id) && ! set.emoticons[emote_id].hidden && emotes.push(set.emoticons[emote_id]); + + emotes.sort(function(a,b) { + var an = a.name.toLowerCase(), + bn = b.name.toLowerCase(); + + if ( an < bn ) return -1; + else if ( an > bn ) return 1; + if ( a.id < b.id ) return -1; + if ( a.id > b.id ) return 1; + return 0; + }); + + for(var i=0; i < emotes.length; i++) { + var emote = emotes[i], + + em = document.createElement('span'), + img_set = 'image-set(url("' + emote.urls[1] + '") 1x'; + + if ( emote.urls[2] ) + img_set += ', url("' + emote.urls[2] + '") 2x'; + + if ( emote.urls[4] ) + img_set += ', url("' + emote.urls[4] + '") 4x'; + + img_set += ')'; + + em.className = 'emoticon tooltip'; + em.style.backgroundImage = 'url("' + emote.urls[1] + '")'; + em.style.backgroundImage = '-webkit-' + img_set; + em.style.backgroundImage = '-moz-' + img_set; + em.style.backgroundImage = '-ms-' + img_set; + em.style.backgroudnImage = img_set; + + if ( emote.height ) + em.style.height = emote.height + "px"; + if ( emote.width ) + em.style.width = emote.width + "px"; + + em.title = emote.tooltip || emote.name; + em.addEventListener("click", this._add_emote.bind(this, view, emote.name)); + menu.appendChild(em); + } + + return menu; + }, + + draw_menu: function(view, container, twitch_sets) { + // Make sure we're still on the My Emoticons page. Since this is + // asynchronous, the user could've tabbed away. + if ( container.getAttribute('data-page') !== 'my_emotes' ) + return; + + container.innerHTML = ""; + try { + var user = this.get_user(), + ffz_sets = this.getEmotes(user && user.login, null), + sets = []; + + // Start with Twitch Sets + for(var set_id in twitch_sets) { + if ( ! twitch_sets.hasOwnProperty(set_id) || ( ! this.settings.global_emotes_in_menu && set_id === '0' ) ) + continue; + + var set = twitch_sets[set_id]; + if ( ! set.length ) + continue; + + sets.push([this._twitch_set_to_channel[set_id], FFZ.menu_pages.my_emotes.draw_twitch_set.bind(this)(view, set_id, set)]); + } + + + // Now, FFZ! + for(var i=0; i < ffz_sets.length; i++) { + var set_id = ffz_sets[i], + set = this.emote_sets[set_id]; + + if ( ! set || ! set.count || ( ! this.settings.global_emotes_in_menu && this.default_sets.indexOf(set_id) !== -1 ) ) + continue; + + sets.push([set.title.toLowerCase(), FFZ.menu_pages.my_emotes.draw_ffz_set.bind(this)(view, set)]); + } + + + // Finally, sort and add them all. + sets.sort(function(a,b) { + var an = a[0], bn = b[0]; + if ( an === "turbo" || an === "tfaces" ) + an = "zza|" + an; + else if ( an === "global" || an === "global emoticons" ) + an = "zzz|" + an; + + if ( bn === "turbo" || bn === "tfaces" ) + bn = "zza|" + bn; + else if ( bn === "global" || bn === "global emoticons" ) + bn = "zzz|" + bn; + + if ( an < bn ) return -1; + if ( an > bn ) return 1; + return 0; + }); + + for(var i=0; i < sets.length; i++) + container.appendChild(sets[i][1]); + + } catch(err) { + this.error("my_emotes draw_menu: " + err); + container.innerHTML = ""; + + var menu = document.createElement('div'), + heading = document.createElement('div'), + p = document.createElement('p'); + + heading.className = 'heading'; + heading.innerHTML = 'Error Loading Menu'; + menu.appendChild(heading); + + p.className = 'clearfix'; + p.textContent = err; + menu.appendChild(p); + + menu.className = 'chat-menu-content'; + container.appendChild(menu); + } + } +}; +},{"../constants":3,"../utils":29}],24:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -5160,7 +6271,7 @@ FFZ.prototype._update_race = function(not_timer) { } } } -},{"../utils":28}],26:[function(require,module,exports){ +},{"../utils":29}],26:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -5190,8 +6301,112 @@ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'); + +// ------------------- +// Subscriber Display +// ------------------- + +FFZ.prototype._update_subscribers = function() { + if ( this._update_subscribers_timer ) { + clearTimeout(this._update_subscribers_timer); + delete this._update_subscribers_timer; + } + + var user = this.get_user(), f = this, + match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, + id = this.is_dashboard && match && match[1]; + + if ( this.has_bttv || ! id || id !== user.login ) { + var el = document.querySelector("#ffz-sub-display"); + if ( el ) + el.parentElement.removeChild(el); + return; + } + + // Schedule an update. + this._update_subscribers_timer = setTimeout(this._update_subscribers.bind(this), 60000); + + // Spend a moment wishing we could just hit the subscribers API from the + // context of the web user. + + // Get the count! + jQuery.ajax({url: "/broadcast/dashboard/partnership"}).done(function(data) { + try { + var html = document.createElement('span'), dash; + + html.innerHTML = data; + dash = html.querySelector("#dash_main"); + + var match = dash && dash.textContent.match(/([\d,\.]+) total active subscribers/), + sub_count = match && match[1]; + + if ( ! sub_count ) { + var el = document.querySelector("#ffz-sub-display"); + if ( el ) + el.parentElement.removeChild(el); + + if ( f._update_subscribers_timer ) { + clearTimeout(f._update_subscribers_timer); + delete f._update_subscribers_timer; + } + + return; + } + + var el = document.querySelector('#ffz-sub-display span'); + if ( ! el ) { + var cont = f.is_dashboard ? document.querySelector("#stats") : document.querySelector("#channel .stats-and-actions .channel-stats"); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-sub-display'; + stat.title = 'Active Channel Subscribers'; + + stat.innerHTML = constants.STAR + ' '; + + el = document.createElement('span'); + stat.appendChild(el); + + Twitch.api.get("chat/" + id + "/badges", null, {version: 3}) + .done(function(data) { + if ( data.subscriber && data.subscriber.image ) { + stat.innerHTML = ''; + stat.appendChild(el); + + stat.style.backgroundImage = 'url("' + data.subscriber.image + '")'; + stat.style.backgroundRepeat = 'no-repeat'; + stat.style.paddingLeft = '23px'; + stat.style.backgroundPosition = '0 50%'; + } + }); + + cont.appendChild(stat); + jQuery(stat).tipsy(f.is_dashboard ? {"gravity":"s"} : undefined); + } + + el.innerHTML = utils.number_commas(parseInt(sub_count)); + + } catch(err) { + f.error("_update_subscribers: " + err); + } + }).fail(function(){ + var el = document.querySelector("#ffz-sub-display"); + if ( el ) + el.parentElement.removeChild(el); + return; + });; +} + +},{"../constants":3,"../utils":29}],28:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'); + + // ------------ -// Set Viewers +// FFZ Viewers // ------------ FFZ.ws_commands.viewers = function(data) { @@ -5201,21 +6416,32 @@ FFZ.ws_commands.viewers = function(data) { match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, id = this.is_dashboard ? match && match[1] : controller && controller.get && controller.get('id'); - if ( id !== channel ) + if ( ! this.is_dashboard ) { + var room = this.rooms && this.rooms[channel]; + if ( room ) { + room.ffz_chatters = count; + if ( this._cindex ) + this._cindex.ffzUpdateChatters(); + } + return; + } + + + if ( ! this.settings.chatter_count || id !== channel ) return; - var view_count = document.querySelector('#ffz-viewer-display'), + var view_count = document.querySelector('#ffz-ffzchatter-display'), content = constants.ZREKNARF + ' ' + utils.number_commas(count); if ( view_count ) view_count.innerHTML = content; else { - var parent = document.querySelector(this.is_dashboard ? "#stats" : '.stats-and-actions .channel-stats'); + var parent = document.querySelector("#stats"); if ( ! parent ) return; view_count = document.createElement('span'); - view_count.id = "ffz-viewer-display"; + view_count.id = "ffz-ffzchatter-display"; view_count.className = 'ffz stat'; view_count.title = 'Chatters with FrankerFaceZ'; view_count.innerHTML = content; @@ -5224,7 +6450,7 @@ FFZ.ws_commands.viewers = function(data) { jQuery(view_count).tipsy(this.is_dashboard ? {"gravity":"s"} : undefined); } } -},{"../constants":3,"../utils":28}],28:[function(require,module,exports){ +},{"../constants":3,"../utils":29}],29:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); diff --git a/script.min.js b/script.min.js index e6bd3440..af2cd4ae 100644 --- a/script.min.js +++ b/script.min.js @@ -1,3 +1,4 @@ -!function(e){!function t(e,n,a){function o(s,r){if(!n[s]){if(!e[s]){var l="function"==typeof require&&require;if(!r&&l)return l(s,!0);if(i)return i(s,!0);throw new Error("Cannot find module '"+s+"'")}var c=n[s]={exports:{}};e[s][0].call(c.exports,function(t){var n=e[s][1][t];return o(n?n:t)},c,c.exports,t,e,n,a)}return n[s].exports}for(var i="function"==typeof require&&require,s=0;se&&this._legacy_load_bots(e))})},n.prototype._legacy_load_donors=function(e){jQuery.ajax(a.SERVER+"script/donors.txt",{cache:!1,context:this}).done(function(e){this._legacy_parse_badges(e,1,1)}).fail(function(t){return 404!=t.status?(e=(e||0)+1,10>e?this._legacy_load_donors(e):void 0):void 0})},n.prototype._legacy_parse_badges=function(e,t,n){var a=this.badges[n].title,i=0;if(ds=1==n?".donor":"",null!=e)for(var s=e.trim().split(/\W+/),r=0;r50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var a=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/unmod "+o)}return"Sent unmod command for "+a+" users."},t.ffz_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.ffz_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var n=this.get_user();if(!n||!n.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var a=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/mod "+o)}return"Sent mod command for "+a+" users."},t.ffz_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var n='',a="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:a,SERVER:a?"//localhost:8000/":"//cdn.frankerfacez.com/",SVGPATH:n,ZREKNARF:''+n+"",CHAT_BUTTON:''+n+"",CLOCK:'',GEAR:'',HEART:'',EMOTE:''}},{}],4:[function(){var t=e.FrankerFaceZ;t.settings_info.developer_mode={type:"boolean",value:!1,storage_key:"ffzDebugMode",visible:function(){return this.settings.developer_mode||Date.now()-parseInt(localStorage.ffzLastDevMode||"0")<6048e5},category:"Debugging",name:"Developer Mode",help:"Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",on_update:function(){localStorage.ffzLastDevMode=Date.now()}},t.ffz_commands.developer_mode=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Developer Mode is currently "+(this.settings.developer_mode?"enabled.":"disabled."):(this.settings.set("developer_mode",n),"Developer Mode is now "+(n?"enabled":"disabled")+". Please refresh your browser.")},t.ffz_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[function(t){var n=e.FrankerFaceZ,a=t("../utils"),o=t("../constants");n.prototype.setup_channel=function(){this.channels={},this.log("Creating channel style element.");var e=this._channel_style=document.createElement("style");e.id="ffz-channel-css",document.head.appendChild(e),this.log("Hooking the Ember Channel view.");var t=App.__container__.lookup("controller:channel"),n=this;t&&(t.reopen({ffzUpdateUptime:function(){n.update_uptime()}.observes("isLive","content.id").on("init")}),this.update_uptime())},n.settings_info.stream_uptime={type:"boolean",value:!1,category:"Channel Metadata",name:"Stream Uptime",help:"Display the stream uptime under a channel by the viewer count.",on_update:function(){this.update_uptime()}},n.prototype.update_uptime=function(e){this._uptime_update&&(clearTimeout(this._uptime_update),delete this._uptime_update);var t=App.__container__.lookup("controller:channel");if(e||!this.settings.stream_uptime||!t||!t.get("isLiveAccordingToKraken")){var n=document.querySelector("#ffz-uptime-display");return void(n&&n.parentElement.removeChild(n))}this._update_uptime=setTimeout(this.update_uptime.bind(this),1e3);var i=t.get("content.stream.created_at");if(i&&(i=a.parse_date(i))){var s=Math.floor((Date.now()-i.getTime())/1e3);if(!(0>s)){var n=document.querySelector("#ffz-uptime-display span");if(!n){var r=document.querySelector("#channel .stats-and-actions .channel-stats");if(!r)return;var l=document.createElement("span");l.className="ffz stat",l.id="ffz-uptime-display",l.title="Stream Uptime (since "+i.toLocaleString()+")",l.innerHTML=o.CLOCK+" ",n=document.createElement("span"),l.appendChild(n);var c=r.querySelector(".live-count");if(c)r.insertBefore(l,c.nextSibling);else try{c=r.querySelector("script:nth-child(0n+2)"),r.insertBefore(l,c.nextSibling)}catch(u){r.insertBefore(l,r.childNodes[0])}jQuery(l).tipsy({html:!0})}n.innerHTML=a.time_to_string(s)}}}},{"../constants":3,"../utils":28}],6:[function(){var t=e.FrankerFaceZ;t.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e);try{e.create().destroy()}catch(t){}for(var n in Ember.View.views)if(Ember.View.views.hasOwnProperty(n)){var a=Ember.View.views[n];if(a instanceof e){this.log("Adding UI link manually to Chat view.",a);try{a.$(".textarea-contain").append(this.build_ui_link(a))}catch(t){this.error("setup: build_ui_link: "+t)}}}},t.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.$()&&this.$(".textarea-contain").append(t.build_ui_link(this))}catch(e){t.error("didInsertElement: build_ui_link: "+e)}},willClearRender:function(){this._super();try{this.$(".ffz-ui-toggle").remove()}catch(e){t.error("willClearRender: remove ui link: "+e)}},ffzUpdateLink:Ember.observer("controller.currentRoom",function(){try{t.update_ui_link()}catch(e){t.error("ffzUpdateLink: update_ui_link: "+e)}})})}},{}],7:[function(t){var n=e.FrankerFaceZ,a=t("../utils"),o=function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},i="[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]",s=new RegExp(i+"*,"+i+"*"),r=function(e){return(e+"").replace(/&/g,"&").replace(/'/g,"'").replace(/"/g,""").replace(//g,">")},l="http://static-cdn.jtvnw.net/emoticons/v1/",c=function(e){return l+e+"/1.0 1x, "+l+e+"/2.0 2x, "+l+e+"/3.0 4x"},u=function(e){var t=e.set,n=e.set_type;return void 0===n&&(n="Channel"),t?(("00000turbo"==t||"turbo"==t)&&(t="Twitch Turbo",n=null),"Emoticon: "+e.code+"\n"+(n?n+": ":"")+t):e.code},d=function(e){{var t=this._twitch_emotes[e];t?t.set:null}return t?"string"==typeof t?t:t.tooltip?t.tooltip:t.tooltip=u(t):"???"},h=function(e,t,n,a){if(n){t&&(a.code=t),this._twitch_emotes[e]=a;for(var o=d.bind(this)(e),i=document.querySelectorAll('img[emote-id="'+e+'"]'),s=0;sYouTube: "+a.sanitize(n.title)+"
",t+="Channel: "+a.sanitize(n.channel)+" | "+a.time_to_string(n.duration)+"
",t+=a.number_commas(n.views||0)+" Views | 👍 "+a.number_commas(n.likes||0)+" 👎 "+a.number_commas(n.dislikes||0);else if("strawpoll"==n.type){t="Strawpoll: "+a.sanitize(n.title)+"
";for(var o in n.items){{var i=n.items[o];Math.floor(i/n.total*100)}t+='"}t+="
'+a.sanitize(o)+''+a.number_commas(i)+"

Total: "+a.number_commas(n.total);var s=a.parse_date(n.fetched);if(s){var r=Math.floor((s.getTime()-Date.now())/1e3);r>60&&(t+="
Data was cached "+a.time_to_string(r)+" ago.")}}else if("twitch"==n.type){t="Twitch: "+a.sanitize(n.display_name)+"
";var l=a.parse_date(n.since);l&&(t+="Member Since: "+a.date_string(l)+"
"),t+="Views: "+a.number_commas(n.views)+" | Followers: "+a.number_commas(n.followers)+""}else if("twitter"==n.type)t="Tweet By: "+a.sanitize(n.user)+"
",t+=a.sanitize(n.tweet);else if("reputation"==n.type){if(t=''+a.sanitize(n.full.toLowerCase())+"",n.trust<50||n.safety<50||n.tags&&n.tags.length>0){t+="
";var c=!1;(n.trust<50||n.safety<50)&&(n.unsafe=!0,t+="Potentially Unsafe Link
",t+="Trust: "+n.trust+"% | Child Safety: "+n.safety+"%",c=!0),n.tags&&n.tags.length>0&&(t+=(c?"
":"")+"Tags: "+n.tags.join(", ")),t+="
Data Source: WOT"}}else n.full&&(t=''+a.sanitize(n.full.toLowerCase())+"");return t||(t=''+a.sanitize(e.toLowerCase())+""),n.tooltip=t,t},m=function(e,t,n){if(t){this._link_data[e]=n,n.unsafe=!1;var a,o=p.bind(this)(e),i="/"==e.charAt(e.length-1)?e.substr(0,e.length-1):null;if(a=document.querySelectorAll(i?'span.message a[href="'+e+'"], span.message a[href="'+i+'"], span.message a[data-url="'+e+'"], span.message a[data-url="'+i+'"]':'span.message a[href="'+e+'"], span.message a[data-url="'+e+'"]'),this.settings.link_info)for(var s=0;sBeta",help:"Check links against known bad websites, unshorten URLs, and show YouTube info."},n.settings_info.chat_rows={type:"boolean",value:!1,category:"Chat",no_bttv:!0,name:"Chat Line Backgrounds",help:"Display alternating background colors for lines in chat.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-background",e)}},n.prototype.setup_line=function(){document.body.classList.toggle("ffz-chat-colors",!this.has_bttv&&this.settings.fix_color),document.body.classList.toggle("ffz-chat-background",!this.has_bttv&&this.settings.chat_rows),this._colors={},this._last_row={};var t=this._fix_color_style=document.createElement("style");t.id="ffz-style-username-colors",t.type="text/css",document.head.appendChild(t),this._twitch_emotes={},this._link_data={},this.log("Hooking the Ember Line controller.");var a=App.__container__.resolve("controller:line"),o=this;a.reopen({tokenizedMessage:function(){var e=this._super();try{var t=performance.now();e=o._remove_banned(e),e=o._emoticonize(this,e);var a=o.get_user();a&&this.get("model.from")==a.login||(e=o._mentionize(this,e));var i=this.get("model.tags.display-name");i&&i.length&&(n.capitalization[this.get("model.from")]=[i.trim(),Date.now()]);var s=performance.now();s-t>5&&o.log("Tokenizing Message Took Too Long - "+(s-t)+"ms",e,!1,!0)}catch(r){try{o.error("LineController tokenizedMessage: "+r)}catch(r){}}return e}.property("model.message","isModeratorOrHigher")}),this.log("Hooking the Ember Line view.");var a=App.__container__.resolve("view:line");a.reopen({didInsertElement:function(){this._super();try{var t=performance.now(),a=this.get("element"),i=this.get("context"),s=i.get("model.from"),r=i.get("parentController.content.id"),l=i.get("model.color"),d=i.get("model.ffz_alternate");l&&o._handle_color(l),void 0===d&&(d=o._last_row[r]=o._last_row.hasOwnProperty(r)?!o._last_row[r]:!1,this.set("context.model.ffz_alternate",d)),a.classList.toggle("ffz-alternate",d),a.setAttribute("data-room",r),a.setAttribute("data-sender",s),a.setAttribute("data-deleted",i.get("model.deleted")),o.render_badge(this);var p=a.querySelector("span.mentioned");if(p&&(a.classList.add("ffz-mentioned"),o.settings.highlight_notifications&&!document.hasFocus()&&!this.get("context.model.ffz_notified"))){var _=n.get_capitalization(r),f=n.get_capitalization(s),g=_,v=this.get("context.model.message");this.get("context.parentController.content.isGroupRoom")&&(g=this.get("context.parentController.content.tmiRoom.displayName")),v="action"==this.get("context.model.style")?"* "+f+" "+v:f+": "+v,o.show_notification(v,"Twitch Chat Mention in "+g,_,6e4,e.focus.bind(e))}this.set("context.model.ffz_notified",!0);for(var b=a.querySelectorAll("a.deleted-link"),y=0;y-1&&(-1===t.indexOf("/")||t.indexOf("@")5&&o.log("Line Took Too Long - "+P+"ms",a.innerHTML,!1,!0)}catch(H){try{o.error("LineView didInsertElement: "+H)}catch(H){}}}});var i=this.get_user();i&&i.name&&(n.capitalization[i.login]=[i.name,Date.now()])},n.prototype._handle_color=function(e){if(e&&!this._colors[e]){this._colors[e]=!0;var t=parseInt(e.substr(1),16),n=[t>>16,t>>8&255,255&t],o=a.get_luminance(n),i="",s='span[style="color:'+e+'"]',r=!1;if(o>.3){r=!0;for(var l=127,c=n;l--&&(c=a.darken(c),!(a.get_luminance(c)<=.3)););i+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+s+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+s+" { color: "+a.rgb_to_css(c)+" !important; }\n"}else i+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+s+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+s+" { color: "+e+" !important; }\n";if(.15>o){r=!0;for(var l=127,c=n;l--&&(c=a.brighten(c),!(a.get_luminance(c)>=.15)););i+=".ffz-chat-colors .theatre .chat-container .chat-line "+s+", .ffz-chat-colors .chat-container.dark .chat-line "+s+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+s+" { color: "+a.rgb_to_css(c)+" !important; }\n"}else i+=".ffz-chat-colors .theatre .chat-container .chat-line "+s+", .ffz-chat-colors .chat-container.dark .chat-line "+s+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+s+" { color: "+e+" !important; }\n";r&&(this._fix_color_style.innerHTML+=i)}},n.capitalization={},n._cap_fetching=0,n.get_capitalization=function(t,a){if(e.BetterTTV&&BetterTTV.chat&&BetterTTV.chat.helpers.lookupDisplayName)return BetterTTV.chat.helpers.lookupDisplayName(t);if(!t)return t;if(t=t.toLowerCase(),"jtv"==t||"twitchnotify"==t)return t;var o=n.capitalization[t];return o&&Date.now()-o[1]<36e5?o[0]:(n._cap_fetching<25&&(n._cap_fetching++,n.get().ws_send("get_display_name",t,function(e,o){var i=e?o:t;n.capitalization[t]=[i,Date.now()],n._cap_fetching--,"function"==typeof a&&a(i)})),o?o[0]:t)},n._regex_cache={},n._get_regex=function(e){return n._regex_cache[e]=n._regex_cache[e]||RegExp("\\b"+o(e)+"\\b","ig")},n._words_to_regex=function(e){var t=n._regex_cache[e];if(!t){for(var a="",s=0;s<banned link>',own:!0}:s)}return o},n.prototype._emoticonize=function(e,t){var n=e.get("parentController.model.id"),a=e.get("model.from"),o=this,i=this.getEmotes(a,n),s=[];return _.each(i,function(e){var n=o.emote_sets[e];n&&_.each(n.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&s.push(e)})}),s.length?("string"==typeof t&&(t=[t]),_.each(s,function(e){var n={isEmoticon:!0,cls:e.klass,srcSet:e.url+" 1x",emoticonSrc:e.url+'" data-ffz-emote="'+encodeURIComponent(JSON.stringify([e.id,e.set_id])),altText:e.hidden?"???":e.name};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var a=t.split(e.regex),o=[];return a.forEach(function(e,t){o.push(e),t!==a.length-1&&o.push(n)}),o})))}),t):t}},{"../utils":28}],8:[function(t){var n=e.FrankerFaceZ,a=t("../utils"),o={ESC:27,P:80,B:66,T:84,U:85},i=[["5m",300],["10m",600],["1hr",3600],["12hr",43200],["24hr",86400]],s='',r='';n.settings_info.enhanced_moderation={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Enhanced Moderation",help:"Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards."},n.prototype.setup_mod_card=function(){this.log("Hooking the Ember Moderation Card view.");var t=App.__container__.resolve("component:moderation-card"),n=this;t.reopen({didInsertElement:function(){this._super(),e._card=this;try{if(n.has_bttv||!n.settings.enhanced_moderation)return;var t=this.get("element"),l=this.get("controller");if(t.classList.add("ffz-moderation-card"),l.get("cardInfo.isModeratorOrHigher")){t.classList.add("ffz-is-mod"),t.setAttribute("tabindex",1),t.addEventListener("keyup",function(e){var t=e.keyCode||e.which,n=l.get("cardInfo.user.id"),a=App.__container__.lookup("controller:chat").get("currentRoom");if(t==o.P)a.send("/timeout "+n+" 1");else if(t==o.B)a.send("/ban "+n);else if(t==o.T)a.send("/timeout "+n+" 600");else if(t==o.U)a.send("/unban "+n);else if(t!=o.ESC)return;l.send("hideModOverlay")});var c=document.createElement("div");c.className="interface clearfix"; -var u=function(e){var t=l.get("cardInfo.user.id"),n=App.__container__.lookup("controller:chat").get("currentRoom");n.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},d=function(e,t){var n=document.createElement("button");return n.className="button",n.innerHTML=e,n.title="Timeout User for "+a.number_commas(t)+" Second"+(1!=t?"s":""),600===t?n.title="(T)"+n.title.substr(1):1===t&&(n.title="(P)urge - "+n.title),jQuery(n).tipsy(),n.addEventListener("click",u.bind(this,t)),n};c.appendChild(d("Purge",1));var h=document.createElement("span");h.className="right",c.appendChild(h);for(var p=0;p button");b&&b.classList.contains("message-button")&&(b.innerHTML=s,b.classList.add("glyph-only"),b.classList.add("message"),b.title="Message User",jQuery(b).tipsy()),this.$().draggable({start:function(){t.focus()}}),t.focus()}catch(y){try{n.error("ModerationCardView didInsertElement: "+y)}catch(y){}}}})},n.chat_commands.purge=n.chat_commands.p=function(e,t){if(!t||!t.length)return"Purge Usage: /p username [more usernames separated by spaces]";if(t.length>10)return"Please only purge up to 10 users at once.";for(var n=0;n10)return"Please only ban up to 10 users at once.";for(var n=0;n10)return"Please only unban up to 10 users at once.";for(var n=0;nn?this._legacy_add_room(e,t,n):void 0)})},n.prototype._legacy_load_room_css=function(e,t,n){var s=e,r=s.match(i);r&&r[1]&&(s=r[1]);var l={id:e,menu_sets:[s],sets:[s],moderator_badge:null,css:null};return n&&(n=n.replace(a,"").trim()),n&&(n=n.replace(o,function(e,t){return l.moderator_badge||"modicon.png"!==t.substr(-11)?e:(l.moderator_badge=t,"")})),l.css=n||null,this._load_room_json(e,t,l)}},{"../constants":3,"../utils":28}],10:[function(){var t=e.FrankerFaceZ;t.prototype.setup_viewers=function(){this.log("Hooking the Ember Viewers controller.");var e=App.__container__.resolve("controller:viewers");this._modify_viewers(e)},t.prototype._modify_viewers=function(e){var n=this;e.reopen({lines:function(){var e=this._super();try{var a=[],o={},i=null,s=App.__container__.lookup("controller:channel"),r=this.get("parentController.model.id"),l=s&&s.get("id");if(l){var c=s.get("display_name");c&&(t.capitalization[l]=[c,Date.now()])}r!=l&&(l=null);for(var u=0;un?this._legacy_load_set(e,t,n):"function"==typeof t&&t(!1))})},n.prototype._legacy_load_css=function(e,t,n){var i={},s={id:e,emotes:i,extra_css:null},r=this;"global"==e?s.title="Global":"globalevent"==e?s.title="Global Event":".donor"==e&&(s.title="Donor"),n=n.replace(a,function(t,n,a,o,s,c,u,d){s=parseInt(s),c=parseInt(c),u=l(u,s);var h="."===o.substr(o.lastIndexOf("/")+1,1),p=++r._last_emote_id,m={id:p,set_id:e,hidden:h,name:a,height:s,width:c,url:o,margins:u,extra_css:d};return i[p]=m,""}).trim(),n&&n.replace(o,function(e,t){s.icon||"modicon.png"!==t.substr(-11)||(s.icon=t)}),this._load_set_json(e,t,s)}},{"./constants":3,"./utils":28}],12:[function(){var t=e.FrankerFaceZ;t.prototype.find_bttv=function(t,n){return this.has_bttv=!1,e.BTTVLOADED?this.setup_bttv(n||0):void(n>=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(n||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,this.log("WOO"),document.body.classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background");var t=BetterTTV.chat.helpers.sendMessage,n=this;BetterTTV.chat.helpers.sendMessage=function(e){var a=e.split(" ",1)[0].toLowerCase();return"/ffz"!==a?t(e):void n.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var a,o=BetterTTV.chat.handlers.onPrivmsg;BetterTTV.chat.handlers.onPrivmsg=function(e,t){a=e;var n=o(e,t);return a=null,n};var i=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,o,s,r){try{return n.bttv_badges(r),'
'+BetterTTV.chat.templates.timestamp(r.time)+" "+(s?BetterTTV.chat.templates.modicons():"")+" "+BetterTTV.chat.templates.badges(r.badges)+BetterTTV.chat.templates.from(r.nickname,r.color)+BetterTTV.chat.templates.message(r.sender,r.message,r.emotes,t?r.color:!1)+"
"}catch(l){return n.log("Error: ",l),i(e,t,o,s,r)}};var s,r=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,a,o){try{o=o||!1;var i=encodeURIComponent(t);if("jtv"!==e){s=e;var l=BetterTTV.chat.templates.emoticonize(t,a);s=null;for(var c=0;c'+t+"
"}catch(u){return n.log("Error: ",u),r(e,t,a,o)}};var l=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var o=l(e,t),i=n.getEmotes(s,a),t=[];return _.each(i,function(e){var a=n.emote_sets[e];a&&_.each(a.emotes,function(e){_.any(o,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=[''+e.name+''],n=o;if(o=[],!n||!n.length)return o;for(var a=0;a=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(n||0)+t),t))},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),n=e?e.login:null,a=App.__container__.lookup("controller:chat"),o=a?a.get("currentRoom.id"):null,i=this.getEmotes(n,o),s=[],r=0;r=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(n||0)+t),t)))},n.prototype.setup_normal=function(t){var a=e.performance&&performance.now?performance.now():Date.now();this.log("Found non-Ember Twitch after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this.find_bttv(10);var o=e.performance&&performance.now?performance.now():Date.now(),i=o-a;this.log("Initialization complete in "+i+"ms")},n.prototype.is_dashboard=!1,n.prototype.setup_dashboard=function(t){var a=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch Dashboard after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.is_dashboard=!0,this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this.find_bttv(10);var o=e.performance&&performance.now?performance.now():Date.now(),i=o-a;this.log("Initialization complete in "+i+"ms")},n.prototype.setup_ember=function(t){var a=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_channel(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_mod_card(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_my_emotes(),this.setup_races(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var o=e.performance&&performance.now?performance.now():Date.now(),i=o-a;this.log("Initialization complete in "+i+"ms")}},{"./badges":1,"./commands":2,"./debug":4,"./ember/channel":5,"./ember/chatview":6,"./ember/line":7,"./ember/moderation-card":8,"./ember/room":9,"./ember/viewers":10,"./emoticons":11,"./ext/betterttv":12,"./ext/emote_menu":13,"./featurefriday":15,"./settings":16,"./shims":17,"./socket":18,"./ui/about_page":19,"./ui/dark":20,"./ui/menu":21,"./ui/menu_button":22,"./ui/my_emotes":23,"./ui/notifications":24,"./ui/races":25,"./ui/styles":26,"./ui/viewer_count":27}],15:[function(t){var n=e.FrankerFaceZ,a=t("./constants");n.prototype.feature_friday=null,n.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(a.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},n.ws_commands.reload_ff=function(){this.check_ff()},n.prototype._feature_friday_ui=function(e,t,n){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,n,[this.feature_friday.set],this.feature_friday.title,this.feature_friday.icon,"FrankerFaceZ");var a=App.__container__.lookup("controller:channel");if(!a||a.get("id")!=this.feature_friday.channel){var o=this.feature_friday,i=document.createElement("div"),s=document.createElement("a");i.className="chat-menu-content",i.style.textAlign="center";var r=o.display_name+(o.live?" is live now!":"");s.className="button primary",s.classList.toggle("live",o.live),s.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),s.href="http://www.twitch.tv/"+o.channel,s.title=r,s.target="_new",s.innerHTML=""+r+"",i.appendChild(s),t.appendChild(i)}}},n.prototype._load_ff=function(e){if(this.feature_friday){this.global_sets.removeObject(this.feature_friday.set);var t=this.emote_sets[this.feature_friday.set];t&&(t.global=!1),this.feature_friday=null,this.update_ui_link()}e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,title:e.title||"Feature Friday",display_name:n.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.load_set(e.set,this._update_ff_set.bind(this)),this._update_ff_live())},n.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},n.prototype._update_ff_set=function(e,t){t&&(t.global=!0)},n.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],16:[function(t){var n=e.FrankerFaceZ,a=t("./constants");make_ls=function(e){return"ffz_setting_"+e},toggle_setting=function(e,t){var n=!this.settings.get(t);this.settings.set(t,n),e.classList.toggle("active",n)},n.settings_info={},n.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var t in n.settings_info)if(n.settings_info.hasOwnProperty(t)){var a=n.settings_info[t],o=a.storage_key||make_ls(t),i=a.hasOwnProperty("value")?a.value:void 0;if(localStorage.hasOwnProperty(o))try{i=JSON.parse(localStorage.getItem(o))}catch(s){this.log('Error loading value for "'+t+'": '+s)}this.settings[t]=i}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this),!1)},n.settings_info.replace_twitch_menu={type:"boolean",value:!1,name:"Replace Twitch Emoticon Menu Beta",help:"Completely replace the default Twitch emoticon menu.",on_update:function(e){document.body.classList.toggle("ffz-menu-replace",e)}},n.menu_pages.settings={render:function(e,t){var a={},o=[];for(var i in n.settings_info)if(n.settings_info.hasOwnProperty(i)){var s=n.settings_info[i],r=s.category||"Miscellaneous",l=a[r];if(void 0!==s.visible&&null!==s.visible){var c=s.visible;if("function"==typeof s.visible&&(c=s.visible.bind(this)()),!c)continue}l||(o.push(r),l=a[r]=[]),l.push([i,s])}o.sort(function(e,t){var e=e.toLowerCase(),t=t.toLowerCase();return"debugging"===e&&(e="zzz"+e),"debugging"===t&&(t="zzz"+t),t>e?-1:e>t?1:0});for(var u=0;un?-1:n>a?1:i>o?-1:o>i?1:0});for(var _=0;_",v.className="switch-label",v.innerHTML=s.name,f.appendChild(y),f.appendChild(v),y.addEventListener("click",toggle_setting.bind(this,y,i))}else{f.classList.add("option");var w=document.createElement("a");w.innerHTML=s.name,w.href="#",f.appendChild(w),w.addEventListener("click",s.method.bind(this))}if(s.help){var b=document.createElement("span");b.className="help",b.innerHTML=s.help,f.appendChild(b)}}p.appendChild(f)}t.appendChild(p)}},name:"Settings",icon:a.GEAR,sort_order:99999,wide:!0},n.prototype._setting_update=function(t){if(t||(t=e.event),t.key&&"ffz_setting_"===t.key.substr(0,12)){var a=t.key,o=a.substr(12),i=void 0,s=n.settings_info[o];if(!s){for(o in n.settings_info)if(n.settings_info.hasOwnProperty(o)&&(s=n.settings_info[o],s.storage_key==a))break;if(s.storage_key!=a)return}this.log("Updated Setting: "+o);try{i=JSON.parse(t.newValue)}catch(r){this.log('Error loading new value for "'+o+'": '+r),i=s.value||void 0}if(this.settings[o]=i,s.on_update)try{s.on_update.bind(this)(i,!1)}catch(r){this.log('Error running updater for setting "'+o+'": '+r)}}},n.prototype._setting_get=function(e){return this.settings[e]},n.prototype._setting_set=function(e,t){var a=n.settings_info[e],o=a.storage_key||make_ls(e),i=JSON.stringify(t);if(this.settings[e]=t,localStorage.setItem(o,i),this.log('Changed Setting "'+e+'" to: '+i),a.on_update)try{a.on_update.bind(this)(t,!0)}catch(s){this.log('Error running updater for setting "'+e+'": '+s)}},n.prototype._setting_del=function(e){var t=n.settings_info[e],a=t.storage_key||make_ls(e),o=void 0;if(localStorage.hasOwnProperty(a)&&localStorage.removeItem(a),delete this.settings[e],t&&(o=this.settings[e]=t.hasOwnProperty("value")?t.value:void 0),t.on_update)try{t.on_update.bind(this)(o,!0)}catch(i){this.log('Error running updater for setting "'+e+'": '+i)}}},{"./constants":3}],17:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,n=this.length;n>t;t++)if(this[t]instanceof Array&&e[t]instanceof Array){if(!this[t].equals(e[t]))return!1}else if(this[t]!=e[t])return!1;return!0}},{}],18:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var e,n=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{e=this._ws_sock=new WebSocket("ws://catbag.frankerfacez.com/")}catch(a){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+a)}this._ws_exists=!0,e.onopen=function(){n._ws_open=!0,n._ws_delay=0,n.log("Socket connected.");var e=n.get_user();if(e&&n.ws_send("setuser",e.login),n.is_dashboard){var t=location.pathname.match(/\/([^\/]+)/);t&&n.ws_send("sub",t[1])}for(var a in n.rooms)n.rooms.hasOwnProperty(a)&&n.ws_send("sub",a);var o=n._ws_pending;n._ws_pending=[];for(var i=0;i0){o=!0;break}}var l=document.createElement("div"),c="";c+="

FrankerFaceZ

",c+='
new ways to woof
',l.className="chat-menu-content center",l.innerHTML=c,t.appendChild(l);var u=0,d=l.querySelector("h1");d&&d.addEventListener("click",function(){if(d.style.cursor="pointer",u++,u>=3){u=0;var e=document.querySelector(".app-main")||document.querySelector(".ember-chat-container");e&&e.classList.toggle("ffz-flip")}setTimeout(function(){u=0,d.style.cursor=""},2e3)});var h=document.createElement("div"),p=document.createElement("a"),m="To use custom emoticons in "+(o?"this channel":"tons of channels")+", get FrankerFaceZ from http://www.frankerfacez.com";p.className="button primary",p.innerHTML="Advertise in Chat",p.addEventListener("click",this._add_emote.bind(this,e,m)),h.appendChild(p);var _=document.createElement("a");_.className="button ffz-donate",_.href="http://www.frankerfacez.com/donate.html",_.target="_new",_.innerHTML="Donate",h.appendChild(_),h.className="chat-menu-content center",t.appendChild(h);var f=document.createElement("div");c='',c+='',c+='',c+='',c+='',f.className="chat-menu-content center",f.innerHTML=c; -var g=!1;f.querySelector("#ffz-debug-logs").addEventListener("click",function(){g||(g=!0,i._pastebin(i._log_data.join("\n"),function(e){g=!1,e?prompt("Your FrankerFaceZ logs have been uploaded to the URL:",e):alert("There was an error uploading the FrankerFaceZ logs.")}))}),t.appendChild(f)}}},{"../constants":3}],20:[function(t){var n=e.FrankerFaceZ,a=t("../constants");n.settings_info.twitch_chat_dark={type:"boolean",value:!1,visible:!1},n.settings_info.dark_twitch={type:"boolean",value:!1,no_bttv:!0,name:"Dark Twitch",help:"Apply a dark background to channels and other related pages for easier viewing.",on_update:function(t){if(!this.has_bttv){document.body.classList.toggle("ffz-dark",t);var n=e.App?App.__container__.lookup("controller:settings").get("model"):void 0;t?(this._load_dark_css(),n&&this.settings.set("twitch_chat_dark",n.get("darkMode")),n&&n.set("darkMode",!0)):n&&n.set("darkMode",this.settings.twitch_chat_dark)}}},n.prototype.setup_dark=function(){this.has_bttv||(document.body.classList.toggle("ffz-dark",this.settings.dark_twitch),this.settings.dark_twitch&&e.App&&App.__container__.lookup("controller:settings").set("model.darkMode",!0),this.settings.dark_twitch&&this._load_dark_css())},n.prototype._load_dark_css=function(){if(!this._dark_style){this.log("Injecting FrankerFaceZ Dark Twitch CSS.");var e=this._dark_style=document.createElement("link");e.id="ffz-dark-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",a.SERVER+"script/dark.css?_="+Date.now()),document.head.appendChild(e)}}},{"../constants":3}],21:[function(t){var n=e.FrankerFaceZ,a=t("../constants"),o=t("../utils"),i="http://static-cdn.jtvnw.net/emoticons/v1/";n.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var n,a=e._popup;a&&(a=jQuery(a),n=a.parent(),n.is(t.target)||0!==n.has(t.target).length||(a.remove(),delete e._popup,e._popup_kill&&e._popup_kill(),delete e._popup_kill))}),document.body.classList.toggle("ffz-menu-replace",this.settings.replace_twitch_menu)},n.menu_pages={},n.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),delete this._popup,this._popup_kill&&this._popup_kill(),void delete this._popup_kill;var o=document.createElement("div"),i=document.createElement("div"),s=document.createElement("ul"),r=this.has_bttv?BetterTTV.settings.get("darkenedMode"):!1;o.className="emoticon-selector chat-menu ffz-ui-popup",i.className="emoticon-selector-box dropmenu",o.appendChild(i),o.classList.toggle("dark",r);var l=document.createElement("div");l.className="ffz-ui-menu-page",i.appendChild(l),s.className="menu clearfix",i.appendChild(s);var c=document.createElement("li");c.className="title",c.innerHTML=""+(a.DEBUG?"[DEV] ":"")+"FrankerFaceZ",s.appendChild(c);var u=[];for(var d in n.menu_pages)if(n.menu_pages.hasOwnProperty(d)){var h=n.menu_pages[d];h&&(!h.hasOwnProperty("visible")||h.visible&&("function"!=typeof h.visible||h.visible.bind(this)()))&&u.push([h.sort_order||0,d,h])}u.sort(function(e,t){if(e[0]t[0])return-1;var n=e[1].toLowerCase(),a=t[1].toLowerCase();return a>n?1:n>a?-1:0});for(var p=0;p0,u.className="emoticon-grid",d.className="heading",c&&(d.style.backgroundImage='url("'+c+'")'),d.innerHTML='TwitchSubscriber Emoticons',u.appendChild(d);for(var p=r.get("emoticons"),m=0;m0&&t.appendChild(u),l){var b=a.room.get("channel.isSubscribed.content");if(b=b.length>0?b[b.length-1]:void 0,b&&b.purchase_profile&&!b.purchase_profile.will_renew){var y=o.parse_date(b.access_end||"");w=document.createElement("div"),k=document.createElement("div"),F=document.createElement("span"),end_time=y?Math.floor((y.getTime()-Date.now())/1e3):null,w.className="subscribe-message",k.className="non-subscriber-message",w.appendChild(k),F.className="unlock-text",F.innerHTML="Subscription expires in "+o.time_to_string(end_time,!0,!0),k.appendChild(F),t.appendChild(w)}}else{var w=document.createElement("div"),k=document.createElement("div"),F=document.createElement("span"),E=document.createElement("a");w.className="subscribe-message",k.className="non-subscriber-message",w.appendChild(k),F.className="unlock-text",F.innerHTML="Subscribe to unlock Emoticons",k.appendChild(F),E.className="action subscribe-button button primary",E.href=r.get("product_url"),E.innerHTML='",k.appendChild(E),t.appendChild(w)}}}this._emotes_for_sets(t,e,a&&a.menu_sets||[],this.feature_friday||s?"Channel Emoticons":null,"http://cdn.frankerfacez.com/channel/global/devicon.png","FrankerFaceZ"),this._feature_friday_ui(n,t,e)},name:"Channel",icon:a.ZREKNARF},n.prototype._emotes_for_sets=function(e,t,n,a,o,i){var s=document.createElement("div"),r=0;if(s.className="emoticon-grid",null!=a){var l=document.createElement("div");if(l.className="heading",i){var c=document.createElement("span");c.className="right",c.appendChild(document.createTextNode(i)),l.appendChild(c)}l.appendChild(document.createTextNode(a)),o&&(l.style.backgroundImage='url("'+o+'")'),s.appendChild(l)}for(var u=0;u0){i=!0;break}}t.classList.toggle("no-emotes",!i),t.classList.toggle("live",l),t.classList.toggle("dark",s),t.classList.toggle("blue",r)}}},{"../constants":3}],23:[function(t){var n=e.FrankerFaceZ,a=t("../constants"),o="http://static-cdn.jtvnw.net/emoticons/v1/",i={"00000turbo":!0},s={"#-?[\\\\/]":"#-/",":-?(?:7|L)":":-7","\\<\\;\\]":"<]","\\:-?(S|s)":":-S","\\:-?\\\\":":-\\","\\:\\>\\;":":>","B-?\\)":"B-)","\\:-?[z|Z|\\|]":":-Z","\\:-?\\)":":-)","\\:-?\\(":":-(","\\:-?(p|P)":":-P","\\;-?(p|P)":";-P","\\<\\;3":"<3","\\:-?[\\\\/]":":-/","\\;-?\\)":";-)","R-?\\)":"R-)","[o|O](_|\\.)[o|O]":"O.o","\\:-?D":":-D","\\:-?(o|O)":":-O","\\>\\;\\(":">(","Gr(a|e)yFace":"GrayFace"},r=function(e){var t=App.__container__.lookup("controller:chat"),n=t.get("currentRoom.id"),a=e.rooms[n],o=a?a.room.tmiSession:null,i=o&&o._emotesParser&&o._emotesParser.emoticonSetIds||"0",s=e.get_user(),r=s&&e.users[s.login]&&e.users[s.login].sets||[];return i=i.split(",").removeObject("0"),e.settings.global_emotes_in_menu&&(i.push("0"),r=_.union(r,e.global_sets)),[i,r]};n.settings_info.global_emotes_in_menu={type:"boolean",value:!1,name:"Display Global Emotes in My Emotes",help:"Display the global Twitch emotes in the My Emoticons menu."},n.prototype.setup_my_emotes=function(){if(this._twitch_emote_sets={},this._twitch_set_to_channel={},localStorage.ffzTwitchSets)try{this._twitch_set_to_channel=JSON.parse(localStorage.ffzTwitchSets)}catch(e){}this._twitch_set_to_channel[0]="twitch_global",this._twitch_set_to_channel[33]="twitch_tfaces",this._twitch_set_to_channel[42]="twitch_tfaces"},n.menu_pages.my_emotes={name:"My Emoticons",icon:a.EMOTE,visible:function(){var e=r(this);return e[0].length>0||e[1].length>0},render:function(e,t){var a=r(this),l=this;new RSVP.Promise(function(e){for(var t=[],o=0;oe?-1:e>t?1:0}),a.emotes=o,a.source="Twitch"}e()}).fail(function(){e()}):e()}),new RSVP.Promise(function(e){if(!t.length)return e();var a=[],o=t,s=function(e,t){var o=l._twitch_emote_sets[e]=l._twitch_emote_sets[e]||{};if(t&&!i[t]){if("twitch_global"==t)return n.capitalization["global emoticons"]=["Global Emoticons",Date.now()],o.channel="Global Emoticons",void(o.badge="//cdn.frankerfacez.com/channel/global/twitch_logo.png");if("turbo"==t||"twitch_tfaces"==t)return o.channel="Twitch Turbo",void(o.badge="//cdn.frankerfacez.com/script/turbo_badge.png");a.push(new RSVP.Promise(function(e,t,n){Twitch.api.get("chat/"+t+"/badges",null,{version:3}).done(function(t){t.subscriber&&t.subscriber.image&&(e.badge=t.subscriber.image),n()}).fail(n)}.bind(this,o,t)));var s=t.toLowerCase(),r=n.capitalization[s];return r&&Date.now()-r[1]<36e5?void(o.channel=r[0]):void a.push(new RSVP.Promise(function(e,t,a,o){l.ws_send("get_display_name",t,function(i,s){var r=i?s:a;n.capitalization[t]=[r,Date.now()],e.channel=r,o()})||(e.channel=a,o()),setTimeout(function(e,t,n){e.channel||(e.channel=t),n()}.bind(this,e,a,o),500)}.bind(this,o,s,t)))}},r=function(){a.length?RSVP.all(a).then(e,e):e()};t=[];for(var c=0;c0?(l.ws_send("twitch_sets",t,function(e,n){if(t=[],e){for(var a in n)n.hasOwnProperty(a)&&(l._twitch_set_to_channel[a]=n[a],s(a,n[a]));localStorage.ffzTwitchSets=JSON.stringify(l._twitch_set_to_channel)}r()}),setTimeout(function(){t.length&&r()},2e3)):r()})]).then(function(){for(var t={},n=0;n0&&c.push([2,d.id,d])}c.sort(function(e,t){if(e[0]t[0])return 1;var n=e[1].toLowerCase(),a=t[1].toLowerCase();return"twitch turbo"===n||"twitch_tfaces"===n?n="zza|"+n:"global emoticons"===n&&(n="zzz|"+n),"twitch turbo"===a||"twitch_tfaces"===a?a="zza|"+a:"global emoticons"===a&&(a="zzz|"+a),a>n?-1:n>a?1:0});for(var h=0;h'+b+""+m,p&&(g.style.backgroundImage='url("'+p+'")'),v.className="emoticon-grid",v.appendChild(g);for(var k=0;k<_.length;k++){var w=_[k],F=s[w.code]||w.code,E=document.createElement("span");if(E.className="emoticon tooltip",E.style.backgroundImage='url("'+(w.url?w.url:o+w.id+"/1.0")+'")',w.height&&(E.style.height=w.height+"px"),w.width&&(E.style.width=w.width+"px"),!w.url){var z='image-set(url("'+o+w.id+'/1.0") 1x, url("'+o+w.id+'/2.0") 2x, url("'+o+w.id+'/3.0") 4x)';E.style.backgroundImage="-webkit-"+z,E.style.backgroundImage="-moz-"+z,E.style.backgroundImage="-ms-"+z,E.style.backgroundImage=z}E.title=F,E.addEventListener("click",l._add_emote.bind(l,e,F)),v.appendChild(E)}t.appendChild(v)}}if(!c.length){var v=document.createElement("div");v.className="chat-menu-content center",v.innerHTML="Error Loading Subscriptions",t.appendChild(v)}}catch(T){l.log("My Emotes Menu Error",T),t.innerHTML="";var v=document.createElement("div"),g=document.createElement("div"),C=document.createElement("p");g.className="heading",g.innerHTML="Error Loading Menu",v.appendChild(g),C.className="clearfix",C.textContent=T,v.appendChild(C),v.className="chat-menu-content",t.appendChild(v)}})}}},{"../constants":3}],24:[function(){var t=e.FrankerFaceZ;t.prototype.setup_notifications=function(){this.log("Adding event handler for window focus."),e.addEventListener("focus",this.clear_notifications.bind(this))},t.settings_info.highlight_notifications={type:"boolean",value:!1,category:"Chat",no_bttv:!0,name:"Highlight Notifications",help:"Display notifications when a highlighted word appears in chat in an unfocused tab.",on_update:function(e,t){if(e&&t){if("denied"===Notification.permission)return this.log("Notifications have been denied by the user."),void this.settings.set("highlight_notifications",!1);if("granted"!==Notification.permission){var n=this;Notification.requestPermission(function(e){"denied"===e&&(n.log("Notifications have been denied by the user."),n.settings.set("highlight_notifications",!1))})}}}},t.ws_commands.message=function(e){this.show_message(e)},t._notifications={},t._last_notification=0,t.prototype.clear_notifications=function(){for(var e in t._notifications){var n=t._notifications[e];if(n)try{n.close()}catch(a){}}t._notifications={},t._last_notification=0},t.prototype.show_notification=function(e,n,a,o,i,s){var r=Notification.permission;if("denied "===r)return!1;if("granted"===r){n=n||"FrankerFaceZ",o=o||1e4;var l={lang:"en-US",dir:"ltr",body:e,tag:a||"FrankerFaceZ",icon:"http://cdn.frankerfacez.com/icon32.png"},c=this,u=new Notification(n,l),d=t._last_notification++;return t._notifications[d]=u,u.addEventListener("click",function(){delete t._notifications[d],i&&i.bind(c)()}),u.addEventListener("close",function(){delete t._notifications[d],s&&s.bind(c)()}),void("number"==typeof o&&u.addEventListener("show",function(){setTimeout(function(){delete t._notifications[d],u.close()},o)}))}var c=this;Notification.requestPermission(function(){c.show_notification(e,n,a)})},t.prototype.show_message=function(t){e.noty({text:t,theme:"ffzTheme",layout:"bottomCenter",closeWith:["button"]}).show()}},{}],25:[function(t){var n=e.FrankerFaceZ,a=t("../utils");n.prototype.setup_races=function(){this.log("Initializing race support."),this.srl_races={}},n.settings_info.srl_races={type:"boolean",value:!0,category:"Channel Metadata",name:"SRL Race Information",help:'Display information about SpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},n.ws_on_close.push(function(){var t=e.App&&App.__container__.lookup("controller:channel"),n=t&&t.get("id"),a=!1;if(t){for(var o in this.srl_races)delete this.srl_races[o],o==n&&(a=!0);a&&this.rebuild_race_ui()}}),n.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),n=t.get("id"),a=!1,o=0;o=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var l="http://kadgar.net/live",c=!1;for(var u in s.entrants){var d=s.entrants[u].state;s.entrants.hasOwnProperty(u)&&s.entrants[u].channel&&("racing"==d||"entered"==d)&&(l+="/"+s.entrants[u].channel,c=!0)}var h=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,p=App.__container__.lookup("controller:channel"),m=p?p.get("display_name"):n.get_capitalization(i),_=encodeURIComponent("I'm watching "+m+" race "+s.goal+" in "+s.game+" on SpeedRunsLive!");r='
',r+='
Developers
Dan Salvato  
Stendec  
Version '+n.version_info+'Logs
',r+="
#Entrant Time
",r+='
',r+='',r+='

SRL',c&&(r+='   Multitwitch'),r+="

",e.innerHTML=r,t.appendChild(e),this._update_race(!0)}}},n.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var n=t.getAttribute("data-channel"),o=this.srl_races[n];if(!o)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var i=o.twitch_entrants[n],s=o.entrants[i],r=t.querySelector("#ffz-race-popup"),l=Date.now()/1e3,c=Math.floor(l-o.time);if(t.querySelector(".logo").innerHTML=a.placement(s),r){var u=r.querySelector("tbody"),d=r.querySelector(".heading span"),h=r.querySelector(".heading div");u.innerHTML="";var p=[],m=!0;for(var _ in o.entrants)o.entrants.hasOwnProperty(_)&&("racing"==o.entrants[_].state&&(m=!1),p.push(o.entrants[_]));p.sort(function(e,t){var n=e.place||9999,a=t.place||9999,o=e.time||c,i=t.time||c;return("forfeit"==e.state||"dq"==e.state)&&(n=1e4),("forfeit"==t.state||"dq"==t.state)&&(a=1e4),a>n?-1:n>a?1:e.namet.name?1:i>o?-1:o>i?1:void 0});for(var f=0;f'+_.display_name+"",v=_.channel?'':"",b=_.hitbox?'':"",y=c?a.time_to_string(_.time||c):"",w=a.place_string(_.place),k=_.comment?a.sanitize(_.comment):"";u.innerHTML+="'+w+""+g+""+v+b+''+("forfeit"==_.state?"Forfeit":y)+""}if(this._race_game!=o.game||this._race_goal!=o.goal){this._race_game=o.game,this._race_goal=o.goal;var F=a.sanitize(o.game),E=a.sanitize(o.goal);h.innerHTML='

'+F+"

Goal: "+E}c?m?d.innerHTML="Done":(d.innerHTML=a.time_to_string(c),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):d.innerHTML="Entry Open"}}}},{"../utils":28}],26:[function(t){var n=e.FrankerFaceZ,a=t("../constants");n.prototype.setup_css=function(){this.log("Injecting main FrankerFaceZ CSS.");var e=this._main_style=document.createElement("link");e.id="ffz-ui-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",a.SERVER+"script/style.css?_="+Date.now()),document.head.appendChild(e),jQuery.noty.themes.ffzTheme={name:"ffzTheme",style:function(){this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type)},callback:{onShow:function(){},onClose:function(){}}}}},{"../constants":3}],27:[function(t){var n=e.FrankerFaceZ,a=t("../constants"),o=t("../utils");n.ws_commands.viewers=function(t){var n=t[0],i=t[1],s=e.App&&App.__container__.lookup("controller:channel"),r=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,l=this.is_dashboard?r&&r[1]:s&&s.get&&s.get("id");if(l===n){var c=document.querySelector("#ffz-viewer-display"),u=a.ZREKNARF+" "+o.number_commas(i);if(c)c.innerHTML=u;else{var d=document.querySelector(this.is_dashboard?"#stats":".stats-and-actions .channel-stats");if(!d)return;c=document.createElement("span"),c.id="ffz-viewer-display",c.className="ffz stat",c.title="Chatters with FrankerFaceZ",c.innerHTML=u,d.appendChild(c),jQuery(c).tipsy(this.is_dashboard?{gravity:"s"}:void 0)}}}},{"../constants":3,"../utils":28}],28:[function(t,n){var a=(e.FrankerFaceZ,t("./constants"),{}),o=document.createElement("span"),i=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"},s=function(e,t){t=0===t?0:t||1,t=Math.round(255*-(t/100));var n=Math.max(0,Math.min(255,e[0]-t)),a=Math.max(0,Math.min(255,e[1]-t)),o=Math.max(0,Math.min(255,e[2]-t));return[n,a,o]},r=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},l=function(e,t){return t=0===t?0:t||1,s(e,-t)},c=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;ts;(l||n)&&(l&&(a=a.substr(0,s)+a.substr(r+i.length)),n&&(a+=o+n+i),e.innerHTML=a)},get_luminance:c,brighten:s,darken:l,rgb_to_css:r,parse_date:d,number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:i,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?i(e.place):""},sanitize:function(e){var t=a[e];return t||(o.textContent=e,t=a[e]=o.innerHTML,o.innerHTML=""),t},date_string:function(e){return e.getFullYear()+"-"+(e.getMonth()+1)+"-"+e.getDate()},time_to_string:function(e,t,n){var a=e%60,o=Math.floor(e/60),i=Math.floor(o/60),s="";if(o%=60,t){if(s=Math.floor(i/24),i%=24,n&&s>0)return s+" days";s=s>0?s+" days, ":""}return s+(10>i?"0":"")+i+":"+(10>o?"0":"")+o+":"+(10>a?"0":"")+a}}},{"./constants":3}]},{},[14]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file +!function(e){!function t(e,n,s){function i(a,r){if(!n[a]){if(!e[a]){var c="function"==typeof require&&require;if(!r&&c)return c(a,!0);if(o)return o(a,!0);throw new Error("Cannot find module '"+a+"'")}var l=n[a]={exports:{}};e[a][0].call(l.exports,function(t){var n=e[a][1][t];return i(n?n:t)},l,l.exports,t,e,n,s)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;ae&&this._legacy_load_bots(e))})},n.prototype._legacy_load_donors=function(e){jQuery.ajax(s.SERVER+"script/donors.txt",{cache:!1,context:this}).done(function(e){this._legacy_parse_badges(e,1,1)}).fail(function(t){return 404!=t.status?(e=(e||0)+1,10>e?this._legacy_load_donors(e):void 0):void 0})},n.prototype._legacy_parse_badges=function(e,t,n){var s=this.badges[n].title,o=0;if(ds=null,null!=e)for(var a=e.trim().split(/\W+/),r=0;r50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var s=t.length;t.length;){var i=t.shift();e.room.tmiRoom.sendMessage("/unmod "+i)}return"Sent unmod command for "+s+" users."},t.ffz_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.ffz_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var n=this.get_user();if(!n||!n.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var s=t.length;t.length;){var i=t.shift();e.room.tmiRoom.sendMessage("/mod "+i)}return"Sent mod command for "+s+" users."},t.ffz_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var n='',s="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:s,SERVER:s?"//localhost:8000/":"//cdn.frankerfacez.com/",API_SERVER:"//api.frankerfacez.com/",SVGPATH:n,ZREKNARF:''+n+"",CHAT_BUTTON:''+n+"",ROOMS:'',CAMERA:'',INVITE:'',EYE:'',CLOCK:'',GEAR:'',HEART:'',EMOTE:'',STAR:''}},{}],4:[function(){var t=e.FrankerFaceZ;t.settings_info.developer_mode={type:"boolean",value:!1,storage_key:"ffzDebugMode",visible:function(){return this.settings.developer_mode||Date.now()-parseInt(localStorage.ffzLastDevMode||"0")<6048e5},category:"Debugging",name:"Developer Mode",help:"Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",on_update:function(){localStorage.ffzLastDevMode=Date.now()}},t.ffz_commands.developer_mode=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Developer Mode is currently "+(this.settings.developer_mode?"enabled.":"disabled."):(this.settings.set("developer_mode",n),"Developer Mode is now "+(n?"enabled":"disabled")+". Please refresh your browser.")},t.ffz_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i=t("../constants");n.prototype.setup_channel=function(){document.body.classList.toggle("ffz-hide-view-count",!this.settings.channel_views),this.log("Creating channel style element.");var e=this._channel_style=document.createElement("style");e.id="ffz-channel-css",document.head.appendChild(e),this.log("Hooking the Ember Channel Index view.");var t=App.__container__.resolve("view:channel/index"),s=this;if(t){this._modify_cindex(t);try{t.create().destroy()}catch(i){}for(var o in Ember.View.views)if(Ember.View.views.hasOwnProperty(o)){var a=Ember.View.views[o];a instanceof t&&(this.log("Manually updating Channel Index view.",a),this._modify_cindex(a),a.ffzInit())}this.log("Hooking the Ember Channel controller."),t=App.__container__.lookup("controller:channel"),t&&t.reopen({ffzUpdateUptime:function(){s._cindex&&s._cindex.ffzUpdateUptime()}.observes("isLive","content.id"),ffzUpdateTitle:function(){var e=this.get("content.name"),t=this.get("content.display_name");t&&(n.capitalization[e]=[t,Date.now()]),s._cindex&&s._cindex.ffzFixTitle()}.observes("content.status","content.id")})}},n.prototype._modify_cindex=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.ffzInit()}catch(e){t.error("CIndex didInsertElement: "+e)}},willClearRender:function(){try{this.ffzTeardown()}catch(e){t.error("CIndex willClearRender: "+e)}return this._super()},ffzInit:function(){t._cindex=this,this.get("element").setAttribute("data-channel",this.get("controller.id")),this.ffzFixTitle(),this.ffzUpdateUptime(),this.ffzUpdateChatters();var e=this.get("element").querySelector(".svg-glyph_views:not(.ffz-svg)");e&&e.parentNode.classList.add("twitch-channel-views")},ffzFixTitle:function(){if(!t.has_bttv&&t.settings.stream_title){var e=this.get("controller.status"),n=this.get("controller.id");e=t.render_tokens(t.tokenize_line(n,n,e,!0)),this.$(".title span").each(function(t,n){var s=n.querySelectorAll("script");n.innerHTML=s[0].outerHTML+e+s[1].outerHTML})}},ffzUpdateChatters:function(){var e=this.get("controller.id"),n=t.rooms&&t.rooms[e];if(!n||!t.settings.chatter_count){var o=this.get("element").querySelector("#ffz-chatter-display");return o&&o.parentElement.removeChild(o),o=this.get("element").querySelector("#ffz-ffzchatter-display"),void(o&&o.parentElement.removeChild(o))}var a=Object.keys(n.room.get("ffz_chatters")||{}).length,r=n.ffz_chatters||0,o=this.get("element").querySelector("#ffz-chatter-display span");if(!o){var c=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!c)return;var l=document.createElement("span");l.className="ffz stat",l.id="ffz-chatter-display",l.title="Current Chatters",l.innerHTML=i.ROOMS+" ",o=document.createElement("span"),l.appendChild(o);var u=c.querySelector("#ffz-ffzchatter-display");u?c.insertBefore(l,u):c.appendChild(l),jQuery(l).tipsy()}if(o.innerHTML=s.number_commas(a),!r)return o=this.get("element").querySelector("#ffz-ffzchatter-display"),void(o&&o.parentNode.removeChild(o));if(o=this.get("element").querySelector("#ffz-ffzchatter-display span"),!o){var c=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!c)return;var l=document.createElement("span");l.className="ffz stat",l.id="ffz-ffzchatter-display",l.title="Chatters with FrankerFaceZ",l.innerHTML=i.ZREKNARF+" ",o=document.createElement("span"),l.appendChild(o);var u=c.querySelector("#ffz-chatter-display");u?c.insertBefore(l,u.nextSibling):c.appendChild(l),jQuery(l).tipsy()}o.innerHTML=s.number_commas(r)},ffzUpdateUptime:function(){if(this._ffz_update_uptime&&(clearTimeout(this._ffz_update_uptime),delete this._ffz_update_uptime),!t.settings.stream_uptime||!this.get("controller.isLiveAccordingToKraken")){var e=this.get("element").querySelector("#ffz-uptime-display");return void(e&&e.parentElement.removeChild(e))}this._ffz_update_uptime=setTimeout(this.ffzUpdateUptime.bind(this),1e3);var n=this.get("controller.content.stream.created_at");if(n&&(n=s.parse_date(n))){var o=Math.floor((Date.now()-n.getTime())/1e3);if(!(0>o)){var e=this.get("element").querySelector("#ffz-uptime-display span");if(!e){var a=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!a)return;var r=document.createElement("span");r.className="ffz stat",r.id="ffz-uptime-display",r.title="Stream Uptime (since "+n.toLocaleString()+")",r.innerHTML=i.CLOCK+" ",e=document.createElement("span"),r.appendChild(e);var c=a.querySelector(".live-count");if(c)a.insertBefore(r,c.nextSibling);else try{c=a.querySelector("script:nth-child(0n+2)"),a.insertBefore(r,c.nextSibling)}catch(l){a.insertBefore(r,a.childNodes[0])}jQuery(r).tipsy({html:!0})}e.innerHTML=s.time_to_string(o)}}},ffzTeardown:function(){this.get("element").setAttribute("data-channel",""),t._cindex=void 0,this._ffz_update_uptime&&clearTimeout(this._ffz_update_uptime)}})},n.settings_info.chatter_count={type:"boolean",value:!1,category:"Channel Metadata",name:"Chatter Count",help:"Display the current number of users connected to chat beneath the channel.",on_update:function(e){if(this._cindex&&this._cindex.ffzUpdateChatters(),e&&this.rooms)for(var t in this.rooms)this.rooms.hasOwnProperty(t)&&this.rooms[t].room&&this.rooms[t].room.ffzInitChatterCount()}},n.settings_info.channel_views={type:"boolean",value:!0,category:"Channel Metadata",name:"Channel Views",help:"Display the number of times the channel has been viewed beneath the stream.",on_update:function(e){document.body.classList.toggle("ffz-hide-view-count",!e)}},n.settings_info.stream_uptime={type:"boolean",value:!1,category:"Channel Metadata",name:"Stream Uptime",help:"Display the stream uptime under a channel by the viewer count.",on_update:function(){this._cindex&&this._cindex.ffzUpdateUptime()}},n.settings_info.stream_title={type:"boolean",value:!0,no_bttv:!0,category:"Channel Metadata",name:"Title Links",help:"Make links in stream titles clickable.",on_update:function(){this._cindex&&this._cindex.ffzFixTitle()}}},{"../constants":3,"../utils":29}],6:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i=t("../constants"),o=function(e){return 1>e?"":e>=99?"99+":""+e};n.settings_info.group_tabs={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Chat Room Tabs Beta",help:"Enhanced UI for switching the current chat room and noticing new messages.",on_update:function(e){var t=!this.has_bttv&&e;this._chatv&&t!==this._group_tabs_state&&(t?this._chatv.ffzEnableTabs():this._chatv.ffzDisableTabs())}},n.settings_info.pinned_rooms={type:"button",value:[],category:"Chat",visible:!1,name:"Pinned Chat Rooms",help:"Set a list of channels that should always be available in chat."},n.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat controller.");var e=App.__container__.lookup("controller:chat"),t=this;e&&e.reopen({ffzUpdateChannels:function(){t.settings.group_tabs&&t._chatv&&t._chatv.ffzRebuildTabs()}.observes("currentChannelRoom","connectedPrivateGroupRooms")}),this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e);try{e.create().destroy()}catch(n){}for(var s in Ember.View.views)if(Ember.View.views.hasOwnProperty(s)){var i=Ember.View.views[s];if(i instanceof e){this.log("Manually updating existing Chat view.",i);try{i.ffzInit()}catch(n){this.error("setup: build_ui_link: "+n)}}}this.log("Hooking the Ember Layout controller.");var o=App.__container__.lookup("controller:layout");if(o){o.reopen({ffzFixTabs:function(){t.settings.group_tabs&&t._chatv&&t._chatv._ffz_tabs&&setTimeout(function(){t._chatv&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px")},0)}.observes("isRightColumnClosed")}),this.log("Hooking the Ember 'Right Column' controller. Seriously...");var a=App.__container__.lookup("controller:right-column");a&&a.reopen({ffzFixTabs:function(){t.settings.group_tabs&&t._chatv&&t._chatv._ffz_tabs&&setTimeout(function(){t._chatv&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px")},0)}.observes("firstTabSelected")})}},n.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.ffzInit()}catch(e){t.error("ChatView didInsertElement: "+e)}},willClearRender:function(){try{this.ffzTeardown()}catch(e){t.error("ChatView willClearRender: "+e)}this._super()},ffzInit:function(){t._chatv=this,this.$(".textarea-contain").append(t.build_ui_link(this)),!t.has_bttv&&t.settings.group_tabs&&this.ffzEnableTabs(),setTimeout(function(){t.settings.group_tabs&&t._chatv._ffz_tabs&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px");var e=t._chatv.get("controller");e&&e.set("showList",!1)},1e3)},ffzTeardown:function(){t._chatv===this&&(t._chatv=null),this.$(".textarea-contain .ffz-ui-toggle").remove(),t.settings.group_tabs&&this.ffzDisableTabs()},ffzChangeRoom:Ember.observer("controller.currentRoom",function(){try{if(t.update_ui_link(),!t.has_bttv&&t.settings.group_tabs&&this._ffz_tabs){var e=this.get("controller.currentRoom");e&&e.resetUnreadCount();var n=jQuery(this._ffz_tabs);n.children(".ffz-chat-tab").removeClass("active"),e&&n.children('.ffz-chat-tab[data-room="'+e.get("id")+'"]').removeClass("tab-mentioned").addClass("active").children("span").text("");var s=e&&e.get("canInvite");this._ffz_invite&&this._ffz_invite.classList.toggle("hidden",!s),this.set("controller.showInviteUser",s&&this.get("controller.showInviteUser")),this.$(".chat-room").css("top",this._ffz_tabs.offsetHeight+"px")}}catch(i){t.error("ChatView ffzUpdateLink: "+i)}}),ffzEnableTabs:function(){if(!t.has_bttv&&t.settings.group_tabs){this.$(".chat-header").addClass("hidden");var e=this._ffz_tabs=document.createElement("div");e.id="ffz-group-tabs",this.$(".chat-header").after(e),this.ffzRebuildTabs()}},ffzRebuildTabs:function(){if(!t.has_bttv&&t.settings.group_tabs){var e=this._ffz_tabs||this.get("element").querySelector("#ffz-group-tabs");if(e){e.innerHTML="";var n=document.createElement("a"),s=this;n.className="button glyph-only tooltip",n.title="Chat Room Management",n.innerHTML=i.ROOMS,n.addEventListener("click",function(){var e=s.get("controller");e&&e.set("showList",!e.get("showList"))}),e.appendChild(n),n=document.createElement("a"),n.className="button glyph-only tooltip invite",n.title="Invite a User",n.innerHTML=i.INVITE,n.addEventListener("click",function(){var e=s.get("controller");e&&e.set("showInviteUser",e.get("currentRoom.canInvite")&&!e.get("showInviteUser"))}),n.classList.toggle("hidden",!this.get("controller.currentRoom.canInvite")),s._ffz_invite=n,e.appendChild(n);var o,a=this.get("controller.currentChannelRoom");a&&(o=this.ffzBuildTab(s,a,!0),o&&e.appendChild(o));var r=App.__container__.lookup("controller:channel"),c=App.__container__.resolve("model:room");if(target=r&&r.get("hostModeTarget"),target&&c){var l=target.get("id");this._ffz_host!==l&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),this._ffz_host=l,this._ffz_host_room=c.findOne(l))}else this._ffz_host&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),delete this._ffz_host,delete this._ffz_host_room);this._ffz_host_room&&(o=s.ffzBuildTab(s,this._ffz_host_room,!1,!0),o&&e.appendChild(o));for(var u=0;u"+l+"",u.addEventListener("click",function(){e.get("controller").focusRoom(t)}),u},ffzDisableTabs:function(){this._ffz_tabs&&(this._ffz_tabs.parentElement.removeChild(this._ffz_tabs),delete this._ffz_tabs,delete this._ffz_invite),this._ffz_host&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),delete this._ffz_host,delete this._ffz_host_room),this.$(".chat-room").css("top",""),this.$(".chat-header").removeClass("hidden")}})},n.prototype.connect_extra_chat=function(){if(!this.has_bttv){for(var e=0;e1)return"Join Usage: /join ";var n=t[0].toLowerCase();return"#"===n.charAt(0)&&(n=n.substr(1)),this._join_room(n)?"Joining "+n+". You will always connect to this channel's chat unless you later /part from it.":"You have already joined "+n+'. Please use "/part '+n+'" to leave it.'},n.chat_commands.part=function(e,t){if(!t||!t.length||t.length>1)return"Part Usage: /part ";var n=t[0].toLowerCase();return"#"===n.charAt(0)&&(n=n.substr(1)),this._leave_room(n)?"Leaving "+n+".":this.rooms[n]?"You do not have "+n+" pinned and you cannot leave the current channel or hosted channels via /part.":"You are not in "+n+"."}},{"../constants":3,"../utils":29}],7:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i="[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]",o=new RegExp(i+"*,"+i+"*"),a=function(e){return(e+"").replace(/&/g,"&").replace(/'/g,"'").replace(/"/g,""").replace(//g,">")},r="http://static-cdn.jtvnw.net/emoticons/v1/",c=function(e){return r+e+"/1.0 1x, "+r+e+"/2.0 2x, "+r+e+"/3.0 4x"},l=function(e){var t=e.set,n=e.set_type,s=e.owner;return void 0===n&&(n="Channel"),t?(("00000turbo"==t||"turbo"==t)&&(t="Twitch Turbo",n=null),"Emoticon: "+e.code+"\n"+(n?n+": ":"")+t+(s?"\nBy: "+s.display_name:"")):e.code},u=function(e){{var t=this._twitch_emotes[e];t?t.set:null}return t?"string"==typeof t?t:t.tooltip?t.tooltip:t.tooltip=l(t):"???"},h=function(e,t,n,s){if(n){t&&(s.code=t),this._twitch_emotes[e]=s;for(var i=u.bind(this)(e),o=document.querySelectorAll('img[emote-id="'+e+'"]'),a=0;aYouTube: "+s.sanitize(n.title)+"
",t+="Channel: "+s.sanitize(n.channel)+" | "+s.time_to_string(n.duration)+"
",t+=s.number_commas(n.views||0)+" Views | 👍 "+s.number_commas(n.likes||0)+" 👎 "+s.number_commas(n.dislikes||0);else if("strawpoll"==n.type){t="Strawpoll: "+s.sanitize(n.title)+"
";for(var i in n.items){{var o=n.items[i];Math.floor(o/n.total*100)}t+='"}t+="
'+s.sanitize(i)+''+s.number_commas(o)+"

Total: "+s.number_commas(n.total);var a=s.parse_date(n.fetched);if(a){var r=Math.floor((a.getTime()-Date.now())/1e3);r>60&&(t+="
Data was cached "+s.time_to_string(r)+" ago.")}}else if("twitch"==n.type){t="Twitch: "+s.sanitize(n.display_name)+"
";var c=s.parse_date(n.since);c&&(t+="Member Since: "+s.date_string(c)+"
"),t+="Views: "+s.number_commas(n.views)+" | Followers: "+s.number_commas(n.followers)+""}else if("twitch_vod"==n.type)t="Twitch "+("highlight"==n.broadcast_type?"Highlight":"Broadcast")+": "+s.sanitize(n.title)+"
",t+="By: "+s.sanitize(n.display_name)+(n.game?" | Playing: "+s.sanitize(n.game):" | Not Playing")+"
",t+="Views: "+s.number_commas(n.views)+" | "+s.time_to_string(n.length);else if("twitter"==n.type)t="Tweet By: "+s.sanitize(n.user)+"
",t+=s.sanitize(n.tweet);else if("reputation"==n.type){if(t=''+s.sanitize(n.full.toLowerCase())+"",n.trust<50||n.safety<50||n.tags&&n.tags.length>0){t+="
";var l=!1;(n.trust<50||n.safety<50)&&(n.unsafe=!0,t+="Potentially Unsafe Link
",t+="Trust: "+n.trust+"% | Child Safety: "+n.safety+"%",l=!0),n.tags&&n.tags.length>0&&(t+=(l?"
":"")+"Tags: "+n.tags.join(", ")),t+="
Data Source: WOT"}}else n.full&&(t=''+s.sanitize(n.full.toLowerCase())+"");return t||(t=''+s.sanitize(e.toLowerCase())+""),n.tooltip=t,t},f=function(e,t,n){if(t){this._link_data[e]=n,n.unsafe=!1;var s,i=d.bind(this)(e),o="/"==e.charAt(e.length-1)?e.substr(0,e.length-1):null;if(s=document.querySelectorAll(o?'span.message a[href="'+e+'"], span.message a[href="'+o+'"], span.message a[data-url="'+e+'"], span.message a[data-url="'+o+'"]':'span.message a[href="'+e+'"], span.message a[data-url="'+e+'"]'),this.settings.link_info)for(var a=0;aBeta",help:"Check links against known bad websites, unshorten URLs, and show YouTube info."},n.settings_info.chat_rows={type:"boolean",value:!1,category:"Chat",no_bttv:!0,name:"Chat Line Backgrounds",help:"Display alternating background colors for lines in chat.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-background",e)}},n.prototype.setup_line=function(){document.body.classList.toggle("ffz-chat-colors",!this.has_bttv&&this.settings.fix_color),document.body.classList.toggle("ffz-chat-background",!this.has_bttv&&this.settings.chat_rows),this._colors={},this._last_row={};var e=this._fix_color_style=document.createElement("style");e.id="ffz-style-username-colors",e.type="text/css",document.head.appendChild(e),this._twitch_emotes={},this._link_data={},this.log("Hooking the Ember Line controller.");var t=App.__container__.resolve("component:message-line"),s=this;t.reopen({tokenizedMessage:function(){var e=this.get("msgObject.cachedTokens");if(e)return e;e=this._super();try{var t=performance.now(),i=s.get_user(),o=i&&this.get("msgObject.from")===i.login;e=s._remove_banned(e),e=s._emoticonize(this,e);var a=this.get("msgObject.tags.display-name");a&&a.length&&(n.capitalization[this.get("msgObject.from")]=[a.trim(),Date.now()]),o||(e=s.tokenize_mentions(e));for(var r=0;r5&&s.log("Tokenizing Message Took Too Long - "+(l-t)+"ms",e,!1,!0)}catch(u){try{s.error("LineController tokenizedMessage: "+u)}catch(u){}}return this.set("msgObject.cachedTokens",e),e}.property("msgObject.message","isChannelLinksDisabled","currentUserNick","msgObject.from","msgObject.tags.emotes"),didInsertElement:function(){this._super();try{var e=performance.now(),t=this.get("element"),n=this.get("msgObject.from"),i=this.get("msgObject.room")||App.__container__.lookup("controller:chat").get("currentRoom.id"),o=this.get("msgObject.color"),a=this.get("msgObject.ffz_alternate");o&&s._handle_color(o),void 0===a&&(a=s._last_row[i]=s._last_row.hasOwnProperty(i)?!s._last_row[i]:!1,this.set("msgObject.ffz_alternate",a)),t.classList.toggle("ffz-alternate",a),t.setAttribute("data-room",i),t.setAttribute("data-sender",n),t.setAttribute("data-deleted",this.get("deleted")||!1),s.render_badge(this),this.get("msgObject.ffz_has_mention")&&t.classList.add("ffz-mentioned");for(var r=t.querySelectorAll("a.deleted-link"),l=0;l-1&&(-1===t.indexOf("/")||t.indexOf("@")5&&s.log("Line Took Too Long - "+E+"ms",t.innerHTML,!1,!0)}catch(T){try{s.error("LineView didInsertElement: "+T)}catch(T){}}}});var i=this.get_user();i&&i.name&&(n.capitalization[i.login]=[i.name,Date.now()])},n.prototype._handle_color=function(e){if(e&&!this._colors[e]){this._colors[e]=!0;var t=parseInt(e.substr(1),16),n=[t>>16,t>>8&255,255&t],i=s.get_luminance(n),o="",a='span[style="color:'+e+'"]',r=!1;if(i>.3){r=!0;for(var c=127,l=n;c--&&(l=s.darken(l),!(s.get_luminance(l)<=.3)););o+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+a+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+a+" { color: "+s.rgb_to_css(l)+" !important; }\n"}else o+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+a+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+a+" { color: "+e+" !important; }\n";if(.15>i){r=!0;for(var c=127,l=n;c--&&(l=s.brighten(l),!(s.get_luminance(l)>=.15)););o+=".ffz-chat-colors .theatre .chat-container .chat-line "+a+", .ffz-chat-colors .chat-container.dark .chat-line "+a+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+a+" { color: "+s.rgb_to_css(l)+" !important; }\n"}else o+=".ffz-chat-colors .theatre .chat-container .chat-line "+a+", .ffz-chat-colors .chat-container.dark .chat-line "+a+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+a+" { color: "+e+" !important; }\n";r&&(this._fix_color_style.innerHTML+=o)}},n.capitalization={},n._cap_fetching=0,n.get_capitalization=function(t,s){if(e.BetterTTV&&BetterTTV.chat&&BetterTTV.chat.helpers.lookupDisplayName)return BetterTTV.chat.helpers.lookupDisplayName(t);if(!t)return t;if(t=t.toLowerCase(),"jtv"==t||"twitchnotify"==t)return t;var i=n.capitalization[t];return i&&Date.now()-i[1]<36e5?i[0]:(n._cap_fetching<25&&(n._cap_fetching++,n.get().ws_send("get_display_name",t,function(e,i){var o=e?i:t;n.capitalization[t]=[o,Date.now()],n._cap_fetching--,"function"==typeof s&&s(o)})),i?i[0]:t)},n.prototype._remove_banned=function(e){var t=this.settings.banned_words;if(!t||!t.length)return e;"string"==typeof e&&(e=[e]);for(var s=n._words_to_regex(t),i=[],o=0;o<banned link>',own:!0}:r)}return i},n.prototype._emoticonize=function(e,t){var n=e.get("msgObject.room")||App.__container__.lookup("controller:chat").get("currentRoom.id"),s=e.get("msgObject.from");return this.tokenize_emotes(s,n,t)}},{"../utils":29}],8:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i={ESC:27,P:80,B:66,T:84,U:85},o=[["5m",300],["10m",600],["1hr",3600],["12hr",43200],["24hr",86400]],a='',r='';n.settings_info.enhanced_moderation={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Enhanced Moderation",help:"Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards."},n.prototype.setup_mod_card=function(){this.log("Hooking the Ember Moderation Card view.");var t=App.__container__.resolve("component:moderation-card"),n=this;t.reopen({didInsertElement:function(){this._super(),e._card=this;try{if(n.has_bttv||!n.settings.enhanced_moderation)return;var t=this.get("element"),c=this.get("controller");if(t.classList.add("ffz-moderation-card"),c.get("cardInfo.isModeratorOrHigher")){t.classList.add("ffz-is-mod"),t.setAttribute("tabindex",1),t.addEventListener("keyup",function(e){var t=e.keyCode||e.which,n=c.get("cardInfo.user.id"),s=App.__container__.lookup("controller:chat").get("currentRoom");if(t==i.P)s.send("/timeout "+n+" 1");else if(t==i.B)s.send("/ban "+n);else if(t==i.T)s.send("/timeout "+n+" 600");else if(t==i.U)s.send("/unban "+n);else if(t!=i.ESC)return;c.send("hideModOverlay")});var l=document.createElement("div");l.className="interface clearfix";var u=function(e){var t=c.get("cardInfo.user.id"),n=App.__container__.lookup("controller:chat").get("currentRoom");n.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},h=function(e,t){var n=document.createElement("button");return n.className="button",n.innerHTML=e,n.title="Timeout User for "+s.number_commas(t)+" Second"+(1!=t?"s":""),600===t?n.title="(T)"+n.title.substr(1):1===t&&(n.title="(P)urge - "+n.title),jQuery(n).tipsy(),n.addEventListener("click",u.bind(this,t)),n};l.appendChild(h("Purge",1));var d=document.createElement("span");d.className="right",l.appendChild(d);for(var _=0;_ button");b&&b.classList.contains("message-button")&&(b.innerHTML=a,b.classList.add("glyph-only"),b.classList.add("message"),b.title="Message User",jQuery(b).tipsy()),this.$().draggable({start:function(){t.focus()}}),t.focus()}catch(y){try{n.error("ModerationCardView didInsertElement: "+y)}catch(y){}}}})},n.chat_commands.purge=n.chat_commands.p=function(e,t){if(!t||!t.length)return"Purge Usage: /p username [more usernames separated by spaces]";if(t.length>10)return"Please only purge up to 10 users at once.";for(var n=0;n10)return"Please only ban up to 10 users at once.";for(var n=0;n10)return"Please only unban up to 10 users at once.";for(var n=0;nn?i.load_room(e,t,n):"function"==typeof t&&t(!1))})},n.prototype._load_room_json=function(e,t,n){return n&&n.room?(n=n.room,this.rooms[e]&&(n.room=this.rooms[e].room),this.rooms[e]=n,(n.css||n.moderator_badge)&&i.update_css(this._room_style,e,o(n)+(n.css||"")),this.emote_sets.hasOwnProperty(n.set)||this.load_set(n.set),this.update_ui_link(),void(t&&t(!0,n))):"function"==typeof t&&t(!1)},n.prototype._modify_room=function(t){var n=this;t.reopen({init:function(){this._super();try{n.add_room(this.id,this),this.set("ffz_chatters",{})}catch(e){n.error("add_room: "+e)}},willDestroy:function(){this._super();try{n.remove_room(this.id)}catch(e){n.error("remove_room: "+e)}},addMessage:function(e){try{e&&(e.room=this.get("id"),n.tokenize_chat_line(e))}catch(t){n.error("Room addMessage: "+t)}return this._super(e)},setHostMode:function(e){var t=App.__container__.lookup("controller:chat");if(t&&t.get("currentChannelRoom")===this)return this._super(e)},send:function(e){try{var t=e.split(" ",1)[0].toLowerCase();if("/ffz"===t)return this.set("messageToSend",""),void n.run_ffz_command(e.substr(5),this.get("id"));if("/"===t.charAt(0)&&n.run_command(e,this.get("id")))return void this.set("messageToSend","")}catch(s){n.error("send: "+s)}return this._super(e)},ffzUpdateUnread:function(){if(n.settings.group_tabs){var e=App.__container__.lookup("controller:chat");e&&e.get("currentRoom")===this?this.resetUnreadCount():n._chatv&&n._chatv.ffzTabUnread(this.get("id"))}}.observes("unreadCount"),ffzInitChatterCount:function(){if(this.tmiRoom){var e=this;this.tmiRoom.list().done(function(t){var n={};t=t.data.chatters;for(var s=0;s0)){var i=n.emoticonSetIds;n.emoticonSetIds="",n.updateEmoticons(i),this._twitch_emote_check=setTimeout(this.check_twitch_emotes.bind(this),1e4)}},n.prototype.getEmotes=function(e,t){var n=this.users&&this.users[e],s=this.rooms&&this.rooms[t];return _.union(n&&n.sets||[],s&&s.set&&[s.set]||[],this.default_sets)},n.ws_commands.reload_set=function(e){this.emote_sets.hasOwnProperty(e)&&this.load_set(e)},n.ws_commands.load_set=function(e){this.load_set(e)},n.prototype._emote_tooltip=function(e){if(!e)return null;if(e._tooltip)return e._tooltip;var t=this.emote_sets[e.set_id],n=e.owner,s=t&&t.title||"Global";return e._tooltip="Emoticon: "+(e.hidden?"???":e.name)+"\nFFZ "+s+(n?"\nBy: "+n.display_name:""),e._tooltip},n.prototype.load_global_sets=function(e,t){var n=this;jQuery.getJSON(s.API_SERVER+"v1/set/global").done(function(e){n.default_sets=e.default_sets;var t=n.global_sets=[],s=e.sets||{};for(var i in s)if(s.hasOwnProperty(i)){var o=s[i];t.push(i),n._load_set_json(i,void 0,o)}}).fail(function(s){return 404==s.status?"function"==typeof e&&e(!1):(t=t||0,t++,50>t?n.load_global_sets(e,t):"function"==typeof e&&e(!1))})},n.prototype.load_set=function(e,t,n){var i=this;jQuery.getJSON(s.API_SERVER+"v1/set/"+e).done(function(n){i._load_set_json(e,t,n&&n.set)}).fail(function(s){return 404==s.status?"function"==typeof t&&t(!1):(n=n||0,n++,10>n?i.load_set(e,t,n):"function"==typeof t&&t(!1))})},n.prototype.unload_set=function(e){var t=this.emote_sets[e];t&&(this.log("Unloading emoticons for set: "+e),i.update_css(this._emote_style,e,null),delete this.emote_sets[e])},n.prototype._load_set_json=function(e,t,n){if(!n)return"function"==typeof t&&t(!1);this.emote_sets[e]=n,n.users=[],n.count=0;var s="",o=n.emoticons;n.emoticons={};for(var a=0;a=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(n||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,document.body.classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style),this.settings.group_tabs&&this._chatv&&this._chatv.ffzDisableTabs(),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background"),this.is_dashboard&&this._update_subscribers();var t=BetterTTV.chat.helpers.sendMessage,n=this;BetterTTV.chat.helpers.sendMessage=function(e){var s=e.split(" ",1)[0].toLowerCase();return"/ffz"!==s?t(e):void n.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var s,i=BetterTTV.chat.handlers.onPrivmsg;BetterTTV.chat.handlers.onPrivmsg=function(e,t){s=e;var n=i(e,t);return s=null,n};var o=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,i,a,r){try{return n.bttv_badges(r),'
'+BetterTTV.chat.templates.timestamp(r.time)+" "+(a?BetterTTV.chat.templates.modicons():"")+" "+BetterTTV.chat.templates.badges(r.badges)+BetterTTV.chat.templates.from(r.nickname,r.color)+BetterTTV.chat.templates.message(r.sender,r.message,r.emotes,t?r.color:!1)+"
"}catch(c){return n.log("Error: ",c),o(e,t,i,a,r)}};var a,r=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,s,i){try{i=i||!1;var o=encodeURIComponent(t);if("jtv"!==e){a=e;var c=BetterTTV.chat.templates.emoticonize(t,s);a=null;for(var l=0;l'+t+"
"}catch(u){return n.log("Error: ",u),r(e,t,s,i)}};var c=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var i=c(e,t),o=s||BetterTTV.getChannel(),r=o&&o.toLowerCase(),l=a&&a.toLowerCase(),u=n.getEmotes(l,r),t=[],h=n.get_user(),d=h&&h.login===l;return _.each(u,function(e){var s=n.emote_sets[e];s&&_.each(s.emoticons,function(e){_.any(i,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=n._emote_tooltip(e),s=[''+t+''],o=i;if(i=[],!o||!o.length)return i;for(var a=0;a=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(n||0)+t),t)) +},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),n=e?e.login:null,s=App.__container__.lookup("controller:chat"),i=s?s.get("currentRoom.id"):null,o=this.getEmotes(n,i),a=[],r=0;r=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(n||0)+t),t)))},n.prototype.setup_normal=function(t){var s=e.performance&&performance.now?performance.now():Date.now();this.log("Found non-Ember Twitch after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.find_bttv(10);var i=e.performance&&performance.now?performance.now():Date.now(),o=i-s;this.log("Initialization complete in "+o+"ms")},n.prototype.is_dashboard=!1,n.prototype.setup_dashboard=function(t){var s=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch Dashboard after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.is_dashboard=!0,this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this._update_subscribers(),this.setup_message_event(),this.find_bttv(10);var i=e.performance&&performance.now?performance.now():Date.now(),o=i-s;this.log("Initialization complete in "+o+"ms")},n.prototype.setup_ember=function(t){var s=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_channel(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_mod_card(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_my_emotes(),this.setup_races(),this.connect_extra_chat(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var i=e.performance&&performance.now?performance.now():Date.now(),o=i-s;this.log("Initialization complete in "+o+"ms")},n.prototype.setup_message_event=function(){this.log("Listening for Window Messages."),e.addEventListener("message",this._on_window_message.bind(this),!1)},n.prototype._on_window_message=function(e){if(e.data&&e.data.from_ffz){var t=e.data;this.log("Window Message",t)}}},{"./badges":1,"./commands":2,"./debug":4,"./ember/channel":5,"./ember/chatview":6,"./ember/line":7,"./ember/moderation-card":8,"./ember/room":9,"./ember/viewers":10,"./emoticons":11,"./ext/betterttv":12,"./ext/emote_menu":13,"./featurefriday":15,"./settings":16,"./socket":17,"./tokenize":18,"./ui/about_page":19,"./ui/dark":20,"./ui/menu":21,"./ui/menu_button":22,"./ui/my_emotes":23,"./ui/notifications":24,"./ui/races":25,"./ui/styles":26,"./ui/sub_count":27,"./ui/viewer_count":28}],15:[function(t){var n=e.FrankerFaceZ,s=t("./constants");n.prototype.feature_friday=null,n.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(s.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},n.ws_commands.reload_ff=function(){this.check_ff()},n.prototype._feature_friday_ui=function(e,t,n){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,n,[this.feature_friday.set],this.feature_friday.title,this.feature_friday.icon,"FrankerFaceZ");var s=App.__container__.lookup("controller:channel");if(!s||s.get("id")!=this.feature_friday.channel){var i=this.feature_friday,o=document.createElement("div"),a=document.createElement("a");o.className="chat-menu-content",o.style.textAlign="center";var r=i.display_name+(i.live?" is live now!":"");a.className="button primary",a.classList.toggle("live",i.live),a.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),a.href="http://www.twitch.tv/"+i.channel,a.title=r,a.target="_new",a.innerHTML=""+r+"",o.appendChild(a),t.appendChild(o)}}},n.prototype._load_ff=function(e){this.feature_friday&&(this.global_sets.removeObject(this.feature_friday.set),this.default_sets.removeObject(this.feature_friday.set),this.feature_friday=null,this.update_ui_link()),e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,title:e.title||"Feature Friday",display_name:n.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.default_sets.push(e.set),this.load_set(e.set),this._update_ff_live())},n.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},n.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],16:[function(t){var n=e.FrankerFaceZ,s=t("./constants");make_ls=function(e){return"ffz_setting_"+e},toggle_setting=function(e,t){var n=!this.settings.get(t);this.settings.set(t,n),e.classList.toggle("active",n)},n.settings_info={},n.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var t in n.settings_info)if(n.settings_info.hasOwnProperty(t)){var s=n.settings_info[t],i=s.storage_key||make_ls(t),o=s.hasOwnProperty("value")?s.value:void 0;if(localStorage.hasOwnProperty(i))try{o=JSON.parse(localStorage.getItem(i))}catch(a){this.log('Error loading value for "'+t+'": '+a)}this.settings[t]=o}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this),!1)},n.settings_info.replace_twitch_menu={type:"boolean",value:!1,name:"Replace Twitch Emoticon Menu Beta",help:"Completely replace the default Twitch emoticon menu.",on_update:function(e){document.body.classList.toggle("ffz-menu-replace",e)}},n.menu_pages.settings={render:function(e,t){var s={},i=[];for(var o in n.settings_info)if(n.settings_info.hasOwnProperty(o)){var a=n.settings_info[o],r=a.category||"Miscellaneous",c=s[r];if(void 0!==a.visible&&null!==a.visible){var l=a.visible;if("function"==typeof a.visible&&(l=a.visible.bind(this)()),!l)continue}c||(i.push(r),c=s[r]=[]),c.push([o,a])}i.sort(function(e,t){var e=e.toLowerCase(),t=t.toLowerCase();return"debugging"===e&&(e="zzz"+e),"debugging"===t&&(t="zzz"+t),t>e?-1:e>t?1:0});for(var u=0;un?-1:n>s?1:o>i?-1:i>o?1:0});for(var m=0;m",v.className="switch-label",v.innerHTML=a.name,p.appendChild(y),p.appendChild(v),y.addEventListener("click",toggle_setting.bind(this,y,o))}else{p.classList.add("option");var w=document.createElement("a");w.innerHTML=a.name,w.href="#",p.appendChild(w),w.addEventListener("click",a.method.bind(this))}if(a.help){var b=document.createElement("span");b.className="help",b.innerHTML=a.help,p.appendChild(b)}}_.appendChild(p)}t.appendChild(_)}},name:"Settings",icon:s.GEAR,sort_order:99999,wide:!0},n.prototype._setting_update=function(t){if(t||(t=e.event),t.key&&"ffz_setting_"===t.key.substr(0,12)){var s=t.key,i=s.substr(12),o=void 0,a=n.settings_info[i];if(!a){for(i in n.settings_info)if(n.settings_info.hasOwnProperty(i)&&(a=n.settings_info[i],a.storage_key==s))break;if(a.storage_key!=s)return}this.log("Updated Setting: "+i);try{o=JSON.parse(t.newValue)}catch(r){this.log('Error loading new value for "'+i+'": '+r),o=a.value||void 0}if(this.settings[i]=o,a.on_update)try{a.on_update.bind(this)(o,!1)}catch(r){this.log('Error running updater for setting "'+i+'": '+r)}}},n.prototype._setting_get=function(e){return this.settings[e]},n.prototype._setting_set=function(e,t){var s=n.settings_info[e],i=s.storage_key||make_ls(e),o=JSON.stringify(t);if(this.settings[e]=t,localStorage.setItem(i,o),this.log('Changed Setting "'+e+'" to: '+o),s.on_update)try{s.on_update.bind(this)(t,!0)}catch(a){this.log('Error running updater for setting "'+e+'": '+a)}},n.prototype._setting_del=function(e){var t=n.settings_info[e],s=t.storage_key||make_ls(e),i=void 0;if(localStorage.hasOwnProperty(s)&&localStorage.removeItem(s),delete this.settings[e],t&&(i=this.settings[e]=t.hasOwnProperty("value")?t.value:void 0),t.on_update)try{t.on_update.bind(this)(i,!0)}catch(o){this.log('Error running updater for setting "'+e+'": '+o)}}},{"./constants":3}],17:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var n,s=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{n=this._ws_sock=new WebSocket("ws://catbag.frankerfacez.com/")}catch(i){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+i)}this._ws_exists=!0,n.onopen=function(){s._ws_open=!0,s._ws_delay=0,s.log("Socket connected.");var n=e.RequestFileSystem||e.webkitRequestFileSystem;n?n(e.TEMPORARY,100,s.ws_send.bind(s,"hello",["ffz_"+t.version_info,localStorage.ffzClientId],s._ws_on_hello.bind(s)),s.log.bind(s,"Operating in Incognito Mode.")):s.ws_send("hello",["ffz_"+t.version_info,localStorage.ffzClientId],s._ws_on_hello.bind(s));var i=s.get_user();if(i&&s.ws_send("setuser",i.login),s.is_dashboard){var o=location.pathname.match(/\/([^\/]+)/);o&&s.ws_send("sub",o[1])}for(var a in s.rooms)s.rooms.hasOwnProperty(a)&&s.ws_send("sub",a);var r=s._ws_pending;s._ws_pending=[];for(var c=0;c';if(e.isLink){if(!t&&void 0!==t)return e.href;var n=e.href;if(n.indexOf("@")>-1&&(-1===n.indexOf("/")||n.indexOf("@")'+n+"";var s=(n.match(/^https?:\/\//)?"":"http://")+n;return''+n+""}return e.mentionedUser?''+e.mentionedUser+"":i.sanitize(e.deletedLink?e.text:e)}).join("")},s.prototype.tokenize_title_emotes=function(e){var t=this,n=App.__container__.lookup("controller:channel"),s=n&&n.get("product.emoticons"),i=[];return _.isString(e)&&(e=[e]),_.each(_.union(t.__twitch_global_emotes||[],s),function(t){if(t&&"inactive"!==t.state){var n=new RegExp("\\b"+t.regex+"\\b");_.any(e,function(e){return _.isString(e)&&e.match(n)})&&i.push(t)}}),(void 0===t.__twitch_global_emotes||null===t.__twitch_global_emotes)&&(t.__twitch_global_emotes=!1,Twitch.api.get("chat/emoticon_images",{emotesets:"0,42"}).done(function(e){if(!e||!e.emoticon_sets||!e.emoticon_sets[0])return void(t.__twitch_global_emotes=[]);var n=t.__twitch_global_emotes=[];e=e.emoticon_sets[0];for(var s=0;s0&&(i=!0)}var r=document.createElement("div"),c="";c+="

FrankerFaceZ

",c+='
new ways to woof
',r.className="chat-menu-content center",r.innerHTML=c,t.appendChild(r);var l=0,u=r.querySelector("h1");u&&u.addEventListener("click",function(){if(u.style.cursor="pointer",l++,l>=3){l=0;var e=document.querySelector(".app-main")||document.querySelector(".ember-chat-container");e&&e.classList.toggle("ffz-flip")}setTimeout(function(){l=0,u.style.cursor=""},2e3)});var h=document.createElement("div"),d=document.createElement("a"),_="To use custom emoticons in "+(i?"this channel":"tons of channels")+", get FrankerFaceZ from http://www.frankerfacez.com";d.className="button primary",d.innerHTML="Advertise in Chat",d.addEventListener("click",this._add_emote.bind(this,e,_)),h.appendChild(d);var f=document.createElement("a");f.className="button ffz-donate",f.href="https://www.frankerfacez.com/donate",f.target="_new",f.innerHTML="Donate",h.appendChild(f),h.className="chat-menu-content center",t.appendChild(h);var m=document.createElement("div");c='',c+='',c+='',c+='',c+='',m.className="chat-menu-content center",m.innerHTML=c;var p=!1;m.querySelector("#ffz-debug-logs").addEventListener("click",function(){p||(p=!0,o._pastebin(o._log_data.join("\n"),function(e){p=!1,e?prompt("Your FrankerFaceZ logs have been uploaded to the URL:",e):alert("There was an error uploading the FrankerFaceZ logs.")}))}),t.appendChild(m)}}},{"../constants":3}],20:[function(t){var n=e.FrankerFaceZ,s=t("../constants");n.settings_info.twitch_chat_dark={type:"boolean",value:!1,visible:!1},n.settings_info.dark_twitch={type:"boolean",value:!1,no_bttv:!0,name:"Dark Twitch",help:"Apply a dark background to channels and other related pages for easier viewing.",on_update:function(t){if(!this.has_bttv){document.body.classList.toggle("ffz-dark",t);var n=e.App?App.__container__.lookup("controller:settings").get("model"):void 0;t?(this._load_dark_css(),n&&this.settings.set("twitch_chat_dark",n.get("darkMode")),n&&n.set("darkMode",!0)):n&&n.set("darkMode",this.settings.twitch_chat_dark)}}},n.prototype.setup_dark=function(){this.has_bttv||(document.body.classList.toggle("ffz-dark",this.settings.dark_twitch),this.settings.dark_twitch&&(e.App&&App.__container__.lookup("controller:settings").set("model.darkMode",!0),this._load_dark_css()))},n.prototype._load_dark_css=function(){if(!this._dark_style){this.log("Injecting FrankerFaceZ Dark Twitch CSS.");var e=this._dark_style=document.createElement("link");e.id="ffz-dark-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",s.SERVER+"script/dark.css?_="+Date.now()),document.head.appendChild(e)}}},{"../constants":3}],21:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils"),o="http://static-cdn.jtvnw.net/emoticons/v1/";n.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var n,s=e._popup;s&&(s=jQuery(s),n=s.parent(),n.is(t.target)||0!==n.has(t.target).length||(s.remove(),delete e._popup,e._popup_kill&&e._popup_kill(),delete e._popup_kill))}),document.body.classList.toggle("ffz-menu-replace",this.settings.replace_twitch_menu)},n.menu_pages={},n.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),delete this._popup,this._popup_kill&&this._popup_kill(),void delete this._popup_kill;var i=document.createElement("div"),o=document.createElement("div"),a=document.createElement("ul"),r=this.has_bttv?BetterTTV.settings.get("darkenedMode"):!1;i.className="emoticon-selector chat-menu ffz-ui-popup",o.className="emoticon-selector-box dropmenu",i.appendChild(o),i.classList.toggle("dark",r);var c=document.createElement("div");c.className="ffz-ui-menu-page",o.appendChild(c),a.className="menu clearfix",o.appendChild(a);var l=document.createElement("li");l.className="title",l.innerHTML=""+(s.DEBUG?"[DEV] ":"")+"FrankerFaceZ",a.appendChild(l);var u=[];for(var h in n.menu_pages)if(n.menu_pages.hasOwnProperty(h)){var d=n.menu_pages[h];d&&(!d.hasOwnProperty("visible")||d.visible&&("function"!=typeof d.visible||d.visible.bind(this)()))&&u.push([d.sort_order||0,h,d])}u.sort(function(e,t){if(e[0]t[0])return-1;var n=e[1].toLowerCase(),s=t[1].toLowerCase();return s>n?1:n>s?-1:0});for(var _=0;_0,h.className="emoticon-grid",d.className="heading",u&&(d.style.backgroundImage='url("'+u+'")'),d.innerHTML='TwitchSubscriber Emoticons',h.appendChild(d);for(var f=r.get("emoticons"),m=0;m0&&t.appendChild(h),l){var y=c.get("content");if(y=y.length>0?y[y.length-1]:void 0,y&&y.purchase_profile&&!y.purchase_profile.will_renew){var w=i.parse_date(y.access_end||"");z=document.createElement("div"),k=document.createElement("div"),F=document.createElement("span"),end_time=w?Math.floor((w.getTime()-Date.now())/1e3):null,z.className="subscribe-message",k.className="non-subscriber-message",z.appendChild(k),F.className="unlock-text",F.innerHTML="Subscription expires in "+i.time_to_string(end_time,!0,!0),k.appendChild(F),t.appendChild(z)}}else{var z=document.createElement("div"),k=document.createElement("div"),F=document.createElement("span"),C=document.createElement("a");z.className="subscribe-message",k.className="non-subscriber-message",z.appendChild(k),F.className="unlock-text",F.innerHTML="Subscribe to unlock Emoticons",k.appendChild(F),C.className="action subscribe-button button primary",C.href=r.get("product_url"),C.innerHTML='",k.appendChild(C),t.appendChild(z)}}}this._emotes_for_sets(t,e,s&&s.set&&[s.set]||[],this.feature_friday||a?"Channel Emoticons":null,"http://cdn.frankerfacez.com/script/devicon.png","FrankerFaceZ"),this._feature_friday_ui(n,t,e)},name:"Channel",icon:s.ZREKNARF},n.prototype._emotes_for_sets=function(e,t,n,s,i,o){var a=document.createElement("div"),r=0;if(a.className="emoticon-grid",null!=s){var c=document.createElement("div");if(c.className="heading",o){var l=document.createElement("span");l.className="right",l.appendChild(document.createTextNode(o)),c.appendChild(l)}c.appendChild(document.createTextNode(s)),i&&(c.style.backgroundImage='url("'+i+'")'),a.appendChild(c)}for(var u=[],h=0;hn?-1:n>s?1:0});for(var h=0;h0&&(o=!0)}t.classList.toggle("no-emotes",!o),t.classList.toggle("live",c),t.classList.toggle("dark",a),t.classList.toggle("blue",r)}}},{"../constants":3}],23:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils"),o="http://static-cdn.jtvnw.net/emoticons/v1/",a={"#-?[\\\\/]":"#-/",":-?(?:7|L)":":-7","\\<\\;\\]":"<]","\\:-?(S|s)":":-S","\\:-?\\\\":":-\\","\\:\\>\\;":":>","B-?\\)":"B-)","\\:-?[z|Z|\\|]":":-Z","\\:-?\\)":":-)","\\:-?\\(":":-(","\\:-?(p|P)":":-P","\\;-?(p|P)":";-P","\\<\\;3":"<3","\\:-?[\\\\/]":":-/","\\;-?\\)":";-)","R-?\\)":"R-)","[o|O](_|\\.)[o|O]":"O.o","\\:-?D":":-D","\\:-?(o|O)":":-O","\\>\\;\\(":">(","Gr(a|e)yFace":"GrayFace"},r=function(e){var t=App.__container__.lookup("controller:chat"),n=t.get("currentRoom.id"),s=e.rooms[n],i=s?s.room.tmiSession:null,o=i&&i._emotesParser&&i._emotesParser.emoticonSetIds||"0",a=e.get_user(),r=a&&e.users[a.login]&&e.users[a.login].sets||[];return o=o.split(",").removeObject("0"),e.settings.global_emotes_in_menu&&(o.push("0"),r=_.union(r,e.default_sets)),[o,r]};n.settings_info.global_emotes_in_menu={type:"boolean",value:!1,name:"Display Global Emotes in My Emotes",help:"Display the global Twitch emotes in the My Emoticons menu."},n.prototype.setup_my_emotes=function(){if(this._twitch_set_to_channel={},this._twitch_badges={},localStorage.ffzTwitchSets)try{this._twitch_set_to_channel=JSON.parse(localStorage.ffzTwitchSets),this._twitch_badges=JSON.parse(localStorage.ffzTwitchBadges)}catch(e){}this._twitch_set_to_channel[0]="global",this._twitch_set_to_channel[33]="tfaces",this._twitch_set_to_channel[42]="tfaces",this._twitch_badges.global="//cdn.frankerfacez.com/script/twitch_logo.png",this._twitch_badges.tfaces=this._twitch_badges.turbo="//cdn.frankerfacez.com/script/turbo_badge.png"},n.menu_pages.my_emotes={name:"My Emoticons",icon:s.EMOTE,visible:function(){var e=r(this);return e[0].length>0||e[1].length>0},render:function(e,t){var s=e.get("controller.currentRoom.tmiSession"),i=(s&&s.getEmotes()||{emoticon_sets:{}}).emoticon_sets,o=[];for(var a in i)i.hasOwnProperty(a)&&!this._twitch_set_to_channel.hasOwnProperty(a)&&o.push(a);return o.length?void(t.innerHTML=JSON.stringify(o)):n.menu_pages.my_emotes.draw_menu.bind(this)(e,t,i)},draw_twitch_set:function(e,t,s){var r,c=document.createElement("div"),l=document.createElement("div"),u=this._twitch_set_to_channel[t];if(r="global"===u?"Global Emoticons":"turbo"===u?"Twitch Turbo":n.get_capitalization(u,function(e){c.innerHTML='Twitch'+i.sanitize(e)}),c.className="heading",c.innerHTML='Twitch'+i.sanitize(r),this._twitch_badges[u])c.style.backgroundImage='url("'+this._twitch_badges[u]+'")';else{var h=this;Twitch.api.get("chat/"+u+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(h._twitch_badges[u]=e.subscriber.image,c.style.backgroundImage='url("'+e.subscriber.image+'")')})}l.className="emoticon-grid",l.appendChild(c);for(var d=0;dFrankerFaceZ'+t.title,n.style.backgroundImage='url("'+(t.icon||"//cdn.frankerfacez.com/script/devicon.png")+'")',s.className="emoticon-grid",s.appendChild(n);for(var o in t.emoticons)t.emoticons.hasOwnProperty(o)&&!t.emoticons[o].hidden&&i.push(t.emoticons[o]);i.sort(function(e,t){var n=e.name.toLowerCase(),s=t.name.toLowerCase();return s>n?-1:n>s?1:e.idt.id?1:0});for(var a=0;an?-1:n>s?1:0});for(var l=0;lSpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},n.ws_on_close.push(function(){var t=e.App&&App.__container__.lookup("controller:channel"),n=t&&t.get("id"),s=!1;if(t){for(var i in this.srl_races)delete this.srl_races[i],i==n&&(s=!0);s&&this.rebuild_race_ui()}}),n.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),n=t.get("id"),s=!1,i=0;i=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var c="http://kadgar.net/live",l=!1;for(var u in a.entrants){var h=a.entrants[u].state;a.entrants.hasOwnProperty(u)&&a.entrants[u].channel&&("racing"==h||"entered"==h)&&(c+="/"+a.entrants[u].channel,l=!0)}var d=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,_=App.__container__.lookup("controller:channel"),f=_?_.get("display_name"):n.get_capitalization(o),m=encodeURIComponent("I'm watching "+f+" race "+a.goal+" in "+a.game+" on SpeedRunsLive!");r='
',r+='
Developers
Dan Salvato  
Stendec  
Version '+n.version_info+'Logs
',r+="
#Entrant Time
",r+='
',r+='',r+='

SRL',l&&(r+='   Multitwitch'),r+="

",e.innerHTML=r,t.appendChild(e),this._update_race(!0)}}},n.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var n=t.getAttribute("data-channel"),i=this.srl_races[n];if(!i)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var o=i.twitch_entrants[n],a=i.entrants[o],r=t.querySelector("#ffz-race-popup"),c=Date.now()/1e3,l=Math.floor(c-i.time);if(t.querySelector(".logo").innerHTML=s.placement(a),r){var u=r.querySelector("tbody"),h=r.querySelector(".heading span"),d=r.querySelector(".heading div");u.innerHTML="";var _=[],f=!0;for(var m in i.entrants)i.entrants.hasOwnProperty(m)&&("racing"==i.entrants[m].state&&(f=!1),_.push(i.entrants[m]));_.sort(function(e,t){var n=e.place||9999,s=t.place||9999,i=e.time||l,o=t.time||l;return("forfeit"==e.state||"dq"==e.state)&&(n=1e4),("forfeit"==t.state||"dq"==t.state)&&(s=1e4),s>n?-1:n>s?1:e.namet.name?1:o>i?-1:i>o?1:void 0});for(var p=0;p<_.length;p++){var m=_[p],g=''+m.display_name+"",v=m.channel?'':"",b=m.hitbox?'':"",y=l?s.time_to_string(m.time||l):"",w=s.place_string(m.place),z=m.comment?s.sanitize(m.comment):"";u.innerHTML+="'+w+""+g+""+v+b+''+("forfeit"==m.state?"Forfeit":y)+""}if(this._race_game!=i.game||this._race_goal!=i.goal){this._race_game=i.game,this._race_goal=i.goal;var k=s.sanitize(i.game),F=s.sanitize(i.goal);d.innerHTML='

'+k+"

Goal: "+F}l?f?h.innerHTML="Done":(h.innerHTML=s.time_to_string(l),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):h.innerHTML="Entry Open"}}}},{"../utils":29}],26:[function(t){var n=e.FrankerFaceZ,s=t("../constants");n.prototype.setup_css=function(){this.log("Injecting main FrankerFaceZ CSS.");var e=this._main_style=document.createElement("link");e.id="ffz-ui-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",s.SERVER+"script/style.css?_="+Date.now()),document.head.appendChild(e),jQuery.noty.themes.ffzTheme={name:"ffzTheme",style:function(){this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type)},callback:{onShow:function(){},onClose:function(){}}}}},{"../constants":3}],27:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils");n.prototype._update_subscribers=function(){this._update_subscribers_timer&&(clearTimeout(this._update_subscribers_timer),delete this._update_subscribers_timer);var e=this.get_user(),t=this,n=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,o=this.is_dashboard&&n&&n[1];if(this.has_bttv||!o||o!==e.login){var a=document.querySelector("#ffz-sub-display");return void(a&&a.parentElement.removeChild(a))}this._update_subscribers_timer=setTimeout(this._update_subscribers.bind(this),6e4),jQuery.ajax({url:"/broadcast/dashboard/partnership"}).done(function(e){try{var n,a=document.createElement("span");a.innerHTML=e,n=a.querySelector("#dash_main");var r=n&&n.textContent.match(/([\d,\.]+) total active subscribers/),c=r&&r[1];if(!c){var l=document.querySelector("#ffz-sub-display");return l&&l.parentElement.removeChild(l),void(t._update_subscribers_timer&&(clearTimeout(t._update_subscribers_timer),delete t._update_subscribers_timer))}var l=document.querySelector("#ffz-sub-display span");if(!l){var u=document.querySelector(t.is_dashboard?"#stats":"#channel .stats-and-actions .channel-stats");if(!u)return;var h=document.createElement("span");h.className="ffz stat",h.id="ffz-sub-display",h.title="Active Channel Subscribers",h.innerHTML=s.STAR+" ",l=document.createElement("span"),h.appendChild(l),Twitch.api.get("chat/"+o+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(h.innerHTML="",h.appendChild(l),h.style.backgroundImage='url("'+e.subscriber.image+'")',h.style.backgroundRepeat="no-repeat",h.style.paddingLeft="23px",h.style.backgroundPosition="0 50%")}),u.appendChild(h),jQuery(h).tipsy(t.is_dashboard?{gravity:"s"}:void 0)}l.innerHTML=i.number_commas(parseInt(c))}catch(d){t.error("_update_subscribers: "+d)}}).fail(function(){var e=document.querySelector("#ffz-sub-display");e&&e.parentElement.removeChild(e)})}},{"../constants":3,"../utils":29}],28:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils");n.ws_commands.viewers=function(t){var n=t[0],o=t[1],a=e.App&&App.__container__.lookup("controller:channel"),r=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,c=this.is_dashboard?r&&r[1]:a&&a.get&&a.get("id");if(!this.is_dashboard){var l=this.rooms&&this.rooms[n];return void(l&&(l.ffz_chatters=o,this._cindex&&this._cindex.ffzUpdateChatters()))}if(this.settings.chatter_count&&c===n){var u=document.querySelector("#ffz-ffzchatter-display"),h=s.ZREKNARF+" "+i.number_commas(o);if(u)u.innerHTML=h;else{var d=document.querySelector("#stats");if(!d)return;u=document.createElement("span"),u.id="ffz-ffzchatter-display",u.className="ffz stat",u.title="Chatters with FrankerFaceZ",u.innerHTML=h,d.appendChild(u),jQuery(u).tipsy(this.is_dashboard?{gravity:"s"}:void 0)}}}},{"../constants":3,"../utils":29}],29:[function(t,n){var s=(e.FrankerFaceZ,t("./constants"),{}),i=document.createElement("span"),o=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"},a=function(e,t){t=0===t?0:t||1,t=Math.round(255*-(t/100));var n=Math.max(0,Math.min(255,e[0]-t)),s=Math.max(0,Math.min(255,e[1]-t)),i=Math.max(0,Math.min(255,e[2]-t));return[n,s,i]},r=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},c=function(e,t){return t=0===t?0:t||1,a(e,-t)},l=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;ta;(c||n)&&(c&&(s=s.substr(0,a)+s.substr(r+o.length)),n&&(s+=i+n+o),e.innerHTML=s)},get_luminance:l,brighten:a,darken:c,rgb_to_css:r,parse_date:h,number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:o,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?o(e.place):""},sanitize:function(e){var t=s[e];return t||(i.textContent=e,t=s[e]=i.innerHTML,i.innerHTML=""),t},date_string:function(e){return e.getFullYear()+"-"+(e.getMonth()+1)+"-"+e.getDate()},time_to_string:function(e,t,n){var s=e%60,i=Math.floor(e/60),o=Math.floor(i/60),a="";if(i%=60,t){if(a=Math.floor(o/24),o%=24,n&&a>0)return a+" days";a=a>0?a+" days, ":""}return a+(10>o?"0":"")+o+":"+(10>i?"0":"")+i+":"+(10>s?"0":"")+s}}},{"./constants":3}]},{},[14]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index c64c7e6f..2156521d 100644 --- a/src/badges.js +++ b/src/badges.js @@ -7,13 +7,13 @@ var FFZ = window.FrankerFaceZ, // Settings // -------------------- -FFZ.settings_info.bot_badges = { +FFZ.settings_info.show_badges = { type: "boolean", value: true, category: "Chat", - name: "Bot Badges", - help: "Give special badges to known bots." + name: "Additional Badges", + help: "Show additional badges for bots, FrankerFaceZ donors, and other special users." }; @@ -74,6 +74,9 @@ var badge_css = function(badge) { // -------------------- FFZ.prototype.bttv_badges = function(data) { + if ( ! this.settings.show_badges ) + return; + var user_id = data.sender, user = this.users[user_id], badges_out = [], @@ -154,10 +157,13 @@ FFZ.prototype.bttv_badges = function(data) { } -FFZ.prototype.render_badge = function(view) { - var user = view.get('context.model.from'), - room_id = view.get('context.parentController.content.id'), - badges = view.$('.badges'); +FFZ.prototype.render_badge = function(component) { + if ( ! this.settings.show_badges ) + return; + + var user = component.get('msgObject.from'), + room_id = App.__container__.lookup('controller:chat').get('currentRoom.id'), + badges = component.$('.badges'); var data = this.users[user]; if ( ! data || ! data.badges ) @@ -233,17 +239,17 @@ FFZ.bttv_known_bots = ["nightbot","moobot","sourbot","xanbot","manabot","mtgbot" FFZ.prototype._legacy_add_donors = function() { // Developer Badge - this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "//cdn.frankerfacez.com/channel/global/devicon.png"}; + this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "//cdn.frankerfacez.com/script/devicon.png"}; utils.update_css(this._badge_style, 0, badge_css(this.badges[0])); // Donor Badge - this.badges[1] = {id: 1, title: "FFZ Donor", color: "#755000", image: "//cdn.frankerfacez.com/channel/global/donoricon.png"}; + this.badges[1] = {id: 1, title: "FFZ Donor", color: "#755000", image: "//cdn.frankerfacez.com/script/donoricon.png"}; utils.update_css(this._badge_style, 1, badge_css(this.badges[1])); // Bot Badge - this.badges[2] = {id: 2, title: "Bot", color: "#595959", image: "//cdn.frankerfacez.com/channel/global/boticon.png", + this.badges[2] = {id: 2, title: "Bot", color: "#595959", image: "//cdn.frankerfacez.com/script/boticon.png", replaces: 'moderator', - visible: function(r,user) { return this.settings.bot_badges && !(this.has_bttv && FFZ.bttv_known_bots.indexOf(user)!==-1); }}; + visible: function(r,user) { return !(this.has_bttv && FFZ.bttv_known_bots.indexOf(user)!==-1); }}; utils.update_css(this._badge_style, 2, badge_css(this.badges[2])); // Load BTTV Bots @@ -257,10 +263,8 @@ FFZ.prototype._legacy_add_donors = function() { } // Special Badges - this.users.sirstendec = {badges: {1: {id:0}}}; - this.users.zenwan = {badges: {0: {id:2, image: "//cdn.frankerfacez.com/channel/global/momiglee_badge.png", title: "WAN"}}}; - - this.load_set(".donor"); + this.users.sirstendec = {badges: {1: {id:0}}, sets: [4330]}; + this.users.zenwan = {badges: {0: {id:2, image: "//cdn.frankerfacez.com/script/momiglee_badge.png", title: "WAN"}}}; this._legacy_load_bots(); this._legacy_load_donors(); @@ -300,7 +304,7 @@ FFZ.prototype._legacy_load_donors = function(tries) { FFZ.prototype._legacy_parse_badges = function(data, slot, badge_id) { var title = this.badges[badge_id].title, count = 0; - ds = badge_id == 1 ? ".donor" : ""; + ds = null; if ( data != null ) { var lines = data.trim().split(/\W+/); @@ -310,7 +314,7 @@ FFZ.prototype._legacy_parse_badges = function(data, slot, badge_id) { badges = user.badges = user.badges || {}, sets = user.sets = user.sets || []; - if ( sets.indexOf(ds) === -1 ) + if ( ds !== null && sets.indexOf(ds) === -1 ) sets.push(ds); if ( badges[slot] ) diff --git a/src/constants.js b/src/constants.js index 0352a5f2..20281eee 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,13 +4,20 @@ var SVGPATH = '' + SVGPATH + '', + ZREKNARF: '' + SVGPATH + '', CHAT_BUTTON: '' + SVGPATH + '', - CLOCK: '', + ROOMS: '', + CAMERA: '', + INVITE: '', + + EYE: '', + CLOCK: '', GEAR: '', HEART: '', - EMOTE: '' + EMOTE: '', + STAR: '' } \ No newline at end of file diff --git a/src/ember/channel.js b/src/ember/channel.js index 7c153873..56473a1d 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -8,35 +8,277 @@ var FFZ = window.FrankerFaceZ, // -------------------- FFZ.prototype.setup_channel = function() { - this.channels = {}; + // Settings stuff! + document.body.classList.toggle("ffz-hide-view-count", !this.settings.channel_views); this.log("Creating channel style element."); var s = this._channel_style = document.createElement('style'); s.id = "ffz-channel-css"; document.head.appendChild(s); - this.log("Hooking the Ember Channel view."); - - var Channel = App.__container__.lookup('controller:channel'), + this.log("Hooking the Ember Channel Index view."); + var Channel = App.__container__.resolve('view:channel/index'), f = this; if ( ! Channel ) return; + this._modify_cindex(Channel); + + // The Stupid View Fix. Is this necessary still? + try { + Channel.create().destroy(); + } catch(err) { } + + // Update Existing + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Channel) ) + continue; + + this.log("Manually updating Channel Index view.", view); + this._modify_cindex(view); + view.ffzInit(); + }; + + + this.log("Hooking the Ember Channel controller."); + + Channel = App.__container__.lookup('controller:channel'); + if ( ! Channel ) + return; + Channel.reopen({ ffzUpdateUptime: function() { - f.update_uptime(); - }.observes("isLive", "content.id").on("init") + if ( f._cindex ) + f._cindex.ffzUpdateUptime(); - /*ffzUpdateInfo: function() { - f.log("Updated! ID: " + this.get("content.id")); - f.update_stream_info(true); - }.observes("content.id").on("init")*/ + }.observes("isLive", "content.id"), + + ffzUpdateTitle: function() { + var name = this.get('content.name'), + display_name = this.get('content.display_name'); + + if ( display_name ) + FFZ.capitalization[name] = [display_name, Date.now()]; + + if ( f._cindex ) + f._cindex.ffzFixTitle(); + }.observes("content.status", "content.id") + + /*ffzHostTarget: function() { + var target = this.get('content.hostModeTarget'), + name = target && target.get('name'), + display_name = target && target.get('display_name'); + + if ( display_name ) + FFZ.capitalization[name] = [display_name, Date.now()]; + + if ( f.settings.group_tabs && f._chatv ) + f._chatv.ffzRebuildTabs(); + }.observes("content.hostModeTarget")*/ }); +} - // Do uptime the first time. - this.update_uptime(); - //this.update_stream_info(true); + +FFZ.prototype._modify_cindex = function(view) { + var f = this; + + view.reopen({ + didInsertElement: function() { + this._super(); + try { + this.ffzInit(); + } catch(err) { + f.error("CIndex didInsertElement: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("CIndex willClearRender: " + err); + } + return this._super(); + }, + + ffzInit: function() { + f._cindex = this; + this.get('element').setAttribute('data-channel', this.get('controller.id')); + this.ffzFixTitle(); + this.ffzUpdateUptime(); + this.ffzUpdateChatters(); + + var el = this.get('element').querySelector('.svg-glyph_views:not(.ffz-svg)') + if ( el ) + el.parentNode.classList.add('twitch-channel-views'); + }, + + ffzFixTitle: function() { + if ( f.has_bttv || ! f.settings.stream_title ) + return; + + var status = this.get("controller.status"), + channel = this.get("controller.id"); + + status = f.render_tokens(f.tokenize_line(channel, channel, status, true)); + + this.$(".title span").each(function(i, el) { + var scripts = el.querySelectorAll("script"); + el.innerHTML = scripts[0].outerHTML + status + scripts[1].outerHTML; + }); + }, + + + ffzUpdateChatters: function() { + // Get the counts. + var room_id = this.get('controller.id'), + room = f.rooms && f.rooms[room_id]; + + if ( ! room || ! f.settings.chatter_count ) { + var el = this.get('element').querySelector('#ffz-chatter-display'); + el && el.parentElement.removeChild(el); + el = this.get('element').querySelector('#ffz-ffzchatter-display'); + el && el.parentElement.removeChild(el); + return; + } + + var chatter_count = Object.keys(room.room.get('ffz_chatters') || {}).length, + ffz_chatters = room.ffz_chatters || 0; + + var el = this.get('element').querySelector('#ffz-chatter-display span'); + if ( ! el ) { + var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-chatter-display'; + stat.title = "Current Chatters"; + + stat.innerHTML = constants.ROOMS + " "; + el = document.createElement("span"); + stat.appendChild(el); + + var other = cont.querySelector("#ffz-ffzchatter-display"); + if ( other ) + cont.insertBefore(stat, other); + else + cont.appendChild(stat); + + jQuery(stat).tipsy(); + } + + el.innerHTML = utils.number_commas(chatter_count); + + if ( ! ffz_chatters ) { + el = this.get('element').querySelector('#ffz-ffzchatter-display'); + el && el.parentNode.removeChild(el); + return; + } + + el = this.get('element').querySelector('#ffz-ffzchatter-display span'); + if ( ! el ) { + var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-ffzchatter-display'; + stat.title = "Chatters with FrankerFaceZ"; + + stat.innerHTML = constants.ZREKNARF + " "; + el = document.createElement("span"); + stat.appendChild(el); + + var other = cont.querySelector("#ffz-chatter-display"); + if ( other ) + cont.insertBefore(stat, other.nextSibling); + else + cont.appendChild(stat); + + jQuery(stat).tipsy(); + } + + el.innerHTML = utils.number_commas(ffz_chatters); + }, + + + ffzUpdateUptime: function() { + if ( this._ffz_update_uptime ) { + clearTimeout(this._ffz_update_uptime); + delete this._ffz_update_uptime; + } + + if ( ! f.settings.stream_uptime || ! this.get("controller.isLiveAccordingToKraken") ) { + var el = this.get('element').querySelector('#ffz-uptime-display'); + if ( el ) + el.parentElement.removeChild(el); + return; + } + + // Schedule an update. + this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000); + + // Determine when the channel last went live. + var online = this.get("controller.content.stream.created_at"); + if ( ! online ) + return; + + online = utils.parse_date(online); + if ( ! online ) + return; + + var uptime = Math.floor((Date.now() - online.getTime()) / 1000); + if ( uptime < 0 ) + return; + + var el = this.get('element').querySelector('#ffz-uptime-display span'); + if ( ! el ) { + var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-uptime-display'; + stat.title = "Stream Uptime (since " + online.toLocaleString() + ")"; + + stat.innerHTML = constants.CLOCK + " "; + el = document.createElement("span"); + stat.appendChild(el); + + var viewers = cont.querySelector(".live-count"); + if ( viewers ) + cont.insertBefore(stat, viewers.nextSibling); + else { + try { + viewers = cont.querySelector("script:nth-child(0n+2)"); + cont.insertBefore(stat, viewers.nextSibling); + } catch(err) { + cont.insertBefore(stat, cont.childNodes[0]); + } + } + + jQuery(stat).tipsy({html: true}); + } + + el.innerHTML = utils.time_to_string(uptime); + }, + + ffzTeardown: function() { + this.get('element').setAttribute('data-channel', ''); + f._cindex = undefined; + if ( this._ffz_update_uptime ) + clearTimeout(this._ffz_update_uptime); + } + }); } @@ -44,6 +286,42 @@ FFZ.prototype.setup_channel = function() { // Settings // --------------- +FFZ.settings_info.chatter_count = { + type: "boolean", + value: false, + + category: "Channel Metadata", + + name: "Chatter Count", + help: "Display the current number of users connected to chat beneath the channel.", + + on_update: function(val) { + if ( this._cindex ) + this._cindex.ffzUpdateChatters(); + + if ( ! val || ! this.rooms ) + return; + + // Refresh the data. + for(var room_id in this.rooms) + this.rooms.hasOwnProperty(room_id) && this.rooms[room_id].room && this.rooms[room_id].room.ffzInitChatterCount(); + } + }; + + +FFZ.settings_info.channel_views = { + type: "boolean", + value: true, + + category: "Channel Metadata", + name: "Channel Views", + help: 'Display the number of times the channel has been viewed beneath the stream.', + on_update: function(val) { + document.body.classList.toggle("ffz-hide-view-count", !val); + } + }; + + FFZ.settings_info.stream_uptime = { type: "boolean", value: false, @@ -52,120 +330,22 @@ FFZ.settings_info.stream_uptime = { name: "Stream Uptime", help: 'Display the stream uptime under a channel by the viewer count.', on_update: function(val) { - this.update_uptime(); + if ( this._cindex ) + this._cindex.ffzUpdateUptime(); } }; -// -------------------- -// Stream Data Update -// -------------------- +FFZ.settings_info.stream_title = { + type: "boolean", + value: true, + no_bttv: true, -/*FFZ.prototype.update_stream_info = function(just_schedule) { - if ( this._stream_info_update ) { - clearTimeout(this._stream_info_update); - delete this._stream_info_update; - } - - this._stream_info_update = setTimeout(this.update_stream_info.bind(this), 90000); - - if ( just_schedule ) - return; - - var Channel = App.__container__.lookup('controller:channel'), - channel_id = Channel ? Channel.get('content.id') : undefined, - f = this; - if ( ! channel_id ) - return; - - Twitch.api.get("streams/" + channel_id, {}, {version: 3}) - .done(function(data) { - var channel_id = Channel.get('content.id'), d = data.stream; - if ( ! data.stream || d.channel.name != channel_id ) - return; - - // Override the data in Twitch. We can't just .load() the stream - // because that resets the whole channel layout, resetting the - // video player. Twitch pls fix - var old_created = Channel.get('content.stream.created_at'); - - Channel.set('content.stream.created_at', d.created_at); - Channel.set('content.stream.average_fps', d.average_fps); - Channel.set('content.stream.viewers', d.viewers); - Channel.set('content.stream.video_height', d.video_height); - Channel.set('content.stream.csGoSkill', Twitch.uri.csGoSkillImg(("0" + d.skill).slice(-2))); - - Channel.set('content.stream.game', d.game); - Channel.set('content.stream.gameUrl', Twitch.uri.game(d.game)); - Channel.set('content.stream.gameBoxart', Twitch.uri.gameBoxArtJpg(d.game)); - - - // Update the uptime display. - if ( f.settings.stream_uptime && old_created != d.created_at ) - f.update_uptime(true) && f.update_uptime(); - }); -}*/ - - -// -------------------- -// Uptime Display -// -------------------- - -FFZ.prototype.update_uptime = function(destroy) { - if ( this._uptime_update ) { - clearTimeout(this._uptime_update); - delete this._uptime_update; - } - - var Channel = App.__container__.lookup('controller:channel'); - if ( destroy || ! this.settings.stream_uptime || ! Channel || ! Channel.get('isLiveAccordingToKraken') ) { - var el = document.querySelector("#ffz-uptime-display"); - if ( el ) - el.parentElement.removeChild(el); - return; - } - - // Schedule an update. - this._update_uptime = setTimeout(this.update_uptime.bind(this), 1000); - - // Determine when the channel last went live. - var online = Channel.get('content.stream.created_at'); - if ( ! online ) return; - - online = utils.parse_date(online); - if ( ! online ) return; - - var uptime = Math.floor((Date.now() - online.getTime()) / 1000); - if ( uptime < 0 ) return; - - var el = document.querySelector("#ffz-uptime-display span"); - if ( ! el ) { - var cont = document.querySelector("#channel .stats-and-actions .channel-stats"); - if ( ! cont ) return; - - var stat = document.createElement("span"); - stat.className = "ffz stat"; - stat.id = "ffz-uptime-display"; - stat.title = "Stream Uptime (since " + online.toLocaleString() + ")"; - - stat.innerHTML = constants.CLOCK + " "; - el = document.createElement("span"); - stat.appendChild(el); - - var viewers = cont.querySelector(".live-count"); - if ( viewers ) - cont.insertBefore(stat, viewers.nextSibling); - else { - try { - viewers = cont.querySelector("script:nth-child(0n+2)"); - cont.insertBefore(stat, viewers.nextSibling); - } catch(err) { - cont.insertBefore(stat, cont.childNodes[0]); - } + category: "Channel Metadata", + name: "Title Links", + help: "Make links in stream titles clickable.", + on_update: function(val) { + if ( this._cindex ) + this._cindex.ffzFixTitle(); } - - jQuery(stat).tipsy({html:true}); - } - - el.innerHTML = utils.time_to_string(uptime); -} \ No newline at end of file + }; \ No newline at end of file diff --git a/src/ember/chatview.js b/src/ember/chatview.js index f1d9377e..5d57b8c7 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -1,4 +1,55 @@ -var FFZ = window.FrankerFaceZ; +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + constants = require('../constants'), + + format_unread = function(count) { + if ( count < 1 ) + return ""; + + else if ( count >= 99 ) + return "99+"; + + return "" + count; + }; + + +// -------------------- +// Settings +// -------------------- + +FFZ.settings_info.group_tabs = { + type: "boolean", + value: false, + + no_bttv: true, + + category: "Chat", + name: "Chat Room Tabs Beta", + help: "Enhanced UI for switching the current chat room and noticing new messages.", + + on_update: function(val) { + var enabled = !this.has_bttv && val; + if ( ! this._chatv || enabled === this._group_tabs_state ) + return; + + if ( enabled ) + this._chatv.ffzEnableTabs(); + else + this._chatv.ffzDisableTabs(); + } + }; + + +FFZ.settings_info.pinned_rooms = { + type: "button", + value: [], + + category: "Chat", + visible: false, + + name: "Pinned Chat Rooms", + help: "Set a list of channels that should always be available in chat." + }; // -------------------- @@ -6,6 +57,21 @@ var FFZ = window.FrankerFaceZ; // -------------------- FFZ.prototype.setup_chatview = function() { + this.log("Hooking the Ember Chat controller."); + + var Chat = App.__container__.lookup('controller:chat'), + f = this; + + if ( Chat ) { + Chat.reopen({ + ffzUpdateChannels: function() { + if ( f.settings.group_tabs && f._chatv ) + f._chatv.ffzRebuildTabs(); + }.observes("currentChannelRoom", "connectedPrivateGroupRooms") + }); + } + + this.log("Hooking the Ember Chat view."); var Chat = App.__container__.resolve('view:chat'); @@ -17,7 +83,6 @@ FFZ.prototype.setup_chatview = function() { Chat.create().destroy(); } catch(err) { } - // Modify all existing Chat views. for(var key in Ember.View.views) { if ( ! Ember.View.views.hasOwnProperty(key) ) @@ -27,13 +92,45 @@ FFZ.prototype.setup_chatview = function() { if ( !(view instanceof Chat) ) continue; - this.log("Adding UI link manually to Chat view.", view); + this.log("Manually updating existing Chat view.", view); try { - view.$('.textarea-contain').append(this.build_ui_link(view)); + view.ffzInit(); } catch(err) { this.error("setup: build_ui_link: " + err); } } + + + this.log("Hooking the Ember Layout controller."); + var Layout = App.__container__.lookup('controller:layout'); + if ( ! Layout ) + return; + + Layout.reopen({ + ffzFixTabs: function() { + if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { + setTimeout(function() { + f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + },0); + } + }.observes("isRightColumnClosed") + }); + + + this.log("Hooking the Ember 'Right Column' controller. Seriously..."); + var Column = App.__container__.lookup('controller:right-column'); + if ( ! Column ) + return; + + Column.reopen({ + ffzFixTabs: function() { + if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { + setTimeout(function() { + f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + },0); + } + }.observes("firstTabSelected") + }); } @@ -47,28 +144,391 @@ FFZ.prototype._modify_cview = function(view) { view.reopen({ didInsertElement: function() { this._super(); + try { - this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + this.ffzInit(); } catch(err) { - f.error("didInsertElement: build_ui_link: " + err); + f.error("ChatView didInsertElement: " + err); } }, willClearRender: function() { - this._super(); try { - this.$(".ffz-ui-toggle").remove(); + this.ffzTeardown(); } catch(err) { - f.error("willClearRender: remove ui link: " + err); + f.error("ChatView willClearRender: " + err); } + this._super(); }, - ffzUpdateLink: Ember.observer('controller.currentRoom', function() { + ffzInit: function() { + f._chatv = this; + this.$('.textarea-contain').append(f.build_ui_link(this)); + + if ( !f.has_bttv && f.settings.group_tabs ) + this.ffzEnableTabs(); + + setTimeout(function() { + if ( f.settings.group_tabs && f._chatv._ffz_tabs ) + f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + + var controller = f._chatv.get('controller'); + controller && controller.set('showList', false); + }, 1000); + }, + + ffzTeardown: function() { + if ( f._chatv === this ) + f._chatv = null; + + this.$('.textarea-contain .ffz-ui-toggle').remove(); + + if ( f.settings.group_tabs ) + this.ffzDisableTabs(); + }, + + ffzChangeRoom: Ember.observer('controller.currentRoom', function() { try { f.update_ui_link(); + + if ( !f.has_bttv && f.settings.group_tabs && this._ffz_tabs ) { + var room = this.get('controller.currentRoom'); + room && room.resetUnreadCount(); + + var tabs = jQuery(this._ffz_tabs); + tabs.children('.ffz-chat-tab').removeClass('active'); + if ( room ) + tabs.children('.ffz-chat-tab[data-room="' + room.get('id') + '"]').removeClass('tab-mentioned').addClass('active').children('span').text(''); + + // Invite Link + var can_invite = room && room.get('canInvite'); + this._ffz_invite && this._ffz_invite.classList.toggle('hidden', !can_invite); + this.set('controller.showInviteUser', can_invite && this.get('controller.showInviteUser')) + + // Now, adjust the chat-room. + this.$('.chat-room').css('top', this._ffz_tabs.offsetHeight + "px"); + } + } catch(err) { - f.error("ffzUpdateLink: update_ui_link: " + err); + f.error("ChatView ffzUpdateLink: " + err); } - }) + }), + + // Group Tabs~! + + ffzEnableTabs: function() { + if ( f.has_bttv || ! f.settings.group_tabs ) + return; + + // Hide the existing chat UI. + this.$(".chat-header").addClass("hidden"); + + // Create our own UI. + var tabs = this._ffz_tabs = document.createElement("div"); + tabs.id = "ffz-group-tabs"; + this.$(".chat-header").after(tabs); + + // List the Rooms + this.ffzRebuildTabs(); + }, + + ffzRebuildTabs: function() { + if ( f.has_bttv || ! f.settings.group_tabs ) + return; + + var tabs = this._ffz_tabs || this.get('element').querySelector('#ffz-group-tabs'); + if ( ! tabs ) + return; + + tabs.innerHTML = ""; + + var link = document.createElement('a'), + view = this; + + link.className = 'button glyph-only tooltip'; + link.title = "Chat Room Management"; + link.innerHTML = constants.ROOMS; + + link.addEventListener('click', function() { + var controller = view.get('controller'); + controller && controller.set('showList', !controller.get('showList')); + }); + + tabs.appendChild(link); + + + link = document.createElement('a'), + link.className = 'button glyph-only tooltip invite'; + link.title = "Invite a User"; + link.innerHTML = constants.INVITE; + + link.addEventListener('click', function() { + var controller = view.get('controller'); + controller && controller.set('showInviteUser', controller.get('currentRoom.canInvite') && !controller.get('showInviteUser')); + }); + + link.classList.toggle('hidden', !this.get("controller.currentRoom.canInvite")); + view._ffz_invite = link; + tabs.appendChild(link); + + var room = this.get('controller.currentChannelRoom'), tab; + if ( room ) { + tab = this.ffzBuildTab(view, room, true); + tab && tabs.appendChild(tab); + } + + // Check Host Target + var Channel = App.__container__.lookup('controller:channel'), + Room = App.__container__.resolve('model:room'); + target = Channel && Channel.get('hostModeTarget'); + + if ( target && Room ) { + var target_id = target.get('id'); + if ( this._ffz_host !== target_id ) { + if ( this._ffz_host_room ) { + if ( this.get('controller.currentRoom') === this._ffz_host_room ) + this.get('controller').blurRoom(); + this._ffz_host_room.destroy(); + } + + this._ffz_host = target_id; + this._ffz_host_room = Room.findOne(target_id); + } + } else if ( this._ffz_host ) { + if ( this._ffz_host_room ) { + if ( this.get('controller.currentRoom') === this._ffz_host_room ) + this.get('controller').blurRoom(); + this._ffz_host_room.destroy(); + } + + delete this._ffz_host; + delete this._ffz_host_room; + } + + if ( this._ffz_host_room ) { + tab = view.ffzBuildTab(view, this._ffz_host_room, false, true); + tab && tabs.appendChild(tab); + } + + // Pinned Rooms + for(var i=0; i < f.settings.pinned_rooms.length; i++) { + var room_id = f.settings.pinned_rooms[i]; + if ( room && room.get('id') !== room_id && this._ffz_host !== room_id && f.rooms[room_id] && f.rooms[room_id].room ) { + var tab = view.ffzBuildTab(view, f.rooms[room_id].room, false, false); + tab && tabs.appendChild(tab); + } + } + + _.each(this.get('controller.connectedPrivateGroupRooms'), function(room) { + var tab = view.ffzBuildTab(view, room); + tab && tabs.appendChild(tab); + }); + + // Now, adjust the chat-room. + this.$('.chat-room').css('top', tabs.offsetHeight + "px"); + }, + + ffzTabUnread: function(room_id) { + if ( f.has_bttv || ! f.settings.group_tabs ) + return; + + var tabs = this._ffz_tabs || this.get('element').querySelector('#ffz-group-tabs'), + current_id = this.get('controller.currentRoom.id'); + if ( ! tabs ) + return; + + if ( room_id ) { + var tab = tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'), + room = f.rooms && f.rooms[room_id]; + + if ( tab && room ) { + var unread = format_unread(room_id === current_id ? 0 : room.room.get('unreadCount')); + tab.querySelector('span').innerHTML = unread; + } + + // Now, adjust the chat-room. + return this.$('.chat-room').css('top', tabs.offsetHeight + "px"); + } + + var children = tabs.querySelectorAll('.ffz-chat-tab'); + for(var i=0; i < children.length; i++) { + var tab = children[i], + room_id = tab.getAttribute('data-room'), + room = f.rooms && f.rooms[room_id]; + + if ( ! room ) + continue; + + var unread = format_unread(room_id === current_id ? 0 : room.room.get('unreadCount')); + tab.querySelector('span').innerHTML = unread; + } + + // Now, adjust the chat-room. + this.$('.chat-room').css('top', tabs.offsetHeight + "px"); + }, + + ffzBuildTab: function(view, room, current_channel, host_channel) { + var tab = document.createElement('span'), name, unread, + group = room.get('isGroupRoom'), + current = room === view.get('controller.currentRoom'); + + tab.setAttribute('data-room', room.id); + + tab.className = 'ffz-chat-tab tooltip'; + tab.classList.toggle('current-channel', current_channel); + tab.classList.toggle('host-channel', host_channel); + tab.classList.toggle('group-chat', group); + tab.classList.toggle('active', current); + + name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'))); + unread = format_unread(current ? 0 : room.get('unreadCount')); + + if ( current_channel ) { + tab.innerHTML = constants.CAMERA; + tab.title = "Current Channel"; + } else if ( host_channel ) { + tab.innerHTML = constants.EYE; + tab.title = "Hosted Channel"; + } else if ( group ) + tab.title = "Group Chat"; + else + tab.title = "Pinned Channel"; + + tab.innerHTML += utils.sanitize(name) + '' + unread + ''; + + tab.addEventListener('click', function() { + view.get('controller').focusRoom(room); + }); + + return tab; + }, + + ffzDisableTabs: function() { + if ( this._ffz_tabs ) { + this._ffz_tabs.parentElement.removeChild(this._ffz_tabs); + delete this._ffz_tabs; + delete this._ffz_invite; + } + + if ( this._ffz_host ) { + if ( this._ffz_host_room ) { + if ( this.get('controller.currentRoom') === this._ffz_host_room ) + this.get('controller').blurRoom(); + this._ffz_host_room.destroy(); + } + + delete this._ffz_host; + delete this._ffz_host_room; + } + + // Show the old chat UI. + this.$('.chat-room').css('top', ''); + this.$(".chat-header").removeClass("hidden"); + }, }); +} + + +// ---------------------- +// Chat Room Connections +// ---------------------- + +FFZ.prototype.connect_extra_chat = function() { + if ( this.has_bttv ) + return; + + for(var i=0; i < this.settings.pinned_rooms.length; i++) + this._join_room(this.settings.pinned_rooms[i], true); + + if ( ! this.has_bttv && this._chatv && this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); +} + + +FFZ.prototype._join_room = function(room_id, no_rebuild) { + var did_join = false; + if ( this.settings.pinned_rooms.indexOf(room_id) === -1 ) { + this.settings.pinned_rooms.push(room_id); + this.settings.set("pinned_rooms", this.settings.pinned_rooms); + did_join = true; + } + + // Make sure we're not already there. + if ( this.rooms[room_id] && this.rooms[room_id].room ) + return did_join; + + // Okay, fine. Get it. + var Room = App.__container__.resolve('model:room'), + r = Room && Room.findOne(room_id); + + // Finally, rebuild the chat UI. + if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); + + return did_join; +} + + +FFZ.prototype._leave_room = function(room_id, no_rebuild) { + var did_leave = false; + if ( this.settings.pinned_rooms.indexOf(room_id) !== -1 ) { + this.settings.pinned_rooms.removeObject(room_id); + this.settings.set("pinned_rooms", this.settings.pinned_rooms); + did_leave = true; + } + + if ( ! this.rooms[room_id] || ! this.rooms[room_id].room ) + return did_leave; + + var Chat = App.__container__.lookup('controller:chat'), + r = this.rooms[room_id].room; + + if ( ! Chat || Chat.get('currentChannelRoom.id') === room_id || (this._chatv && this._chatv._ffz_host === room_id) ) + return did_leave; + + if ( Chat.get('currentRoom.id') === room_id ) + Chat.blurRoom(); + + r.destroy(); + + if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); + + return did_leave; +} + + +// ---------------------- +// Commands +// ---------------------- + +FFZ.chat_commands.join = function(room, args) { + if ( ! args || ! args.length || args.length > 1 ) + return "Join Usage: /join "; + + var room_id = args[0].toLowerCase(); + if ( room_id.charAt(0) === "#" ) + room_id = room_id.substr(1); + + if ( this._join_room(room_id) ) + return "Joining " + room_id + ". You will always connect to this channel's chat unless you later /part from it."; + else + return "You have already joined " + room_id + ". Please use \"/part " + room_id + "\" to leave it."; +} + + +FFZ.chat_commands.part = function(room, args) { + if ( ! args || ! args.length || args.length > 1 ) + return "Part Usage: /part "; + + var room_id = args[0].toLowerCase(); + if ( room_id.charAt(0) === "#" ) + room_id = room_id.substr(1); + + if ( this._leave_room(room_id) ) + return "Leaving " + room_id + "."; + else if ( this.rooms[room_id] ) + return "You do not have " + room_id + " pinned and you cannot leave the current channel or hosted channels via /part."; + else + return "You are not in " + room_id + "."; } \ No newline at end of file diff --git a/src/ember/line.js b/src/ember/line.js index b849b281..6248baf9 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -1,10 +1,6 @@ var FFZ = window.FrankerFaceZ, utils = require("../utils"), - reg_escape = function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - SEPARATORS = "[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]", SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"), @@ -26,7 +22,8 @@ data_to_tooltip = function(data) { var set = data.set, - set_type = data.set_type; + set_type = data.set_type, + owner = data.owner; if ( set_type === undefined ) set_type = "Channel"; @@ -39,7 +36,7 @@ set_type = null; } - return "Emoticon: " + data.code + "\n" + (set_type ? set_type + ": " : "") + set; + return "Emoticon: " + data.code + "\n" + (set_type ? set_type + ": " : "") + set + (owner ? "\nBy: " + owner.display_name : ""); }, build_tooltip = function(id) { @@ -112,6 +109,12 @@ tooltip += "Views: " + utils.number_commas(link_data.views) + " | Followers: " + utils.number_commas(link_data.followers) + ""; + } else if ( link_data.type == "twitch_vod" ) { + tooltip = "Twitch " + (link_data.broadcast_type == "highlight" ? "Highlight" : "Broadcast") + ": " + utils.sanitize(link_data.title) + "
"; + tooltip += "By: " + utils.sanitize(link_data.display_name) + (link_data.game ? " | Playing: " + utils.sanitize(link_data.game) : " | Not Playing") + "
"; + tooltip += "Views: " + utils.number_commas(link_data.views) + " | " + utils.time_to_string(link_data.length); + + } else if ( link_data.type == "twitter" ) { tooltip = "Tweet By: " + utils.sanitize(link_data.user) + "
"; tooltip += utils.sanitize(link_data.tweet); @@ -321,27 +324,41 @@ FFZ.prototype.setup_line = function() { this.log("Hooking the Ember Line controller."); - var Line = App.__container__.resolve('controller:line'), + var Line = App.__container__.resolve('component:message-line'), f = this; Line.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. - var tokens = this._super(); + var tokens = this.get("msgObject.cachedTokens"); + if ( tokens ) + return tokens; + + tokens = this._super(); try { - var start = performance.now(); + var start = performance.now(), + user = f.get_user(), + from_me = user && this.get("msgObject.from") === user.login; + tokens = f._remove_banned(tokens); tokens = f._emoticonize(this, tokens); - var user = f.get_user(); - - if ( ! user || this.get("model.from") != user.login ) - tokens = f._mentionize(this, tokens); // Store the capitalization. - var display = this.get("model.tags.display-name"); + var display = this.get("msgObject.tags.display-name"); if ( display && display.length ) - FFZ.capitalization[this.get("model.from")] = [display.trim(), Date.now()]; + FFZ.capitalization[this.get("msgObject.from")] = [display.trim(), Date.now()]; + + if ( ! from_me ) + tokens = f.tokenize_mentions(tokens); + + for(var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if ( ! _.isString(token) && token.mentionedUser && ! token.own ) { + this.set('msgObject.ffz_has_mention', true); + break; + } + } var end = performance.now(); if ( end - start > 5 ) @@ -353,38 +370,30 @@ FFZ.prototype.setup_line = function() { } catch(err) { } } + this.set("msgObject.cachedTokens", tokens); return tokens; - }.property("model.message", "isModeratorOrHigher") - }); + }.property("msgObject.message", "isChannelLinksDisabled", "currentUserNick", "msgObject.from", "msgObject.tags.emotes"), - - this.log("Hooking the Ember Line view."); - var Line = App.__container__.resolve('view:line'); - - Line.reopen({ didInsertElement: function() { this._super(); try { var start = performance.now(); var el = this.get('element'), - controller = this.get('context'), - user = controller.get('model.from'), - room = controller.get('parentController.content.id'), - color = controller.get('model.color'), - - row_type = controller.get('model.ffz_alternate'); + user = this.get('msgObject.from'), + room = this.get('msgObject.room') || App.__container__.lookup('controller:chat').get('currentRoom.id'), + color = this.get('msgObject.color'), + row_type = this.get('msgObject.ffz_alternate'); // Color Processing if ( color ) f._handle_color(color); - // Row Alternation if ( row_type === undefined ) { row_type = f._last_row[room] = f._last_row.hasOwnProperty(room) ? !f._last_row[room] : false; - this.set("context.model.ffz_alternate", row_type); + this.set("msgObject.ffz_alternate", row_type); } el.classList.toggle('ffz-alternate', row_type); @@ -393,7 +402,7 @@ FFZ.prototype.setup_line = function() { // Basic Data el.setAttribute('data-room', room); el.setAttribute('data-sender', user); - el.setAttribute('data-deleted', controller.get('model.deleted')); + el.setAttribute('data-deleted', this.get('deleted')||false); // Badge @@ -401,36 +410,9 @@ FFZ.prototype.setup_line = function() { // Mention Highlighting - var mentioned = el.querySelector('span.mentioned'); - if ( mentioned ) { + if ( this.get("msgObject.ffz_has_mention") ) el.classList.add("ffz-mentioned"); - if ( f.settings.highlight_notifications && !document.hasFocus() && !this.get('context.model.ffz_notified') ) { - var cap_room = FFZ.get_capitalization(room), - cap_user = FFZ.get_capitalization(user), - room_name = cap_room, - msg = this.get("context.model.message"); - - if ( this.get("context.parentController.content.isGroupRoom") ) - room_name = this.get("context.parentController.content.tmiRoom.displayName"); - - if ( this.get("context.model.style") == "action" ) - msg = "* " + cap_user + " " + msg; - else - msg = cap_user + ": " + msg; - - f.show_notification( - msg, - "Twitch Chat Mention in " + room_name, - cap_room, - 60000, - window.focus.bind(window) - ); - } - } - - // Mark that we've checked this message for mentions. - this.set('context.model.ffz_notified', true); // Banned Links var bad_links = el.querySelectorAll('a.deleted-link'); @@ -541,19 +523,16 @@ FFZ.prototype.setup_line = function() { set_id = data && data[1] || null, set = f.emote_sets[set_id], - emote = set ? set.emotes[id] : null, + emote = set ? set.emoticons[id] : null; - set_name = set ? (set.title || set.id) : "Unknown FFZ Set", - set_type = (set && set.title) ? "FrankerFaceZ" : "FFZ Channel"; + // High-DPI! + if ( emote && emote.srcSet ) + img.setAttribute('srcset', emote.srcSet); if ( set && f.feature_friday && set.id == f.feature_friday.set ) set_name = f.feature_friday.title + " - " + f.feature_friday.display_name; - img.title = data_to_tooltip({ - code: emote ? (emote.hidden ? "???" : emote.name) : name, - set: set_name, - set_type: set_type - }); + img.title = f._emote_tooltip(emote); } } @@ -681,75 +660,6 @@ FFZ.get_capitalization = function(name, callback) { } -// --------------------- -// Extra Mentions -// --------------------- - -FFZ._regex_cache = {}; - -FFZ._get_regex = function(word) { - return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig"); -} - -FFZ._words_to_regex = function(list) { - var regex = FFZ._regex_cache[list]; - if ( ! regex ) { - var reg = ""; - for(var i=0; i < list.length; i++) { - if ( ! list[i] ) - continue; - - reg += (reg ? "|" : "") + reg_escape(list[i]); - } - - regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig"); - } - - return regex; -} - - -FFZ.prototype._mentionize = function(controller, tokens) { - var mention_words = this.settings.keywords; - if ( ! mention_words || ! mention_words.length ) - return tokens; - - if ( typeof tokens == "string" ) - tokens = [tokens]; - - var regex = FFZ._words_to_regex(mention_words), - new_tokens = []; - - for(var i=0; i < tokens.length; i++) { - var token = tokens[i]; - if ( ! _.isString(token) ) { - new_tokens.push(token); - continue; - } - - if ( ! token.match(regex) ) { - new_tokens.push(token); - continue; - } - - token = token.replace(regex, function(all, prefix, match) { - new_tokens.push(prefix); - new_tokens.push({ - mentionedUser: match, - own: false - }); - - return ""; - }); - - if ( token ) - new_tokens.push(token); - } - - return new_tokens; -} - - // --------------------- // Banned Words // --------------------- @@ -790,58 +700,9 @@ FFZ.prototype._remove_banned = function(tokens) { // Emoticon Replacement // --------------------- -FFZ.prototype._emoticonize = function(controller, tokens) { - var room_id = controller.get("parentController.model.id"), - user_id = controller.get("model.from"), - f = this; +FFZ.prototype._emoticonize = function(component, tokens) { + var room_id = component.get("msgObject.room") || App.__container__.lookup('controller:chat').get('currentRoom.id'), + user_id = component.get("msgObject.from"); - // Get our sets. - var sets = this.getEmotes(user_id, room_id), - emotes = []; - - // Build a list of emotes that match. - _.each(sets, function(set_id) { - var set = f.emote_sets[set_id]; - if ( ! set ) - return; - - _.each(set.emotes, function(emote) { - _.any(tokens, function(token) { - return _.isString(token) && token.match(emote.regex); - }) && emotes.push(emote); - }); - }); - - // Don't bother proceeding if we have no emotes. - if ( ! emotes.length ) - return tokens; - - // Now that we have all the matching tokens, do crazy stuff. - if ( typeof tokens == "string" ) - tokens = [tokens]; - - // This is weird stuff I basically copied from the old Twitch code. - // Here, for each emote, we split apart every text token and we - // put it back together with the matching bits of text replaced - // with an object telling Twitch's line template how to render the - // emoticon. - _.each(emotes, function(emote) { - //var eo = {isEmoticon:true, cls: emote.klass}; - var eo = {isEmoticon:true, cls: emote.klass,srcSet: emote.url + ' 1x', emoticonSrc: emote.url + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), altText: (emote.hidden ? "???" : emote.name)}; - - tokens = _.compact(_.flatten(_.map(tokens, function(token) { - if ( _.isObject(token) ) - return token; - - var tbits = token.split(emote.regex), bits = []; - tbits.forEach(function(val, ind) { - bits.push(val); - if ( ind !== tbits.length - 1 ) - bits.push(eo); - }); - return bits; - }))); - }); - - return tokens; + return this.tokenize_emotes(user_id, room_id, tokens); } \ No newline at end of file diff --git a/src/ember/room.js b/src/ember/room.js index 66272401..15000da9 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -51,6 +51,7 @@ FFZ.prototype.setup_room = function() { var inst = instances[key]; this.add_room(inst.id, inst); this._modify_room(inst); + inst.ffzPatchTMI(); } } @@ -199,7 +200,7 @@ FFZ.prototype.add_room = function(id, room) { this.ws_send("sub", id); // For now, we use the legacy function to grab the .css file. - this._legacy_add_room(id); + this.load_room(id); } @@ -219,14 +220,14 @@ FFZ.prototype.remove_room = function(id) { delete this.rooms[id]; // Clean up sets we aren't using any longer. - for(var i=0; i < room.sets.length; i++) { - var set_id = room.sets[i], set = this.emote_sets[set_id]; - if ( ! set ) - continue; + if ( id.charAt(0) === "_" ) + return; + var set = this.emote_sets[room.set]; + if ( set ) { set.users.removeObject(id); - if ( !set.global && !set.users.length ) - this.unload_set(set_id); + if ( ! this.global_sets.contains(room.set) && ! set.users.length ) + this.unload_set(room.set); } } @@ -235,12 +236,36 @@ FFZ.prototype.remove_room = function(id) { // Receiving Set Info // -------------------- -FFZ.prototype.load_room = function(room_id, callback) { - return this._legacy_load_room(room_id, callback); +FFZ.prototype.load_room = function(room_id, callback, tries) { + var f = this; + jQuery.getJSON(constants.API_SERVER + "v1/room/" + room_id) + .done(function(data) { + if ( data.sets ) { + for(var key in data.sets) + data.sets.hasOwnProperty(key) && f._load_set_json(key, undefined, data.sets[key]); + } + + f._load_room_json(room_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return typeof callback == "function" && callback(false); + + tries = (tries || 0) + 1; + if ( tries < 10 ) + return f.load_room(room_id, callback, tries); + + return typeof callback == "function" && callback(false); + }); } FFZ.prototype._load_room_json = function(room_id, callback, data) { + if ( ! data || ! data.room ) + return typeof callback == "function" && callback(false); + + data = data.room; + // Preserve the pointer to the Room instance. if ( this.rooms[room_id] ) data.room = this.rooms[room_id].room; @@ -250,11 +275,8 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { if ( data.css || data.moderator_badge ) utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); - for(var i=0; i < data.sets.length; i++) { - var set_id = data.sets[i]; - if ( ! this.emote_sets.hasOwnProperty(set_id) ) - this.load_set(set_id); - } + if ( ! this.emote_sets.hasOwnProperty(data.set) ) + this.load_set(data.set); this.update_ui_link(); @@ -275,6 +297,7 @@ FFZ.prototype._modify_room = function(room) { this._super(); try { f.add_room(this.id, this); + this.set("ffz_chatters", {}); } catch(err) { f.error("add_room: " + err); } @@ -289,20 +312,25 @@ FFZ.prototype._modify_room = function(room) { } }, - getSuggestions: function() { - // This returns auto-complete suggestions for use in chat. We want - // to apply our capitalizations here. Overriding the - // filteredSuggestions property of the chat-input component would - // be even better, but I was already hooking the room model. - var suggestions = this._super(); - + addMessage: function(msg) { try { - suggestions = _.map(suggestions, FFZ.get_capitalization); + if ( msg ) { + msg.room = this.get('id'); + f.tokenize_chat_line(msg); + } } catch(err) { - f.error("get_suggestions: " + err); + f.error("Room addMessage: " + err); } - return suggestions; + return this._super(msg); + }, + + setHostMode: function(e) { + var Chat = App.__container__.lookup('controller:chat'); + if ( ! Chat || Chat.get('currentChannelRoom') !== this ) + return; + + return this._super(e); }, send: function(text) { @@ -323,54 +351,168 @@ FFZ.prototype._modify_room = function(room) { } return this._super(text); - } + }, + + ffzUpdateUnread: function() { + if ( f.settings.group_tabs ) { + var Chat = App.__container__.lookup('controller:chat'); + if ( Chat && Chat.get('currentRoom') === this ) + this.resetUnreadCount(); + else if ( f._chatv ) + f._chatv.ffzTabUnread(this.get('id')); + } + }.observes('unreadCount'), + + + ffzInitChatterCount: function() { + if ( ! this.tmiRoom ) + return; + + var room = this; + this.tmiRoom.list().done(function(data) { + var chatters = {}; + data = data.data.chatters; + for(var i=0; i < data.admins.length; i++) + chatters[data.admins[i]] = true; + for(var i=0; i < data.global_mods.length; i++) + chatters[data.global_mods[i]] = true; + for(var i=0; i < data.moderators.length; i++) + chatters[data.moderators[i]] = true; + for(var i=0; i < data.staff.length; i++) + chatters[data.staff[i]] = true; + for(var i=0; i < data.viewers.length; i++) + chatters[data.viewers[i]] = true; + + room.set("ffz_chatters", chatters); + room.ffzUpdateChatters(); + }); + }, + + ffzUpdateChatters: function(add, remove) { + var chatters = this.get("ffz_chatters") || {}; + if ( add ) + chatters[add] = true; + if ( remove && chatters[remove] ) + delete chatters[remove]; + + if ( ! f.settings.chatter_count ) + return; + + if ( f._cindex ) + f._cindex.ffzUpdateChatters(); + + if ( window.parent && window.parent.postMessage ) + window.parent.postMessage({from_ffz: true, command: 'chatter_count', message: Object.keys(this.get('ffz_chatters') || {}).length}, "http://www.twitch.tv/"); + }, + + + ffzPatchTMI: function() { + if ( this.get('ffz_is_patched') || ! this.get('tmiRoom') ) + return; + + if ( f.settings.chatter_count ) + this.ffzInitChatterCount(); + + var tmi = this.get('tmiRoom'), + room = this; + + // This method is stupid and bad and it leaks between rooms. + if ( ! tmi.ffz_notice_patched ) { + tmi.ffz_notice_patched = true; + + tmi._roomConn.off("notice", tmi._onNotice, tmi); + tmi._roomConn.on("notice", function(ircMsg) { + var target = ircMsg.target || (ircMsg.params && ircMsg.params[0]) || this.ircChannel; + if( target != this.ircChannel ) + return; + + this._trigger("notice", { + msgId: ircMsg.tags['msg-id'], + message: ircMsg.message + }); + }, tmi); + } + + // Let's get chatter information! + var connection = tmi._roomConn._connection; + if ( ! connection.ffz_cap_patched ) { + connection.ffz_cap_patched = true; + connection._send("CAP REQ :twitch.tv/membership"); + + connection.on("opened", function() { + this._send("CAP REQ :twitch.tv/membership"); + }, connection); + + // Since TMI starts sending SPECIALUSER with this, we need to + // ignore that. \ CatBag / + var orig_handle = connection._handleTmiPrivmsg.bind(connection); + connection._handleTmiPrivmsg = function(msg) { + if ( msg.message && msg.message.split(" ",1)[0] === "SPECIALUSER" ) + return; + return orig_handle(msg); + } + } + + + // Check this shit. + tmi._roomConn._connection.off("message", tmi._roomConn._onIrcMessage, tmi._roomConn); + + tmi._roomConn._onIrcMessage = function(ircMsg) { + if ( ircMsg.target != this.ircChannel ) + return; + + switch ( ircMsg.command ) { + case "JOIN": + if ( this._session && this._session.nickname === ircMsg.sender ) { + this._onIrcJoin(ircMsg); + } else + f.settings.chatter_count && room.ffzUpdateChatters(ircMsg.sender); + break; + + case "PART": + if ( this._session && this._session.nickname === ircMsg.sender ) { + this._resetActiveState(); + this._connection._exitedRoomConn(); + this._trigger("exited"); + } else + f.settings.chatter_count && room.ffzUpdateChatters(null, ircMsg.sender); + break; + + default: + break; + } + } + + tmi._roomConn._connection.on("message", tmi._roomConn._onIrcMessage, tmi._roomConn); + + + // Okay, we need to patch the *session's* updateUserState + if ( ! tmi.session.ffz_patched ) { + tmi.session.ffz_patched = true; + var uus = tmi.session._updateUserState.bind(tmi.session); + + tmi.session._updateUserState = function(user, tags) { + try { + if ( tags.color ) + this._onUserColorChanged(user, tags.color); + + if ( tags['display-name'] ) + this._onUserDisplayNameChanged(user, tags['display-name']); + + if ( tags.turbo ) + this._onUserSpecialAdded(user, 'turbo'); + + if ( tags['user_type'] === 'staff' || tags['user_type'] === 'admin' || tags['user_type'] === 'global_mod' ) + this._onUserSpecialAdded(user, tags['user-type']); + + } catch(err) { + f.error("SessionManager _updateUserState: " + err); + } + } + } + + this.set('ffz_is_patched', true); + + }.observes('tmiRoom') }); -} - - -// -------------------- -// Legacy Data Support -// -------------------- - -FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_room_css(room_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return this._legacy_load_room_css(room_id, callback, null); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_add_room(room_id, callback, tries); - }); -} - - -FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { - var set_id = room_id, - match = set_id.match(GROUP_CHAT); - - if ( match && match[1] ) - set_id = match[1]; - - var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; - - if ( data ) - data = data.replace(CSS, "").trim(); - - if ( data ) { - data = data.replace(MOD_CSS, function(match, url) { - if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) - return match; - - output.moderator_badge = url; - return ""; - }); - } - - output.css = data || null; - return this._load_room_json(room_id, callback, output); } \ No newline at end of file diff --git a/src/emoticons.js b/src/emoticons.js index 21251eb7..38b4908e 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -5,15 +5,6 @@ var FFZ = window.FrankerFaceZ, utils = require('./utils'), - loaded_global = function(set_id, success, data) { - if ( ! success ) - return; - - data.global = true; - this.global_sets.push(set_id); - }, - - check_margins = function(margins, height) { var mlist = margins.split(/ +/); if ( mlist.length != 2 ) @@ -30,18 +21,29 @@ var FFZ = window.FrankerFaceZ, build_legacy_css = function(emote) { - var margin = emote.margins; + var margin = emote.margins, srcset = ""; if ( ! margin ) margin = ((emote.height - 18) / -2) + "px 0"; - return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; + + if ( emote.urls[2] || emote.urls[4] ) { + srcset = 'url("' + emote.urls[1] + '") 1x'; + if ( emote.urls[2] ) + srcset += ', url("' + emote.urls[2] + '") 2x'; + if ( emote.urls[4] ) + srcset += ', url("' + emote.urls[4] + '") 4x'; + + srcset = '-webkit-image-set(' + srcset + '); image-set(' + srcset + ');'; + } + + return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.urls[1] + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (srcset ? '; ' + srcset : '') + (emote.css ? "; " + emote.css : "") + "}\n"; }, build_new_css = function(emote) { - if ( ! emote.margins && ! emote.extra_css ) + if ( ! emote.margins && ! emote.css ) return build_legacy_css(emote); - return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; + return build_legacy_css(emote) + 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n"; }, @@ -57,27 +59,103 @@ FFZ.prototype.setup_emoticons = function() { this.emote_sets = {}; this.global_sets = []; + this.default_sets = []; this._last_emote_id = 0; + // Usage Data + this.emote_usage = {}; + + this.log("Creating emoticon style element."); var s = this._emote_style = document.createElement('style'); s.id = "ffz-emoticon-css"; document.head.appendChild(s); - this.log("Loading global emote set."); - this.load_set("global", loaded_global.bind(this, "global")); + this.log("Loading global emote sets."); + this.load_global_sets(); + + this.log("Watching Twitch emoticon parser to ensure it loads."); + this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); } +// ------------------------ +// Emote Usage +// ------------------------ + +FFZ.prototype.add_usage = function(room_id, emote_id, count) { + var rooms = this.emote_usage[emote_id] = this.emote_usage[emote_id] || {}; + rooms[room_id] = (rooms[room_id] || 0) + (count || 1); + + if ( this._emote_report_scheduled ) + return; + + this._emote_report_scheduled = setTimeout(this._report_emotes.bind(this), 30000); +} + + +FFZ.prototype._report_emotes = function() { + if ( this._emote_report_scheduled ) + delete this._emote_report_scheduled; + + var usage = this.emote_usage; + this.emote_usage = {}; + this.ws_send("emoticon_uses", [usage], function(){}, true); +} + + +// ------------------------ +// Twitch Emoticon Checker +// ------------------------ + +FFZ.prototype.check_twitch_emotes = function() { + if ( this._twitch_emote_check ) { + clearTimeout(this._twitch_emote_check); + delete this._twitch_emote_check; + } + + var room; + if ( this.rooms ) { + for(var key in this.rooms) { + if ( this.rooms.hasOwnProperty(key) ) { + room = this.rooms[key]; + break; + } + } + } + + if ( ! room || ! room.room || ! room.room.tmiSession ) { + this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); + return; + } + + var parser = room.room.tmiSession._emotesParser, + emotes = Object.keys(parser.emoticonRegexToIds).length; + + // If we have emotes, we're done! + if ( emotes > 0 ) + return; + + // No emotes. Try loading them. + var sets = parser.emoticonSetIds; + parser.emoticonSetIds = ""; + parser.updateEmoticons(sets); + + // Check again in a bit to see if we've got them. + this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); +} + + + // --------------------- // Set Management // --------------------- FFZ.prototype.getEmotes = function(user_id, room_id) { - var user = this.users[user_id], - room = this.rooms[room_id]; + var user = this.users && this.users[user_id], + room = this.rooms && this.rooms[room_id]; - return _.union(user && user.sets || [], room && room.sets || [], this.global_sets); + return _.union(user && user.sets || [], room && room.set && [room.set] || [], this.default_sets); } @@ -86,16 +164,87 @@ FFZ.prototype.getEmotes = function(user_id, room_id) { // --------------------- FFZ.ws_commands.reload_set = function(set_id) { + if ( this.emote_sets.hasOwnProperty(set_id) ) + this.load_set(set_id); +} + + +FFZ.ws_commands.load_set = function(set_id) { this.load_set(set_id); } +// --------------------- +// Tooltip Powah! +// --------------------- + +FFZ.prototype._emote_tooltip = function(emote) { + if ( ! emote ) + return null; + + if ( emote._tooltip ) + return emote._tooltip; + + var set = this.emote_sets[emote.set_id], + owner = emote.owner, + title = set && set.title || "Global"; + + emote._tooltip = "Emoticon: " + (emote.hidden ? "???" : emote.name) + "\nFFZ " + title + (owner ? "\nBy: " + owner.display_name : ""); + return emote._tooltip; +} + + // --------------------- // Set Loading // --------------------- -FFZ.prototype.load_set = function(set_id, callback) { - return this._legacy_load_set(set_id, callback); +FFZ.prototype.load_global_sets = function(callback, tries) { + var f = this; + jQuery.getJSON(constants.API_SERVER + "v1/set/global") + .done(function(data) { + f.default_sets = data.default_sets; + var gs = f.global_sets = [], + sets = data.sets || {}; + + for(var key in sets) { + if ( ! sets.hasOwnProperty(key) ) + continue; + + var set = sets[key]; + gs.push(key); + f._load_set_json(key, undefined, set); + } + }).fail(function(data) { + if ( data.status == 404 ) + return typeof callback == "function" && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 50 ) + return f.load_global_sets(callback, tries); + + return typeof callback == "function" && callback(false); + }); +} + + +FFZ.prototype.load_set = function(set_id, callback, tries) { + var f = this; + jQuery.getJSON(constants.API_SERVER + "v1/set/" + set_id) + .done(function(data) { + f._load_set_json(set_id, callback, data && data.set); + + }).fail(function(data) { + if ( data.status == 404 ) + return typeof callback == "function" && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return f.load_set(set_id, callback, tries); + + return typeof callback == "function" && callback(false); + }); } @@ -108,97 +257,55 @@ FFZ.prototype.unload_set = function(set_id) { utils.update_css(this._emote_style, set_id, null); delete this.emote_sets[set_id]; - - for(var i=0; i < set.users.length; i++) { - var room = this.rooms[set.users[i]]; - if ( room ) - room.sets.removeObject(set_id); - } } FFZ.prototype._load_set_json = function(set_id, callback, data) { + if ( ! data ) + return typeof callback == "function" && callback(false); + // Store our set. this.emote_sets[set_id] = data; data.users = []; - data.global = false; data.count = 0; + // Iterate through all the emoticons, building CSS and regex objects as appropriate. - var output_css = ""; + var output_css = "", + ems = data.emoticons; - for(var key in data.emotes) { - if ( ! data.emotes.hasOwnProperty(key) ) - continue; + data.emoticons = {}; + + for(var i=0; i < ems.length; i++) { + var emote = ems[i]; - var emote = data.emotes[key]; emote.klass = "ffz-emote-" + emote.id; + emote.set_id = set_id; + + emote.srcSet = emote.urls[1] + " 1x"; + if ( emote.urls[2] ) + emote.srcSet += ", " + emote.urls[2] + " 2x"; + if ( emote.urls[4] ) + emote.srcSet += ", " + emote.urls[4] + " 4x"; if ( emote.name[emote.name.length-1] === "!" ) - emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); + emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")(?=\\W|$)", "g"); else - emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); + emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")\\b", "g"); output_css += build_css(emote); data.count++; + data.emoticons[emote.id] = emote; } - utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); - this.log("Updated emoticons for set: " + set_id, data); + utils.update_css(this._emote_style, set_id, output_css + (data.css || "")); + this.log("Updated emoticons for set #" + set_id + ": " + data.title, data); + + if ( this._cindex ) + this._cindex.ffzFixTitle(); + this.update_ui_link(); if ( callback ) callback(true, data); -} - - -FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_css(set_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return typeof callback == "function" && callback(false); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_load_set(set_id, callback, tries); - - return typeof callback == "function" && callback(false); - }); -} - - -FFZ.prototype._legacy_load_css = function(set_id, callback, data) { - var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; - - if ( set_id == "global" ) - output.title = "Global"; - else if ( set_id == "globalevent" ) - output.title = "Global Event"; - else if ( set_id == ".donor" ) - output.title = "Donor"; - - data = data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { - height = parseInt(height); width = parseInt(width); - margins = check_margins(margins, height); - var hidden = path.substr(path.lastIndexOf("/") + 1, 1) === ".", - id = ++f._last_emote_id, - emote = {id: id, set_id: set_id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra}; - - emotes[id] = emote; - return ""; - }).trim(); - - if ( data ) - data.replace(MOD_CSS, function(match, url) { - if ( output.icon || url.substr(-11) !== 'modicon.png' ) - return; - - output.icon = url; - }); - - this._load_set_json(set_id, callback, output); } \ No newline at end of file diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index f3ffa6e3..d5e25407 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -23,8 +23,6 @@ FFZ.prototype.setup_bttv = function(delay) { this.log("BetterTTV was detected after " + delay + "ms. Hooking."); this.has_bttv = true; - this.log("WOO"); - // this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); // Disable Dark if it's enabled. @@ -34,10 +32,19 @@ FFZ.prototype.setup_bttv = function(delay) { delete this._dark_style; } + // Disable Chat Tabs + if ( this.settings.group_tabs && this._chatv ) { + this._chatv.ffzDisableTabs(); + } + // Disable other features too. document.body.classList.remove("ffz-chat-colors"); document.body.classList.remove("ffz-chat-background"); + // Remove Sub Count + if ( this.is_dashboard ) + this._update_subscribers(); + // Send Message Behavior var original_send = BetterTTV.chat.helpers.sendMessage, f = this; @@ -71,7 +78,7 @@ FFZ.prototype.setup_bttv = function(delay) { f.bttv_badges(data); // Now, do everything else manually because things are hard-coded. - return '
'+ + return '
'+ BetterTTV.chat.templates.timestamp(data.time)+' '+ (isMod?BetterTTV.chat.templates.modicons():'')+' '+ BetterTTV.chat.templates.badges(data.badges)+ @@ -94,6 +101,7 @@ FFZ.prototype.setup_bttv = function(delay) { var rawMessage = encodeURIComponent(message); if(sender !== 'jtv') { + // Hackilly send our state across. received_sender = sender; var tokenizedMessage = BetterTTV.chat.templates.emoticonize(message, emotes); received_sender = null; @@ -109,7 +117,7 @@ FFZ.prototype.setup_bttv = function(delay) { message = tokenizedMessage.join(' '); } - return ''+message+''; + return ''+message+''; } catch(err) { f.log("Error: ", err); return original_message(sender, message, emotes, colored); @@ -121,8 +129,13 @@ FFZ.prototype.setup_bttv = function(delay) { var original_emoticonize = BetterTTV.chat.templates.emoticonize; BetterTTV.chat.templates.emoticonize = function(message, emotes) { var tokens = original_emoticonize(message, emotes), - sets = f.getEmotes(received_sender, received_room), - emotes = []; + room = (received_room || BetterTTV.getChannel()), + l_room = room && room.toLowerCase(), + l_sender = received_sender && received_sender.toLowerCase(), + sets = f.getEmotes(l_sender, l_room), + emotes = [], + user = f.get_user(), + mine = user && user.login === l_sender; // Build a list of emotes that match. _.each(sets, function(set_id) { @@ -130,7 +143,7 @@ FFZ.prototype.setup_bttv = function(delay) { if ( ! set ) return; - _.each(set.emotes, function(emote) { + _.each(set.emoticons, function(emote) { _.any(tokens, function(token) { return _.isString(token) && token.match(emote.regex); }) && emotes.push(emote); @@ -143,7 +156,8 @@ FFZ.prototype.setup_bttv = function(delay) { // Why is emote parsing so bad? ;_; _.each(emotes, function(emote) { - var eo = ['' + emote.name + ''], + var tooltip = f._emote_tooltip(emote), + eo = ['' + tooltip + ''], old_tokens = tokens; tokens = []; @@ -159,13 +173,22 @@ FFZ.prototype.setup_bttv = function(delay) { } var tbits = token.split(emote.regex); - tbits.forEach(function(val, ind) { - if ( val && val.length ) - tokens.push(val); + while(tbits.length) { + var bit = tbits.shift(); + if ( tbits.length ) { + bit += tbits.shift(); + if ( bit ) + tokens.push(bit); - if ( ind !== tbits.length - 1 ) + tbits.shift(); tokens.push(eo); - }); + + if ( mine && l_room ) + f.add_usage(l_room, emote.id); + + } else + tokens.push(bit); + } } }); diff --git a/src/ext/emote_menu.js b/src/ext/emote_menu.js index 78b235f5..c872e0e3 100644 --- a/src/ext/emote_menu.js +++ b/src/ext/emote_menu.js @@ -38,14 +38,14 @@ FFZ.prototype._emote_menu_enumerator = function() { for(var x = 0; x < sets.length; x++) { var set = this.emote_sets[sets[x]]; - if ( ! set || ! set.emotes ) + if ( ! set || ! set.emoticons ) continue; - for(var emote_id in set.emotes) { - if ( ! set.emotes.hasOwnProperty(emote_id) ) + for(var emote_id in set.emoticons) { + if ( ! set.emoticons.hasOwnProperty(emote_id) ) continue; - var emote = set.emotes[emote_id]; + var emote = set.emoticons[emote_id]; if ( emote.hidden ) continue; @@ -66,7 +66,7 @@ FFZ.prototype._emote_menu_enumerator = function() { } else title = "FrankerFaceZ: " + title; - emotes.push({text: emote.name, url: emote.url, + emotes.push({text: emote.name, url: emote.urls[1], hidden: false, channel: title, badge: badge}); } } diff --git a/src/featurefriday.js b/src/featurefriday.js index 002027c5..fed32535 100644 --- a/src/featurefriday.js +++ b/src/featurefriday.js @@ -91,10 +91,7 @@ FFZ.prototype._load_ff = function(data) { if ( this.feature_friday ) { // Remove the global set, delete the data, and reset the UI link. this.global_sets.removeObject(this.feature_friday.set); - - var set = this.emote_sets[this.feature_friday.set]; - if ( set ) - set.global = false; + this.default_sets.removeObject(this.feature_friday.set); this.feature_friday = null; this.update_ui_link(); @@ -111,7 +108,8 @@ FFZ.prototype._load_ff = function(data) { // Add the set. this.global_sets.push(data.set); - this.load_set(data.set, this._update_ff_set.bind(this)); + this.default_sets.push(data.set); + this.load_set(data.set); // Check to see if the channel is live. this._update_ff_live(); @@ -134,13 +132,6 @@ FFZ.prototype._update_ff_live = function() { } -FFZ.prototype._update_ff_set = function(success, set) { - // Prevent the set from being unloaded. - if ( set ) - set.global = true; -} - - FFZ.prototype._update_ff_name = function(name) { if ( this.feature_friday ) this.feature_friday.display_name = name; diff --git a/src/main.js b/src/main.js index 3b54d6e6..db59e240 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,5 @@ // Modify Array and others. -require('./shims'); - +// require('./shims'); // ---------------- // The Constructor @@ -22,7 +21,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 3, revision: 1, + major: 3, minor: 4, revision: 2, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -113,6 +112,8 @@ require('./socket'); require('./emoticons'); require('./badges'); +require('./tokenize'); + // Analytics: require('./ember/router'); require('./ember/channel'); @@ -136,14 +137,13 @@ require('./ui/styles'); require('./ui/dark'); require('./ui/notifications'); require('./ui/viewer_count'); +require('./ui/sub_count'); require('./ui/menu_button'); require('./ui/races'); require('./ui/my_emotes'); require('./ui/about_page'); -//require('./ui/group_chat'); - require('./commands'); @@ -156,7 +156,7 @@ FFZ.prototype.initialize = function(increment, delay) { // Twitch ember application is ready. // Check for special non-ember pages. - if ( /\/(?:settings|messages?\/)/.test(location.pathname) ) { + if ( /^\/(?:settings|m\/|messages?\/)/.test(location.pathname) ) { this.setup_normal(delay); return; } @@ -203,6 +203,7 @@ FFZ.prototype.setup_normal = function(delay) { this.setup_notifications(); this.setup_css(); + this.setup_menu(); this.find_bttv(10); @@ -235,6 +236,11 @@ FFZ.prototype.setup_dashboard = function(delay) { this.setup_notifications(); this.setup_css(); + this._update_subscribers(); + + // Set up the FFZ message passer. + this.setup_message_event(); + this.find_bttv(10); var end = (window.performance && performance.now) ? performance.now() : Date.now(), @@ -277,7 +283,8 @@ FFZ.prototype.setup_ember = function(delay) { this.setup_menu(); this.setup_my_emotes(); this.setup_races(); - //this.setup_group_chat(); + + this.connect_extra_chat(); this.find_bttv(10); this.find_emote_menu(10); @@ -288,4 +295,23 @@ FFZ.prototype.setup_ember = function(delay) { duration = end - start; this.log("Initialization complete in " + duration + "ms"); +} + + +// ------------------------ +// Dashboard Message Event +// ------------------------ + +FFZ.prototype.setup_message_event = function() { + this.log("Listening for Window Messages."); + window.addEventListener("message", this._on_window_message.bind(this), false); +} + + +FFZ.prototype._on_window_message = function(e) { + if ( ! e.data || ! e.data.from_ffz ) + return; + + var msg = e.data; + this.log("Window Message", msg); } \ No newline at end of file diff --git a/src/socket.js b/src/socket.js index cc7c9dde..c1646a5e 100644 --- a/src/socket.js +++ b/src/socket.js @@ -32,6 +32,18 @@ FFZ.prototype.ws_create = function() { f._ws_delay = 0; f.log("Socket connected."); + // Check for incognito. We don't want to do a hello in incognito mode. + var fs = window.RequestFileSystem || window.webkitRequestFileSystem; + if (!fs) + // Assume not. + f.ws_send("hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)); + + else + fs(window.TEMPORARY, 100, + f.ws_send.bind(f, "hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)), + f.log.bind(f, "Operating in Incognito Mode.")); + + var user = f.get_user(); if ( user ) f.ws_send("setuser", user.login); @@ -139,6 +151,43 @@ FFZ.prototype.ws_send = function(func, data, callback, can_wait) { return request; } + +// ---------------- +// HELLO Response +// ---------------- + +FFZ.prototype._ws_on_hello = function(success, data) { + if ( ! success ) + return this.log("Error Saying Hello: " + data); + + localStorage.ffzClientId = data; + this.log("Client ID: " + data); + + var survey = {}, + set = survey['settings'] = {}; + + for(var key in FFZ.settings_info) + set[key] = this.settings[key]; + + set["keywords"] = this.settings.keywords.length; + set["banned_words"] = this.settings.banned_words.length; + + + // Detect BTTV. + survey['bttv'] = this.has_bttv || !!document.head.querySelector('script[src*="betterttv"]'); + + + // Client Info + survey['user-agent'] = navigator.userAgent; + survey['screen'] = [screen.width, screen.height]; + survey['language'] = navigator.language; + survey['platform'] = navigator.platform; + + this.ws_send("survey", [survey]); +} + + + // ---------------- // Authorization // ---------------- diff --git a/src/tokenize.js b/src/tokenize.js new file mode 100644 index 00000000..90fca6ce --- /dev/null +++ b/src/tokenize.js @@ -0,0 +1,370 @@ +var FFZ = window.FrankerFaceZ, + utils = require("./utils"), + TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", + helpers, + + reg_escape = function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + + SEPARATORS = "[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]", + SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"); + +try { + helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers"); +} catch(err) { } + + +// --------------------- +// Tokenization +// --------------------- + +FFZ.prototype.tokenize_chat_line = function(msgObject) { + if ( msgObject.cachedTokens ) + return msgObject.cachedTokens; + + var msg = msgObject.message, + user = this.get_user(), + room_id = msgObject.room, + from_me = user && msgObject.from === user.login, + emotes = msgObject.tags && msgObject.tags.emotes, + + tokens = [msg]; + + // Standard tokenization + tokens = helpers.linkifyMessage(tokens); + if ( user && user.login ) + tokens = helpers.mentionizeMessage(tokens, user.login, from_me); + tokens = helpers.emoticonizeMessage(tokens, emotes); + + // FrankerFaceZ Extras + tokens = this._remove_banned(tokens); + tokens = this.tokenize_emotes(msgObject.from, room_id, tokens, from_me); + + // Capitalization + var display = msgObject.tags && msgObject.tags['display-name']; + if ( display && display.length ) + FFZ.capitalization[msgObject.from] = [display.trim(), Date.now()]; + + + // Mentions! + if ( ! from_me ) { + tokens = this.tokenize_mentions(tokens); + + for(var i=0; i < tokens.length; i++) { + var token = tokens[i]; + if ( _.isString(token) || ! token.mentionedUser || token.own ) + continue; + + // We have a mention! + msgObject.ffz_has_mention = true; + + // If we have chat tabs, update the status. + if ( ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { + var el = this._chatv._ffz_tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'); + if ( el && ! el.classList.contains('active') ) + el.classList.add('tab-mentioned'); + } + + // Display notifications if that setting is enabled. Also make sure + // that we have a chat view because showing a notification when we + // can't actually go to it is a bad thing. + if ( this._chatv && this.settings.highlight_notifications && ! document.hasFocus() ) { + var room = this.rooms[room_id] && this.rooms[room_id].room, + room_name; + + if ( room && room.get('isGroupRoom') ) + room_name = room.get('tmiRoom.displayName'); + else + room_name = FFZ.get_capitalization(room_id); + + display = display || Twitch.display.capitalize(msgObject.from); + + if ( msgObject.style === 'action' ) + msg = '* ' + display + ' ' + msg; + else + msg = display + ': ' + msg; + + var f = this; + this.show_notification( + msg, + "Twitch Chat Mention in " + room_name, + room_id, + 60000, + function() { + window.focus(); + var cont = App.__container__.lookup('controller:chat'); + room && cont && cont.focusRoom(room); + } + ); + } + + break; + } + } + + msgObject.cachedTokens = tokens; + return tokens; +} + + +FFZ.prototype.tokenize_line = function(user, room, message, no_emotes) { + if ( typeof message === "string" ) + message = [message]; + + if ( helpers && helpers.linkifyMessage ) + message = helpers.linkifyMessage(message); + + if ( helpers && helpers.mentionizeMessage ) { + var u = this.get_user(); + if ( u && u.login ) + message = helpers.mentionizeMessage(message, u.login, user === u.login); + } + + if ( ! no_emotes ) + message = this.tokenize_emotes(user, room, message); + + return message; +} + + +FFZ.prototype.render_tokens = function(tokens, render_links) { + return _.map(tokens, function(token) { + if ( token.emoticonSrc ) + return '' + token.altText + ''; + + if ( token.isLink ) { + if ( ! render_links && render_links !== undefined ) + return token.href; + + var s = token.href; + if ( s.indexOf("@") > -1 && (-1 === s.indexOf("/") || s.indexOf("@") < s.indexOf("/")) ) + return '' + s + ''; + + var n = (s.match(/^https?:\/\//) ? "" : "http://") + s; + return '' + s + ''; + } + + if ( token.mentionedUser ) + return '' + token.mentionedUser + ""; + + if ( token.deletedLink ) + return utils.sanitize(token.text); + + return utils.sanitize(token); + }).join(""); +} + + +// --------------------- +// Emoticon Processing +// --------------------- + +FFZ.prototype.tokenize_title_emotes = function(tokens) { + var f = this, + Channel = App.__container__.lookup('controller:channel'), + possible = Channel && Channel.get('product.emoticons'), + emotes = []; + + if ( _.isString(tokens) ) + tokens = [tokens]; + + // Build a list of emotes that match. + _.each(_.union(f.__twitch_global_emotes||[], possible), function(emote) { + if ( ! emote || emote.state === "inactive" ) + return; + + var r = new RegExp("\\b" + emote.regex + "\\b"); + + _.any(tokens, function(token) { + return _.isString(token) && token.match(r); + }) && emotes.push(emote); + }); + + // Include Global Emotes~! + if ( f.__twitch_global_emotes === undefined || f.__twitch_global_emotes === null ) { + f.__twitch_global_emotes = false; + Twitch.api.get("chat/emoticon_images", {emotesets:"0,42"}).done(function(data) { + if ( ! data || ! data.emoticon_sets || ! data.emoticon_sets[0] ) { + f.__twitch_global_emotes = []; + return; + } + + var emotes = f.__twitch_global_emotes = []; + data = data.emoticon_sets[0]; + for(var i=0; i < data.length; i++) { + var em = data[i]; + emotes.push({regex: em.code, url: TWITCH_BASE + em.id + "/1.0"}); + } + + if ( f._cindex ) + f._cindex.ffzFixTitle(); + }).fail(function() { + setTimeout(function(){f.__twitch_global_emotes = null;},5000); + });; + } + + if ( ! emotes.length ) + return tokens; + + if ( typeof tokens === "string" ) + tokens = [tokens]; + + _.each(emotes, function(emote) { + var eo = {isEmoticon:true, srcSet: emote.url + ' 1x', emoticonSrc: emote.url, altText: emote.regex}; + var r = new RegExp("\\b" + emote.regex + "\\b"); + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(r), bits = []; + tbits.forEach(function(val, ind) { + bits.push(val); + if ( ind !== tbits.length - 1 ) + bits.push(eo); + }); + return bits; + }))); + }); + + return tokens; +} + + +FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { + var f = this; + + // Get our sets. + var sets = this.getEmotes(user, room), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emoticons, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Now that we have all the matching tokens, do crazy stuff. + if ( typeof tokens == "string" ) + tokens = [tokens]; + + // This is weird stuff I basically copied from the old Twitch code. + // Here, for each emote, we split apart every text token and we + // put it back together with the matching bits of text replaced + // with an object telling Twitch's line template how to render the + // emoticon. + _.each(emotes, function(emote) { + var eo = { + srcSet: emote.srcSet, + emoticonSrc: emote.urls[1] + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), + altText: (emote.hidden ? "???" : emote.name) + }; + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(emote.regex), bits = []; + while(tbits.length) { + var bit = tbits.shift(); + if ( tbits.length ) { + bit += tbits.shift(); + if ( bit ) + bits.push(bit); + + tbits.shift(); + bits.push(eo); + + if ( do_report && room ) + f.add_usage(room, emote.id); + + } else + bits.push(bit); + } + return bits; + }))); + }); + + return tokens; +} + + +// --------------------- +// Mention Parsing +// --------------------- + +FFZ._regex_cache = {}; + +FFZ._get_regex = function(word) { + return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig"); +} + +FFZ._words_to_regex = function(list) { + var regex = FFZ._regex_cache[list]; + if ( ! regex ) { + var reg = ""; + for(var i=0; i < list.length; i++) { + if ( ! list[i] ) + continue; + + reg += (reg ? "|" : "") + reg_escape(list[i]); + } + + regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig"); + } + + return regex; +} + + +FFZ.prototype.tokenize_mentions = function(tokens) { + var mention_words = this.settings.keywords; + if ( ! mention_words || ! mention_words.length ) + return tokens; + + if ( typeof tokens === "string" ) + tokens = [tokens]; + + var regex = FFZ._words_to_regex(mention_words), + new_tokens = []; + + for(var i=0; i < tokens.length; i++) { + var token = tokens[i]; + if ( ! _.isString(token) ) { + new_tokens.push(token); + continue; + } + + if ( ! token.match(regex) ) { + new_tokens.push(token); + continue; + } + + token = token.replace(regex, function(all, prefix, match) { + new_tokens.push(prefix); + new_tokens.push({ + mentionedUser: match, + own: false + }); + + return ""; + }); + + if ( token ) + new_tokens.push(token); + } + + return new_tokens; +} \ No newline at end of file diff --git a/src/ui/about_page.js b/src/ui/about_page.js index db730f62..31ebfa13 100644 --- a/src/ui/about_page.js +++ b/src/ui/about_page.js @@ -16,14 +16,10 @@ FFZ.menu_pages.about = { has_emotes = false, f = this; // Check for emoticons. - if ( room && room.sets.length ) { - for(var i=0; i < room.sets.length; i++) { - var set = this.emote_sets[room.sets[i]]; - if ( set && set.count > 0 ) { - has_emotes = true; - break; - } - } + if ( room && room.set ) { + var set = this.emote_sets[room.set]; + if ( set && set.count > 0 ) + has_emotes = true; } // Heading @@ -65,7 +61,7 @@ FFZ.menu_pages.about = { var donate_button = document.createElement('a'); donate_button.className = 'button ffz-donate'; - donate_button.href = "http://www.frankerfacez.com/donate.html"; + donate_button.href = "https://www.frankerfacez.com/donate"; donate_button.target = "_new"; donate_button.innerHTML = "Donate"; diff --git a/src/ui/dark.js b/src/ui/dark.js index db8c0655..0a967e47 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -50,11 +50,11 @@ FFZ.prototype.setup_dark = function() { return; document.body.classList.toggle("ffz-dark", this.settings.dark_twitch); - if ( this.settings.dark_twitch ) - window.App && App.__container__.lookup('controller:settings').set('model.darkMode', true); + if ( ! this.settings.dark_twitch ) + return; - if ( this.settings.dark_twitch ) - this._load_dark_css(); + window.App && App.__container__.lookup('controller:settings').set('model.darkMode', true); + this._load_dark_css(); } diff --git a/src/ui/group_chat.js b/src/ui/group_chat.js new file mode 100644 index 00000000..269fb491 --- /dev/null +++ b/src/ui/group_chat.js @@ -0,0 +1,47 @@ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + + +// -------------------- +// Configuration +// -------------------- + +FFZ.settings_info.group_tabs = { + type: "boolean", + value: false, + + no_bttv: true, + + category: "Chat", + name: "Chat Room Tabs Beta", + help: "Enhanced UI for switching the current chat room and noticing new messages.", + + on_update: function(val) { + var enabled = !this.has_bttv && val; + if ( ! this._chatv || enabled === this._group_tabs_state ) + return; + + if ( enabled ) + this._chatv.ffzEnableTabs(); + else + this._chatv.ffzDisableTabs(); + } +} + + +// -------------------- +// Initializer +// -------------------- + +FFZ.prototype.setup_group_chat = function() { + if ( this.has_bttv || ! this.settings.group_tabs ) + return; + + this.log("Initializing secondary group chat UI."); + //this.group_tabs_enable(); +} + + +// -------------------- +// +// -------------------- \ No newline at end of file diff --git a/src/ui/menu.js b/src/ui/menu.js index 43879a13..965a712d 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -169,7 +169,8 @@ FFZ.menu_pages.channel = { if ( product && !product.get("error") ) { // We have a product, and no error~! has_product = true; - var is_subscribed = room.room.get("channel.isSubscribed.content"), + var tickets = App.__container__.resolve('model:ticket').find('user', {channel: room_id}), + is_subscribed = tickets ? tickets.get('content') : false, icon = room.room.get("badgeSet.subscriber.image"), grid = document.createElement("div"), @@ -237,7 +238,7 @@ FFZ.menu_pages.channel = { inner.appendChild(sub_message); } else { - var last_content = room.room.get("channel.isSubscribed.content"); + var last_content = tickets.get("content"); last_content = last_content.length > 0 ? last_content[last_content.length-1] : undefined; if ( last_content && last_content.purchase_profile && !last_content.purchase_profile.will_renew ) { var ends_at = utils.parse_date(last_content.access_end || ""); @@ -260,7 +261,7 @@ FFZ.menu_pages.channel = { } // Basic Emote Sets - this._emotes_for_sets(inner, view, room && room.menu_sets || [], (this.feature_friday || has_product) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/channel/global/devicon.png", "FrankerFaceZ"); + this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); // Feature Friday! this._feature_friday_ui(room_id, inner, view); @@ -298,31 +299,62 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub grid.appendChild(el_header); } + var emotes = []; for(var i=0; i < sets.length; i++) { var set = this.emote_sets[sets[i]]; - if ( ! set || ! set.emotes ) + if ( ! set || ! set.emoticons ) continue; - for(var eid in set.emotes) { - if ( ! set.emotes.hasOwnProperty(eid) ) + for(var eid in set.emoticons) { + if ( ! set.emoticons.hasOwnProperty(eid) || set.emoticons[eid].hidden ) continue; - var emote = set.emotes[eid]; - if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) - continue; - - c++; - var s = document.createElement('span'); - s.className = 'emoticon tooltip'; - s.style.backgroundImage = 'url("' + emote.url + '")'; - s.style.width = emote.width + "px"; - s.style.height = emote.height + "px"; - s.title = emote.name; - s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); - grid.appendChild(s); + emotes.push(set.emoticons[eid]); } } + // Sort the emotes! + emotes.sort(function(a,b) { + var an = a.name.toLowerCase(), + bn = b.name.toLowerCase(); + + if ( an < bn ) return -1; + else if ( an > bn ) return 1; + return 0; + }); + + for(var i=0; i < emotes.length; i++) { + var emote = emotes[i], srcset = null; + + if ( emote.urls[2] || emote.urls[4] ) { + srcset = 'url("' + emote.urls[1] + '") 1x'; + if ( emote.urls[2] ) + srcset += ', url("' + emote.urls[2] + '") 2x'; + if ( emote.urls[4] ) + srcset += ', url("' + emote.urls[4] + '") 4x'; + } + + c++; + var s = document.createElement('span'); + s.className = 'emoticon tooltip'; + s.style.backgroundImage = 'url("' + emote.urls[1] + '")'; + + if ( srcset ) { + var img_set = 'image-set(' + srcset + ')'; + s.style.backgroundImage = '-webkit-' + img_set; + s.style.backgroundImage = '-moz-' + img_set; + s.style.backgroundImage = '-ms-' + img_set; + s.style.backgroundImage = img_set; + } + + s.style.width = emote.width + "px"; + s.style.height = emote.height + "px"; + s.title = this._emote_tooltip(emote); + + s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); + grid.appendChild(s); + } + if ( !c ) { grid.innerHTML += "This channel has no emoticons."; grid.className = "emoticon-grid ffz-no-emotes center"; diff --git a/src/ui/menu_button.js b/src/ui/menu_button.js index b1aa3d7e..975b9225 100644 --- a/src/ui/menu_button.js +++ b/src/ui/menu_button.js @@ -33,14 +33,10 @@ FFZ.prototype.update_ui_link = function(link) { // Check for emoticons. - if ( room && room.sets.length ) { - for(var i=0; i < room.sets.length; i++) { - var set = this.emote_sets[room.sets[i]]; - if ( set && set.count > 0 ) { - has_emotes = true; - break; - } - } + if ( room && room.set ) { + var set = this.emote_sets[room.set]; + if ( set && set.count > 0 ) + has_emotes = true; } link.classList.toggle('no-emotes', ! has_emotes); diff --git a/src/ui/my_emotes.js b/src/ui/my_emotes.js index d83fc540..7ecf3cea 100644 --- a/src/ui/my_emotes.js +++ b/src/ui/my_emotes.js @@ -1,5 +1,6 @@ var FFZ = window.FrankerFaceZ, constants = require("../constants"), + utils = require("../utils"), TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", BANNED_SETS = {"00000turbo":true}, @@ -43,7 +44,7 @@ var FFZ = window.FrankerFaceZ, if ( ffz.settings.global_emotes_in_menu ) { set_ids.push("0"); - user_sets = _.union(user_sets, ffz.global_sets); + user_sets = _.union(user_sets, ffz.default_sets); } return [set_ids, user_sets]; @@ -64,18 +65,22 @@ FFZ.settings_info.global_emotes_in_menu = { FFZ.prototype.setup_my_emotes = function() { - this._twitch_emote_sets = {}; this._twitch_set_to_channel = {}; + this._twitch_badges = {}; if ( localStorage.ffzTwitchSets ) { try { this._twitch_set_to_channel = JSON.parse(localStorage.ffzTwitchSets); + this._twitch_badges = JSON.parse(localStorage.ffzTwitchBadges); } catch(err) { } } - this._twitch_set_to_channel[0] = "twitch_global"; - this._twitch_set_to_channel[33] = "twitch_tfaces"; - this._twitch_set_to_channel[42] = "twitch_tfaces"; + this._twitch_set_to_channel[0] = "global"; + this._twitch_set_to_channel[33] = "tfaces"; + this._twitch_set_to_channel[42] = "tfaces"; + + this._twitch_badges["global"] = "//cdn.frankerfacez.com/script/twitch_logo.png"; + this._twitch_badges["tfaces"] = this._twitch_badges["turbo"] = "//cdn.frankerfacez.com/script/turbo_badge.png"; } @@ -93,324 +98,212 @@ FFZ.menu_pages.my_emotes = { }, render: function(view, container) { - var emotes = get_emotes(this), f = this; + var tmi = view.get('controller.currentRoom.tmiSession'), + twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets'], + needed_sets = []; - new RSVP.Promise(function(done) { - var needed_sets = []; - for(var i=0; i < emotes[0].length; i++) { - var set_id = emotes[0][i]; - if ( ! f._twitch_emote_sets[set_id] ) - needed_sets.push(set_id); - } + for(var set_id in twitch_sets) + if ( twitch_sets.hasOwnProperty(set_id) && ! this._twitch_set_to_channel.hasOwnProperty(set_id) ) + needed_sets.push(set_id); - RSVP.all([ - new RSVP.Promise(function(d) { - if ( ! needed_sets.length ) - return d(); + if ( ! needed_sets.length ) + return FFZ.menu_pages.my_emotes.draw_menu.bind(this)(view, container, twitch_sets); - Twitch.api.get("chat/emoticon_images", {emotesets: needed_sets.join(",")}, {version: 3}) - .done(function(data) { - if ( data.emoticon_sets ) { - for(var set_id in data.emoticon_sets) { - if ( ! data.emoticon_sets.hasOwnProperty(set_id) ) - continue; + container.innerHTML = JSON.stringify(needed_sets); + }, - var set = f._twitch_emote_sets[set_id] = f._twitch_emote_sets[set_id] || {}, - emotes = data.emoticon_sets[set_id]; + draw_twitch_set: function(view, set_id, set) { + var heading = document.createElement('div'), + menu = document.createElement('div'), - // Sort Emoticons - emotes.sort(function(a,b) { - var a = (KNOWN_CODES[a.code] ? "000" + KNOWN_CODES[a.code] : a.code).toLowerCase(), - b = (KNOWN_CODES[b.code] ? "000" + KNOWN_CODES[b.code] : b.code).toLowerCase(); + channel_id = this._twitch_set_to_channel[set_id], title; - if ( a == "000grayface" ) a = "grayface"; - if ( b == "000grayface" ) b = "grayface"; - - if ( a < b ) return -1; - else if ( a > b ) return 1; - return 0; - }); - - set.emotes = emotes; - set.source = "Twitch"; - } - } - d(); - }).fail(function() { - d(); - }); - }), - new RSVP.Promise(function(d) { - if ( ! needed_sets.length ) - return d(); - - var promises = [], - old_needed = needed_sets, - handle_set = function(id, name) { - var set = f._twitch_emote_sets[id] = f._twitch_emote_sets[id] || {}; - - if ( !name || BANNED_SETS[name] ) - return; - - if ( name == "twitch_global" ) { - FFZ.capitalization["global emoticons"] = ["Global Emoticons", Date.now()]; - set.channel = "Global Emoticons"; - set.badge = "//cdn.frankerfacez.com/channel/global/twitch_logo.png"; - return; - } - - if ( name == "turbo" || name == "twitch_tfaces" ) { - set.channel = "Twitch Turbo"; - set.badge = "//cdn.frankerfacez.com/script/turbo_badge.png"; - return; - } - - // Badge Lookup - promises.push(new RSVP.Promise(function(set, name, dn) { - Twitch.api.get("chat/" + name + "/badges", null, {version: 3}) - .done(function(data) { - if ( data.subscriber && data.subscriber.image ) - set.badge = data.subscriber.image; - dn(); - }).fail(dn)}.bind(this,set,name))); - - // Mess Up Capitalization - var lname = name.toLowerCase(), - old_data = FFZ.capitalization[lname]; - if ( old_data && Date.now() - old_data[1] < 3600000 ) { - set.channel = old_data[0]; - return; - } - - promises.push(new RSVP.Promise(function(set, lname, name, dn) { - if ( ! f.ws_send("get_display_name", lname, function(success, data) { - var cap_name = success ? data : name; - FFZ.capitalization[lname] = [cap_name, Date.now()]; - set.channel = cap_name; - dn(); - }) ) { - // Can't use socket. - set.channel = name; - dn(); - } - - // Timeout - setTimeout(function(set,name,dn) { - if ( ! set.channel ) - set.channel = name; - dn(); - }.bind(this,set,name,dn), 500); - }.bind(this, set, lname, name))); - }, - handle_promises = function() { - if ( promises.length ) - RSVP.all(promises).then(d,d); - else - d(); - }; - - // Process all the sets we already have. - needed_sets = []; - for(var i=0;i 0 ) { - f.ws_send("twitch_sets", needed_sets, function(success, data) { - needed_sets = []; - if ( success ) { - for(var set_id in data) { - if ( ! data.hasOwnProperty(set_id) ) - continue; - - f._twitch_set_to_channel[set_id] = data[set_id]; - handle_set(set_id, data[set_id]); - } - - localStorage.ffzTwitchSets = JSON.stringify(f._twitch_set_to_channel); - } - - handle_promises(); - }); - - // Timeout! - setTimeout(function() { - if ( needed_sets.length ) - handle_promises(); - }, 2000); - - } else - handle_promises(); - }) - ]).then(function() { - var sets = {}; - for(var i=0; i < emotes[0].length; i++) { - var set_id = emotes[0][i]; - if ( f._twitch_emote_sets[set_id] ) - sets[set_id] = f._twitch_emote_sets[set_id]; - } - done(sets); - }, function() { done({}); }) - }).then(function(twitch_sets) { - try { - - // Don't override a different page. We can wait. - if ( container.getAttribute('data-page') != "my_emotes" ) - return; - - container.innerHTML = ""; - - var ffz_sets = emotes[1], - sets = []; - - for(var set_id in twitch_sets) { - if ( ! twitch_sets.hasOwnProperty(set_id) ) - continue; - - var set = twitch_sets[set_id]; - if ( set.channel && set.emotes && set.emotes.length ) - sets.push([1, set.channel, set]); - } - - for(var i=0; i < ffz_sets.length; i++) { - var set_id = ffz_sets[i], - set = f.emote_sets[set_id]; - - if ( f.feature_friday && set_id == f.feature_friday.set ) - continue; - - if ( set.count > 0 ) - sets.push([2, set.id, set]); - } - - sets.sort(function(a,b) { - if ( a[0] < b[0] ) return -1; - else if ( a[0] > b[0] ) return 1; - - var an = a[1].toLowerCase(), - bn = b[1].toLowerCase(); - - if ( an === "twitch turbo" || an === "twitch_tfaces" ) - an = "zza|" + an; - - else if ( an === "global emoticons" ) - an = "zzz|" + an; - - if ( bn === "twitch turbo" || bn === "twitch_tfaces" ) - bn = "zza|" + bn; - else if ( bn === "global emoticons" ) - bn = "zzz|" + bn; - - if ( an < bn ) return -1; - else if ( an > bn ) return 1; - return 0; - }); - - for(var i=0; i < sets.length; i++) { - var ffz_set = sets[i][0] === 2, - set = sets[i][2], - heading = document.createElement('div'), - menu = document.createElement('div'), - - source = ffz_set ? "FrankerFaceZ" : set.source, - badge, title, ems; - - if ( ffz_set ) { - ems = []; - for(var emote_id in set.emotes) { - var emote = set.emotes[emote_id]; - if ( emote.hidden ) - continue; - - ems.push({code: emote.name, url: emote.url, width: emote.width, height: emote.height}); - } - - if ( set.id === "global" ) - title = "Global Emoticons"; - else - title = set.title || set.id; - - badge = set.icon || "http://cdn.frankerfacez.com/channel/global/devicon.png"; - - } else { - ems = set.emotes; - title = set.channel == "Twitch Turbo" ? set.channel : FFZ.get_capitalization(set.channel); - badge = set.badge; - } - - if ( ! ems.length ) - continue; - - heading.className = 'heading'; - heading.innerHTML = '' + source + '' + title; - if ( badge ) - heading.style.backgroundImage = 'url("' + badge + '")'; - - menu.className = 'emoticon-grid'; - menu.appendChild(heading); - - for(var x=0; x < ems.length; x++) { - var emote = ems[x], - code = KNOWN_CODES[emote.code] || emote.code; - - var s = document.createElement('span'); - s.className = 'emoticon tooltip'; - s.style.backgroundImage = 'url("' + (emote.url ? emote.url : (TWITCH_BASE + emote.id + '/1.0')) + '")'; - - if ( emote.height ) - s.style.height = emote.height + "px"; - - if ( emote.width ) - s.style.width = emote.width + "px"; - - if ( ! emote.url ) { - var img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; - s.style.backgroundImage = '-webkit-' + img_set; - s.style.backgroundImage = '-moz-' + img_set; - s.style.backgroundImage = '-ms-' + img_set; - s.style.backgroundImage = img_set; - } - - s.title = code; - s.addEventListener('click', f._add_emote.bind(f, view, code)); - menu.appendChild(s); - } - - container.appendChild(menu); - } - - if ( ! sets.length ) { - var menu = document.createElement('div'); - - menu.className = 'chat-menu-content center'; - menu.innerHTML = "Error Loading Subscriptions"; - - container.appendChild(menu); - } - - } catch(err) { - f.log("My Emotes Menu Error", err); - container.innerHTML = ""; - - var menu = document.createElement('div'), - heading = document.createElement('div'), - p = document.createElement('p'); - - heading.className = 'heading'; - heading.innerHTML = 'Error Loading Menu'; - menu.appendChild(heading); - - p.className = 'clearfix'; - p.textContent = err; - menu.appendChild(p); - - menu.className = 'chat-menu-content'; - container.appendChild(menu); - } + if ( channel_id === "global" ) + title = "Global Emoticons"; + else if ( channel_id === "turbo" ) + title = "Twitch Turbo"; + else + title = FFZ.get_capitalization(channel_id, function(name) { + heading.innerHTML = 'Twitch' + utils.sanitize(name); }); + + heading.className = 'heading'; + heading.innerHTML = 'Twitch' + utils.sanitize(title); + + if ( this._twitch_badges[channel_id] ) + heading.style.backgroundImage = 'url("' + this._twitch_badges[channel_id] + '")'; + else { + var f = this; + Twitch.api.get("chat/" + channel_id + "/badges", null, {version: 3}) + .done(function(data) { + if ( data.subscriber && data.subscriber.image ) { + f._twitch_badges[channel_id] = data.subscriber.image; + heading.style.backgroundImage = 'url("' + data.subscriber.image + '")'; + } + }); } - }; + + menu.className = 'emoticon-grid'; + menu.appendChild(heading); + + for(var i=0; i < set.length; i++) { + var emote = set[i], + code = KNOWN_CODES[emote.code] || emote.code, + + em = document.createElement('span'), + img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; + + em.className = 'emoticon tooltip'; + em.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")'; + em.style.backgroundImage = '-webkit-' + img_set; + em.style.backgroundImage = '-moz-' + img_set; + em.style.backgroundImage = '-ms-' + img_set; + em.style.backgroudnImage = img_set; + + em.title = code; + em.addEventListener("click", this._add_emote.bind(this, view, code)); + menu.appendChild(em); + } + + return menu; + }, + + draw_ffz_set: function(view, set) { + var heading = document.createElement('div'), + menu = document.createElement('div'), + emotes = []; + + heading.className = 'heading'; + heading.innerHTML = 'FrankerFaceZ' + set.title; + heading.style.backgroundImage = 'url("' + (set.icon || '//cdn.frankerfacez.com/script/devicon.png') + '")'; + + menu.className = 'emoticon-grid'; + menu.appendChild(heading); + + for(var emote_id in set.emoticons) + set.emoticons.hasOwnProperty(emote_id) && ! set.emoticons[emote_id].hidden && emotes.push(set.emoticons[emote_id]); + + emotes.sort(function(a,b) { + var an = a.name.toLowerCase(), + bn = b.name.toLowerCase(); + + if ( an < bn ) return -1; + else if ( an > bn ) return 1; + if ( a.id < b.id ) return -1; + if ( a.id > b.id ) return 1; + return 0; + }); + + for(var i=0; i < emotes.length; i++) { + var emote = emotes[i], + + em = document.createElement('span'), + img_set = 'image-set(url("' + emote.urls[1] + '") 1x'; + + if ( emote.urls[2] ) + img_set += ', url("' + emote.urls[2] + '") 2x'; + + if ( emote.urls[4] ) + img_set += ', url("' + emote.urls[4] + '") 4x'; + + img_set += ')'; + + em.className = 'emoticon tooltip'; + em.style.backgroundImage = 'url("' + emote.urls[1] + '")'; + em.style.backgroundImage = '-webkit-' + img_set; + em.style.backgroundImage = '-moz-' + img_set; + em.style.backgroundImage = '-ms-' + img_set; + em.style.backgroudnImage = img_set; + + if ( emote.height ) + em.style.height = emote.height + "px"; + if ( emote.width ) + em.style.width = emote.width + "px"; + + em.title = emote.tooltip || emote.name; + em.addEventListener("click", this._add_emote.bind(this, view, emote.name)); + menu.appendChild(em); + } + + return menu; + }, + + draw_menu: function(view, container, twitch_sets) { + // Make sure we're still on the My Emoticons page. Since this is + // asynchronous, the user could've tabbed away. + if ( container.getAttribute('data-page') !== 'my_emotes' ) + return; + + container.innerHTML = ""; + try { + var user = this.get_user(), + ffz_sets = this.getEmotes(user && user.login, null), + sets = []; + + // Start with Twitch Sets + for(var set_id in twitch_sets) { + if ( ! twitch_sets.hasOwnProperty(set_id) || ( ! this.settings.global_emotes_in_menu && set_id === '0' ) ) + continue; + + var set = twitch_sets[set_id]; + if ( ! set.length ) + continue; + + sets.push([this._twitch_set_to_channel[set_id], FFZ.menu_pages.my_emotes.draw_twitch_set.bind(this)(view, set_id, set)]); + } + + + // Now, FFZ! + for(var i=0; i < ffz_sets.length; i++) { + var set_id = ffz_sets[i], + set = this.emote_sets[set_id]; + + if ( ! set || ! set.count || ( ! this.settings.global_emotes_in_menu && this.default_sets.indexOf(set_id) !== -1 ) ) + continue; + + sets.push([set.title.toLowerCase(), FFZ.menu_pages.my_emotes.draw_ffz_set.bind(this)(view, set)]); + } + + + // Finally, sort and add them all. + sets.sort(function(a,b) { + var an = a[0], bn = b[0]; + if ( an === "turbo" || an === "tfaces" ) + an = "zza|" + an; + else if ( an === "global" || an === "global emoticons" ) + an = "zzz|" + an; + + if ( bn === "turbo" || bn === "tfaces" ) + bn = "zza|" + bn; + else if ( bn === "global" || bn === "global emoticons" ) + bn = "zzz|" + bn; + + if ( an < bn ) return -1; + if ( an > bn ) return 1; + return 0; + }); + + for(var i=0; i < sets.length; i++) + container.appendChild(sets[i][1]); + + } catch(err) { + this.error("my_emotes draw_menu: " + err); + container.innerHTML = ""; + + var menu = document.createElement('div'), + heading = document.createElement('div'), + p = document.createElement('p'); + + heading.className = 'heading'; + heading.innerHTML = 'Error Loading Menu'; + menu.appendChild(heading); + + p.className = 'clearfix'; + p.textContent = err; + menu.appendChild(p); + + menu.className = 'chat-menu-content'; + container.appendChild(menu); + } + } +}; \ No newline at end of file diff --git a/src/ui/sub_count.js b/src/ui/sub_count.js new file mode 100644 index 00000000..1981fc75 --- /dev/null +++ b/src/ui/sub_count.js @@ -0,0 +1,101 @@ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'); + + +// ------------------- +// Subscriber Display +// ------------------- + +FFZ.prototype._update_subscribers = function() { + if ( this._update_subscribers_timer ) { + clearTimeout(this._update_subscribers_timer); + delete this._update_subscribers_timer; + } + + var user = this.get_user(), f = this, + match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, + id = this.is_dashboard && match && match[1]; + + if ( this.has_bttv || ! id || id !== user.login ) { + var el = document.querySelector("#ffz-sub-display"); + if ( el ) + el.parentElement.removeChild(el); + return; + } + + // Schedule an update. + this._update_subscribers_timer = setTimeout(this._update_subscribers.bind(this), 60000); + + // Spend a moment wishing we could just hit the subscribers API from the + // context of the web user. + + // Get the count! + jQuery.ajax({url: "/broadcast/dashboard/partnership"}).done(function(data) { + try { + var html = document.createElement('span'), dash; + + html.innerHTML = data; + dash = html.querySelector("#dash_main"); + + var match = dash && dash.textContent.match(/([\d,\.]+) total active subscribers/), + sub_count = match && match[1]; + + if ( ! sub_count ) { + var el = document.querySelector("#ffz-sub-display"); + if ( el ) + el.parentElement.removeChild(el); + + if ( f._update_subscribers_timer ) { + clearTimeout(f._update_subscribers_timer); + delete f._update_subscribers_timer; + } + + return; + } + + var el = document.querySelector('#ffz-sub-display span'); + if ( ! el ) { + var cont = f.is_dashboard ? document.querySelector("#stats") : document.querySelector("#channel .stats-and-actions .channel-stats"); + if ( ! cont ) + return; + + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-sub-display'; + stat.title = 'Active Channel Subscribers'; + + stat.innerHTML = constants.STAR + ' '; + + el = document.createElement('span'); + stat.appendChild(el); + + Twitch.api.get("chat/" + id + "/badges", null, {version: 3}) + .done(function(data) { + if ( data.subscriber && data.subscriber.image ) { + stat.innerHTML = ''; + stat.appendChild(el); + + stat.style.backgroundImage = 'url("' + data.subscriber.image + '")'; + stat.style.backgroundRepeat = 'no-repeat'; + stat.style.paddingLeft = '23px'; + stat.style.backgroundPosition = '0 50%'; + } + }); + + cont.appendChild(stat); + jQuery(stat).tipsy(f.is_dashboard ? {"gravity":"s"} : undefined); + } + + el.innerHTML = utils.number_commas(parseInt(sub_count)); + + } catch(err) { + f.error("_update_subscribers: " + err); + } + }).fail(function(){ + var el = document.querySelector("#ffz-sub-display"); + if ( el ) + el.parentElement.removeChild(el); + return; + });; +} diff --git a/src/ui/viewer_count.js b/src/ui/viewer_count.js index 8bfb7fcc..8fc19d18 100644 --- a/src/ui/viewer_count.js +++ b/src/ui/viewer_count.js @@ -2,8 +2,9 @@ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'); + // ------------ -// Set Viewers +// FFZ Viewers // ------------ FFZ.ws_commands.viewers = function(data) { @@ -13,21 +14,32 @@ FFZ.ws_commands.viewers = function(data) { match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, id = this.is_dashboard ? match && match[1] : controller && controller.get && controller.get('id'); - if ( id !== channel ) + if ( ! this.is_dashboard ) { + var room = this.rooms && this.rooms[channel]; + if ( room ) { + room.ffz_chatters = count; + if ( this._cindex ) + this._cindex.ffzUpdateChatters(); + } + return; + } + + + if ( ! this.settings.chatter_count || id !== channel ) return; - var view_count = document.querySelector('#ffz-viewer-display'), + var view_count = document.querySelector('#ffz-ffzchatter-display'), content = constants.ZREKNARF + ' ' + utils.number_commas(count); if ( view_count ) view_count.innerHTML = content; else { - var parent = document.querySelector(this.is_dashboard ? "#stats" : '.stats-and-actions .channel-stats'); + var parent = document.querySelector("#stats"); if ( ! parent ) return; view_count = document.createElement('span'); - view_count.id = "ffz-viewer-display"; + view_count.id = "ffz-ffzchatter-display"; view_count.className = 'ffz stat'; view_count.title = 'Chatters with FrankerFaceZ'; view_count.innerHTML = content; diff --git a/style.css b/style.css index f6cd1f69..f80da38e 100644 --- a/style.css +++ b/style.css @@ -12,6 +12,7 @@ cursor: pointer; } +.ffz-hide-view-count .stat.twitch-channel-views, .ffz-menu-replace .emoticon-selector-toggle { display: none !important; } @@ -266,14 +267,14 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle #ffz-ui-race .button span.logo { padding-left: 44px; - background-image: url("//cdn.frankerfacez.com/channel/global/srl_button.png"); + background-image: url("//cdn.frankerfacez.com/script/srl_button.png"); } #ffz-race-popup { position: absolute; bottom: 5px; - background-image: url("//cdn.frankerfacez.com/channel/602race/zreknarf.png"); + background-image: url("//cdn.frankerfacez.com/script/zreknarf-bg.png"); background-repeat: no-repeat; background-position: 115% 110%; } @@ -341,23 +342,23 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle .ffz-about-table a.youtube { width: 23px; - background-image: url("//cdn.frankerfacez.com/channel/global/youtube_logo.png"); + background-image: url("//cdn.frankerfacez.com/script/youtube_logo.png"); } .ffz-about-table a.twitter { width: 20px; - background-image: url("//cdn.frankerfacez.com/channel/global/twitter_logo.png"); + background-image: url("//cdn.frankerfacez.com/script/twitter_logo.png"); } #ffz-race-popup a.twitch, .ffz-about-table a.twitch { width: 15px; - background-image: url("//cdn.frankerfacez.com/channel/global/twitch_logo.png"); + background-image: url("//cdn.frankerfacez.com/script/twitch_logo.png"); } #ffz-race-popup a.hitbox { width: 12px; - background-image: url("//cdn.frankerfacez.com/channel/global/hitbox_logo.png"); + background-image: url("//cdn.frankerfacez.com/script/hitbox_logo.png"); } #ffz-race-popup table tbody tr.done:nth-child(0n+1) td { background-color: rgba(255,255,0,.2); } @@ -372,6 +373,10 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle /* Menu Options */ +#small_nav .content ul li#ffz_small_menu .filter_icon svg { + margin: 11px 13px; +} + @media screen and (max-width: 369px) { .ember-chat-container .ember-chat .chat-interface .emoticon-selector { right: -10px; @@ -383,6 +388,18 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle .ffz-ui-menu-page[data-page="about"], .ffz-ui-menu-page .chat-menu-content p { padding: 0 20px; } + +.chat-container.dark .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, +.app-main.theatre .chat-container .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, +.chat-container.force-dark .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, +.ember-chat-container.dark .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, +.ember-chat-container.force-dark .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box { + background-color: rgb(16,16,16); + color: rgb(195,195,195); + border-color: #32323e; +} + + .chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content .heading, .chat-menu.ffz-ui-popup .ffz-ui-menu-page .emoticon-grid .heading { padding: 10px 20px; @@ -440,6 +457,9 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle .app-main.theatre .ffz-ui-popup ul.menu, .chat-container.dark .ffz-ui-popup ul.menu, +.chat-container.force-dark .ffz-ui-popup ul.menu, +.ember-chat-container.dark .ffz-ui-popup ul.menu, +.ember-chat-container.force-dark .ffz-ui-popup ul.menu, .ffz-ui-popup.dark ul.menu { background-color: #282828; } @@ -477,16 +497,37 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle border-top-color: #fff; } +.chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active, +.chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active, +.ember-chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active, +.ember-chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active, +.app-main.theatre .chat-container .chat-interface .ffz-ui-popup ul.menu li.active, .ffz-ui-popup.dark ul.menu li.active { - background-color: #1e1e1e; + background-color: rgb(16,16,16); } +.chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active a, +.chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active a, +.ember-chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active a, +.ember-chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active a, +.app-main.theatre .chat-container .chat-interface .ffz-ui-popup ul.menu li.active a, .ffz-ui-popup.dark ul.menu li.active a { - border-top-color: #1e1e1e; + border-top-color: rgb(16,16,16); } +.chat-container.dark .chat-interface .ffz-ui-popup a, +.chat-container.force-dark .chat-interface .ffz-ui-popup a, +.ember-chat-container.dark .chat-interface .ffz-ui-popup a, +.ember-chat-container.force-dark .chat-interface .ffz-ui-popup a, +.app-main.theatre .chat-container .chat-interface .ffz-ui-popup a, .ffz-ui-popup.dark .ffz-ui-menu-page a { color: #fff; } + +.chat-container.dark .chat-interface .ffz-ui-popup ul.menu svg path, +.chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu svg path, +.ember-chat-container.dark .chat-interface .ffz-ui-popup ul.menu svg path, +.ember-chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu svg path, +.app-main.theatre .chat-container .chat-interface .ffz-ui-popup ul.menu svg path, .ffz-dark .ffz-ui-popup ul.menu svg path, .ffz-ui-popup.dark ul.menu svg path { fill: #d3d3d3; } @@ -741,10 +782,11 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle /* Dumb Fixes */ -.dropmenu, .ui-menu, .ui-multiselect-menu { - margin-bottom: 0; +.notification-controls .notify-menu { + bottom: 40px; } + /* Unsafe Links */ a.unsafe-link { @@ -753,4 +795,107 @@ a.unsafe-link { .chat-container.dark .chat-line a.unsafe-link { color: #d28e8e !important; +} + +/* Group Tabs */ + +#ffz-group-tabs { + padding: 10px 0 6px; + box-shadow: inset 0 -1px 0 0 rgba(0,0,0,0.2); +} + +.ffz-chat-tab { + cursor: pointer; + padding: 5px; + margin: 0 4px 4px 0; + + display: inline-block; + + background-color: rgba(127,127,127,0.1); + color: #6441A5; +} + + +.ffz-chat-tab svg path { + fill: #6441A5; +} + +#ffz-group-tabs .button { + height: 18px; + padding-bottom: 10px; + margin-bottom: -10px; + margin-right: 4px; +} + +#ffz-group-tabs .button.glyph-only svg { + margin: 6px 0; +} + +.ffz-chat-tab svg { + width: 18px; + height: 18px; + margin: -5px 5px -5px 0; +} + +.ffz-chat-tab:hover, +.ffz-chat-tab:focus, +.ffz-chat-tab.active { + background-color: #6441A5; + color: #fff !important; +} + +.ffz-chat-tab.tab-mentioned { + background-color: rgba(128,50,50,0.1); + color: red !important; +} + +.ffz-chat-tab.tab-mentioned:not(.active):hover, +.ffz-chat-tab.tab-mentioned:not(.active):focus { + background-color: #a54141; + color: #fff !important; +} + +.ffz-chat-tab:not(.active):hover, +.ffz-chat-tab:not(.active):focus { + background-color: #7550ba; +} + +.ffz-chat-tab:hover svg path, +.ffz-chat-tab:focus svg path, +.ffz-chat-tab.active svg path { + fill: #fff !important; +} + +.ffz-chat-tab.active { + cursor: default; +} + +.ffz-chat-tab span:empty { display: none; } + +.ffz-chat-tab span { + padding: 2px; + margin-left: 5px; + background-color: #777; + color: #fff; +} + + +/* Dark Group Tabs */ + +.app-main.theatre #ffz-group-tabs, +.chat-container.dark #ffz-group-tabs, +.ember-chat-container.dark #ffz-group-tabs { + box-shadow: inset 0 -1px 0 0 #32323e; +} + +.app-main.theatre .ffz-chat-tab, +.chat-container.dark .ffz-chat-tab, +.ember-chat-container.dark .ffz-chat-tab { + color: #B9A3E3; +} + +.app-main.theatre .ffz-chat-tab svg path, +.chat-container.dark .ffz-chat-tab svg path, +.ember-chat-container.dark .ffz-chat-tab svg path { + fill: #B9A3E3; } \ No newline at end of file