Files
pixel.horse/src/ts/components/shared/chat-log/chat-log.ts
T
2019-09-07 14:40:56 +02:00

832 lines
25 KiB
TypeScript

import {
Component, ViewChild, ElementRef, NgZone, AfterViewInit, OnDestroy, HostListener, Output, EventEmitter, DoCheck
} from '@angular/core';
import { Subscription } from 'rxjs';
import { clamp, escapeRegExp } from 'lodash';
import { PonyTownGame } from '../../../client/game';
import { MessageType, isPartyMessage, ChatMessage, Pony, FakeEntity, isWhisper, isWhisperTo } from '../../../common/interfaces';
import { SettingsService } from '../../services/settingsService';
import { AgDragEvent } from '../directives/agDrag';
import { element, textNode, removeAllNodes, replaceNodes } from '../../../client/htmlUtils';
import { DEFAULT_CHATLOG_OPACITY, PONY_TYPE, SECOND } from '../../../common/constants';
import { faCaretUp, faArrowDown } from '../../../client/icons';
import { sampleMessages } from '../../../common/debugData';
import { findEntityById } from '../../../common/worldMap';
interface IndexEntryUser {
id: number;
crc: number | undefined;
}
interface IndexEntry {
users: IndexEntryUser[];
counter: number;
}
interface ChatLogLineDOM {
entry: ChatLogMessage;
root: HTMLElement;
label: HTMLElement;
time: HTMLElement;
timeContent: HTMLElement;
labelText: Text;
name: HTMLElement;
nameContent: HTMLElement;
index: HTMLElement;
indexText: Text;
prefixText: Text;
suffixText: Text;
message: HTMLElement;
}
export interface ChatLogMessage {
message: string;
name?: string;
crc?: number;
prefix?: string;
suffix?: string;
label?: string;
index: number;
classes?: string;
entityId?: number;
dom?: ChatLogLineDOM;
}
type Tab = 'local' | 'party' | 'whisper';
type ClickHandler = (entry: ChatLogMessage) => void;
const GENERAL_CHAT_LIMIT = 100;
const PARTY_CHAT_LIMIT = 100;
const WHISPER_CHAT_LIMIT = 100;
const FORGET_INDEX_AFTER = 1000;
const SCROLL_END_THRESHOLD = 60;
const LABELS: (string | undefined)[] = [];
LABELS[MessageType.System] = 'system';
LABELS[MessageType.Admin] = 'admin';
LABELS[MessageType.Mod] = 'mod';
LABELS[MessageType.Party] = 'party';
LABELS[MessageType.PartyThinking] = 'party';
LABELS[MessageType.PartyAnnouncement] = 'party';
const PREFIXES: (string | undefined)[] = [];
PREFIXES[MessageType.WhisperTo] = 'To ';
PREFIXES[MessageType.WhisperToAnnouncement] = 'To ';
const SUFFIXES: (string | undefined)[] = [];
SUFFIXES[MessageType.Thinking] = 'thinks';
SUFFIXES[MessageType.PartyThinking] = 'thinks';
SUFFIXES[MessageType.Whisper] = 'whispers';
SUFFIXES[MessageType.WhisperAnnouncement] = 'whispers';
const CLASSES: (string | undefined)[] = [];
CLASSES[MessageType.System] = 'chat-line-system';
CLASSES[MessageType.Admin] = 'chat-line-admin';
CLASSES[MessageType.Mod] = 'chat-line-mod';
CLASSES[MessageType.Party] = 'chat-line-party';
CLASSES[MessageType.Thinking] = 'chat-line-thinking';
CLASSES[MessageType.PartyThinking] = 'chat-line-party-thinking';
CLASSES[MessageType.Announcement] = 'chat-line-announcement';
CLASSES[MessageType.PartyAnnouncement] = 'chat-line-party-announcement';
CLASSES[MessageType.Supporter1] = 'chat-line-supporter-1';
CLASSES[MessageType.Supporter2] = 'chat-line-supporter-2';
CLASSES[MessageType.Supporter3] = 'chat-line-supporter-3';
CLASSES[MessageType.Whisper] = 'chat-line-whisper';
CLASSES[MessageType.WhisperTo] = 'chat-line-whisper';
CLASSES[MessageType.WhisperAnnouncement] = 'chat-line-whisper-announcement';
CLASSES[MessageType.WhisperToAnnouncement] = 'chat-line-whisper-announcement';
export function createChatLogLineDOM(clickLabel: ClickHandler, clickName: ClickHandler): ChatLogLineDOM {
const line: ChatLogLineDOM = {} as any;
line.root = element('div', 'chat-line', [
element('span', 'chat-line-lead'),
line.time = element('span', 'chat-line-name', [
textNode('['),
line.timeContent = element(
'span', 'chat-line-time-content', [textNode('')], undefined),
line.index = element('span', 'chat-line-time-index', [line.indexText = textNode('')]),
textNode('] '),
]),
line.label = element(
'span', 'chat-line-label mr-1', [line.labelText = textNode('')], undefined, { click: () => clickLabel(line.entry) }),
line.prefixText = textNode(''),
line.name = element('span', 'chat-line-name', [
textNode(`[`),
line.nameContent = element(
'span', 'chat-line-name-content', [textNode('')], undefined, { click: () => clickName(line.entry) }),
line.index = element('span', 'chat-line-name-index', [line.indexText = textNode('')], { title: 'duplicate name' }),
textNode(']'),
]),
line.suffixText = textNode(''),
line.message = element('span', 'chat-line-message', [textNode('')]),
]);
return line;
}
export function updateChatLogLine(line: ChatLogLineDOM, entry: ChatLogMessage) {
const { classes, label, message, prefix, suffix } = entry;
const hasSpace = message.indexOf(' ') !== -1;
line.entry = entry;
line.root.className = `chat-line ${hasSpace ? '' : 'chat-line-break '}${classes}`.trim();
line.label.style.display = label ? 'inline' : 'none';
line.labelText.nodeValue = label ? `[${label}]` : '';
updateChatLogName(line, entry);
updateTime(line);
line.prefixText.nodeValue = prefix || '';
line.suffixText.nodeValue = suffix ? ` ${suffix}: ` : ': ';
replaceNodes(line.message, message);
}
function updateTime(line: ChatLogLineDOM) {
line.time.style.display = 'inline';
replaceNodes(line.timeContent, getCurrentTime());
}
function getCurrentTime() {
const date = new Date();
const hours = date.getHours().toString();
const minutes = date.getMinutes().toString();
const seconds = date.getSeconds().toString();
return `${hours.length === 1 ? `0${hours}` : hours}:${minutes.length === 1 ? `0${minutes}` : minutes}:${seconds.length === 1 ? `0${seconds}` : seconds}`;
}
function setNameColors(line: ChatLogLineDOM | undefined, colors?: string[] | null) {
if (!colors || !line) return;
if (colors[1]) line.name.style.color = colors[1];
if (colors[0]) line.nameContent.style.color = colors[0];
}
function updateChatLogName(line: ChatLogLineDOM, { name, index }: ChatLogMessage) {
if (name) {
line.name.style.display = 'inline';
replaceNodes(line.nameContent, name);
line.index.style.display = (index > 0) ? 'inline' : 'none';
line.indexText.nodeValue = (index > 0) ? ` #${index + 1}` : '';
} else {
line.name.style.display = 'none';
}
}
function isMatch(e: ChatLogMessage, id: number, name: string, crc: number | undefined) {
return e.name === name && (e.entityId === id || (e.crc === crc && crc !== undefined));
}
function addOrUpdatePony(pony: Pony, list: ChatLogMessage[]) {
for (const e of list) {
if (isMatch(e, pony.id, pony.name!, pony.crc)) {
e.entityId = pony.id;
}
}
}
function updateEntityId(list: ChatLogMessage[], oldId: number, newId: number) {
for (const e of list) {
if (e.entityId === oldId) {
e.entityId = newId;
}
}
}
function findUserIndex(users: IndexEntryUser[], id: number, crc: number | undefined) {
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (user.id === id || (crc !== undefined && user.crc === crc)) {
user.id = id;
return i;
}
}
return -1;
}
@Component({
selector: 'chat-log',
templateUrl: 'chat-log.pug',
styleUrls: ['chat-log.scss'],
})
export class ChatLog implements AfterViewInit, OnDestroy, DoCheck {
readonly toBottomIcon = faArrowDown;
readonly resizeIcon = faCaretUp;
@ViewChild('chatLog', { static: true }) chatLog!: ElementRef;
@ViewChild('scroll', { static: true }) scroll!: ElementRef;
@ViewChild('lines', { static: true }) lines!: ElementRef;
@ViewChild('localTab', { static: true }) localTab!: ElementRef;
@ViewChild('partyTab', { static: true }) partyTab!: ElementRef;
@ViewChild('whisperTab', { static: true }) whisperTab!: ElementRef;
@ViewChild('filterTab', { static: true }) filterTab!: ElementRef;
@ViewChild('toggleButton', { static: true }) toggleButton!: ElementRef;
@ViewChild('count', { static: true }) countElement!: ElementRef;
@ViewChild('content', { static: true }) contentElement!: ElementRef;
@Output() toggleType = new EventEmitter<string>();
@Output() nameClick = new EventEmitter<ChatLogMessage>();
innerWidth = 0;
// TODO: move to game ?
local: ChatLogMessage[] = [];
party: ChatLogMessage[] = [];
whisper: ChatLogMessage[] = [];
unread = 0;
private subscriptions: Subscription[] = [];
private startX = 0;
private startY = 0;
private shouldScrollToEnd = false;
private scrolledToEnd = true;
private scrollingToEnd = false;
private scrollToEndAtFrame = false;
private indexes = new Map<string, IndexEntry>();
private messageCounter = 0;
private lastOpacity = 0;
private autoClear: null | NodeJS.Timeout = null;
private autoUnfocus: null | NodeJS.Timeout = null;
filterColor = this.inactiveBg;
constructor(
private game: PonyTownGame,
private settingsService: SettingsService,
private element: ElementRef,
private zone: NgZone,
) {
// TODO: just put reference to chatlog on game ???
this.subscriptions.push(
game.onFrame.subscribe(() => {
if (this.scrollToEndAtFrame) {
this.scrollToEndAtFrame = false;
this.scrollHandler();
}
}),
game.onMessage.subscribe(message => {
this.addMessage(message);
}),
// TODO: move to game ?
game.onPonyAddOrUpdate.subscribe(pony => {
addOrUpdatePony(pony, this.local);
addOrUpdatePony(pony, this.party);
addOrUpdatePony(pony, this.whisper);
}),
game.onJoined.subscribe(() => {
if (!DEVELOPMENT) {
// TODO: move to game ?
this.local = [];
this.party = [];
this.whisper = [];
this.messageCounter = 0;
this.indexes = new Map();
this.clearList();
}
}),
game.onEntityIdUpdate.subscribe(update => {
updateEntityId(this.local, update.old, update.new);
updateEntityId(this.party, update.old, update.new);
updateEntityId(this.whisper, update.old, update.new);
}),
);
}
get linesElement() {
return this.lines.nativeElement as HTMLElement;
}
private updateOpen() {
this.updateChatlog();
if (this.open) {
this.setUnread(0);
this.regenerateList();
this.scrollToEnd();
} else {
this.clearList();
}
this.updateInnerWidth();
}
private updateChatlog() {
const element = this.chatLog.nativeElement as HTMLElement;
element.style.display = this.open ? 'flex' : 'none';
if (this.open) {
element.style.width = `${this.width}px`;
element.style.height = `${this.height}px`;
}
}
ngAfterViewInit() {
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
canvas.addEventListener('click', () => this.unFocus());
this.game.findEntityFromChatLog = this.findEntityFromMessages;
this.game.findEntityFromChatLogByName = this.findEntityFromMessagesByName;
this.updateTabs();
this.updateOpen();
this.zone.runOutsideAngular(() => {
const scroll = this.scroll.nativeElement as HTMLElement;
scroll.addEventListener('scroll', () => {
if (this.scrollingToEnd) {
this.scrolledToEnd = true;
this.scrollingToEnd = false;
} else {
const clientHeight = scroll.getBoundingClientRect().height;
this.scrolledToEnd = scroll.scrollTop >= (scroll.scrollHeight - clientHeight - SCROLL_END_THRESHOLD);
}
});
});
setTimeout(() => {
this.scrollToEnd();
this.updateInnerWidth();
});
if (DEVELOPMENT) {
sampleMessages.forEach(({ name, id, message, type }) =>
this.addMessage({ id: id || 999999, crc: undefined, name, message, type: type || MessageType.Chat }));
}
}
ngOnDestroy() {
if (this.game.findEntityFromChatLog === this.findEntityFromMessages) {
this.game.findEntityFromChatLog = () => undefined;
}
if (this.game.findEntityFromChatLogByName === this.findEntityFromMessagesByName) {
this.game.findEntityFromChatLogByName = () => undefined;
}
this.subscriptions.forEach(s => s.unsubscribe());
this.subscriptions = [];
}
ngDoCheck() {
if (this.lastOpacity !== this.opacity) {
this.lastOpacity = this.opacity;
this.contentElement.nativeElement.style.backgroundColor = this.bg;
this.updateTabs();
}
}
@HostListener('window:resize')
updateInnerWidth() {
const maxWidth = (this.element.nativeElement as HTMLElement).getBoundingClientRect().width;
const innerWidth = Math.min(maxWidth || this.width, this.width) - 40;
if (this.innerWidth !== innerWidth) {
this.innerWidth = innerWidth;
this.linesElement.style.width = `${innerWidth}px`;
}
if (!maxWidth) {
setTimeout(() => this.updateInnerWidth(), 10);
}
}
get messages() {
return this[this.activeTab];
}
get settings() {
return this.settingsService.browser;
}
get settings2() {
return this.settingsService.account;
}
get activeTab(): Tab {
const tab = this.settings.chatlogTab;
return (tab === 'local' || tab === 'party' || tab === 'whisper') ? tab : 'local';
}
get open() {
return !this.settings.chatlogClosed;
}
get width() {
return this.settings.chatlogWidth || 500;
}
get height() {
return this.settings.chatlogHeight || 310;
}
get opacity() {
return this.settings2.chatlogOpacity === undefined ? DEFAULT_CHATLOG_OPACITY : this.settings2.chatlogOpacity;
}
get bg() {
return `rgba(0, 0, 0, ${this.opacity / 100})`;
}
get inactiveBg() {
return `rgba(0, 0, 0, ${(this.opacity / 200) * 0.5})`;
}
private createEntry({ id, crc, name, message, type }: ChatMessage): ChatLogMessage {
const system = type === MessageType.System;
const entry: ChatLogMessage = {
entityId: system ? 0 : id,
name: system ? '' : name,
index: 0,
crc,
message,
label: LABELS[type] || '',
prefix: PREFIXES[type] || '',
suffix: SUFFIXES[type] || '',
classes: CLASSES[type] || '',
};
if (!system) {
entry.index = this.findOrCreateIndex(name, id, crc);
}
return entry;
}
private findOrCreateIndex(name: string, id: number, crc: number | undefined) {
let found = this.indexes.get(name);
if (!found || (this.messageCounter - found.counter) > FORGET_INDEX_AFTER) {
found = {
users: [{ id, crc }],
counter: 0,
};
this.indexes.set(name, found);
}
found.counter = this.messageCounter;
let index = findUserIndex(found.users, id, crc);
if (index === -1) {
index = found.users.length;
found.users.push({ id, crc });
}
return index;
}
filterChat() {
this.clearTimeOutAutoClear();
this.clearTimeOutAutoUnfocus();
let value = this.filterTab.nativeElement.value;
if (!value) {
this.clearTimeOutAutoClear();
this.unFocus();
this.showHideChatLogLines('', false);
return;
}
else this.filterColor = this.bg;
this.autoClear = setTimeout(() => {
this.filterTab.nativeElement.value = '';
this.filterChat();
this.unFocus();
}, SECOND * 30);
this.autoUnfocus = setTimeout(() => {
this.unFocus();
}, SECOND * 3);
let toLowerCase = false;
if (value.startsWith('#')) value = value.slice(1);
else if (value.startsWith('/')) {
value = value.slice(1);
try {
const regExp = new RegExp(value);
this.showHideChatLogLines(regExp, toLowerCase);
this.filterTab.nativeElement.style.color = '';
} catch (err) {
this.filterTab.nativeElement.style.color = '#ff6666';
}
return;
}
else {
value = value.toLowerCase();
toLowerCase = true;
}
this.showHideChatLogLines(value, toLowerCase);
}
showHideChatLogLines(content: string | RegExp, caseSensitive: boolean) {
const lines = this.linesElement.getElementsByTagName('div');
for (let i = 0; i < lines.length; i++) {
let textContent = lines[i].textContent;
if (textContent) {
textContent = textContent.slice(10);
if (typeof content === 'string') {
if (caseSensitive)
textContent.toLowerCase().includes(content) ? lines[i].hidden = false : lines[i].hidden = true;
else
textContent.includes(content) ? lines[i].hidden = false : lines[i].hidden = true;
} else {
textContent.match(content) ? lines[i].hidden = false : lines[i].hidden = true;
}
}
}
}
unhideAllLines() {
const lines = this.linesElement.getElementsByTagName('div');
for (let i = 0; i < lines.length; i++) {
lines[i].hidden = false;
}
}
private getRGB(hex: string) {
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return [r, g, b];
}
private RGBToHSL(rgb: number[]) {
let r = rgb[0] / 255;
let g = rgb[1] / 255;
let b = rgb[2] / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let delta = max - min;
let h = 0;
let s;
let l;
if (max === min) h = 0;
else if (r === max) h = (g - b) / delta;
else if (g === max) h = 2 + (b - r) / delta;
else if (b === max) h = 4 + (r - g) / delta;
h = Math.min(h * 60, 360);
if (h < 0) h += 360;
l = (min + max) / 2;
if (max === min) s = 0;
else if (l <= 0.5) s = delta / (max + min);
else s = delta / (2 - max - min);
h = Math.floor(h);
s = Math.floor(s * 100);
l = Math.floor(l * 100);
if (l < 40) {
if (l > 20 && l < 40) {
l += 20;
if (s > 11) s -= 11;
} else {
l = 40;
if (s > 11) s = 0;
}
}
return `hsl(${h}, ${s}%, ${l}%)`;
}
fixColor(color: string) {
while (color.length <= 7) {
color = `0${color}`;
}
color = color.replace(/0/g, '1');
return color.substr(0, 6);
}
private getCharaterColors(id: number | undefined) {
if (!id) return null;
const entity = findEntityById(this.game.map, id) as Pony;
if (!entity || !entity.palettePonyInfo) return null;
let colors = [];
let body = entity.palettePonyInfo.body;
let mane = entity.palettePonyInfo.mane;
if (body && body.palette && body.palette.colors[1]) {
let bodyColor = this.fixColor(body.palette.colors[1].toString(16));
const RGB = this.getRGB(bodyColor);
colors.push(this.RGBToHSL(RGB));
}
if (mane && mane.palette && mane.palette.colors[1]) {
let maneColor = this.fixColor(mane.palette.colors[1].toString(16));
const RGB = this.getRGB(maneColor);
colors.push(this.RGBToHSL(RGB));
} else if (colors && colors[0]) {
colors.push((colors[0]));
}
return colors;
}
clearTimeOutAutoClear() {
if (this.autoClear) clearTimeout(this.autoClear);
this.autoClear = null;
this.filterColor = this.inactiveBg;
}
clearTimeOutAutoUnfocus() {
if (this.autoUnfocus) clearTimeout(this.autoUnfocus);
this.autoUnfocus = null;
}
unFocus() {
this.clearTimeOutAutoUnfocus();
if (this.filterTab.nativeElement)
this.filterTab.nativeElement.blur();
}
addMessage(message: ChatMessage) {
if (message.name && message.message) {
const entry = this.createEntry(message);
const party = isPartyMessage(message.type);
const whisper = isWhisper(message.type) || isWhisperTo(message.type);
const open = this.open;
const scrolledToEnd = open ? this.scrolledToEnd : false;
const tab = this.activeTab;
let colors: string[] | null = null;
colors = this.getCharaterColors(message.id);
this.addEntryToList(this.local, GENERAL_CHAT_LIMIT, open && tab === 'local', entry);
setNameColors(entry.dom, colors);
if (party || whisper) {
const partyEntry = { ...entry };
partyEntry.dom = undefined;
partyEntry.label = whisper ? partyEntry.label : undefined;
this.addEntryToList(this.party, PARTY_CHAT_LIMIT, open && tab === 'party', partyEntry);
setNameColors(partyEntry.dom, colors);
}
if (whisper) {
const whisperEntry = { ...entry };
whisperEntry.dom = undefined;
whisperEntry.label = undefined;
this.addEntryToList(this.whisper, WHISPER_CHAT_LIMIT, open && tab === 'whisper', whisperEntry);
setNameColors(whisperEntry.dom, colors);
}
if (message.type === MessageType.Whisper && !this.open) {
this.setUnread(this.unread + 1);
}
if (scrolledToEnd) {
this.scrollToEnd();
}
if (tab !== 'whisper' && isWhisper(message.type)) {
this.whisperTab.nativeElement.classList.add('unread');
this.whisperTab.nativeElement.style.backgroundColor = `rgba(225, 161, 223, ${this.opacity / 100})`;
}
if (tab !== 'party' && party) {
this.partyTab.nativeElement.classList.add('unread');
this.partyTab.nativeElement.style.backgroundColor = `rgba(184, 227, 255, ${this.opacity / 100})`;
}
this.filterChat();
this.messageCounter++;
}
}
private addEntryToList(list: ChatLogMessage[], limit: number, isOpen: boolean, entry: ChatLogMessage) {
let removedDom: ChatLogLineDOM | undefined;
while (list.length >= limit) {
const removed = list.shift();
if (isOpen && removed && removed.dom) {
if (removed.dom.root.parentElement) {
removed.dom.root.parentElement.removeChild(removed.dom.root);
}
removedDom = removed.dom;
removed.dom = undefined;
}
}
list.push(entry);
if (isOpen) {
entry.dom = removedDom || createChatLogLineDOM(this.clickLabel, this.clickNameHandler);
updateChatLogLine(entry.dom, entry);
this.linesElement.appendChild(entry.dom.root);
}
}
toggle() {
this.settings.chatlogClosed = !this.settings.chatlogClosed;
this.settingsService.saveBrowserSettings();
this.updateOpen();
}
switchTab(tab: Tab) {
if (this.activeTab !== tab) {
this.settings.chatlogTab = tab;
this.settingsService.saveBrowserSettings();
this.regenerateList();
this.scrollToEnd();
this.updateTabs();
}
}
private updateTabs() {
this.setActiveTab(this.localTab.nativeElement, this.activeTab === 'local');
this.setActiveTab(this.partyTab.nativeElement, this.activeTab === 'party');
this.setActiveTab(this.whisperTab.nativeElement, this.activeTab === 'whisper');
}
private setActiveTab(tab: HTMLElement, active: boolean) {
if (active) {
tab.classList.add('active');
tab.classList.remove('unread');
tab.style.backgroundColor = this.bg;
} else {
tab.classList.remove('active');
if (!tab.classList.contains('unread'))
tab.style.backgroundColor = this.inactiveBg;
}
}
scrollToEnd() {
// requestAnimationFrame(this.scrollHandler);
this.scrollToEndAtFrame = true;
}
scrollHandler = () => {
this.scrollingToEnd = true;
this.scroll.nativeElement.scrollTop = 99999;
}
clickNameHandler = (message: ChatLogMessage) => {
this.zone.run(() => this.nameClick.emit(message));
}
clickLabel = (message: ChatLogMessage) => {
this.zone.run(() => {
if (message.label) {
this.toggleType.emit(message.label);
}
});
}
private clearList() {
removeAllNodes(this.linesElement);
}
private regenerateList() {
this.clearList();
const lines = this.linesElement;
this.messages.forEach(entry => {
if (!entry.dom) {
entry.dom = createChatLogLineDOM(this.clickLabel, this.clickNameHandler);
setNameColors(entry.dom, this.getCharaterColors(entry.entityId));
updateChatLogLine(entry.dom, entry);
}
lines.appendChild(entry.dom.root);
});
}
drag({ x, y, type, event }: AgDragEvent, resizeY: boolean, resizeX: boolean) {
event.preventDefault();
if (type === 'start') {
const { left, top } = (this.element.nativeElement as HTMLElement).getBoundingClientRect();
this.startX = left;
this.startY = top;
this.shouldScrollToEnd = this.scrolledToEnd;
}
if (resizeX) {
this.settings.chatlogWidth = clamp(x - this.startX, 200 + 74.7, 2000);
}
if (resizeY) {
this.settings.chatlogHeight = clamp(this.startY - y, 120, 2000);
}
this.updateChatlog();
this.updateInnerWidth();
if (type === 'end') {
this.settingsService.saveBrowserSettings();
}
if (this.shouldScrollToEnd) {
this.scrollToEnd();
}
}
private setUnread(value: number) {
if (this.unread !== value) {
this.unread = value;
const count = this.countElement.nativeElement as HTMLElement;
const toggle = this.toggleButton.nativeElement as HTMLElement;
if (value) {
count.textContent = value > 99 ? '99+' : `${value}`;
toggle.classList.add('has-unread');
} else {
count.textContent = '';
toggle.classList.remove('has-unread');
}
}
}
private findEntityFromMessages = (id: number): FakeEntity | undefined => {
return findEntityFromMessages(id, this.whisper) ||
findEntityFromMessages(id, this.party) ||
findEntityFromMessages(id, this.local);
}
private findEntityFromMessagesByName = (name: string): FakeEntity | undefined => {
return findEntityFromMessagesByName(name, this.game.playerId, this.whisper) ||
findEntityFromMessagesByName(name, this.game.playerId, this.party) ||
findEntityFromMessagesByName(name, this.game.playerId, this.local);
}
}
function findEntityFromMessages(id: number, messages: ChatLogMessage[]): FakeEntity | undefined {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].entityId === id) {
return { fake: true, id, type: PONY_TYPE, name: messages[i].name, crc: messages[i].crc };
}
}
return undefined;
}
function findEntityFromMessagesByName(
name: string, playerId: number | undefined, messages: ChatLogMessage[]
): FakeEntity | undefined {
const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i');
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.name && message.entityId && message.entityId !== playerId && regex.test(message.name)) {
return { fake: true, id: message.entityId, type: PONY_TYPE, name: message.name, crc: message.crc };
}
}
return undefined;
}