Skip to content

Commit

Permalink
7TV Emotes 1.4.7
Browse files Browse the repository at this point in the history
* Added: Animated avatars support for the Deck add-on
* Fixed: Further improved handling of animated avatars (Thanks to SirStendec for help with the code)
  • Loading branch information
Lordmau5 committed Nov 7, 2023
1 parent 8a91b07 commit 3deaf87
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 91 deletions.
4 changes: 2 additions & 2 deletions src/7tv-emotes/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main",
"clips"
],
"version": "1.4.6",
"version": "1.4.7",
"short_name": "7TV",
"name": "7TV Emotes",
"author": "Melonify",
Expand All @@ -14,5 +14,5 @@
"website": "https://7tv.app",
"settings": "add_ons.7tv_emotes",
"created": "2021-07-12T23:18:04.000Z",
"updated": "2023-11-06T10:08:06.992Z"
"updated": "2023-11-07T18:28:07.184Z"
}
203 changes: 114 additions & 89 deletions src/7tv-emotes/modules/avatars.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,102 @@ export default class Avatars extends FrankerFaceZ.utilities.module.Module {
}
});

this.updateInterval = false;
this.requestInterval = false;

this.userAvatars = new Map();
this.avatarCache = new Map();
this.socketWaiters = new Map();

this.bufferedAvatars = [];
this.updateInterval = false;
}

onEnable() {
this.settings.getChanges('addon.seventv_emotes.animated_avatars', () => this.onSettingChange());

this.on('common:update-avatar', async event => {
const url = await this.getAvatar(event.user.login);
if (url)
event.url = url;
});

this.onSettingChange();
}

receiveAvatarData(data) {
if (!data.user?.username || !data.host?.files) return;
const login = data.user.username.toLowerCase();

const waiter = this.socketWaiters.get(login);
if (!waiter)
return;

this.socketWaiters.delete(login);

const webpEmoteVersions = data.host.files.filter((value => value.format === 'WEBP'));
if (!webpEmoteVersions.length) return;

const highestQuality = webpEmoteVersions[webpEmoteVersions.length - 1];
waiter(`${data.host.url}/${highestQuality.name}`);
}

getAvatar(login) {
login = login.toLowerCase();

const entry = this.avatarCache.get(login);
if ( entry?.done && Date.now() < entry.expires_at )
return entry.value;

if ( entry && ! entry.done )
return entry.promise;

const promise = new Promise(resolve => {
let timer;
const onDone = value => {
clearTimeout(timer);
if ( ! value )
value = null;

this.avatarCache.set(login, {
done: true,
value,
expires_at: Date.now() + 1000 * 60 * 3 // 3 minutes
});

resolve(value);
};

this.socketWaiters.set(login, onDone);
timer = setTimeout(onDone, 3000);

this.waitingAvatars = this.waitingAvatars || [];
this.waitingAvatars.push(login);

if ( ! this.requestTimer )
this.requestTimer = setTimeout(() => {
this.requestTimer = null;
const waiting = this.waitingAvatars;
this.waitingAvatars = null;

const socket = this.resolve('..socket');
socket.emitSocket({
op: socket.OPCODES.BRIDGE,
d: {
command: 'userstate',
body: {
identifiers: waiting.map(login => `username:${login}`),
platform: 'TWITCH',
kinds: ['AVATAR']
}
}
});
}, 1000); // request them all after 1000ms
});

this.avatarCache.set(login, {
done: false,
promise
});

return promise;
}

onSettingChange() {
const enabled = this.settings.get('addon.seventv_emotes.animated_avatars');

Expand All @@ -41,132 +123,75 @@ export default class Avatars extends FrankerFaceZ.utilities.module.Module {
this.updateAvatars();
}, 1000);
}

if (!this.requestInterval) {
this.requestInterval = setInterval(() => {
this.postAvatarRequests();
}, 500);
}
}
else {
clearInterval(this.updateInterval);
this.updateInterval = false;

clearInterval(this.requestInterval);
this.requestInterval = false;
}

this.updateAvatars();
}

receiveAvatarData(data) {
if (!data.user?.username || !data.host?.files) return;

const webpEmoteVersions = data.host.files.filter((value => value.format === 'WEBP'));
if (!webpEmoteVersions.length) return;

const highestQuality = webpEmoteVersions[webpEmoteVersions.length - 1];

this.userAvatars.set(data.user.username, `${data.host.url}/${highestQuality.name}`);

this.updateAvatars(data.user.username);
}

getVisibleAvatars() {
return document.querySelectorAll('.tw-image-avatar');
}

updateAvatars(username = undefined) {
updateAvatars() {
const enabled = this.settings.get('addon.seventv_emotes.animated_avatars');

const avatars = this.getVisibleAvatars();
for (const avatar of avatars) {
avatars.forEach(async avatar => {
if (!enabled) {
// Check if the avatar has an seventv-original-avatar attribute and set it
if (avatar.hasAttribute('seventv-original-avatar')) {
avatar.setAttribute('src', avatar.getAttribute('seventv-original-avatar'));
avatar.removeAttribute('seventv-original-avatar');
}

continue;
return;
}

// If this avatar has the seventv-original-avatar attribute already, skip it
if (avatar.hasAttribute('seventv-original-avatar')) continue;

// Get the react instance for the avatar element
const avatarComponent = this.fine.getOwner(avatar);
if (!avatarComponent) continue;
if (!avatarComponent) return;

// Find the nearest parent that has information about the user login
const parentWithLogin = this.fine.searchParent(avatarComponent, e => e.props?.user?.login
const parent = this.fine.searchParent(avatarComponent, e => e.props?.user?.login
|| e.props?.targetLogin
|| e.props?.userLogin
|| e.props?.channelLogin,
|| e.props?.channelLogin
|| e.props?.video?.owner?.login,
50);

// props.user.login is for our own avatar in the top right
// props.targetLogin is for viewer cards
// props.userLogin appears to be for channels in the sidebar
// props.channelLogin is for the
// The 'alt' attribute is a fallback
const login = parentWithLogin?.props?.user?.login
|| parentWithLogin?.props?.targetLogin
|| parentWithLogin?.props?.userLogin
|| parentWithLogin?.props?.channelLogin
// props.channelLogin is for the main channel you're watching
// The 'alt' attribute is a fallback
const login = parent?.props?.user?.login
|| parent?.props?.targetLogin
|| parent?.props?.userLogin
|| parent?.props?.channelLogin
|| parent?.props?.video?.owner?.login
|| avatar.getAttribute('alt');

// No login? No avatar.
if (!login || username && login !== username) continue;

// Get the animated avatar URL for this login
const animatedAvatarURL = this.getUserAvatar(login);
if (animatedAvatarURL === undefined) {
if (this.bufferedAvatars.includes(login)) continue;

// The user has not been requested yet, buffer them
this.bufferedAvatars.push(login);
}
else if (animatedAvatarURL) {
// Set the seventv-original-avatar attribute to the current src attribute
avatar.setAttribute('seventv-original-avatar', avatar.getAttribute('src'));

// Set the src attribute to the animated avatar
avatar.setAttribute('src', animatedAvatarURL);
}
}
}
if (!login) return;

postAvatarRequests() {
if (!this.bufferedAvatars.length) return;

const requestArray = [];
for (const login of this.bufferedAvatars) {
requestArray.push(`username:${login}`);
// Get the current image src URL
const current_url = avatar.getAttribute('src');

// Set their avatar to false already so it won't get requested again
this.userAvatars.set(login, false);
}

const socket = this.resolve('..socket');
socket.emitSocket({
op: socket.OPCODES.BRIDGE,
d: {
command: 'userstate',
body: {
identifiers: requestArray,
platform: 'TWITCH',
kinds: ['AVATAR']
// Get the animated avatar URL for this login
const url = await this.getAvatar(login);
if (url && url !== current_url) {
if (!avatar.hasAttribute('seventv-original-avatar')) {
// Set the seventv-original-avatar attribute to the current src attribute
avatar.setAttribute('seventv-original-avatar', avatar.getAttribute('src'));
}

// Set the src attribute to the animated avatar
avatar.setAttribute('src', url);
}
});

this.bufferedAvatars = [];
}

getUserAvatar(_login) {
const login = _login.toLowerCase();

return this.userAvatars.get(login);
}
}
}

0 comments on commit 3deaf87

Please sign in to comment.