Dude, it doesn't appear! he's trying to let you know that "square" is bugged and doesn't show up at all for him, I and I think for others also.

Best posts made by ls-hamed
-
RE: Can't give feedback on GX ControlOpera GX
Latest posts made by ls-hamed
-
RE: Can't give feedback on GX ControlOpera GX
Dude, it doesn't appear! he's trying to let you know that "square" is bugged and doesn't show up at all for him, I and I think for others also.
-
Twitch feature needs more UI buttonsOpera GX
Thanks for fixing it, it was not working, but for the sake of our eyes and our hands, group all online channels in one group, it's like searching for a needle in straw! also, make a clickable button to show/hide offline channels, also adding a sound effect (alert) for every time a streamer goes live would be very useful, also a choice for opened links (streams), like auto open streams in theater mode..maybe adding a fav streams group would also be cool, but most importantly is grouping all currently online streams in the top of the feed.
-
RE: Twitch sidebar button doesn't work at all!Opera GX
@earthplague Yeah I thought that too, but I didn't uninstall it, I hope that code helps the devs to solve it.
-
Twitch sidebar button doesn't work at all!Opera GX
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
"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();