Skip to content

Commit

Permalink
Merge branch 'master' into feat/phrases
Browse files Browse the repository at this point in the history
  • Loading branch information
vyneer committed Aug 22, 2023
2 parents 21aab27 + 3aeb43c commit dbada19
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 170 deletions.
63 changes: 24 additions & 39 deletions assets/chat/js/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class Chat {
// The websocket connection, emits events from the chat server
this.source = new ChatSource();

this.source.on('REFRESH', () => window.location.reload(false));
this.source.on('PING', (data) => this.source.send('PONG', data));
this.source.on('CONNECTING', (data) => this.onCONNECTING(data));
this.source.on('ME', (data) => this.onME(data));
Expand Down Expand Up @@ -140,6 +139,7 @@ class Chat {
this.source.on('GIFTSUB', (data) => this.onGIFTSUB(data));
this.source.on('MASSGIFT', (data) => this.onMASSGIFT(data));
this.source.on('DONATION', (data) => this.onDONATION(data));
this.source.on('UPDATEUSER', (data) => this.onUPDATEUSER(data));
this.source.on('ADDPHRASE', (data) => this.onADDPHRASE(data));
this.source.on('REMOVEPHRASE', (data) => this.onREMOVEPHRASE(data));

Expand Down Expand Up @@ -973,13 +973,19 @@ class Chat {
onDISPATCH({ data }) {
if (data && typeof data === 'object') {
let users = [];
const now = Date.now();
if (Object.hasOwn(data, 'nick')) users.push(this.addUser(data));
if (Object.hasOwn(data, 'users'))
users = users.concat(
Array.from(data.users).map((d) => this.addUser(d))
);
users.forEach((u) => this.autocomplete.add(u.nick, false, now));
if (data.users) {
users = users.concat(data.users.map((d) => this.addUser(d)));
}
if (data.user) {
users.push(this.addUser(data.user));
} else if (data.nick) {
users.push(this.addUser(data));
}
// For sub recipients in `GIFTSUB` events.
if (data.recipient) {
users.push(this.addUser(data.recipient));
}
users.forEach((u) => this.autocomplete.add(u.nick, false, Date.now()));
}
}

Expand Down Expand Up @@ -1241,46 +1247,19 @@ class Chat {
}

onSUBSCRIPTION(data) {
const user = this.users.get(data.nick) ?? new ChatUser(data.nick);
MessageBuilder.subscription(
data.data,
user,
data.tier,
data.tierlabel,
data.streak,
data.timestamp
).into(this);
MessageBuilder.subscription(data).into(this);
}

onGIFTSUB(data) {
const user = this.users.get(data.nick) ?? new ChatUser(data.nick);
MessageBuilder.gift(
data.data,
user,
data.tier,
data.tierlabel,
data.giftee,
data.timestamp
).into(this);
MessageBuilder.gift(data).into(this);
}

onMASSGIFT(data) {
const user = this.users.get(data.nick) ?? new ChatUser(data.nick);
MessageBuilder.massgift(
data.data,
user,
data.tier,
data.tierlabel,
data.quantity,
data.timestamp
).into(this);
MessageBuilder.massgift(data).into(this);
}

onDONATION(data) {
const user = this.users.get(data.nick) ?? new ChatUser(data.nick);
MessageBuilder.donation(data.data, user, data.amount, data.timestamp).into(
this
);
MessageBuilder.donation(data).into(this);
}

onADDPHRASE(data) {
Expand Down Expand Up @@ -1440,6 +1419,12 @@ class Chat {
}
}

onUPDATEUSER(data) {
if (this.user?.id === data.id) {
this.setUser(data);
}
}

cmdSHOWPOLL() {
if (this.chatpoll.poll) {
this.chatpoll.show();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AmazonAssociatesTagInjector } from './formatters';
import AmazonAssociatesTagInjector from './AmazonAssociatesTagInjector';

const chatStub = {
config: {
Expand Down
19 changes: 17 additions & 2 deletions assets/chat/js/formatters/UrlFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,31 @@ export default class UrlFormatter {
const m = decodedUrl.match(self.linkregex);
if (m) {
const encodedUrl = self.encodeUrl(m[0]);
const normalizedUrl = this.normalizeUrl(encodedUrl);
const maxUrlLength = 90;
let urlText = encodedUrl;
let urlText = normalizedUrl;
if (urlText.length > maxUrlLength) {
urlText = `${urlText.slice(0, 40)}...${urlText.slice(-40)}`;
}
const extra = self.encodeUrl(decodedUrl.substring(m[0].length));
const href = `${scheme ? '' : 'http://'}${encodedUrl}`;
const href = `${scheme ? '' : 'http://'}${normalizedUrl}`;
return `<a target="_blank" class="externallink ${extraclass}" href="${href}" rel="nofollow">${urlText}</a>${extra}`;
}
return url;
});
}

/**
* @param {string} url
* @return {string} The normalized URL.
*/
normalizeUrl(url) {
if (/(x|twitter)\.com\/\w{1,15}\/status\/\d{2,19}\?/i.test(url)) {
// Remove the query string from xeet URLs to protect users from clicking
// on a link to a xeet they've already seen.
return url.split('?')[0];
}

return url;
}
}
29 changes: 29 additions & 0 deletions assets/chat/js/formatters/UrlFormatter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import UrlFormatter from './UrlFormatter';

const urlFormatter = new UrlFormatter();

describe('Normalizing URLs', () => {
test('Remove the query string from a tweet URL', () => {
expect(
urlFormatter.normalizeUrl('https://twitter.com/jack/status/20?lang=en')
).toBe('https://twitter.com/jack/status/20');
});

test('Remove the query string from a xeet URL', () => {
expect(
urlFormatter.normalizeUrl('https://x.com/jack/status/20?lang=en')
).toBe('https://x.com/jack/status/20');
});

test("Don't modify a URL to a tweet that doesn't contain a query string", () => {
expect(urlFormatter.normalizeUrl('https://x.com/jack/status/20')).toBe(
'https://x.com/jack/status/20'
);
});

test("Don't modify a URL that isn't Twitter or X", () => {
expect(
urlFormatter.normalizeUrl('https://www.twitch.tv/search?term=vtuber')
).toBe('https://www.twitch.tv/search?term=vtuber');
});
});
7 changes: 7 additions & 0 deletions assets/chat/js/menus/ChatUserInfoMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export default class ChatUserInfoMenu extends ChatMenuFloating {
this.configureButtons();

this.chat.output.on('contextmenu', '.msg-chat .user', (e) => {
// If the target has this class, it's a sub tier label styled to match the
// username color of the sub (which requires the `user` class).
if (e.currentTarget.classList.contains('tier')) return false;

const message = $(e.currentTarget).closest('.msg-chat');
this.showUser(e, message);

Expand Down Expand Up @@ -219,6 +222,8 @@ export default class ChatUserInfoMenu extends ChatMenuFloating {
}

addContent(message) {
// Don't display messages if the giftee was clicked in a gift sub event
// because the message belongs to the gifter.
this.messageArray =
message[0].querySelector('.text') &&
this.clickedNick !== message.data('giftee')
Expand Down Expand Up @@ -326,6 +331,8 @@ export default class ChatUserInfoMenu extends ChatMenuFloating {
this.messageArray.forEach((element) => {
const text = element.find('.text')[0].innerText;
const nick = element.data('username');

// Create a new `ChatUser` to remove username styles for a cleaner look.
const msg = MessageBuilder.message(text, new ChatUser(nick));
displayedMessages.push(msg.html(this.chat));
});
Expand Down
57 changes: 31 additions & 26 deletions assets/chat/js/menus/ChatUserMenu.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { debounce } from 'throttle-debounce';
import ChatMenu from './ChatMenu';
import ChatUser from '../user';

// sections in order.
const UserMenuSections = [
Expand All @@ -20,12 +21,8 @@ const UserMenuSections = [
];

function userComparator(a, b) {
const u1 = this.chat.users.get(a.getAttribute('data-username').toLowerCase());
const u2 = this.chat.users.get(b.getAttribute('data-username').toLowerCase());
if (!u1 || !u2) return 0;

const u1Nick = u1.nick.toLowerCase();
const u2Nick = u2.nick.toLowerCase();
const u1Nick = a.getAttribute('data-username').toLowerCase();
const u2Nick = b.getAttribute('data-username').toLowerCase();
if (u1Nick < u2Nick) return -1;
if (u1Nick > u2Nick) return 1;
return 0;
Expand Down Expand Up @@ -76,9 +73,10 @@ export default class ChatUserMenu extends ChatMenu {
}
return true;
});
this.chat.source.on('JOIN', (data) => this.addAndRedraw(data.nick));
this.chat.source.on('QUIT', (data) => this.removeAndRedraw(data.nick));
this.chat.source.on('NAMES', () => this.addAll());
this.chat.source.on('JOIN', (data) => this.addAndRedraw(data));
this.chat.source.on('QUIT', (data) => this.removeAndRedraw(data));
this.chat.source.on('NAMES', (data) => this.addAll(data.users));
this.chat.source.on('UPDATEUSER', (data) => this.replaceAndRedraw(data));
this.searchinput.on(
'keyup',
debounce(
Expand Down Expand Up @@ -146,7 +144,7 @@ export default class ChatUserMenu extends ChatMenu {
return features !== '' ? `<span class="features">${features}</span>` : '';
}

addAll() {
addAll(users) {
this.totalcount = 0;
this.container.empty();
this.sections = new Map();
Expand All @@ -157,25 +155,32 @@ export default class ChatUserMenu extends ChatMenu {
this.flairSection.set(flair, data.name)
);
});
[...this.chat.users.keys()].forEach((username) =>
this.addElement(username)
);
users.forEach((u) => this.addElement(u));
this.sort();
this.filter();
this.redraw();
}

addAndRedraw(username) {
if (!this.hasElement(username)) {
this.addElement(username, true);
addAndRedraw(user) {
if (!this.hasElement(user)) {
this.addElement(user, true);
this.filter();
this.redraw();
}
}

removeAndRedraw(username) {
if (this.hasElement(username)) {
this.removeElement(username);
removeAndRedraw(user) {
if (this.hasElement(user)) {
this.removeElement(user);
this.redraw();
}
}

replaceAndRedraw(user) {
if (this.hasElement(user)) {
this.removeElement(user);
this.addElement(user, true);
this.filter();
this.redraw();
}
}
Expand Down Expand Up @@ -219,19 +224,19 @@ export default class ChatUserMenu extends ChatMenu {
this.container.append(section);
}

removeElement(username) {
this.container.find(`.user-entry[data-username="${username}"]`).remove();
removeElement(user) {
this.container.find(`.user-entry[data-user-id="${user.id}"]`).remove();
this.totalcount -= 1;
}

addElement(username, sort = false) {
const user = this.chat.users.get(username.toLowerCase());
addElement(messageUser, sort = false) {
const user = new ChatUser(messageUser);
const label =
!user.username || user.username === '' ? 'Anonymous' : user.username;
const features =
user.features.length === 0 ? 'nofeature' : user.features.join(' ');
const usr = $(
`<div class="user-entry" data-username="${user.username}"><span class="user ${features}">${label}</span><div class="user-actions"><i class="mention-nick"></i><i class="whisper-nick"></i></div></div>`
`<div class="user-entry" data-username="${user.username}" data-user-id="${user.id}"><span class="user ${features}">${label}</span><div class="user-actions"><i class="mention-nick"></i><i class="whisper-nick"></i></div></div>`
);
const section = this.sections.get(this.highestSection(user));

Expand All @@ -254,9 +259,9 @@ export default class ChatUserMenu extends ChatMenu {
this.totalcount += 1;
}

hasElement(username) {
hasElement(user) {
return (
this.container.find(`.user-entry[data-username="${username}"]`).length > 0
this.container.find(`.user-entry[data-user-id="${user.id}"]`).length > 0
);
}

Expand Down
Loading

0 comments on commit dbada19

Please sign in to comment.