diff --git a/apps/web/src/components/chat/ChatList.tsx b/apps/web/src/components/chat/ChatList.tsx
index 5063683a..0bf8815b 100644
--- a/apps/web/src/components/chat/ChatList.tsx
+++ b/apps/web/src/components/chat/ChatList.tsx
@@ -103,7 +103,11 @@ export const ChatList = memo(function ChatList({
isLoading: areConnectionsLoading,
isError: areConnectionsUnavailable,
} = useWhatsAppConnections();
- const { data: channelAccounts = [] } = useChannelAccounts();
+ const {
+ data: channelAccounts = [],
+ isLoading: areChannelAccountsLoading,
+ isError: areChannelAccountsUnavailable,
+ } = useChannelAccounts();
useEffect(() => {
if (
@@ -253,8 +257,9 @@ export const ChatList = memo(function ChatList({
]);
const connectionState = resolveInboxConnectionState({
connections,
- isLoading: areConnectionsLoading,
- isError: areConnectionsUnavailable,
+ channelAccounts,
+ isLoading: areConnectionsLoading || areChannelAccountsLoading,
+ isError: areConnectionsUnavailable || areChannelAccountsUnavailable,
});
const isFirstRun =
connectionState === "no-connections" && visibleChats.length === 0;
@@ -750,9 +755,9 @@ export const ChatList = memo(function ChatList({
)}
- {/* First-run state - WhatsApp has not been linked yet. This is also
- the primary mobile onboarding surface, where the main pane is not
- visible until a conversation is selected. */}
+ {/* First-run state - no provider account has been linked yet. This is
+ also the primary mobile onboarding surface, where the main pane is
+ not visible until a conversation is selected. */}
{!isLoading &&
!isError &&
isFirstRun &&
@@ -783,12 +788,12 @@ export const ChatList = memo(function ChatList({
{can("can_manage_connections")
? t(
- "chat.connectWhatsappSidebarHint",
- "Connect one WhatsApp account and conversations will begin appearing here automatically.",
+ "chat.connectChannelsSidebarHint",
+ "Connect WhatsApp or Telegram and conversations will begin appearing here automatically.",
)
: t(
- "chat.askAdminToConnectShort",
- "Ask a workspace owner or admin to connect a WhatsApp account.",
+ "chat.askAdminToConnectChannelShort",
+ "Ask a workspace owner or admin to connect a channel.",
)}
@@ -800,14 +805,14 @@ export const ChatList = memo(function ChatList({
{t(
- "chat.sidebarConnectStep",
- "Connect a WhatsApp account",
+ "chat.sidebarConnectChannelStep",
+ "Connect your first channel",
)}
{t(
- "chat.sidebarConnectStepHint",
- "Pair securely by scanning a QR code",
+ "chat.sidebarConnectChannelStepHint",
+ "Choose WhatsApp or Telegram to start",
)}
@@ -835,7 +840,7 @@ export const ChatList = memo(function ChatList({
)}
className="mt-6 inline-flex h-11 items-center justify-center gap-2 rounded-xl bg-[#14795e] px-4 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[#0f684f] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00a884] focus-visible:ring-offset-2 dark:bg-[#20b68e] dark:text-[#071b16] dark:hover:bg-[#35c59e] dark:focus-visible:ring-offset-dark-secondary md:hidden"
>
- {t("chat.connectWhatsapp", "Connect WhatsApp")}
+ {t("chat.connectChannels", "Connect channels")}
)}
diff --git a/apps/web/src/components/chat/inbox-connection-state.test.ts b/apps/web/src/components/chat/inbox-connection-state.test.ts
index a1dd9203..cb90cfff 100644
--- a/apps/web/src/components/chat/inbox-connection-state.test.ts
+++ b/apps/web/src/components/chat/inbox-connection-state.test.ts
@@ -45,4 +45,23 @@ describe("resolveInboxConnectionState", () => {
}),
).toBe("offline");
});
+
+ it("never shows first-run setup when any provider account is already known", () => {
+ expect(
+ resolveInboxConnectionState({
+ connections: [],
+ channelAccounts: [{ status: "connected" }],
+ isLoading: true,
+ isError: false,
+ }),
+ ).toBe("connected");
+ expect(
+ resolveInboxConnectionState({
+ connections: [],
+ channelAccounts: [{ status: "paused" }],
+ isLoading: false,
+ isError: true,
+ }),
+ ).toBe("offline");
+ });
});
diff --git a/apps/web/src/components/chat/inbox-connection-state.ts b/apps/web/src/components/chat/inbox-connection-state.ts
index 9a008569..a0c97a3d 100644
--- a/apps/web/src/components/chat/inbox-connection-state.ts
+++ b/apps/web/src/components/chat/inbox-connection-state.ts
@@ -10,22 +10,30 @@ interface ConnectionStatus {
}
/**
- * Resolves the inbox landing state without treating a failed or pending query
- * as proof that a workspace has never linked WhatsApp.
+ * Resolves the inbox landing state across every provider without treating a
+ * failed or pending query as proof that a workspace has never linked a channel.
*/
export function resolveInboxConnectionState({
connections,
+ channelAccounts = [],
isLoading,
isError,
}: {
connections: readonly ConnectionStatus[];
+ channelAccounts?: readonly ConnectionStatus[];
isLoading: boolean;
isError: boolean;
}): InboxConnectionState {
+ // A known account wins over another provider source that is still loading
+ // or unavailable. Otherwise a connected Telegram bot plus an empty or
+ // failed WhatsApp query can incorrectly reopen first-run onboarding.
+ const accounts = [...connections, ...channelAccounts];
+ if (accounts.length > 0) {
+ return accounts.some((connection) => connection.status === "connected")
+ ? "connected"
+ : "offline";
+ }
if (isError) return "unavailable";
if (isLoading) return "loading";
- if (connections.length === 0) return "no-connections";
- return connections.some((connection) => connection.status === "connected")
- ? "connected"
- : "offline";
+ return "no-connections";
}
diff --git a/apps/web/src/locales/en.json b/apps/web/src/locales/en.json
index 6114d84d..816d6981 100644
--- a/apps/web/src/locales/en.json
+++ b/apps/web/src/locales/en.json
@@ -299,6 +299,21 @@
"whatsappSynced": "WhatsApp synced",
"whatsappOffline": "WhatsApp needs attention",
"checkingWhatsapp": "Checking WhatsApp connection",
+ "checkingChannels": "Checking connected channels",
+ "sharedInboxSetup": "Shared inbox setup",
+ "connectChannelsTitle": "Connect your first channel",
+ "connectChannelsHint": "Bring WhatsApp and Telegram into one team inbox. Messages, assignments, and follow-ups stay together as you add more channels.",
+ "connectChannels": "Connect channels",
+ "askAdminToConnectChannel": "Ask a workspace owner or admin to connect a channel. Conversations will appear here once an account is linked.",
+ "channelCredentialsHint": "Channel credentials are encrypted, and each connected account stays under your control.",
+ "chooseChannelTitle": "Choose where to start",
+ "chooseChannelHint": "Connect the channels your customers already use.",
+ "whatsappChannel": "WhatsApp",
+ "telegramChannel": "Telegram Bot",
+ "readyNow": "Ready now",
+ "whatsappConnectHint": "Pair securely with a QR code",
+ "telegramConnectHint": "Add a bot token from BotFather",
+ "moreChannelsSoon": "More channels are on the way",
"connectWhatsappEyebrow": "One-minute setup",
"connectWhatsappTitle": "Bring WhatsApp into your team inbox",
"connectWhatsappHint": "Link your WhatsApp account once. Conversations will sync here so your team can reply, assign, and follow up together.",
@@ -315,9 +330,13 @@
"oneStepRemaining": "One step remaining",
"sidebarSetupTitle": "Your inbox is almost ready",
"connectWhatsappSidebarHint": "Connect one WhatsApp account and conversations will begin appearing here automatically.",
+ "connectChannelsSidebarHint": "Connect WhatsApp or Telegram and conversations will begin appearing here automatically.",
"askAdminToConnectShort": "Ask a workspace owner or admin to connect a WhatsApp account.",
+ "askAdminToConnectChannelShort": "Ask a workspace owner or admin to connect a channel.",
"sidebarConnectStep": "Connect a WhatsApp account",
"sidebarConnectStepHint": "Pair securely by scanning a QR code",
+ "sidebarConnectChannelStep": "Connect your first channel",
+ "sidebarConnectChannelStepHint": "Choose WhatsApp or Telegram to start",
"sidebarReadyStep": "Chats and groups sync automatically",
"sidebarContinueHint": "Continue setup in the main panel. It usually takes about a minute.",
"selectChatHint": "Select a chat from the list to begin",
diff --git a/apps/web/src/locales/zh-CN.json b/apps/web/src/locales/zh-CN.json
index 1b3f9197..b238b2dd 100644
--- a/apps/web/src/locales/zh-CN.json
+++ b/apps/web/src/locales/zh-CN.json
@@ -299,6 +299,21 @@
"whatsappSynced": "已与 WhatsApp 同步",
"whatsappOffline": "WhatsApp 连接需要处理",
"checkingWhatsapp": "正在检查 WhatsApp 连接",
+ "checkingChannels": "正在检查已连接的渠道",
+ "sharedInboxSetup": "共享收件箱设置",
+ "connectChannelsTitle": "连接你的第一个渠道",
+ "connectChannelsHint": "将 WhatsApp 和 Telegram 接入同一个团队收件箱。随着更多渠道加入,消息、分配和跟进始终集中管理。",
+ "connectChannels": "连接渠道",
+ "askAdminToConnectChannel": "请联系工作区所有者或管理员连接渠道。账号关联后,会话将显示在这里。",
+ "channelCredentialsHint": "渠道凭据会被加密,每个已连接账号始终由你掌控。",
+ "chooseChannelTitle": "选择起始渠道",
+ "chooseChannelHint": "连接客户已经在使用的渠道。",
+ "whatsappChannel": "WhatsApp",
+ "telegramChannel": "Telegram 机器人",
+ "readyNow": "现在可用",
+ "whatsappConnectHint": "扫描二维码安全配对",
+ "telegramConnectHint": "添加 BotFather 提供的机器人令牌",
+ "moreChannelsSoon": "更多渠道即将上线",
"connectWhatsappEyebrow": "一分钟完成设置",
"connectWhatsappTitle": "将 WhatsApp 接入团队收件箱",
"connectWhatsappHint": "只需关联一次 WhatsApp 账号,会话便会同步到这里,供团队共同回复、分配和跟进。",
@@ -315,9 +330,13 @@
"oneStepRemaining": "还剩一个步骤",
"sidebarSetupTitle": "收件箱即将就绪",
"connectWhatsappSidebarHint": "连接一个 WhatsApp 账号后,会话会自动显示在这里。",
+ "connectChannelsSidebarHint": "连接 WhatsApp 或 Telegram 后,会话会自动显示在这里。",
"askAdminToConnectShort": "请联系工作区所有者或管理员连接 WhatsApp 账号。",
+ "askAdminToConnectChannelShort": "请联系工作区所有者或管理员连接渠道。",
"sidebarConnectStep": "连接 WhatsApp 账号",
"sidebarConnectStepHint": "扫描二维码进行安全配对",
+ "sidebarConnectChannelStep": "连接你的第一个渠道",
+ "sidebarConnectChannelStepHint": "选择 WhatsApp 或 Telegram 开始",
"sidebarReadyStep": "聊天和群组将自动同步",
"sidebarContinueHint": "请在主面板继续设置,通常约一分钟即可完成。",
"selectChatHint": "从列表中选择一个会话开始",
diff --git a/apps/web/src/pages/ChatPage.tsx b/apps/web/src/pages/ChatPage.tsx
index 319a2c9c..3a82e979 100644
--- a/apps/web/src/pages/ChatPage.tsx
+++ b/apps/web/src/pages/ChatPage.tsx
@@ -1,3 +1,4 @@
+import type { WhatsAppConnectionIdentity } from "@wateaminbox/shared";
import {
ArrowLeft,
ArrowRight,
@@ -6,16 +7,17 @@ import {
MessagesSquare,
QrCode,
RotateCcw,
+ Send,
UsersRound,
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router";
import { toast } from "sonner";
-import { ChatSidebar, type SidebarView } from "../components/chat/ChatSidebar";
-import { channelDisplayName } from "../components/connections/channel-catalog";
import { ChannelComposerGate } from "../components/chat/ChannelComposerGate";
+import { ChatSidebar, type SidebarView } from "../components/chat/ChatSidebar";
import { ComposerLifecycleArea } from "../components/chat/ComposerLifecycleArea";
+import { ConversationLifecycleActions } from "../components/chat/ConversationLifecycleActions";
import { ConversationSearch } from "../components/chat/ConversationSearch";
import { ContactProfile } from "../components/chat/contact-profile";
import { DeleteMessageDialog } from "../components/chat/DeleteMessageDialog";
@@ -28,6 +30,7 @@ import { MessageComposer } from "../components/chat/MessageComposer";
import { MessageHeader } from "../components/chat/MessageHeader";
import { MessageThread } from "../components/chat/MessageThread";
import { SharedContactSheet } from "../components/chat/SharedContactSheet";
+import { channelDisplayName } from "../components/connections/channel-catalog";
import { AppLayout, ResponsiveLayout } from "../components/layout/app-layout";
import { CONVERSATION_HEADER_INSET_CLASS } from "../components/layout/conversation-chrome";
import { MainContent } from "../components/layout/main-content";
@@ -40,20 +43,21 @@ import {
} from "../contexts/message-actions-context";
import { useWorkspace } from "../contexts/workspace-context";
import { useChatPageState } from "../hooks/chat";
-import type { WhatsAppConnectionIdentity } from "@wateaminbox/shared";
-import { ConversationLifecycleActions } from "../components/chat/ConversationLifecycleActions";
import { useCustomerChats } from "../hooks/contact/useCustomerChats";
import { useKeyboardInset } from "../hooks/ui";
-import { useChannelAccountCapabilities } from "../hooks/useChannelAccounts";
+import {
+ useChannelAccountCapabilities,
+ useChannelAccounts,
+} from "../hooks/useChannelAccounts";
import { useChannelConversation } from "../hooks/useChannelConversations";
import { useComposerAccess } from "../hooks/useComposerAccess";
import { useCreateContact } from "../hooks/useContact";
import { useGroup } from "../hooks/useGroups";
import { useWhatsAppConnectionsList } from "../hooks/whatsapp";
import { sendChannelMessage } from "../lib/api/channel-conversations";
-import { transformChannelConversationToChat } from "../lib/api/transformers";
-import { uploadMedia } from "../lib/api/messages";
import { ApiRequestError } from "../lib/api/client";
+import { uploadMedia } from "../lib/api/messages";
+import { transformChannelConversationToChat } from "../lib/api/transformers";
import { cn } from "../lib/utils";
import {
parseChatView,
@@ -76,13 +80,19 @@ export function ChatPage() {
isLoading: areConnectionsLoading,
isError: areConnectionsUnavailable,
} = useWhatsAppConnectionsList();
+ const {
+ data: channelAccounts = [],
+ isLoading: areChannelAccountsLoading,
+ isError: areChannelAccountsUnavailable,
+ } = useChannelAccounts();
const inboxConnectionState = resolveInboxConnectionState({
connections,
- isLoading: areConnectionsLoading,
- isError: areConnectionsUnavailable,
+ channelAccounts,
+ isLoading: areConnectionsLoading || areChannelAccountsLoading,
+ isError: areConnectionsUnavailable || areChannelAccountsUnavailable,
});
const canManageConnections = can("can_manage_connections");
- const handleConnectWhatsApp = useCallback(() => {
+ const handleConnectChannels = useCallback(() => {
if (!activeWorkspace || !canManageConnections) return;
navigate(workspacePath(activeWorkspace.id, "settings", "connections"));
}, [activeWorkspace, canManageConnections, navigate]);
@@ -344,7 +354,7 @@ export function ChatPage() {
{!selectedChatId && inboxConnectionState === "no-connections" && (
)}
@@ -721,7 +731,7 @@ function InboxConnectionLoadingState() {
-
+
- {t("chat.connectWhatsappEyebrow", "One-minute setup")}
+ {t("chat.sharedInboxSetup", "Shared inbox setup")}
- {t(
- "chat.connectWhatsappTitle",
- "Bring WhatsApp into your team inbox",
- )}
+ {t("chat.connectChannelsTitle", "Connect your first channel")}
{canManageConnections
? t(
- "chat.connectWhatsappHint",
- "Link your WhatsApp account once. Conversations will sync here so your team can reply, assign, and follow up together.",
+ "chat.connectChannelsHint",
+ "Bring WhatsApp and Telegram into one team inbox. Messages, assignments, and follow-ups stay together as you add more channels.",
)
: t(
- "chat.askAdminToConnect",
- "Ask a workspace owner or admin to connect WhatsApp. Conversations will appear here once an account is linked.",
+ "chat.askAdminToConnectChannel",
+ "Ask a workspace owner or admin to connect a channel. Conversations will appear here once an account is linked.",
)}
@@ -802,7 +809,7 @@ function InboxFirstRunState({
onClick={onConnect}
className="mt-8 inline-flex h-12 w-fit items-center gap-3 rounded-xl bg-[#14795e] px-5 text-sm font-semibold text-white shadow-[0_14px_30px_-16px_rgba(20,121,94,0.9)] transition-all hover:-translate-y-0.5 hover:bg-[#0f684f] hover:shadow-[0_18px_34px_-16px_rgba(20,121,94,0.95)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00a884] focus-visible:ring-offset-2 dark:bg-[#20b68e] dark:text-[#071b16] dark:hover:bg-[#35c59e] dark:focus-visible:ring-offset-[#111d22]"
>
- {t("chat.connectWhatsapp", "Connect WhatsApp")}
+ {t("chat.connectChannels", "Connect channels")}
@@ -813,61 +820,100 @@ function InboxFirstRunState({
{t(
- "chat.securePairingHint",
- "Secure QR pairing—your phone stays in control of the account.",
+ "chat.channelCredentialsHint",
+ "Channel credentials are encrypted, and each connected account stays under your control.",
)}
-
-
-
- {t("chat.onYourPhone", "On your phone")}
-
-
- {t("chat.keepPhoneNearby", "Keep WhatsApp nearby")}
-
-
-
- {t("chat.aboutOneMinute", "≈ 1 minute")}
-
+
+
+ {t("chat.chooseChannelTitle", "Choose where to start")}
+
+
+ {t(
+ "chat.chooseChannelHint",
+ "Connect the channels your customers already use.",
+ )}
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {t("chat.whatsappChannel", "WhatsApp")}
+
+
+ {t("chat.readyNow", "Ready now")}
+
+
+
+ {t(
+ "chat.whatsappConnectHint",
+ "Pair securely with a QR code",
+ )}
+
-
-
-
-
- {[
- t("chat.openWhatsappStep", "Open WhatsApp"),
- t("chat.linkedDevicesStep", "Linked devices"),
- t("chat.scanCodeStep", "Scan code"),
- ].map((label, index) => (
- -
-
- {index + 1}
-
-
- {label}
+
+
+
+
+
-
- ))}
-
+
+
+
+ {t("chat.telegramChannel", "Telegram Bot")}
+
+
+ {t("chat.readyNow", "Ready now")}
+
+
+
+ {t(
+ "chat.telegramConnectHint",
+ "Add a bot token from BotFather",
+ )}
+
+
+
+
+
+
+
+
+ {["#df8b63", "#7b9bd1", "#b58bca"].map((color) => (
+
+ ))}
+
+
+ {t("chat.moreChannelsSoon", "More channels are on the way")}
+
+