diff --git a/internal/protocol/protocol.go b/internal/protocol/protocol.go index 093f7dc..535d6f9 100644 --- a/internal/protocol/protocol.go +++ b/internal/protocol/protocol.go @@ -81,6 +81,7 @@ type Metadata struct { // ChannelInfo describes a single feed channel. type ChannelInfo struct { Name string + DisplayName string // human-readable title; empty means fall back to Name Blocks uint16 LastMsgID uint32 ContentHash uint32 // CRC32 of serialized message data; changes on edits @@ -104,12 +105,16 @@ func ContentHashOf(msgs []Message) uint32 { // SerializeMetadata encodes metadata into bytes for channel 0 blocks. // Format: marker(3) + timestamp(4) + nextFetch(4) + flags(1) + channelCount(2) + per-channel data -// Per-channel: nameLen(1) + name + blocks(2) + lastMsgID(4) + contentHash(4) + chatType(1) + flags(1) +// Per-channel: nameLen(1) + name + blocks(2) + lastMsgID(4) + contentHash(4) + chatType(1) + flags(1) + displayNameLen(1) + displayName func SerializeMetadata(m *Metadata) []byte { // 3 marker + 4 timestamp + 4 nextFetch + 1 flags + 2 channel count + per-channel data size := MarkerSize + 4 + 4 + 1 + 2 for _, ch := range m.Channels { - size += 1 + len(ch.Name) + 2 + 4 + 4 + 1 + 1 // +4 for contentHash + dn := ch.DisplayName + if len(dn) > 255 { + dn = dn[:255] + } + size += 1 + len(ch.Name) + 2 + 4 + 4 + 1 + 1 + 1 + len(dn) } buf := make([]byte, size) off := 0 @@ -156,6 +161,14 @@ func SerializeMetadata(m *Metadata) []byte { } buf[off] = chFlags off++ + dnBytes := []byte(ch.DisplayName) + if len(dnBytes) > 255 { + dnBytes = dnBytes[:255] + } + buf[off] = byte(len(dnBytes)) + off++ + copy(buf[off:], dnBytes) + off += len(dnBytes) } return buf @@ -213,8 +226,19 @@ func ParseMetadata(data []byte) (*Metadata, error) { chFlags := data[off] off++ + var displayName string + if off < len(data) { + dnLen := int(data[off]) + off++ + if off+dnLen <= len(data) { + displayName = string(data[off : off+dnLen]) + off += dnLen + } + } + m.Channels = append(m.Channels, ChannelInfo{ Name: name, + DisplayName: displayName, Blocks: blocks, LastMsgID: lastID, ContentHash: contentHash, diff --git a/internal/server/feed.go b/internal/server/feed.go index 614f634..74a8449 100644 --- a/internal/server/feed.go +++ b/internal/server/feed.go @@ -14,6 +14,7 @@ type Feed struct { mu sync.RWMutex marker [protocol.MarkerSize]byte channels []string + displayNames map[int]string blocks map[int][][]byte lastIDs map[int]uint32 contentHashes map[int]uint32 @@ -31,6 +32,7 @@ type Feed struct { func NewFeed(channels []string) *Feed { f := &Feed{ channels: channels, + displayNames: make(map[int]string), blocks: make(map[int][][]byte), lastIDs: make(map[int]uint32), contentHashes: make(map[int]uint32), @@ -135,6 +137,7 @@ func (f *Feed) rebuildMetaBlocks() { } meta.Channels = append(meta.Channels, protocol.ChannelInfo{ Name: name, + DisplayName: f.displayNames[chNum], Blocks: blockCount, LastMsgID: f.lastIDs[chNum], ContentHash: f.contentHashes[chNum], @@ -211,19 +214,20 @@ func (f *Feed) SetChannels(channels []string) { f.rebuildMetaBlocks() } -// SetChannelDisplayName updates the display name for a specific channel number (1-indexed). -// This allows replacing the raw handle (e.g. "networkti") with the channel's -// actual title (e.g. "Sarto") after it has been fetched. +// SetChannelDisplayName stores a human-readable title for a channel (1-indexed). +// It never mutates the handle in f.channels, which remains the stable identifier. func (f *Feed) SetChannelDisplayName(channelNum int, displayName string) { + if displayName == "" { + return + } f.mu.Lock() defer f.mu.Unlock() - idx := channelNum - 1 - if idx < 0 || idx >= len(f.channels) { + if channelNum < 1 || channelNum > len(f.channels) { return } - if displayName == "" || f.channels[idx] == displayName { + if f.displayNames[channelNum] == displayName { return } - f.channels[idx] = displayName + f.displayNames[channelNum] = displayName f.rebuildMetaBlocks() } diff --git a/internal/server/public.go b/internal/server/public.go index 0b79810..413bcc9 100644 --- a/internal/server/public.go +++ b/internal/server/public.go @@ -144,9 +144,7 @@ func (pr *PublicReader) fetchAll(ctx context.Context) { pr.feed.UpdateChannel(chNum, msgs) pr.feed.SetChatInfo(chNum, protocol.ChatTypeChannel, false) - if title != "" { - pr.feed.SetChannelDisplayName(chNum, title) - } + pr.feed.SetChannelDisplayName(chNum, title) fetched++ log.Printf("[public] updated %s (%s): %d messages", username, title, len(msgs)) } @@ -178,12 +176,10 @@ func (pr *PublicReader) fetchChannel(ctx context.Context, username string) ([]pr if err != nil { return nil, "", err } - title := extractChannelTitle(body) - return msgs, title, nil + return msgs, extractChannelTitle(body), nil } // extractChannelTitle parses the channel display name from the Telegram public page. -// It looks for
Title
. func extractChannelTitle(body []byte) string { doc, err := html.Parse(strings.NewReader(string(body))) if err != nil { diff --git a/internal/server/telegram.go b/internal/server/telegram.go index ed1d476..48b793d 100644 --- a/internal/server/telegram.go +++ b/internal/server/telegram.go @@ -79,6 +79,7 @@ type resolvedPeer struct { peer tg.InputPeerClass chatType protocol.ChatType canSend bool + title string } type cachedMessages struct { @@ -267,8 +268,9 @@ func (tr *TelegramReader) fetchAll(ctx context.Context, api *tg.Client) { // Update feed with messages and chat type info tr.feed.UpdateChannel(chNum, msgs) tr.feed.SetChatInfo(chNum, rp.chatType, rp.canSend) + tr.feed.SetChannelDisplayName(chNum, rp.title) fetched++ - log.Printf("[telegram] updated %s: %d messages (type=%d, canSend=%v)", username, len(msgs), rp.chatType, rp.canSend) + log.Printf("[telegram] updated %s (%s): %d messages (type=%d, canSend=%v)", username, rp.title, len(msgs), rp.chatType, rp.canSend) } log.Printf("[telegram] fetch cycle done in %s: %d fetched, %d failed, %d total", time.Since(start).Round(time.Millisecond), fetched, failed, len(tr.channels)) } @@ -294,6 +296,7 @@ func (tr *TelegramReader) resolvePeer(ctx context.Context, api *tg.Client, usern }, chatType: protocol.ChatTypeChannel, canSend: canSend, + title: ch.Title, }, nil } } @@ -308,6 +311,7 @@ func (tr *TelegramReader) resolvePeer(ctx context.Context, api *tg.Client, usern }, chatType: protocol.ChatTypePrivate, canSend: true, + title: user.FirstName, }, nil } } diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 2a07cce..4ef35a0 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -3085,7 +3085,8 @@ var needsFullRebuild = false; for (var ci = 0; ci < channels.length; ci++) { var ch = channels[ci], nm = ch.Name || ch.name || 'Channel ' + (ci + 1); - if (existingItems[ci].dataset.name !== nm) { needsFullRebuild = true; break } + var lbl = ch.DisplayName || ch.displayName || nm; + if (existingItems[ci].dataset.name !== nm || existingItems[ci].dataset.label !== lbl) { needsFullRebuild = true; break } } if (!needsFullRebuild) { for (var ui = 0; ui < channels.length; ui++) { @@ -3113,21 +3114,22 @@ if (title) h += '
' + esc(title) + '
'; for (var j = 0; j < items.length; j++) { var e = items[j], num2 = e.idx + 1; - var name = e.ch.Name || e.ch.name || 'Channel ' + num2; + var handle = e.ch.Name || e.ch.name || 'Channel ' + num2; + var label = e.ch.DisplayName || e.ch.displayName || handle; var ct2 = e.ch.ChatType || e.ch.chatType || 0; var isPriv = e.ch.ChatType === 1 || e.ch.chatType === 1; var isX = ct2 === 2; - var avatarName = name; - if (isX && avatarName.toLowerCase().indexOf('x/') === 0) avatarName = avatarName.substring(2); + var avatarName = label; + if (isX && handle.toLowerCase().indexOf('x/') === 0) avatarName = handle.substring(2); if (avatarName.charAt(0) === '@') avatarName = avatarName.substring(1); var avatarText = (avatarName || '?').charAt(0).toUpperCase(); var active = num2 === selectedChannel ? ' active' : ''; var lastID = e.ch.LastMsgID || e.ch.lastMsgID || 0; var chNm2 = e.ch.Name || e.ch.name || ''; var badge = (previousMsgIDs[chNm2] > 0 && lastID > previousMsgIDs[chNm2] && num2 !== selectedChannel) ? 'NEW' : ''; - h += '
'; + h += '
'; h += '
' + esc(avatarText) + '
'; - h += '
' + esc(name) + (isPriv ? '' + t('private') + '' : (isX ? '' + t('x_label') + '' : '')) + '
'; + h += '
' + esc(label) + (isPriv ? '' + t('private') + '' : (isX ? '' + t('x_label') + '' : '')) + '
'; h += '
' + badge + '
'; } return h; @@ -3154,7 +3156,7 @@ currentMaxTimestamp = 0; newMsgScrollDone = false; openChat(); - var ch = channels[num - 1]; var name = (ch && (ch.Name || ch.name)) || 'Channel ' + num; + var ch = channels[num - 1]; var name = (ch && (ch.DisplayName || ch.displayName || ch.Name || ch.name)) || 'Channel ' + num; document.getElementById('chatName').textContent = name; renderChannels(); updateSendPanel(); document.getElementById('messages').innerHTML = '

' + t('loading') + '

';