Merge 13c657182ac8f5a7e781942e93f87314b4ff287e into 8a05c05596ca9ba0735dafd8e359885de4c2c969

This commit is contained in:
Ted Li 2026-03-20 22:53:20 -07:00 committed by GitHub
commit 10695667f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 137 additions and 9 deletions

View File

@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp } from "../format.ts";
import type { DiscordStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderDiscordCard(params: {
@ -10,6 +11,7 @@ export function renderDiscordCard(params: {
accountCountLabel: unknown;
}) {
const { props, discord, accountCountLabel } = params;
const configured = resolveChannelConfigured("discord", props);
return html`
<div class="card">
@ -20,7 +22,7 @@ export function renderDiscordCard(params: {
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${discord?.configured ? "Yes" : "No"}</span>
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>

View File

@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp } from "../format.ts";
import type { GoogleChatStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderGoogleChatCard(params: {
@ -10,6 +11,7 @@ export function renderGoogleChatCard(params: {
accountCountLabel: unknown;
}) {
const { props, googleChat, accountCountLabel } = params;
const configured = resolveChannelConfigured("googlechat", props);
return html`
<div class="card">
@ -20,7 +22,7 @@ export function renderGoogleChatCard(params: {
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${googleChat ? (googleChat.configured ? "Yes" : "No") : "n/a"}</span>
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>

View File

@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp } from "../format.ts";
import type { IMessageStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderIMessageCard(params: {
@ -10,6 +11,7 @@ export function renderIMessageCard(params: {
accountCountLabel: unknown;
}) {
const { props, imessage, accountCountLabel } = params;
const configured = resolveChannelConfigured("imessage", props);
return html`
<div class="card">
@ -20,7 +22,7 @@ export function renderIMessageCard(params: {
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${imessage?.configured ? "Yes" : "No"}</span>
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>

View File

@ -0,0 +1,88 @@
import { describe, expect, it } from "vitest";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
function createProps(snapshot: ChannelsProps["snapshot"]): ChannelsProps {
return {
connected: true,
loading: false,
snapshot,
lastError: null,
lastSuccessAt: null,
whatsappMessage: null,
whatsappQrDataUrl: null,
whatsappConnected: null,
whatsappBusy: false,
configSchema: null,
configSchemaLoading: false,
configForm: null,
configUiHints: {},
configSaving: false,
configFormDirty: false,
nostrProfileFormState: null,
nostrProfileAccountId: null,
onRefresh: () => {},
onWhatsAppStart: () => {},
onWhatsAppWait: () => {},
onWhatsAppLogout: () => {},
onConfigPatch: () => {},
onConfigSave: () => {},
onConfigReload: () => {},
onNostrProfileEdit: () => {},
onNostrProfileCancel: () => {},
onNostrProfileFieldChange: () => {},
onNostrProfileSave: () => {},
onNostrProfileImport: () => {},
onNostrProfileToggleAdvanced: () => {},
};
}
describe("resolveChannelConfigured", () => {
it("returns the channel summary configured flag when present", () => {
const props = createProps({
ts: Date.now(),
channelOrder: ["discord"],
channelLabels: { discord: "Discord" },
channels: { discord: { configured: false } },
channelAccounts: {
discord: [{ accountId: "discord-main", configured: true }],
},
channelDefaultAccountId: { discord: "discord-main" },
});
expect(resolveChannelConfigured("discord", props)).toBe(false);
});
it("falls back to the default account when the channel summary omits configured", () => {
const props = createProps({
ts: Date.now(),
channelOrder: ["discord"],
channelLabels: { discord: "Discord" },
channels: { discord: { running: true } },
channelAccounts: {
discord: [
{ accountId: "default", configured: false },
{ accountId: "discord-main", configured: true },
],
},
channelDefaultAccountId: { discord: "discord-main" },
});
expect(resolveChannelConfigured("discord", props)).toBe(true);
});
it("falls back to the first account when no default account id is available", () => {
const props = createProps({
ts: Date.now(),
channelOrder: ["slack"],
channelLabels: { slack: "Slack" },
channels: { slack: { running: true } },
channelAccounts: {
slack: [{ accountId: "workspace-a", configured: true }],
},
channelDefaultAccountId: {},
});
expect(resolveChannelConfigured("slack", props)).toBe(true);
});
});

View File

@ -19,6 +19,28 @@ export function channelEnabled(key: ChannelKey, props: ChannelsProps) {
return configured || running || connected || accountActive;
}
export function resolveChannelConfigured(key: ChannelKey, props: ChannelsProps): boolean | null {
const snapshot = props.snapshot;
const channels = snapshot?.channels as Record<string, unknown> | null;
const channelStatus = channels?.[key] as Record<string, unknown> | undefined;
if (typeof channelStatus?.configured === "boolean") {
return channelStatus.configured;
}
const accounts = snapshot?.channelAccounts?.[key] ?? [];
const defaultAccountId = snapshot?.channelDefaultAccountId?.[key];
const defaultAccount =
(defaultAccountId
? accounts.find((account) => account.accountId === defaultAccountId)
: undefined) ?? accounts[0];
if (typeof defaultAccount?.configured === "boolean") {
return defaultAccount.configured;
}
return null;
}
export function getChannelAccountCount(
key: ChannelKey,
channelAccounts?: Record<string, ChannelAccountSnapshot[]> | null,

View File

@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp } from "../format.ts";
import type { SignalStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderSignalCard(params: {
@ -10,6 +11,7 @@ export function renderSignalCard(params: {
accountCountLabel: unknown;
}) {
const { props, signal, accountCountLabel } = params;
const configured = resolveChannelConfigured("signal", props);
return html`
<div class="card">
@ -20,7 +22,7 @@ export function renderSignalCard(params: {
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${signal?.configured ? "Yes" : "No"}</span>
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>

View File

@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp } from "../format.ts";
import type { SlackStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderSlackCard(params: {
@ -10,6 +11,7 @@ export function renderSlackCard(params: {
accountCountLabel: unknown;
}) {
const { props, slack, accountCountLabel } = params;
const configured = resolveChannelConfigured("slack", props);
return html`
<div class="card">
@ -20,7 +22,7 @@ export function renderSlackCard(params: {
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${slack?.configured ? "Yes" : "No"}</span>
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>

View File

@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp } from "../format.ts";
import type { ChannelAccountSnapshot, TelegramStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderTelegramCard(params: {
@ -12,6 +13,7 @@ export function renderTelegramCard(params: {
}) {
const { props, telegram, telegramAccounts, accountCountLabel } = params;
const hasMultipleAccounts = telegramAccounts.length > 1;
const configured = resolveChannelConfigured("telegram", props);
const renderAccountCard = (account: ChannelAccountSnapshot) => {
const probe = account.probe as { bot?: { username?: string } } | undefined;
@ -69,7 +71,7 @@ export function renderTelegramCard(params: {
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${telegram?.configured ? "Yes" : "No"}</span>
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>

View File

@ -19,7 +19,11 @@ import { renderDiscordCard } from "./channels.discord.ts";
import { renderGoogleChatCard } from "./channels.googlechat.ts";
import { renderIMessageCard } from "./channels.imessage.ts";
import { renderNostrCard } from "./channels.nostr.ts";
import { channelEnabled, renderChannelAccountCount } from "./channels.shared.ts";
import {
channelEnabled,
renderChannelAccountCount,
resolveChannelConfigured,
} from "./channels.shared.ts";
import { renderSignalCard } from "./channels.signal.ts";
import { renderSlackCard } from "./channels.slack.ts";
import { renderTelegramCard } from "./channels.telegram.ts";
@ -184,7 +188,7 @@ function renderGenericChannelCard(
) {
const label = resolveChannelLabel(props.snapshot, key);
const status = props.snapshot?.channels?.[key] as Record<string, unknown> | undefined;
const configured = typeof status?.configured === "boolean" ? status.configured : undefined;
const configured = resolveChannelConfigured(key, props);
const running = typeof status?.running === "boolean" ? status.running : undefined;
const connected = typeof status?.connected === "boolean" ? status.connected : undefined;
const lastError = typeof status?.lastError === "string" ? status.lastError : undefined;

View File

@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
import type { WhatsAppStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
import { resolveChannelConfigured } from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderWhatsAppCard(params: {
@ -10,6 +11,7 @@ export function renderWhatsAppCard(params: {
accountCountLabel: unknown;
}) {
const { props, whatsapp, accountCountLabel } = params;
const configured = resolveChannelConfigured("whatsapp", props);
return html`
<div class="card">
@ -20,7 +22,7 @@ export function renderWhatsAppCard(params: {
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${whatsapp?.configured ? "Yes" : "No"}</span>
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Linked</span>