mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-25 06:05:52 +02:00
Archive commit
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
.chat-log(#chatLog)
|
||||
.d-flex.chat-log-buttons
|
||||
.flex-grow-1.d-flex.flex-column-reverse
|
||||
button.game-button.mb-2((click)="scrollToEnd()" title="Scroll to end")
|
||||
fa-icon([icon]="toBottomIcon" [fixedWidth]="true")
|
||||
|
||||
.chat-log-content(#content)
|
||||
fa-icon.resize-icon([icon]="resizeIcon")
|
||||
.chat-log-tabs
|
||||
a.link-plain.chat-log-tab(#localTab tabindex (click)="switchTab('local')")
|
||||
| Local
|
||||
a.link-plain.chat-log-tab(#partyTab tabindex (click)="switchTab('party')")
|
||||
| Party
|
||||
a.link-plain.chat-log-tab(#whisperTab tabindex (click)="switchTab('whisper')")
|
||||
| Whisper
|
||||
.chat-log-scroll-outer()
|
||||
.chat-log-scroll-inner(#scroll)
|
||||
.chat-log-scroll-inner-inner(#lines)
|
||||
.chat-log-resize-top((agDrag)="drag($event, true, false)")
|
||||
.chat-log-resize-right((agDrag)="drag($event, false, true)")
|
||||
.chat-log-resize-top-right((agDrag)="drag($event, true, true)")
|
||||
|
||||
button.game-button.chat-log-toggle(#toggleButton (click)="toggle()" title="Toggle chatlog")
|
||||
svg.fa-icon(aria-hidden="true" class="svg-inline--fa fa-comments fa-w-18 fa-fw" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512")
|
||||
path.main-bubble(fill="currentColor" d="M416 192c0-88.4-93.1-160-208-160S0 103.6 0 192c0 34.3 14.1 65.9 38 92-13.4 30.2-35.5 54.2-35.8 54.5-2.2 2.3-2.8 5.7-1.5 8.7S4.8 352 8 352c36.6 0 66.9-12.3 88.7-25 32.2 15.7 70.3 25 111.3 25 114.9 0 208-71.6 208-160z")
|
||||
path(fill="currentColor" d="M538 412c23.9-26 38-57.7 38-92 0-66.9-53.5-124.2-129.3-148.1.9 6.6 1.3 13.3 1.3 20.1 0 105.9-107.7 192-240 192-10.8 0-21.3-.8-31.7-1.9C207.8 439.6 281.8 480 368 480c41 0 79.1-9.2 111.3-25 21.8 12.7 52.1 25 88.7 25 3.2 0 6.1-1.9 7.3-4.8 1.3-2.9.7-6.3-1.5-8.7-.3-.3-22.4-24.2-35.8-54.5z")
|
||||
.whispers-count(#count)
|
||||
@@ -0,0 +1,159 @@
|
||||
@import '../../../../styles/partials/variables';
|
||||
|
||||
$chat-log-bg: rgba(0, 0, 0, 0.2);
|
||||
$chat-log-bg-faded: rgba(0, 0, 0, 0.1);
|
||||
$chat-log-bg-hover: rgba(0, 0, 0, 0.5);
|
||||
$chat-log-tabs-height: 30px;
|
||||
$chat-log-buttons-width: 40px;
|
||||
|
||||
$resizer-size: 12px;
|
||||
$resizer-offset: $resizer-size / 2;
|
||||
|
||||
.chat-log {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 220px);
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.chat-log-buttons {
|
||||
flex-direction: column-reverse;
|
||||
width: $chat-log-buttons-width;
|
||||
padding: 5px 3px;
|
||||
padding-bottom: 30px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.chat-log-toggle {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 4px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.has-unread .main-bubble {
|
||||
color: $chat-whisper;
|
||||
}
|
||||
|
||||
.whispers-count {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 9px;
|
||||
width: 16px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.whisper-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-size: 14px;
|
||||
color: $chat-whisper !important;
|
||||
}
|
||||
|
||||
.chat-log-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: $chat-log-bg;
|
||||
padding: 5px 0;
|
||||
padding-left: 40px;
|
||||
margin-left: -$chat-log-buttons-width;
|
||||
border-radius: $border-radius-base;
|
||||
word-break: break-word;
|
||||
white-space: pre-line;
|
||||
line-height: 20px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-log-tabs {
|
||||
position: absolute;
|
||||
top: -$chat-log-tabs-height;
|
||||
left: 30px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chat-log-tab {
|
||||
background: $chat-log-bg-faded;
|
||||
padding: 5px 10px 0;
|
||||
margin-left: 10px;
|
||||
height: $chat-log-tabs-height;
|
||||
border-top-left-radius: $border-radius-base;
|
||||
border-top-right-radius: $border-radius-base;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
background: $chat-log-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-log-scroll-outer {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-log-scroll-inner {
|
||||
width: calc(100% + 50px);
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.chat-log-scroll-inner-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
min-height: 100%;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.chat-log-resize-top {
|
||||
position: absolute;
|
||||
top: -$resizer-offset;
|
||||
right: $resizer-offset;
|
||||
height: $resizer-size;
|
||||
left: 250px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.chat-log-resize-top-right {
|
||||
position: absolute;
|
||||
top: -($resizer-offset + 1);
|
||||
right: -($resizer-offset + 1);
|
||||
width: $resizer-size + 4;
|
||||
height: $resizer-size + 4;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.chat-log-resize-right {
|
||||
position: absolute;
|
||||
top: $resizer-offset;
|
||||
right: -$resizer-offset;
|
||||
width: $resizer-size;
|
||||
bottom: $resizer-offset;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.resize-icon {
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: 1px;
|
||||
transform: rotate(45deg);
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
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 } from '../../../common/constants';
|
||||
import { faCaretUp, faArrowDown } from '../../../client/icons';
|
||||
import { sampleMessages } from '../../../common/debugData';
|
||||
|
||||
interface IndexEntryUser {
|
||||
id: number;
|
||||
crc: number | undefined;
|
||||
}
|
||||
|
||||
interface IndexEntry {
|
||||
users: IndexEntryUser[];
|
||||
counter: number;
|
||||
}
|
||||
|
||||
interface ChatLogLineDOM {
|
||||
entry: ChatLogMessage;
|
||||
root: HTMLElement;
|
||||
label: 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.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);
|
||||
|
||||
line.prefixText.nodeValue = prefix || '';
|
||||
line.suffixText.nodeValue = suffix ? ` ${suffix}: ` : ': ';
|
||||
replaceNodes(line.message, message);
|
||||
}
|
||||
|
||||
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('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;
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
|
||||
this.addEntryToList(this.local, GENERAL_CHAT_LIMIT, open && tab === 'local', entry);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (whisper) {
|
||||
const whisperEntry = { ...entry };
|
||||
whisperEntry.dom = undefined;
|
||||
whisperEntry.label = undefined;
|
||||
this.addEntryToList(this.whisper, WHISPER_CHAT_LIMIT, open && tab === 'whisper', whisperEntry);
|
||||
}
|
||||
|
||||
if (message.type === MessageType.Whisper && !this.open) {
|
||||
this.setUnread(this.unread + 1);
|
||||
}
|
||||
|
||||
if (scrolledToEnd) {
|
||||
this.scrollToEnd();
|
||||
}
|
||||
|
||||
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.style.backgroundColor = this.bg;
|
||||
} else {
|
||||
tab.classList.remove('active');
|
||||
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);
|
||||
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, 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;
|
||||
}
|
||||
Reference in New Issue
Block a user