Do more on the web, with a fast and secure browser!

Download Opera browser with:

  • built-in ad blocker
  • battery saver
  • free VPN
Download Opera

Twitch sidebar button doesn't work at all!

  • 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 ❤

    Annotation 2019-06-12 114724.png
    Opera Snapshot_2019-06-12_115109_extensions.png

    "full error 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}&` +
      `redirect_uri=${REDIR_URL_STR}&response_type=${RESPONSE_TYPE}&` +
      `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 => {});
    
        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 => {});
    
        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 => {});
    
        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 => {}
        );
    
        chrome.contextMenus.update(
          CONTEXT_MENU_ID_MUTE,
          {visible: !this.sounds.isMuted()},
          evt => {}
        );
    
        chrome.contextMenus.update(
          CONTEXT_MENU_ID_UNMUTE,
          {visible: this.sounds.isMuted()},
          evt => {}
        );
      }
    
      async twitchRequest(func, params = {}) {
        if (this.needsAuthentication() && 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 => {
          this.ports.push(port);
          port.onMessage.addListener(msg => this._onMessage(port, msg));
          port.onDisconnect.addListener(port => {
            let index = this.ports.indexOf(port);
            if (index >= 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(() => {
          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 => `00${x.toString(16)}`.slice(-2))
            .join('');
        }
        return this.authStateString;
      }
    
      getAuthUrl() {
        return `${AUTH_URL}&state=${this.getStateString()}`;
      }
    
      needsAuthentication() {
        return !localStorage.accessToken;
      }
    
      parseUrl(url) {
        try {
          return new URL(url);
        } catch (e) {
          return null;
        }
      }
    
      isRedirURL(parsedUrl) {
        return (
          parsedUrl &&
          parsedUrl.origin === REDIR_URL.origin &&
          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 => {
          chrome.identity.launchWebAuthFlow(authInfo, url => {
            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 > 99 ? `${liveCount}+` : String(liveCount);
      }
    
      updateStreamsInfo() {
        return this.twitchRequest(async () => {
          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 => {
              const [user, streams] = await Promise.all([
                api.getUserInfo(follow.to_id),
                api.getStreams(follow.to_id),
              ]);
    
              let isLive = false;
    
              if (streams.data.length > 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) =>
            a.followed_at >= b.followed_at ? 1 : -1
          );
          const oldFollows = this.followsLocal;
          const oldIds = new Set(oldFollows.map(follow => follow.id));
          this.followsLocal = sortedFollows;
    
          if (!sortedFollows.find(follow => 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();
    

    Opera Snapshot_2019-06-12_115030_extensions.png

  • Had the same issue. I thought a reinstall of the extention might be a solution but I can't find that one at all...

  • @earthplague Yeah I thought that too, but I didn't uninstall it, I hope that code helps the devs to solve it.

  • The Twitch sidebar is not working properly, it doesn't show any channel.912757f789061f9ce2e498e7006999e5.png