handle edge case when primary channel has a name (#421)

This commit is contained in:
l5y
2025-11-07 21:39:26 +01:00
committed by GitHub
parent 52486d82ad
commit d00a9fab7c
3 changed files with 110 additions and 32 deletions

View File

@@ -72,17 +72,26 @@ test('buildChatTabModel returns sorted nodes and channel buckets', () => {
['recent-node', 'iso-node', 'encrypted']
);
assert.equal(model.channels.length, 2);
const [channel0, channel1] = model.channels;
assert.equal(channel0.index, 0);
assert.equal(channel0.label, 'MediumFast');
assert.equal(channel0.entries.length, 2);
assert.deepEqual(channel0.entries.map(entry => entry.message.id), ['no-index', 'recent-default']);
assert.equal(model.channels.length, 3);
const [fallbackChannel, namedPrimaryChannel, secondaryChannel] = model.channels;
assert.equal(channel1.index, 1);
assert.equal(channel1.label, 'BerlinMesh');
assert.equal(channel1.entries.length, 2);
assert.deepEqual(channel1.entries.map(entry => entry.message.id), ['iso-ts', 'recent-alt']);
assert.equal(fallbackChannel.index, 0);
assert.equal(fallbackChannel.label, 'Fallback');
assert.equal(fallbackChannel.id, 'channel-0-fallback');
assert.equal(fallbackChannel.entries.length, 1);
assert.deepEqual(fallbackChannel.entries.map(entry => entry.message.id), ['no-index']);
assert.equal(namedPrimaryChannel.index, 0);
assert.equal(namedPrimaryChannel.label, 'MediumFast');
assert.equal(namedPrimaryChannel.id, 'channel-0-mediumfast');
assert.equal(namedPrimaryChannel.entries.length, 1);
assert.deepEqual(namedPrimaryChannel.entries.map(entry => entry.message.id), ['recent-default']);
assert.equal(secondaryChannel.index, 1);
assert.equal(secondaryChannel.label, 'BerlinMesh');
assert.equal(secondaryChannel.id, 'channel-1');
assert.equal(secondaryChannel.entries.length, 2);
assert.deepEqual(secondaryChannel.entries.map(entry => entry.message.id), ['iso-ts', 'recent-alt']);
});
test('buildChatTabModel always includes channel zero bucket', () => {

View File

@@ -58,7 +58,7 @@ export const CHAT_LOG_ENTRY_TYPES = Object.freeze({
* }} params Aggregation inputs.
* @returns {{
* logEntries: Array<{ ts: number, type: string, nodeId?: string, nodeNum?: number }>,
* channels: Array<{ index: number, label: string, entries: Array<{ ts: number, message: Object }> }>
* channels: Array<{ id: string, index: number, label: string, entries: Array<{ ts: number, message: Object }> }>
* }} Sorted tab model data.
*/
export function buildChatTabModel({
@@ -138,23 +138,24 @@ export function buildChatTabModel({
if (channelIndex != null && channelIndex > maxChannelIndex) {
continue;
}
const safeIndex = channelIndex != null && channelIndex >= 0 ? channelIndex : 0;
const bucketKey = safeIndex;
let bucket = channelBuckets.get(bucketKey);
if (!bucket) {
bucket = {
index: safeIndex,
label: String(safeIndex),
entries: [],
hasExplicitName: false
};
channelBuckets.set(bucketKey, bucket);
}
const channelName = normaliseChannelName(
message.channel_name ?? message.channelName ?? message.channel_display ?? message.channelDisplay
);
if (channelName && !bucket.hasExplicitName) {
const safeIndex = channelIndex != null && channelIndex >= 0 ? channelIndex : 0;
const bucketKey = buildChannelBucketKey(safeIndex, channelName);
let bucket = channelBuckets.get(bucketKey);
if (!bucket) {
bucket = {
key: bucketKey,
id: buildChannelTabId(bucketKey),
index: safeIndex,
label: channelName || String(safeIndex),
entries: [],
hasExplicitName: Boolean(channelName),
isPrimaryFallback: bucketKey === '0'
};
channelBuckets.set(bucketKey, bucket);
} else if (channelName && !bucket.hasExplicitName) {
bucket.label = channelName;
bucket.hasExplicitName = true;
}
@@ -164,16 +165,32 @@ export function buildChatTabModel({
logEntries.sort((a, b) => a.ts - b.ts);
if (!channelBuckets.has(0)) {
channelBuckets.set(0, {
let hasPrimaryBucket = false;
for (const bucket of channelBuckets.values()) {
if (bucket.index === 0) {
hasPrimaryBucket = true;
break;
}
}
if (!hasPrimaryBucket) {
const bucketKey = '0';
channelBuckets.set(bucketKey, {
key: bucketKey,
id: buildChannelTabId(bucketKey),
index: 0,
label: '0',
entries: [],
hasExplicitName: false
hasExplicitName: false,
isPrimaryFallback: true
});
}
const channels = Array.from(channelBuckets.values()).sort((a, b) => a.index - b.index);
const channels = Array.from(channelBuckets.values()).sort((a, b) => {
if (a.index !== b.index) {
return a.index - b.index;
}
return a.label.localeCompare(b.label);
});
for (const channel of channels) {
channel.entries.sort((a, b) => a.ts - b.ts);
}
@@ -290,6 +307,52 @@ export function normaliseChannelName(value) {
return null;
}
function buildChannelBucketKey(index, channelName) {
const safeIndex = Number.isFinite(index) ? Math.max(0, Math.trunc(index)) : 0;
if (safeIndex === 0 && channelName) {
return `0::${channelName.toLowerCase()}`;
}
return String(safeIndex);
}
function buildChannelTabId(bucketKey) {
if (bucketKey === '0') {
return 'channel-0';
}
const slug = slugify(bucketKey);
if (slug) {
if (slug !== '0') {
return `channel-${slug}`;
}
return `channel-${slug}-${hashChannelKey(bucketKey)}`;
}
return `channel-${hashChannelKey(bucketKey)}`;
}
function slugify(value) {
if (value == null) return '';
return String(value)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
}
function hashChannelKey(value) {
const input = String(value ?? '');
if (!input) {
return '0';
}
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = (hash * 31 + input.charCodeAt(i)) | 0;
}
if (hash < 0) {
hash = (hash * -1) >>> 0;
}
return hash.toString(36);
}
export const __test__ = {
resolveTimestampSeconds,
normaliseChannelIndex,

View File

@@ -2743,13 +2743,15 @@ let messagesById = new Map();
});
const channelTabs = filteredChannels.map(channel => ({
id: `channel-${channel.index}`,
id: channel.id || `channel-${channel.index}`,
label: channel.label,
content: buildChatFragment({
entries: channel.entries.map(e => ({ ts: e.ts, item: e.message })),
renderEntry: entry => createMessageChatEntry(entry.item),
emptyLabel: 'No messages on this channel.'
})
}),
index: channel.index,
isPrimaryFallback: Boolean(channel.isPrimaryFallback)
}));
const tabs = [
@@ -2758,7 +2760,11 @@ let messagesById = new Map();
];
const previousActive = chatEl.dataset?.activeTab || null;
const defaultActive = channelTabs.find(tab => tab.id === 'channel-0')?.id || channelTabs[0]?.id || 'log';
const defaultActive =
channelTabs.find(tab => tab.isPrimaryFallback)?.id ||
channelTabs.find(tab => tab.index === 0)?.id ||
channelTabs[0]?.id ||
'log';
renderChatTabs({
document,
container: chatEl,