@@ -10,13 +10,14 @@ import {
1010 inWindow ,
1111 displayWindow ,
1212} from "./timer" ;
13- import { formatTimeDistance } from "./duration" ;
13+ import { formatTimeDistance , formatMinutesSecondsAgo } from "./duration" ;
1414import { getSettingByKey , saveSettingByKey } from "./settings" ;
1515import { TIMER_CHANNEL_ID , SHOW_FUTURE_WINDOW } from "../../../../config" ;
1616import { timerPrismaClient } from "../../../../db/timer-client" ;
1717
1818const MAX_DESCRIPTION_LENGTH = 4096 ;
1919const MAX_EMBEDS_PER_MESSAGE = 10 ;
20+ const ENDED_RECENTLY_WINDOW_MS = 60 * 60 * 1000 ;
2021
2122interface TableRow {
2223 name : string ;
@@ -25,6 +26,12 @@ interface TableRow {
2526 remainingMs : number ;
2627}
2728
29+ interface EndedRow {
30+ name : string ;
31+ time : string ;
32+ endedMsAgo : number ;
33+ }
34+
2835function pad ( str : string , len : number ) : string {
2936 if ( str . length > len ) return str . slice ( 0 , len ) ;
3037 return str . padEnd ( len , " " ) ;
@@ -65,33 +72,53 @@ function renderTable(
6572 return "```\n" + lines . join ( "\n" ) + "\n```" ;
6673}
6774
68- function chunkTable (
69- rows : TableRow [ ] ,
70- maxLen : number ,
71- widths : { nameWidth : number ; timeWidth : number ; windowWidth : number } ,
72- thirdColumnLabel ?: string
73- ) : string [ ] {
75+ function chunkRows < T > ( rows : T [ ] , maxLen : number , render : ( rows : T [ ] ) => string ) : string [ ] {
7476 const chunks : string [ ] = [ ] ;
75- let currentRows : TableRow [ ] = [ ] ;
77+ let currentRows : T [ ] = [ ] ;
7678
7779 for ( const row of rows ) {
7880 const testRows = [ ...currentRows , row ] ;
79- const rendered = renderTable ( testRows , widths , thirdColumnLabel ) ;
81+ const rendered = render ( testRows ) ;
8082 if ( rendered . length > maxLen && currentRows . length > 0 ) {
81- chunks . push ( renderTable ( currentRows , widths , thirdColumnLabel ) ) ;
83+ chunks . push ( render ( currentRows ) ) ;
8284 currentRows = [ row ] ;
8385 } else {
8486 currentRows = testRows ;
8587 }
8688 }
8789
8890 if ( currentRows . length > 0 ) {
89- chunks . push ( renderTable ( currentRows , widths , thirdColumnLabel ) ) ;
91+ chunks . push ( render ( currentRows ) ) ;
9092 }
9193
9294 return chunks ;
9395}
9496
97+ function getEndedColumnWidths ( rows : EndedRow [ ] ) : { nameWidth : number ; timeWidth : number } {
98+ return {
99+ nameWidth : Math . max ( 5 , ...rows . map ( ( r ) => r . name . length ) ) ,
100+ timeWidth : Math . max ( 5 , ...rows . map ( ( r ) => r . time . length ) ) ,
101+ } ;
102+ }
103+
104+ function renderEndedTable (
105+ rows : EndedRow [ ] ,
106+ widths : { nameWidth : number ; timeWidth : number }
107+ ) : string {
108+ const { nameWidth, timeWidth } = widths ;
109+ const sep = " | " ;
110+ const header = `${ pad ( "Timer" , nameWidth ) } ${ sep } ${ pad ( "Ended" , timeWidth ) } ` ;
111+ const divider = "-" . repeat ( header . length ) ;
112+
113+ const lines = [
114+ header ,
115+ divider ,
116+ ...rows . map ( ( r ) => `${ pad ( r . name , nameWidth ) } ${ sep } ${ pad ( r . time , timeWidth ) } ` ) ,
117+ ] ;
118+
119+ return "```\n" + lines . join ( "\n" ) + "\n```" ;
120+ }
121+
95122/**
96123 * Update the timer channel with current timer status using standard message embeds.
97124 */
@@ -116,6 +143,7 @@ export async function updateTimersChannel(client: Client): Promise<void> {
116143 const futureRows : TableRow [ ] = [ ] ;
117144 const upcomingRows : TableRow [ ] = [ ] ;
118145 const inWindowRows : TableRow [ ] = [ ] ;
146+ const endedRows : EndedRow [ ] = [ ] ;
119147
120148 for ( const timer of sortedTimers ) {
121149 if ( ! timer . lastTod ) continue ;
@@ -142,6 +170,16 @@ export async function updateTimersChannel(client: Client): Promise<void> {
142170 remainingMs,
143171 } ) ;
144172 }
173+ } else if (
174+ endsAt . getTime ( ) < now . getTime ( ) &&
175+ now . getTime ( ) - endsAt . getTime ( ) <= ENDED_RECENTLY_WINDOW_MS
176+ ) {
177+ const endedMsAgo = now . getTime ( ) - endsAt . getTime ( ) ;
178+ endedRows . push ( {
179+ name : getDisplayName ( timer . name , timer . skipCount ) ,
180+ time : formatMinutesSecondsAgo ( endsAt , now ) ,
181+ endedMsAgo,
182+ } ) ;
145183 } else if ( startsAt . getTime ( ) <= now . getTime ( ) + 24 * 60 * 60 * 1000 ) {
146184 const remainingMs = startsAt . getTime ( ) - now . getTime ( ) ;
147185 upcomingRows . push ( {
@@ -165,6 +203,8 @@ export async function updateTimersChannel(client: Client): Promise<void> {
165203 futureRows . sort ( ( a , b ) => b . remainingMs - a . remainingMs ) ;
166204 upcomingRows . sort ( ( a , b ) => b . remainingMs - a . remainingMs ) ;
167205 inWindowRows . sort ( ( a , b ) => b . remainingMs - a . remainingMs ) ;
206+ // Most recently ended at the bottom, oldest at the top
207+ endedRows . sort ( ( a , b ) => b . endedMsAgo - a . endedMsAgo ) ;
168208
169209 const embeds : EmbedBuilder [ ] = [ ] ;
170210
@@ -177,7 +217,7 @@ export async function updateTimersChannel(client: Client): Promise<void> {
177217 // Future window embed(s)
178218 if ( SHOW_FUTURE_WINDOW ?. toLowerCase ( ) === "true" && futureRows . length > 0 ) {
179219 const widths = getColumnWidths ( futureRows ) ;
180- const descChunks = chunkTable ( futureRows , MAX_DESCRIPTION_LENGTH , widths ) ;
220+ const descChunks = chunkRows ( futureRows , MAX_DESCRIPTION_LENGTH , ( rs ) => renderTable ( rs , widths ) ) ;
181221 for ( let i = 0 ; i < descChunks . length ; i ++ ) {
182222 const embed = new EmbedBuilder ( ) . setDescription ( descChunks [ i ] ) ;
183223 if ( i === 0 ) embed . setTitle ( "Future Windows" ) ;
@@ -188,7 +228,7 @@ export async function updateTimersChannel(client: Client): Promise<void> {
188228 // Upcoming embed(s)
189229 if ( upcomingRows . length > 0 ) {
190230 const widths = getColumnWidths ( upcomingRows ) ;
191- const descChunks = chunkTable ( upcomingRows , MAX_DESCRIPTION_LENGTH , widths ) ;
231+ const descChunks = chunkRows ( upcomingRows , MAX_DESCRIPTION_LENGTH , ( rs ) => renderTable ( rs , widths ) ) ;
192232 for ( let i = 0 ; i < descChunks . length ; i ++ ) {
193233 const embed = new EmbedBuilder ( ) . setDescription ( descChunks [ i ] ) ;
194234 if ( i === 0 ) embed . setTitle ( "Mobs Entering Window In The Next 24 Hours" ) ;
@@ -199,7 +239,7 @@ export async function updateTimersChannel(client: Client): Promise<void> {
199239 // In-window embed(s)
200240 if ( anyInWindow ) {
201241 const widths = getColumnWidths ( inWindowRows ) ;
202- const descChunks = chunkTable ( inWindowRows , MAX_DESCRIPTION_LENGTH , widths , "%" ) ;
242+ const descChunks = chunkRows ( inWindowRows , MAX_DESCRIPTION_LENGTH , ( rs ) => renderTable ( rs , widths , "%" ) ) ;
203243 for ( let i = 0 ; i < descChunks . length ; i ++ ) {
204244 const embed = new EmbedBuilder ( ) . setColor ( 0xe67e22 ) . setDescription ( descChunks [ i ] ) ;
205245 if ( i === 0 ) embed . setTitle ( "Mobs In Window" ) ;
@@ -215,6 +255,17 @@ export async function updateTimersChannel(client: Client): Promise<void> {
215255 ) ;
216256 }
217257
258+ // Ended recently embed(s)
259+ if ( endedRows . length > 0 ) {
260+ const widths = getEndedColumnWidths ( endedRows ) ;
261+ const descChunks = chunkRows ( endedRows , MAX_DESCRIPTION_LENGTH , ( rs ) => renderEndedTable ( rs , widths ) ) ;
262+ for ( let i = 0 ; i < descChunks . length ; i ++ ) {
263+ const embed = new EmbedBuilder ( ) . setColor ( 0x95a5a6 ) . setDescription ( descChunks [ i ] ) ;
264+ if ( i === 0 ) embed . setTitle ( "Ended Recently" ) ;
265+ embeds . push ( embed ) ;
266+ }
267+ }
268+
218269 // Discord allows at most 10 embeds per message
219270 embeds . splice ( MAX_EMBEDS_PER_MESSAGE ) ;
220271
0 commit comments