Skip to content

Commit

Permalink
Tgui say v1.1 (tgstation#75431)
Browse files Browse the repository at this point in the history
## About The Pull Request
"It's better! I promise!"

When I wrote it, I was inexperienced and pretty angry. Not that I'm any
better of a person now, but the code should be. I consolidated instead
of relying on heavy abstractions. I simplified logic and wrote more
tests.

The result should look and feel much more like intended. The bundle size
is reduced by ~43%. Types are much stricter. The logic and css classes
are much more precise.

No major style changes yet
![Screenshot 2023-05-15
003339](https://github.com/tgstation/tgstation/assets/42397676/edeabdcf-5cc6-44ba-9e98-9015bb863547)

## Why It's Good For The Game
Less javascript is better, and being even a few fractions of a second
faster might make better gameplay
## Changelog
:cl:
refactor: Tgui Say is rewritten, becoming "much more performant". Hey,
that's what it says on the tin! I'm not from marketing!
fix: Tguisay drag zones are now ever so slightly larger around the
corner of the window
fix: Pressing one of the chat open keys (T/Y/M/O) will no longer change
channels if it's already open
/:cl:
  • Loading branch information
jlsnow301 committed May 17, 2023
1 parent ec7b9fe commit aa74657
Show file tree
Hide file tree
Showing 45 changed files with 885 additions and 1,003 deletions.
4 changes: 2 additions & 2 deletions code/modules/tgui_input/say_modal/modal.dm
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@
close()
return TRUE
if (type == "thinking")
if(payload["mode"] == TRUE)
if(payload["visible"] == TRUE)
start_thinking()
return TRUE
if(payload["mode"] == FALSE)
if(payload["visible"] == FALSE)
stop_thinking()
return TRUE
return FALSE
Expand Down
39 changes: 39 additions & 0 deletions tgui/packages/common/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* ### Key codes.
* event.keyCode is deprecated, use this reference instead.
*
* Handles modifier keys (Shift, Alt, Control) and arrow keys.
*
* For alphabetical keys, use the actual character (e.g. 'a') instead of the key code.
*
* Something isn't here that you want? Just add it:
* @url https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
* @usage
* ```ts
* import { KEY } from 'tgui/common/keys';
*
* if (event.key === KEY.Enter) {
* // do something
* }
* ```
*/
export enum KEY {
Alt = 'Alt',
Backspace = 'Backspace',
Control = 'Control',
Delete = 'Delete',
Down = 'Down',
End = 'End',
Enter = 'Enter',
Escape = 'Esc',
Home = 'Home',
Insert = 'Insert',
Left = 'Left',
PageDown = 'PageDown',
PageUp = 'PageUp',
Right = 'Right',
Shift = 'Shift',
Space = ' ',
Tab = 'Tab',
Up = 'Up',
}
32 changes: 21 additions & 11 deletions tgui/packages/common/timer.js → tgui/packages/common/timer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@
* called for N milliseconds. If `immediate` is passed, trigger the
* function on the leading edge, instead of the trailing.
*/
export const debounce = (fn, time, immediate = false) => {
let timeout;
return (...args) => {
export const debounce = <F extends (...args: any[]) => any>(
fn: F,
time: number,
immediate = false
): ((...args: Parameters<F>) => void) => {
let timeout: ReturnType<typeof setTimeout> | null;
return (...args: Parameters<F>) => {
const later = () => {
timeout = null;
if (!immediate) {
fn(...args);
}
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
clearTimeout(timeout!);
timeout = setTimeout(later, time);
if (callNow) {
fn(...args);
Expand All @@ -32,18 +36,24 @@ export const debounce = (fn, time, immediate = false) => {
* Returns a function, that, when invoked, will only be triggered at most once
* during a given window of time.
*/
export const throttle = (fn, time) => {
let previouslyRun, queuedToRun;
return function invokeFn(...args) {
export const throttle = <F extends (...args: any[]) => any>(
fn: F,
time: number
): ((...args: Parameters<F>) => void) => {
let previouslyRun: number | null,
queuedToRun: ReturnType<typeof setTimeout> | null;
return function invokeFn(...args: Parameters<F>) {
const now = Date.now();
queuedToRun = clearTimeout(queuedToRun);
if (queuedToRun) {
clearTimeout(queuedToRun);
}
if (!previouslyRun || now - previouslyRun >= time) {
fn.apply(null, args);
previouslyRun = now;
} else {
queuedToRun = setTimeout(
invokeFn.bind(null, ...args),
time - (now - previouslyRun)
() => invokeFn(...args),
time - (now - (previouslyRun ?? 0))
);
}
};
Expand All @@ -54,5 +64,5 @@ export const throttle = (fn, time) => {
*
* @param {number} time
*/
export const sleep = (time) =>
export const sleep = (time: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, time));
47 changes: 47 additions & 0 deletions tgui/packages/tgui-say/ChannelIterator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ChannelIterator } from './ChannelIterator';

describe('ChannelIterator', () => {
let channelIterator: ChannelIterator;

beforeEach(() => {
channelIterator = new ChannelIterator();
});

it('should cycle through channels properly', () => {
expect(channelIterator.current()).toBe('Say');
expect(channelIterator.next()).toBe('Radio');
expect(channelIterator.next()).toBe('Me');
expect(channelIterator.next()).toBe('OOC');
expect(channelIterator.next()).toBe('Say'); // Admin is blacklisted so it should be skipped
});

it('should set a channel properly', () => {
channelIterator.set('OOC');
expect(channelIterator.current()).toBe('OOC');
});

it('should return true when current channel is "Say"', () => {
channelIterator.set('Say');
expect(channelIterator.isSay()).toBe(true);
});

it('should return false when current channel is not "Say"', () => {
channelIterator.set('Radio');
expect(channelIterator.isSay()).toBe(false);
});

it('should return true when current channel is visible', () => {
channelIterator.set('Say');
expect(channelIterator.isVisible()).toBe(true);
});

it('should return false when current channel is not visible', () => {
channelIterator.set('OOC');
expect(channelIterator.isVisible()).toBe(false);
});

it('should not leak a message from a blacklisted channel', () => {
channelIterator.set('Admin');
expect(channelIterator.next()).toBe('Admin');
});
});
50 changes: 50 additions & 0 deletions tgui/packages/tgui-say/ChannelIterator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export type Channel = 'Say' | 'Radio' | 'Me' | 'OOC' | 'Admin';

/**
* ### ChannelIterator
* Cycles a predefined list of channels,
* skipping over blacklisted ones,
* and providing methods to manage and query the current channel.
*/
export class ChannelIterator {
private index: number = 0;
private readonly channels: Channel[] = ['Say', 'Radio', 'Me', 'OOC', 'Admin'];
private readonly blacklist: Channel[] = ['Admin'];
private readonly quiet: Channel[] = ['OOC', 'Admin'];

public next(): Channel {
if (this.blacklist.includes(this.channels[this.index])) {
return this.channels[this.index];
}

for (let index = 1; index <= this.channels.length; index++) {
let nextIndex = (this.index + index) % this.channels.length;
if (!this.blacklist.includes(this.channels[nextIndex])) {
this.index = nextIndex;
break;
}
}

return this.channels[this.index];
}

public set(channel: Channel): void {
this.index = this.channels.indexOf(channel) || 0;
}

public current(): Channel {
return this.channels[this.index];
}

public isSay(): boolean {
return this.channels[this.index] === 'Say';
}

public isVisible(): boolean {
return !this.quiet.includes(this.channels[this.index]);
}

public reset(): void {
this.index = 0;
}
}
50 changes: 50 additions & 0 deletions tgui/packages/tgui-say/ChatHistory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ChatHistory } from './ChatHistory';

describe('ChatHistory', () => {
let chatHistory: ChatHistory;

beforeEach(() => {
chatHistory = new ChatHistory();
});

it('should add a message to the history', () => {
chatHistory.add('Hello');
expect(chatHistory.getOlderMessage()).toEqual('Hello');
});

it('should retrieve older and newer messages', () => {
chatHistory.add('Hello');
chatHistory.add('World');
expect(chatHistory.getOlderMessage()).toEqual('World');
expect(chatHistory.getOlderMessage()).toEqual('Hello');
expect(chatHistory.getNewerMessage()).toEqual('World');
expect(chatHistory.getNewerMessage()).toBeNull();
expect(chatHistory.getOlderMessage()).toEqual('World');
});

it('should limit the history to 5 messages', () => {
for (let i = 1; i <= 6; i++) {
chatHistory.add(`Message ${i}`);
}

expect(chatHistory.getOlderMessage()).toEqual('Message 6');
for (let i = 5; i >= 2; i--) {
expect(chatHistory.getOlderMessage()).toEqual(`Message ${i}`);
}
expect(chatHistory.getOlderMessage()).toBeNull();
});

it('should handle temp message correctly', () => {
chatHistory.saveTemp('Temp message');
expect(chatHistory.getTemp()).toEqual('Temp message');
expect(chatHistory.getTemp()).toBeNull();
});

it('should reset correctly', () => {
chatHistory.add('Hello');
chatHistory.getOlderMessage();
chatHistory.reset();
expect(chatHistory.isAtLatest()).toBe(true);
expect(chatHistory.getOlderMessage()).toEqual('Hello');
});
});
59 changes: 59 additions & 0 deletions tgui/packages/tgui-say/ChatHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* ### ChatHistory
* A class to manage a chat history,
* maintaining a maximum of five messages and supporting navigation,
* temporary message storage, and query operations.
*/
export class ChatHistory {
private messages: string[] = [];
private index: number = -1; // Initialize index at -1
private temp: string | null = null;

public add(message: string): void {
this.messages.unshift(message);
this.index = -1; // Reset index
if (this.messages.length > 5) {
this.messages.pop();
}
}

public getIndex(): number {
return this.index + 1;
}

public getOlderMessage(): string | null {
if (this.messages.length === 0 || this.index >= this.messages.length - 1) {
return null;
}
this.index++;
return this.messages[this.index];
}

public getNewerMessage(): string | null {
if (this.index <= 0) {
this.index = -1;
return null;
}
this.index--;
return this.messages[this.index];
}

public isAtLatest(): boolean {
return this.index === -1;
}

public saveTemp(message: string): void {
this.temp = message;
}

public getTemp(): string | null {
const temp = this.temp;
this.temp = null;
return temp;
}

public reset(): void {
this.index = -1;
this.temp = null;
}
}

0 comments on commit aa74657

Please sign in to comment.