@@ -228,13 +228,20 @@ func (tb *Bot) handleNotifications(ctx context.Context, b *bot.Bot, update *mode
228228 }
229229
230230 var sb strings.Builder
231+ var buttons [][]models.InlineKeyboardButton
231232 fmt .Fprintf (& sb , "*Unread Notifications* \\ (%d\\ )\n \n " , len (notifs ))
232233 for _ , n := range notifs {
233234 emoji := priorityEmoji (n .Priority )
234235 fmt .Fprintf (& sb , "%s *P%d* `%s`\n %s\n _%s_\n \n " ,
235236 emoji , n .Priority , mdv2 .Esc (n .RepoFullName ), mdv2 .Esc (n .SubjectTitle ), mdv2 .Esc (n .Reason ))
237+ if htmlURL := ghAPIToHTML (n .SubjectURL , n .SubjectType ); htmlURL != "" {
238+ label := fmt .Sprintf ("%s %s" , emoji , truncate (n .SubjectTitle , 30 ))
239+ buttons = append (buttons , []models.InlineKeyboardButton {
240+ {Text : label , URL : htmlURL },
241+ })
242+ }
236243 }
237- tb .reply (ctx , b , update , sb .String ())
244+ tb .replyWithKeyboard (ctx , b , update , sb .String (), buttons )
238245}
239246
240247func (tb * Bot ) handleMemory (ctx context.Context , b * bot.Bot , update * models.Update ) {
@@ -312,9 +319,40 @@ func (tb *Bot) handleRemind(ctx context.Context, b *bot.Bot, update *models.Upda
312319}
313320
314321func (tb * Bot ) handleBriefing (ctx context.Context , b * bot.Bot , update * models.Update ) {
322+ if update .Message == nil {
323+ return
324+ }
325+ chatID := update .Message .Chat .ID
315326 tb .sendTyping (ctx , update )
327+
328+ // Send initial message, then edit in-place as sections arrive.
329+ sent , err := b .SendMessage (ctx , & bot.SendMessageParams {
330+ ChatID : chatID ,
331+ Text : "⏳ Loading briefing\\ .\\ .\\ ." ,
332+ ParseMode : models .ParseModeMarkdown ,
333+ LinkPreviewOptions : & models.LinkPreviewOptions {
334+ IsDisabled : bot .True (),
335+ },
336+ })
337+ if err != nil {
338+ tb .logger .Error ("telegram send" , "error" , err )
339+ return
340+ }
341+
316342 msg := briefing .Generate (ctx , tb .briefingSources )
317- tb .reply (ctx , b , update , msg )
343+
344+ _ , err = b .EditMessageText (ctx , & bot.EditMessageTextParams {
345+ ChatID : chatID ,
346+ MessageID : sent .ID ,
347+ Text : msg ,
348+ ParseMode : models .ParseModeMarkdown ,
349+ LinkPreviewOptions : & models.LinkPreviewOptions {
350+ IsDisabled : bot .True (),
351+ },
352+ })
353+ if err != nil {
354+ tb .logger .Error ("telegram edit" , "error" , err )
355+ }
318356}
319357
320358// SetBriefingSources configures the data sources for on-demand briefings.
@@ -389,6 +427,7 @@ func (tb *Bot) handleEmails(ctx context.Context, b *bot.Bot, update *models.Upda
389427 }
390428
391429 var sb strings.Builder
430+ var buttons [][]models.InlineKeyboardButton
392431 fmt .Fprintf (& sb , "*Unread Emails* \\ (%d total\\ )\n \n " , count )
393432 for _ , e := range emails {
394433 fmt .Fprintf (& sb , "📧 *%s*\n From: %s\n " , mdv2 .Esc (e .Subject ), mdv2 .Esc (e .From ))
@@ -400,8 +439,16 @@ func (tb *Bot) handleEmails(ctx context.Context, b *bot.Bot, update *models.Upda
400439 fmt .Fprintf (& sb , " _%s_\n " , mdv2 .Esc (snippet ))
401440 }
402441 sb .WriteString ("\n " )
442+ gmailURL := fmt .Sprintf ("https://mail.google.com/mail/u/0/#inbox/%s" , e .ID )
443+ label := truncate (e .Subject , 35 )
444+ if label == "" {
445+ label = "Open email"
446+ }
447+ buttons = append (buttons , []models.InlineKeyboardButton {
448+ {Text : "📧 " + label , URL : gmailURL },
449+ })
403450 }
404- tb .reply (ctx , b , update , sb .String ())
451+ tb .replyWithKeyboard (ctx , b , update , sb .String (), buttons )
405452}
406453
407454func (tb * Bot ) handleHelp (ctx context.Context , b * bot.Bot , update * models.Update ) {
@@ -457,6 +504,63 @@ func (tb *Bot) reply(ctx context.Context, b *bot.Bot, update *models.Update, tex
457504 }
458505}
459506
507+ func (tb * Bot ) replyWithKeyboard (ctx context.Context , b * bot.Bot , update * models.Update , text string , buttons [][]models.InlineKeyboardButton ) {
508+ if update .Message == nil {
509+ return
510+ }
511+ chatID := update .Message .Chat .ID
512+ // Telegram allows max 100 inline buttons. Trim if needed.
513+ if len (buttons ) > 20 {
514+ buttons = buttons [:20 ]
515+ }
516+ var markup * models.InlineKeyboardMarkup
517+ if len (buttons ) > 0 {
518+ markup = & models.InlineKeyboardMarkup {InlineKeyboard : buttons }
519+ }
520+ chunks := mdv2 .Split (text , telegramMsgMax )
521+ for i , chunk := range chunks {
522+ params := & bot.SendMessageParams {
523+ ChatID : chatID ,
524+ Text : chunk ,
525+ ParseMode : models .ParseModeMarkdown ,
526+ LinkPreviewOptions : & models.LinkPreviewOptions {
527+ IsDisabled : bot .True (),
528+ },
529+ }
530+ // Attach keyboard to last chunk only.
531+ if i == len (chunks )- 1 && markup != nil {
532+ params .ReplyMarkup = markup
533+ }
534+ if _ , err := b .SendMessage (ctx , params ); err != nil {
535+ tb .logger .Error ("telegram send" , "error" , err , "chat_id" , chatID )
536+ return
537+ }
538+ }
539+ }
540+
541+ // ghAPIToHTML converts a GitHub API URL to the corresponding HTML URL.
542+ // e.g. https://api.github.com/repos/owner/repo/pulls/123 -> https://github.com/owner/repo/pull/123
543+ func ghAPIToHTML (apiURL , subjectType string ) string {
544+ if apiURL == "" {
545+ return ""
546+ }
547+ s := strings .Replace (apiURL , "https://api.github.com/repos/" , "https://github.com/" , 1 )
548+ // API uses "pulls" but HTML uses "pull".
549+ s = strings .Replace (s , "/pulls/" , "/pull/" , 1 )
550+ // API uses "issues" which is the same in HTML.
551+ if s == apiURL {
552+ return "" // couldn't convert
553+ }
554+ return s
555+ }
556+
557+ func truncate (s string , max int ) string {
558+ if len (s ) <= max {
559+ return s
560+ }
561+ return s [:max ] + "…"
562+ }
563+
460564func priorityEmoji (p int ) string {
461565 switch p {
462566 case gh .P0 :
0 commit comments