фиксы

This commit is contained in:
Artem Tsyrulnikov
2026-01-08 21:50:04 +03:00
parent 3e5bdb8164
commit eb54b01835
20 changed files with 462 additions and 405 deletions

View File

@@ -500,26 +500,8 @@ func escapeHTML(s string) string {
return s
}
// showCreativeConfirmation показывает превью креатива + панель с предложением добавить
func (s *AddCreative) showCreativeConfirmation(b *bot.Bot) {
// 1. Отправляем/обновляем превью креатива
if s.CreativeMessageID == nil {
s.SendCreativePreview(b)
} else {
s.UpdateCreativePreview(b)
}
// 2. Формируем текст панели управления
// Экранируем HTML-символы в названии для безопасного вывода
escapedName := escapeHTML(*s.Name)
confirmText := fmt.Sprintf(`<b> Добавляем этот креатив?</b>
<b>Название креатива:</b> %s
<i>Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.</i>`, escapedName)
// 3. Показываем панель с кнопками редактирования + сохранения
// buildEditorButtons создает кнопки для панели редактирования креатива
func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][]echotron.InlineKeyboardButton {
var buttons [][]echotron.InlineKeyboardButton
// Кнопки редактирования
@@ -559,15 +541,39 @@ func (s *AddCreative) showCreativeConfirmation(b *bot.Bot) {
// Финальные кнопки
if s.Text != nil {
buttons = append(buttons, bot.Row(
bot.Button("← Отмена", "cancel"),
bot.Button("✓ Сохранить", "confirm_create"),
bot.Button("← "+cancelCallback, "cancel"),
bot.Button(confirmText, "confirm_create"),
))
} else {
buttons = append(buttons, bot.Row(
bot.Button("← Отмена", "cancel"),
bot.Button("← "+cancelCallback, "cancel"),
))
}
return buttons
}
// showCreativeConfirmation показывает превью креатива + панель с предложением добавить
func (s *AddCreative) showCreativeConfirmation(b *bot.Bot) {
// 1. Отправляем/обновляем превью креатива
if s.CreativeMessageID == nil {
s.SendCreativePreview(b)
} else {
s.UpdateCreativePreview(b)
}
// 2. Формируем текст панели управления
// Экранируем HTML-символы в названии для безопасного вывода
escapedName := escapeHTML(*s.Name)
confirmText := fmt.Sprintf(`<b> Добавляем этот креатив?</b>
<b>Название креатива:</b> %s
<i>Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.</i>`, escapedName)
// 3. Показываем панель с кнопками редактирования + сохранения
buttons := s.buildEditorButtons("✓ Сохранить", "Отмена")
s.ShowControlPanel(b, confirmText, buttons)
}
@@ -596,9 +602,9 @@ func (s *AddCreative) createCreative(b *bot.Bot) {
mediaData, err = b.DownloadFileBytes(s.MediaFileID)
if err != nil {
log.Error().Err(err).Msg("Failed to download creative media")
b.SendNew("<b>❌ Не удалось загрузить медиа</b>", bot.Keyboard(
bot.Row(bot.Button("← Назад", "cancel")),
))
// Показываем ошибку через панель управления
buttons := s.buildEditorButtons("✓ Попробовать снова", "Отмена")
s.ShowControlPanel(b, "<b>❌ Не удалось загрузить медиа</b>\n\nПопробуйте удалить медиа и добавить заново.", buttons)
return
}
}
@@ -653,12 +659,16 @@ func (s *AddCreative) createCreative(b *bot.Bot) {
<b>Пример правильного текста:</b>
<i>Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn</i>`
https://t.me/+AbCdEfGhIjKlMn</i>
<b>Нажмите "✎ Текст" ниже чтобы исправить.</b>`
} else if strings.Contains(errMsg, "Creative text must contain only one invite link") {
userMsg = `<b>❌ Слишком много ссылок</b>
Текст должен содержать <b>ТОЛЬКО ОДНУ</b> инвайт-ссылку.
У вас их несколько. Удалите лишние.`
У вас их несколько. Удалите лишние.
<b>Нажмите "✎ Текст" ниже чтобы исправить.</b>`
} else {
userMsg = `<b>❌ Ошибка создания</b>
@@ -666,9 +676,9 @@ https://t.me/+AbCdEfGhIjKlMn</i>`
Попробуйте ещё раз или вернитесь назад.`
}
b.SendNew(userMsg, bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_from_preview")),
))
// Показываем ошибку через панель управления, чтобы не ломать порядок сообщений
buttons := s.buildEditorButtons("✓ Попробовать снова", "Отмена")
s.ShowControlPanel(b, userMsg, buttons)
return
}

View File

@@ -425,9 +425,11 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
mediaData, err := b.DownloadFileBytes(s.MediaFileID)
if err != nil {
log.Error().Err(err).Msg("Failed to download creative media")
b.SendNew("<b>❌ Не удалось загрузить медиа</b>", bot.Keyboard(
// Показываем ошибку через панель управления
s.ShowControlPanel(b, "<b>❌ Не удалось загрузить медиа</b>\n\nПопробуйте удалить медиа и добавить заново, или нажмите \"Сохранить\" без изменения медиа.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")),
))
bot.Row(bot.Button("✓ Сохранить без медиа", "confirm_save")),
})
return
}
input.MediaData = &mediaData
@@ -457,9 +459,11 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
if err != nil {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to update creative")
b.SendNew("<b>❌ Не удалось сохранить изменения</b>", bot.Keyboard(
// Показываем ошибку через панель управления
s.ShowControlPanel(b, "<b>❌ Не удалось сохранить изменения</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")),
))
bot.Row(bot.Button("✓ Попробовать снова", "confirm_save")),
})
return
}
@@ -468,7 +472,7 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
// Возвращаемся назад
if s.BackState != nil {
b.SendNew("✅ Креатив успешно обновлен!", bot.Keyboard())
b.SetState(s.BackState, bot.NewMessage)
b.SetState(s.BackState, bot.EditMessage)
}
}
@@ -491,9 +495,12 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
if err != nil {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to delete creative")
b.SendNew("<b>❌ Не удалось удалить креатив</b>", bot.Keyboard(
// Показываем ошибку через панель управления
s.confirmingDelete = false // Сбрасываем флаг подтверждения
s.ShowControlPanel(b, "<b>❌ Не удалось удалить креатив</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")),
))
bot.Row(bot.Button("⌦ Попробовать снова", "delete_creative")),
})
return
}
@@ -502,6 +509,6 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
// Возвращаемся назад
if s.BackState != nil {
b.SendNew("✅ Креатив успешно удален!", bot.Keyboard())
b.SetState(s.BackState, bot.NewMessage)
b.SetState(s.BackState, bot.EditMessage)
}
}

View File

@@ -45,7 +45,7 @@ type CreativeEditorFields struct {
}
const inviteLinkPlaceholder = "{{invite_link}}"
const inviteLinkPreviewURL = "https://t.me/J7_channel"
const inviteLinkPreviewURL = "https://t.me/+nlOUmIsLGAlmZTMy" //ссылка приглашения для предпросмотра
// GetCreativeText возвращает текст для превью креатива
func (e *CreativeEditorFields) GetCreativeText() string {
@@ -64,8 +64,79 @@ func (e *CreativeEditorFields) GetCreativeText() string {
</i>`
}
// Заменяем пустую ссылку <a class="tg-link"></a> на валидную preview-ссылку
text = strings.ReplaceAll(text, `<a class="tg-link"></a>`, `<a href="`+inviteLinkPreviewURL+`">👉 Подписаться</a>`)
// Заменяем пригласительные ссылки с классом tg-link на preview-ссылку
// Находим все вхождения <a class="tg-link">...</a> и заменяем на кликабельную ссылку
replacementCount := 0
for strings.Contains(text, `<a class="tg-link">`) {
startIdx := strings.Index(text, `<a class="tg-link">`)
if startIdx == -1 {
break
}
endIdx := strings.Index(text[startIdx:], "</a>")
if endIdx == -1 {
log.Warn().Msg("Found <a class=\"tg-link\"> but no closing </a>")
break
}
endIdx += startIdx
// Извлекаем текст между тегами
linkText := text[startIdx+len(`<a class="tg-link">`) : endIdx]
log.Info().
Int("replacement_num", replacementCount).
Str("linkText_before_sanitize", linkText).
Msg("Processing tg-link")
// ВАЖНО: Удаляем все HTML-теги из текста ссылки для предотвращения неправильной вложенности
// Это нужно для старых креативов с неправильным HTML
tagsRemoved := 0
for {
openTag := strings.Index(linkText, "<")
if openTag == -1 {
break
}
closeTag := strings.Index(linkText[openTag:], ">")
if closeTag == -1 {
log.Warn().Str("linkText", linkText).Msg("Found < but no closing >")
break
}
// Удаляем тег
removedTag := linkText[openTag : openTag+closeTag+1]
linkText = linkText[:openTag] + linkText[openTag+closeTag+1:]
tagsRemoved++
log.Info().
Str("removed_tag", removedTag).
Str("linkText_after", linkText).
Int("tags_removed_total", tagsRemoved).
Msg("Removed HTML tag from link text")
}
linkText = strings.TrimSpace(linkText)
// Если текст ссылки не пустой, добавляем атрибут href с примером ссылки
// Если пустой, показываем просто текст примера ссылки (без тега)
var replacement string
if linkText != "" {
// Есть текст - добавляем href атрибут к тегу
replacement = fmt.Sprintf(`<a class="tg-link" href="%s">%s</a>`, inviteLinkPreviewURL, linkText)
} else {
// Пусто - показываем просто текст примера ссылки
replacement = fmt.Sprintf(`%s`, inviteLinkPreviewURL)
}
log.Info().
Str("linkText_after_sanitize", linkText).
Int("tags_removed", tagsRemoved).
Str("replacement", replacement).
Msg("Sanitized link text")
text = text[:startIdx] + replacement + text[endIdx+len("</a>"):]
replacementCount++
}
log.Info().
Int("replacements_made", replacementCount).
Str("final_text", text).
Msg("Finished processing tg-links")
// Также заменяем плейсхолдер {{invite_link}} на preview URL (как в кнопках)
text = strings.ReplaceAll(text, inviteLinkPlaceholder, inviteLinkPreviewURL)
@@ -409,18 +480,55 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
// Преобразуем текст в руны для правильной работы с UTF-8
runes := []rune(text)
// Сначала находим все url entities, чтобы исключить перекрывающиеся форматирования
urlRanges := make([][2]int, 0)
// Также отмечаем диапазоны, которые нужно полностью исключить из текста (простые url ссылки)
skipRanges := make(map[[2]int]bool)
for _, entity := range entities {
if entity.Type == "url" || entity.Type == "text_link" {
urlRanges = append(urlRanges, [2]int{entity.Offset, entity.Offset + entity.Length})
// Для простых url (не text_link) с пригласительными ссылками - пропускаем текст URL
// Создаем пустой <a class="tg-link"></a>, а сам URL не сохраняем
if entity.Type == "url" {
entityText := ""
if entity.Offset+entity.Length <= len(runes) {
entityText = string(runes[entity.Offset : entity.Offset+entity.Length])
}
if strings.Contains(entityText, "t.me/+") || strings.Contains(entityText, "t.me/joinchat/") {
skipRanges[[2]int{entity.Offset, entity.Offset + entity.Length}] = true
}
}
}
}
// Проверяем, находится ли entity внутри URL
isInsideURL := func(offset, length int) bool {
for _, urlRange := range urlRanges {
// Если entity полностью внутри URL (но не сам URL)
if offset > urlRange[0] && offset+length <= urlRange[1] {
return true
}
// Если entity пересекается с URL
if offset < urlRange[1] && offset+length > urlRange[0] && (offset != urlRange[0] || length != urlRange[1]-urlRange[0]) {
return true
}
}
return false
}
var spans []tagSpan
for _, entity := range entities {
// URL уже присутствует в сыром тексте, чтобы не дублировать - пропускаем entity.
if entity.Type == "url" {
continue
}
entityText := ""
if entity.Offset+entity.Length <= len(runes) {
entityText = string(runes[entity.Offset : entity.Offset+entity.Length])
}
// Пропускаем форматирование (bold, italic, etc), если оно находится внутри URL
if entity.Type != "url" && entity.Type != "text_link" && isInsideURL(entity.Offset, entity.Length) {
continue
}
var openTag, closeTag string
switch entity.Type {
case "bold":
@@ -435,10 +543,29 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
openTag, closeTag = "<code>", "</code>"
case "pre":
openTag, closeTag = "<pre>", "</pre>"
case "url":
// Конвертируем простые URL в HTML-ссылки
// Для пригласительных ссылок (t.me/+ или t.me/joinchat/) используем пустой тег
if strings.Contains(entityText, "t.me/+") || strings.Contains(entityText, "t.me/joinchat/") {
// Пригласительная ссылка - создаем пустой <a class="tg-link"></a>
// Текст ссылки будет пропущен при рендеринге (см. skipRanges)
openTag = `<a class="tg-link">`
closeTag = "</a>"
} else {
// Обычная ссылка - оставляем как есть (ссылка на себя)
openTag = fmt.Sprintf("<a href=\"%s\">", entityText)
closeTag = "</a>"
}
case "text_link":
if entity.URL != "" {
openTag = fmt.Sprintf("<a href=\"%s\">", entity.URL)
closeTag = "</a>"
// Проверяем, является ли URL пригласительной ссылкой
if strings.Contains(entity.URL, "t.me/+") || strings.Contains(entity.URL, "t.me/joinchat/") {
openTag = `<a class="tg-link">`
closeTag = "</a>"
} else {
openTag = fmt.Sprintf("<a href=\"%s\">", entity.URL)
closeTag = "</a>"
}
}
default:
_ = entityText
@@ -462,6 +589,16 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
closes[span.end] = append(closes[span.end], span)
}
// Функция для проверки, находится ли позиция в skipRanges
isInSkipRange := func(pos int) bool {
for skipRange := range skipRanges {
if pos >= skipRange[0] && pos < skipRange[1] {
return true
}
}
return false
}
// Пишем текст один раз, вставляя HTML-теги по позициям
// и экранируя HTML-спецсимволы
var result strings.Builder
@@ -479,6 +616,10 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
}
}
if i < len(runes) {
// Пропускаем символы, которые находятся в skipRanges (текст пригласительных ссылок)
if isInSkipRange(i) {
continue
}
// Экранируем HTML-спецсимволы
switch runes[i] {
case '<':

View File

@@ -155,10 +155,6 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
b.SetState(s.BackState, bot.EditMessage)
}
case data == "page_info", data == "empty":
// Неактивные кнопки - ничего не делаем
return
case data == "archive":
// TODO: реализовать экран архива креативов
b.Edit("📦 Архив креативов\n\nЭта функция в разработке", bot.Keyboard(

View File

@@ -7,50 +7,9 @@ import (
type MainMenu struct{}
func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
keyboard := bot.Keyboard(
bot.Row(
bot.Button("Мои проекты", "my_projects"),
echotron.InlineKeyboardButton{Text: "Поддержка", URL: "https://example.com"},
),
bot.Row(
bot.Button("Закупы", "purchases"),
echotron.InlineKeyboardButton{Text: "Дашборд", URL: "https://example.com"},
),
)
const msgAbout = `
<i>Главное меню</i>
text := "<i>Главное меню</i>\n\n" + tgexAbout
b.Render(text, keyboard, mode)
}
func (s *MainMenu) HandleCallback(b *bot.Bot, u *echotron.Update) {
switch u.CallbackQuery.Data {
case "my_projects":
b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.NewMessage)
case "purchases":
b.SetState(&MyProjects{BackState: &MainMenu{}, OpenPurchases: true}, bot.NewMessage)
default:
s.Enter(b, bot.NewMessage)
}
return
}
func (s *MainMenu) HandleMessage(b *bot.Bot, u *echotron.Update) {
if u.Message.Text == "/start login" {
b.SetState(&Login{}, bot.NewMessage)
return
}
return
}
func (s *MainMenu) Handle(b *bot.Bot, u *echotron.Update) {
b.SetState(&MainMenu{}, bot.NewMessage)
return
}
const tgexAbout = `
Smartpost — платформа для покупки и продажи рекламы без ЛС и ручных таблиц.
• Внутренний счёт и безопасные сделки
@@ -58,4 +17,35 @@ Smartpost — платформа для покупки и продажи рек
• Автоматические отчёты
• Повторные закупки в пару кликов
Комиссия — 7%. Без скрытых платежей.`
Комиссия — 7%. Без скрытых платежей.
`
func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
keyboard := bot.Keyboard(
bot.Row(
bot.Button("Мои проекты", "my_projects"),
bot.URLButton("Поддержка", "https://example.com"),
),
bot.Row(
bot.Button("Закупы", "purchases"),
bot.URLButton("Дашборд", "https://example.com"),
),
)
b.Render(msgAbout, keyboard, mode)
}
func (s *MainMenu) HandleCallback(b *bot.Bot, u *echotron.Update) {
switch u.CallbackQuery.Data {
case "my_projects":
b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.EditMessage)
case "purchases":
b.SetState(&MyProjects{BackState: &MainMenu{}, OpenPurchases: true}, bot.EditMessage)
default:
s.Enter(b, bot.NewMessage)
}
}
func (s *MainMenu) HandleMessage(b *bot.Bot, u *echotron.Update) {}
func (s *MainMenu) Handle(b *bot.Bot, u *echotron.Update) { b.SetState(&MainMenu{}, bot.NewMessage) }

View File

@@ -1,194 +0,0 @@
package screens
import (
"context"
"fmt"
"strings"
"example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log"
)
const placementsPerPage = 6
type MyPlacements struct {
CurrentPage int
WorkspaceID string
BackState bot.State
}
func (s *MyPlacements) Enter(b *bot.Bot, mode bot.RenderMode) {
// JWT уже создан в Bot.Update(), просто берем из сессии
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", bot.Keyboard())
return
}
// Если workspace еще не выбран, получаем список workspace'ов
if s.WorkspaceID == "" {
workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt)
if err != nil {
log.Error().Err(err).Msg("Failed to get workspaces")
b.SendNew("❌ Не удалось загрузить workspace'ы", bot.Keyboard())
return
}
if len(workspaces) == 0 {
b.SendNew("У вас пока нет workspace'ов", bot.Keyboard())
return
}
// Используем первый workspace по умолчанию
s.WorkspaceID = workspaces[0].ID
}
s.renderPlacements(b, mode, jwt)
}
func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt string) {
page, err := b.Backend.GetPlacements(
context.Background(),
jwt,
s.WorkspaceID,
nil, // projectID
nil, // placementChannelID
nil, // creativeID
false, // includeArchived
s.CurrentPage+1,
placementsPerPage,
)
if err != nil {
log.Error().Err(err).Msg("Failed to get placements")
b.SendNew("❌ Не удалось загрузить закупы", bot.Keyboard())
return
}
text := "<i>… Главное меню Размещения</i>\n\n<b>Мои размещения</b>\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
text += `У вас пока нет размещений
Размещения создаются автоматически при создании закупа`
} else {
text += fmt.Sprintf("Выберите размещение для деталей%s\n\n", ui.FormatPageInfo(s.CurrentPage, page.Pages))
// Размещения по 2 в ряду
var slots []echotron.InlineKeyboardButton
for _, placement := range page.Items {
placementName := placement.CreativeName
if placement.PlacementChannelTitle != nil && *placement.PlacementChannelTitle != "" {
placementName = fmt.Sprintf("%s → %s", placement.CreativeName, *placement.PlacementChannelTitle)
}
slots = append(slots, bot.Button(placementName, fmt.Sprintf("placement:%s", placement.ID)))
}
if page.Pages >= 2 {
for len(slots) < placementsPerPage {
slots = append(slots, bot.Button(" ", "empty"))
}
}
for i := 0; i < len(slots); i += 2 {
row := []echotron.InlineKeyboardButton{slots[i]}
if i+1 < len(slots) {
row = append(row, slots[i+1])
} else {
row = append(row, bot.Button(" ", "empty"))
}
buttons = append(buttons, row)
}
// Кнопки пагинации с номером страницы посередине (только если есть закупы)
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
TotalPages: page.Pages,
MiddleButtons: []echotron.InlineKeyboardButton{bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, page.Pages), "page_info")},
}); navRow != nil {
buttons = append(buttons, navRow)
}
}
// Кнопки управления
buttons = append(buttons, bot.Row(
bot.Button("▪ Статистика", "statistics"),
))
// Кнопка возврата в главное меню
buttons = append(buttons, bot.Row(bot.Button("← Назад", "back")))
keyboard := bot.Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
func (s *MyPlacements) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
switch {
case data == "prev":
if s.CurrentPage > 0 {
s.CurrentPage--
}
s.Enter(b, bot.EditMessage)
case data == "next":
s.CurrentPage++
s.Enter(b, bot.EditMessage)
case data == "back":
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
} else {
b.SetState(&MainMenu{}, bot.EditMessage)
}
case data == "page_info", data == "empty":
// Неактивные кнопки - ничего не делаем
return
case data == "statistics":
// TODO: реализовать экран статистики
b.Edit("📊 Статистика размещений\n\nЭта функция в разработке", bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_to_placements")),
))
case data == "back_to_placements":
s.Enter(b, bot.EditMessage)
case strings.HasPrefix(data, "placement:"):
// Открытие деталей закупа
parts := strings.Split(data, ":")
if len(parts) == 2 {
placementID := parts[1]
// TODO: реализовать экран деталей закупа
b.Edit(fmt.Sprintf("📄 Детали закупа\n\nID: %s\n\nЭта функция в разработке", placementID), bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_to_placements")),
))
}
case strings.HasPrefix(data, "workspace:"):
// Позволяет переключаться между workspace'ами в будущем
parts := strings.Split(data, ":")
if len(parts) == 2 {
s.WorkspaceID = parts[1]
s.CurrentPage = 0
s.Enter(b, bot.EditMessage)
}
default:
s.Enter(b, bot.NewMessage)
}
return
}
func (s *MyPlacements) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
func (s *MyPlacements) Handle(_ *bot.Bot, _ *echotron.Update) { return }

View File

@@ -188,12 +188,7 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
case data == "menu", data == "main_menu":
b.SetState(&MainMenu{}, bot.EditMessage)
case data == "page_info", data == "empty":
// Неактивные кнопки - ничего не делаем
return
case data == "archive":
// TODO: реализовать экран архива
b.Edit("📦 Архив проектов\n\nЭта функция в разработке", bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_to_projects")),
))
@@ -229,13 +224,13 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
ProjectID: project.ID,
ProjectTitle: project.Title,
BackState: &MyProjects{WorkspaceID: s.WorkspaceID, OpenPurchases: true},
}, bot.NewMessage)
}, bot.EditMessage)
} else {
b.SetState(&ProjectDetails{
Project: project,
WorkspaceID: s.WorkspaceID,
BackState: &MyProjects{WorkspaceID: s.WorkspaceID},
}, bot.NewMessage)
}, bot.EditMessage)
}
}

View File

@@ -136,7 +136,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
ProjectTitle: s.Project.Title,
WorkspaceID: s.WorkspaceID,
BackState: s,
}, bot.NewMessage)
}, bot.EditMessage)
case strings.HasPrefix(data, "purchases:"):
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
@@ -145,7 +145,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
ProjectID: s.Project.ID,
ProjectTitle: s.Project.Title,
BackState: s,
}, bot.NewMessage)
}, bot.EditMessage)
case data == "back_to_project":
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения

View File

@@ -141,7 +141,7 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
ProjectTitle: s.ProjectTitle,
PurchaseID: purchaseID,
BackState: s,
}, bot.NewMessage)
}, bot.EditMessage)
} else {
s.Enter(b, bot.NewMessage)
}