-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathCooldownTimer.ts
More file actions
196 lines (167 loc) · 5.66 KB
/
CooldownTimer.ts
File metadata and controls
196 lines (167 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { StateStore } from './store';
import type { ChannelResponse, LocalMessage } from './types';
import { WithSubscriptions } from './utils/WithSubscriptions';
import type { Channel } from './channel';
export type CooldownTimerState = {
/**
* Slow mode cooldown interval in seconds. Change reported via channel.updated WS event.
*/
cooldownConfigSeconds: number;
/**
* Whether the current user can skip slow mode. Change is not reported via WS.
*/
canSkipCooldown: boolean;
/**
* Latest message creation date authored by the current user in this channel. Change reported via message.new WS event.
*/
ownLatestMessageDate?: Date;
/**
* Remaining cooldown in whole seconds (rounded).
*/
cooldownRemaining: number;
};
const toDateOrUndefined = (value: unknown): Date | undefined => {
if (value instanceof Date) return value;
if (typeof value === 'string' || typeof value === 'number') {
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) return parsed;
}
return undefined;
};
export class CooldownTimer extends WithSubscriptions {
public readonly state: StateStore<CooldownTimerState>;
private timeout: ReturnType<typeof setTimeout> | null = null;
private channel: Channel;
constructor({ channel }: { channel: Channel }) {
super();
this.channel = channel;
this.state = new StateStore<CooldownTimerState>({
cooldownConfigSeconds: 0,
cooldownRemaining: 0,
ownLatestMessageDate: undefined,
canSkipCooldown: false,
});
this.refresh();
}
get cooldownConfigSeconds() {
return this.state.getLatestValue().cooldownConfigSeconds;
}
get cooldownRemaining() {
return this.state.getLatestValue().cooldownRemaining;
}
get canSkipCooldown() {
return this.state.getLatestValue().canSkipCooldown;
}
get ownLatestMessageDate() {
return this.state.getLatestValue().ownLatestMessageDate;
}
public registerSubscriptions = () => {
this.incrementRefCount();
if (this.hasSubscriptions) return;
this.addUnsubscribeFunction(
this.channel.on('message.new', (event) => {
const isOwnMessage =
event.message?.user?.id && event.message.user.id === this.getOwnUserId();
if (!isOwnMessage) return;
this.setOwnLatestMessageDate(toDateOrUndefined(event.message?.created_at));
}).unsubscribe,
);
this.addUnsubscribeFunction(
this.channel.on('channel.updated', (event) => {
const cooldownChanged = event.channel?.cooldown !== this.cooldownConfigSeconds;
if (!cooldownChanged) return;
this.refresh();
}).unsubscribe,
);
};
public setCooldownRemaining = (cooldownRemaining: number) => {
this.state.partialNext({ cooldownRemaining });
};
public clearTimeout = () => {
if (!this.timeout) return;
clearTimeout(this.timeout);
this.timeout = null;
};
public refresh = () => {
const { cooldown: cooldownConfigSeconds = 0, own_capabilities } = (this.channel
.data ?? {}) as Partial<ChannelResponse>;
const canSkipCooldown = (own_capabilities ?? []).includes('skip-slow-mode');
const ownLatestMessageDate = this.findOwnLatestMessageDate({
messages: this.channel.state.latestMessages,
});
if (
cooldownConfigSeconds !== this.cooldownConfigSeconds ||
ownLatestMessageDate?.getTime() !== this.ownLatestMessageDate?.getTime() ||
canSkipCooldown !== this.canSkipCooldown
) {
this.state.partialNext({
cooldownConfigSeconds,
ownLatestMessageDate,
canSkipCooldown,
});
}
if (this.canSkipCooldown || this.cooldownConfigSeconds === 0) {
this.clearTimeout();
if (this.cooldownRemaining !== 0) {
this.setCooldownRemaining(0);
}
return;
}
this.recalculate();
};
/**
* Updates the known latest own message date and recomputes remaining time.
* Prefer calling this when you already know the message date (e.g. from an event).
*/
public setOwnLatestMessageDate = (date: Date | undefined) => {
this.state.partialNext({ ownLatestMessageDate: date });
this.recalculate();
};
private getOwnUserId() {
const client = this.channel.getClient();
return client.userID ?? client.user?.id;
}
private findOwnLatestMessageDate({
messages,
}: {
messages: LocalMessage[];
}): Date | undefined {
const ownUserId = this.getOwnUserId();
if (!ownUserId) return undefined;
let latest: Date | undefined;
for (let i = messages.length - 1; i >= 0; i -= 1) {
const message = messages[i];
if (message.user?.id !== ownUserId) continue;
const createdAt = toDateOrUndefined(message.created_at);
if (!createdAt) continue;
if (!latest || createdAt.getTime() > latest.getTime()) {
latest = createdAt;
}
if (latest.getTime() > createdAt.getTime()) break;
}
return latest;
}
private recalculate = () => {
this.clearTimeout();
const { cooldownConfigSeconds, ownLatestMessageDate, canSkipCooldown } =
this.state.getLatestValue();
const timeSinceOwnLastMessage =
ownLatestMessageDate != null
? // prevent negative values
Math.max(0, (Date.now() - ownLatestMessageDate.getTime()) / 1000)
: undefined;
const remaining =
!canSkipCooldown &&
typeof timeSinceOwnLastMessage !== 'undefined' &&
cooldownConfigSeconds > timeSinceOwnLastMessage
? Math.round(cooldownConfigSeconds - timeSinceOwnLastMessage)
: 0;
if (remaining !== this.cooldownRemaining) {
this.setCooldownRemaining(remaining);
}
if (remaining <= 0) return;
this.timeout = setTimeout(() => {
this.recalculate();
}, 1000);
};
}