mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 05:34:35 +03:00
fix: store display names separately to preserve channel handle identifiers
- Add DisplayName field to ChannelInfo in the wire protocol - Add displayNames map to Feed; SetChannelDisplayName no longer mutates f.channels, keeping handles stable for cache keys and management - Public fetcher: extract title via extractChannelTitle, pass to feed - Telegram fetcher: capture ch.Title / user.FirstName from MTProto API - Frontend: render DisplayName as sidebar label, keep Name as identifier
This commit is contained in:
@@ -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,
|
||||
|
||||
+11
-7
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 <div class="tgme_channel_info_header_title"><span>Title</span></div>.
|
||||
func extractChannelTitle(body []byte) string {
|
||||
doc, err := html.Parse(strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 += '<div class="channel-section-title">' + esc(title) + '</div>';
|
||||
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) ? '<span class="ch-badge">NEW</span>' : '';
|
||||
h += '<div class="ch-item' + active + '" data-name="' + escAttr(name) + '" onclick="selectChannel(' + num2 + ')">';
|
||||
h += '<div class="ch-item' + active + '" data-name="' + escAttr(handle) + '" data-label="' + escAttr(label) + '" onclick="selectChannel(' + num2 + ')">';
|
||||
h += '<div class="ch-avatar">' + esc(avatarText) + '</div>';
|
||||
h += '<div class="ch-info"><div class="ch-name">' + esc(name) + (isPriv ? '<span class="ch-type-tag">' + t('private') + '</span>' : (isX ? '<span class="ch-type-tag x-tag">' + t('x_label') + '</span>' : '')) + '</div>';
|
||||
h += '<div class="ch-info"><div class="ch-name">' + esc(label) + (isPriv ? '<span class="ch-type-tag">' + t('private') + '</span>' : (isX ? '<span class="ch-type-tag x-tag">' + t('x_label') + '</span>' : '')) + '</div>';
|
||||
h += '<div class="ch-preview">' + badge + '</div></div></div>';
|
||||
}
|
||||
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 = '<div class="empty-state"><p>' + t('loading') + '</p></div>';
|
||||
|
||||
Reference in New Issue
Block a user