<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Twitch sidebar button doesn&#x27;t work at all!]]></title><description><![CDATA[<p dir="auto">I'm following 137 streamer, and there's 0 icons, also tried to make it run as admin by default, but still the same bug, I can't actually describe the bug 100% so I will let the images/errors talk! I hope to consider fix it asap <img src="https://forums.opera.com/assets/plugins/nodebb-plugin-emoji/emoji/emoji-one/2764.png?v=89iooh84962" class="not-responsive emoji emoji-emoji-one emoji--red_heart" title="&lt;3" alt="❤" /></p>
<p dir="auto"><img src="/assets/uploads/files/1560332909388-annotation-2019-06-12-114724.png" alt="Annotation 2019-06-12 114724.png" class=" img-responsive img-markdown" /><br />
<img src="/assets/uploads/files/1560333086644-opera-snapshot_2019-06-12_115109_extensions.png" alt="Opera Snapshot_2019-06-12_115109_extensions.png" class=" img-responsive img-markdown" /></p>
<p dir="auto">"full error code"</p>
<pre><code>/**
 * Copyright (C) 2019 Opera Software AS. All rights reserved.
 * This file is an original work developed by Opera Software AS
 */

import {TwitchAPI} from '/tools/twitch_api.js';
import {Colors} from './tools/colors.js';
import {StatsReporter} from './tools/stats.js';

const CLIENT_ID = 'ju0ntw6bpd1i0cx1ama5buw1q377qy';

const REDIR_URL_STR = `https://${chrome.runtime.id}.chromiumapp.org/`;
const REDIR_URL = new URL(REDIR_URL_STR);

// maybe id_token not needed?
const RESPONSE_TYPE = 'token+id_token';

const SCOPE = 'openid';

const AUTH_URL =
  `https://id.twitch.tv/oauth2/authorize?client_id=${CLIENT_ID}&amp;` +
  `redirect_uri=${REDIR_URL_STR}&amp;response_type=${RESPONSE_TYPE}&amp;` +
  `scope=${SCOPE}`;

const REDIR_TOKEN_REGEXP = /access_token=(\w+)/;
const STATE_REGEXP = /state=(\w+)/;

// TODO decide on poll interval
const RERESH_INTERVAL_SECONDS = 60;

const CONTEXT_MENU_ID_LOGOUT = 'logout';
const CONTEXT_MENU_ID_MUTE = 'mute';
const CONTEXT_MENU_ID_UNMUTE = 'unmute';

class Sounds {
  constructor() {
    this.audio = new Audio('assets/notification.mp3');
  }

  play() {
    if (!this.isMuted()) {
      this.audio.play();
    }
  }

  setMuted(muted) {
    localStorage['muted'] = !!muted;
  }

  isMuted() {
    return localStorage['muted'] === 'true';
  }
}

class TwitchApp {
  constructor() {
    this.color = new Colors();
    this.stats = new StatsReporter('gx', 'twitch');
    this.setupConnections();
    this.sounds = new Sounds();
    this.twitchAPI = new TwitchAPI(localStorage.accessToken, CLIENT_ID);
    this.initContextMenu();

    if (this.needsAuthentication()) {
      this.waitForAuthentication();
    } else {
      this.init();
    }
  }

  onContextMenuCommand(info) {
    switch (info.menuItemId) {
      case CONTEXT_MENU_ID_LOGOUT:
        this.logout();
        break;
      case CONTEXT_MENU_ID_MUTE:
        this.sounds.setMuted(true);
        this.updateContextMenu();
        break;
      case CONTEXT_MENU_ID_UNMUTE:
        this.sounds.setMuted(false);
        this.updateContextMenu();
        break;
      default:
        break;
    }
  }

  get followsLocal() {
    try {
      return JSON.parse(localStorage['follows']);
    } catch (e) {
      return [];
    }
  }

  set followsLocal(value) {
    let stringified = JSON.stringify(value);
    if (stringified !== localStorage['follows']) {
      localStorage['follows'] = stringified;
      this.notifyUpdateNeeded();
      this.updateBadge(value);
    }
  }

  initContextMenu() {
    chrome.contextMenus.removeAll();

    const logoutItem = {
      id: CONTEXT_MENU_ID_LOGOUT,
      title: chrome.i18n.getMessage('contextMenuLogout'),
      visible: true,
      contexts: ['sidebar_action', 'browser_action'],
      enabled: !this.needsAuthentication(),
    };
    chrome.contextMenus.create(logoutItem, evt =&gt; {});

    const muteItem = {
      id: CONTEXT_MENU_ID_MUTE,
      title: chrome.i18n.getMessage('mute'),
      visible: !this.sounds.isMuted(),
      contexts: ['sidebar_action', 'browser_action'],
    };
    chrome.contextMenus.create(muteItem, evt =&gt; {});

    const unmuteItem = {
      id: CONTEXT_MENU_ID_UNMUTE,
      title: chrome.i18n.getMessage('unmute'),
      visible: this.sounds.isMuted(),
      contexts: ['sidebar_action', 'browser_action'],
    };
    chrome.contextMenus.create(unmuteItem, evt =&gt; {});

    chrome.contextMenus.onClicked.addListener(
      this.onContextMenuCommand.bind(this)
    );
  }

  setBadge(text) {
    opr.sidebarAction.setBadgeText({text});
  }

  clearBadge() {
    opr.sidebarAction.setBadgeText({text: ''});
  }

  updateContextMenu() {
    chrome.contextMenus.update(
      CONTEXT_MENU_ID_LOGOUT,
      {enabled: !this.needsAuthentication()},
      evt =&gt; {}
    );

    chrome.contextMenus.update(
      CONTEXT_MENU_ID_MUTE,
      {visible: !this.sounds.isMuted()},
      evt =&gt; {}
    );

    chrome.contextMenus.update(
      CONTEXT_MENU_ID_UNMUTE,
      {visible: this.sounds.isMuted()},
      evt =&gt; {}
    );
  }

  async twitchRequest(func, params = {}) {
    if (this.needsAuthentication() &amp;&amp; params.needsAuth !== false) {
      return this._getNeedsAuthData();
    }

    try {
      return await func();
    } catch (err) {
      if (err.status === 401) {
        delete localStorage.accessToken;
        return this._getNeedsAuthData();
      }

      return {error: 'unexpected_error'};
    }
  }

  setupConnections() {
    this.ports = [];
    chrome.runtime.onConnect.addListener(port =&gt; {
      this.ports.push(port);
      port.onMessage.addListener(msg =&gt; this._onMessage(port, msg));
      port.onDisconnect.addListener(port =&gt; {
        let index = this.ports.indexOf(port);
        if (index &gt;= 0) {
          this.ports.splice(index, 1);
        }
      });
    });
  }

  async setupUpdates() {
    // First update should always update the badge.
    const follows = await this.updateStreamsInfo();

    this.updateBadge(follows);

    window.setInterval(() =&gt; {
      this.updateStreamsInfo();
    }, 1000 * RERESH_INTERVAL_SECONDS);
  }

  async notifyUpdateNeeded() {
    for (let port of this.ports) {
      port.postMessage({updateNeeded: true});
    }
  }

  updateBadge(follows) {
    let liveCount = 0;
    for (let follow of follows) {
      if (follow.isLive) {
        ++liveCount;
      }
    }

    // When liveCount = 0, don't show badge nor play sound
    if (liveCount === 0) {
      this.color.setBadgeInactive();
    } else {
      this.color.setBadgeActive();
    }
    this.setBadge(this.capLiveCount(liveCount));
  }

  init() {
    this.twitchAPI = new TwitchAPI(localStorage.accessToken, CLIENT_ID);
    this.updateContextMenu();
    this.setupUpdates();
    this.color.setBadgeInactive();
  }

  getStateString() {
    if (!this.authStateString) {
      const STATE_LENTH = 16;
      let array = new Uint8Array(STATE_LENTH);
      window.crypto.getRandomValues(array);
      // hex encoded
      this.authStateString = Array.prototype.map
        .call(array, x =&gt; `00${x.toString(16)}`.slice(-2))
        .join('');
    }
    return this.authStateString;
  }

  getAuthUrl() {
    return `${AUTH_URL}&amp;state=${this.getStateString()}`;
  }

  needsAuthentication() {
    return !localStorage.accessToken;
  }

  parseUrl(url) {
    try {
      return new URL(url);
    } catch (e) {
      return null;
    }
  }

  isRedirURL(parsedUrl) {
    return (
      parsedUrl &amp;&amp;
      parsedUrl.origin === REDIR_URL.origin &amp;&amp;
      parsedUrl.path === REDIR_URL.path
    );
  }

  // Returns the token if correct, null otherwise
  getTokenFromRedirectUrl(url) {
    let parsedUrl = this.parseUrl(url);
    if (!this.isRedirURL(parsedUrl)) {
      return null;
    }
    let stateMatch = parsedUrl.hash.match(STATE_REGEXP);
    if (stateMatch.length !== 2 || stateMatch[1] !== this.getStateString()) {
      return null;
    }
    let tokenMatch = parsedUrl.hash.match(REDIR_TOKEN_REGEXP);
    if (tokenMatch.length === 2) {
      return tokenMatch[1];
    }
    return null;
  }

  login() {
    if (this._loginPromise !== undefined) {
      return this._loginPromise;
    }

    const authInfo = {
      url: this.getAuthUrl(),
      interactive: true,
    };
    this._loginPromise = new Promise(resolve =&gt; {
      chrome.identity.launchWebAuthFlow(authInfo, url =&gt; {
        const token = this.getTokenFromRedirectUrl(url);
        if (token === null) {
          resolve(false);
        } else {
          localStorage.accessToken = token;
          this.init();
          this.stats.recordBoolean('LoggedIn', true);
          resolve(true);
        }
        this._loginPromise = undefined;
      });
    });
    return this._loginPromise;
  }

  waitForAuthentication() {}

  async _onMessage(port, msg) {
    if (msg.id === undefined) {
      // ERROR
    }

    switch (msg.command) {
      case 'getStreamsInfo': {
        const data = await this.twitchRequest(this.getStreamsInfo.bind(this));
        port.postMessage({isReply: true, id: msg.id, data: data});
        break;
      }
      case 'getUserInfo': {
        const data = await this.twitchRequest(this.getUserInfo.bind(this));
        port.postMessage({isReply: true, id: msg.id, data: data});
        break;
      }

      case 'getTopStreamers': {
        const data = await this.twitchRequest(this.getTopStreams.bind(this), {
          needsAuth: false,
        });
        port.postMessage({isReply: true, id: msg.id, data: data});
        break;
      }

      case 'login': {
        let success = await this.login();
        port.postMessage({isReply: true, id: msg.id, data: {success: success}});
        break;
      }

      case 'logout': {
        await this.logout();
        this.stats.recordBoolean('LoggedIn', false);
        port.postMessage({isReply: true, id: msg.id, data: {}});
        break;
      }
    }
  }

  async logout() {
    await this.twitchAPI.logout();
    delete localStorage.accessToken;
    this.clearBadge();
    chrome.runtime.reload();
    return;
  }

  capLiveCount(liveCount) {
    return liveCount &gt; 99 ? `${liveCount}+` : String(liveCount);
  }

  updateStreamsInfo() {
    return this.twitchRequest(async () =&gt; {
      let api = this.twitchAPI;

      const userInfo = await api.getUserInfo();
      const followedChannels = await api.getFollowedChannels(
        userInfo.data[0].id
      );

      const follows = await Promise.all(
        followedChannels.data.map(async follow =&gt; {
          const [user, streams] = await Promise.all([
            api.getUserInfo(follow.to_id),
            api.getStreams(follow.to_id),
          ]);

          let isLive = false;

          if (streams.data.length &gt; 0) {
            isLive = true;
          }

          return {
            id: follow.to_id,
            name: user.data[0].display_name,
            iconUrl: user.data[0].profile_image_url,
            followed_at: user.data[0].followed_at,
            isLive,
          };
        })
      );
      const sortedFollows = follows.sort((a, b) =&gt;
        a.followed_at &gt;= b.followed_at ? 1 : -1
      );
      const oldFollows = this.followsLocal;
      const oldIds = new Set(oldFollows.map(follow =&gt; follow.id));
      this.followsLocal = sortedFollows;

      if (!sortedFollows.find(follow =&gt; oldIds.has(follow.id))) {
        this.sounds.play();
      }

      return sortedFollows;
    });
  }

  async getUserInfo() {
    const userResults = await this.twitchAPI.getUserInfo();
    const user = userResults.data[0];
    const followersResults = await this.twitchAPI.getFollowersChannels(user.id);
    user.followers = followersResults.total || 0;

    return user;
  }

  toStreamsInfo(follows) {
    return {
      channels: follows,
    };
  }

  _getNeedsAuthData() {
    return {
      needsAuthentication: true,
    };
  }

  async getStreamsInfo() {
    let follows = this.followsLocal;
    if (Object.keys(follows).length === 0) {
      await this.updateStreamsInfo();
      follows = this.followsLocal;
    }
    return this.toStreamsInfo(follows);
  }
}

window.twitch = new TwitchApp();
</code></pre>
<p dir="auto"><img src="/assets/uploads/files/1560333086533-opera-snapshot_2019-06-12_115030_extensions.png" alt="Opera Snapshot_2019-06-12_115030_extensions.png" class=" img-responsive img-markdown" /></p>
]]></description><link>https://forums.opera.com/topic/32957/twitch-sidebar-button-doesn-t-work-at-all</link><generator>RSS for Node</generator><lastBuildDate>Sun, 16 Aug 2026 20:18:32 GMT</lastBuildDate><atom:link href="https://forums.opera.com/topic/32957.rss" rel="self" type="application/rss+xml"/><pubDate>Wed, 12 Jun 2019 09:59:35 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to Twitch sidebar button doesn&#x27;t work at all! on Thu, 13 Jun 2019 15:42:46 GMT]]></title><description><![CDATA[<p dir="auto">The Twitch sidebar is not working properly, it doesn't show any channel.<img src="/assets/uploads/files/1560440559496-912757f789061f9ce2e498e7006999e5.png" alt="912757f789061f9ce2e498e7006999e5.png" class=" img-responsive img-markdown" /></p>
]]></description><link>https://forums.opera.com/post/173360</link><guid isPermaLink="true">https://forums.opera.com/post/173360</guid><dc:creator><![CDATA[[[global:former_user]]]]></dc:creator><pubDate>Thu, 13 Jun 2019 15:42:46 GMT</pubDate></item><item><title><![CDATA[Reply to Twitch sidebar button doesn&#x27;t work at all! on Wed, 12 Jun 2019 21:52:47 GMT]]></title><description><![CDATA[<p dir="auto">@earthplague Yeah I thought that too, but I didn't uninstall it, I hope that code helps the devs to solve it.</p>
]]></description><link>https://forums.opera.com/post/173274</link><guid isPermaLink="true">https://forums.opera.com/post/173274</guid><dc:creator><![CDATA[[[global:former_user]]]]></dc:creator><pubDate>Wed, 12 Jun 2019 21:52:47 GMT</pubDate></item><item><title><![CDATA[Reply to Twitch sidebar button doesn&#x27;t work at all! on Wed, 12 Jun 2019 20:02:15 GMT]]></title><description><![CDATA[<p dir="auto">Had the same issue. I thought a reinstall of the extention might be a solution but I can't find that one at all...</p>
]]></description><link>https://forums.opera.com/post/173262</link><guid isPermaLink="true">https://forums.opera.com/post/173262</guid><dc:creator><![CDATA[[[global:former_user]]]]></dc:creator><pubDate>Wed, 12 Jun 2019 20:02:15 GMT</pubDate></item></channel></rss>