fast integration
This page starts from scratch with a web page that can send and receive messages: log in to IM, display a list of conversations, open a conversation to view messages, send a text message, alert when new messages arrive, and log out. When you're done, you'll get a run-of-the-box example that you can build on to develop your own interface.
preparatory work
- In the console, use the
demoapplication created automatically at registration, or create an application. Under "Access credentials" on the Overview, copy the AppKey (such as1575529652#demo) and the address of the IM service, which is the "REST endpoint" (such ashttps://im.example.com). - Service server accessServer-side REST APITo provide an interface for "obtaining IM login credentials": after the user logs in to your business system, the web page calls this interface, and the business server is the current user.Sign logon credentialsand return.
- Prepare a front-end project. This page uses the Vite Create a native TypeScript project, other build tools are also applicable:
npm create vite@latest im-demo -- --template vanilla-ts
cd im-demoDon't want to take the project can also be downloaded directlyWeb Demo, unzip it and host it on any static server, fill in the AppKey and service address to experience the full capabilities; the steps on this page are used to understand the access process.
The interface of the business server to issue the certificate is roughly as follows (Node.js,/api/im-ticket Is the address of your own definition, the identity of the user from your business system login status):
// 业务服务端:为已登录你的业务系统的用户签发 IM 登录凭证
app.post('/api/im-ticket', async (req, res) => {
const username = req.session.userId; // 你的业务系统中的当前用户
const r = await fetch(`${IM_API}/${ORG}/${APP}/users/${encodeURIComponent(username)}/login-tickets`, {
method: 'POST',
headers: { Authorization: `Bearer ${appToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ auto_create: true }), // 用户不存在时自动创建
});
const { ticket } = await r.json();
res.json({ ticket });
});The credential is valid for 5 minutes and can only be used once. It is equivalent to a user's temporary password and is only returned to the user's web page via HTTPS. App tokens can only be saved on the server, not on the web page.
Development and testing, you can also use curl Issue a certificateA that is copied to a Web page for use.
install
npm install @deeprespond/im-webcreate a client
Create the client in a separate module that is common to the entire page:
// src/im.ts
import { createDRClient } from '@deeprespond/im-web';
export const im = createDRClient({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
});create that client without any req. the SDK will immediately restore the login to any browser that has been log in.im.auth.currentUser is not nullNo need to register again.
Sign in
Obtain the login certificate from the business server and submit it to the SDK for login:
import { createDRClient } from '@deeprespond/im-web';
async function fetchTicket(): Promise<string> {
const res = await fetch('/api/im-ticket', { method: 'POST', credentials: 'include' });
const { ticket } = (await res.json()) as { ticket: string };
return ticket;
}
const im = createDRClient({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
// 凭证失效、或登录状态过期时,SDK 用它重新取一次凭证
ticketProvider: () => fetchTicket(),
});
if (!im.auth.currentUser) {
const { user } = await im.auth.loginWithTicket({ ticket: await fetchTicket() });
console.log('已登录', user.username);
}After a successful login, the SDK connects the long connection and synchronizes the data in the background. The login is automatically restored when the user closes the page and then opens it again, so the SDK only im.auth.currentUser act as null login is required only when the login fails. SeeInitialization, login and connection。
Show session list
im.conversations.list() Returns a list of conversations you can subscribe to, sorted by the time of the top and most recent message. Data is read locally and automatically updated and notified when new data is synchronized:
import { conversationTitle } from '@deeprespond/im-web/render';
const list = im.conversations.list();
list.subscribe(() => {
const { items, loading } = list.getSnapshot();
for (const c of items) {
console.log(conversationTitle(im, c), c.unread_count, c.last_message?.type);
}
if (loading) console.log('加载中');
});conversationTitle Give the title of the conversation: the comment or nickname of the other party for single chat, and the group name for Group chats. The list must be created after logging in, and the call will throw when not logged in not_signed_in。
Open a session and display messages
When the user clicks on a session, the session is updated with its conversation_key Open the message list; to start a chat with someone, use the openSingle Get a single chat with him:
// 从会话列表中打开
const key = list.getSnapshot().items[0]!.conversation_key;
// 或者与某个人开始聊天(还没有会话时得到一个本地的草稿会话)
const view = await im.conversations.openSingle('lisi');
const messages = im.messages.open(view.conversation_key);
messages.subscribe(() => {
for (const m of messages.getSnapshot().items) {
const text = m.type === 'text' ? String(m.body?.text ?? '') : `[${m.type}]`;
console.log(m.sender, text, m._local.status);
}
});
messages.setViewing({ visible: true, atBottom: true }); // 用户正在看这个会话的最新消息
messages.markRead(); // 把这个会话标记为已读- Messages are ordered, with the newest last; messages that are still being sent are ordered last.
_local.statusact assending、failedAnd so on. - Open the display local latest 50, when the page up call
messages.loadOlder()。 - Called when you leave this session
messages.dispose()Releases the list.
sending a text message
const sent = await im.messages.send(
{ conversation_key: messages.conversation_key },
{ type: 'text', body: { text: '你好' } },
);
console.log(sent.client_msg_id, sent._local.status); // queued:已进入发送队列send Messages are returned immediately after they enter the send queue and appear in the open message list. The SDK is then responsible for sending and retrying the message after failure. You are notified of status changes through the message list. Messages are left in the queue when the network is disconnected and are sent automatically after recovery. You can also send the message directly to a person or a group without opening a session:{ to_user: 'lisi' }、{ to_group: '群 ID' }。
Listen for new messages and connection status
im.on('message.received', ({ message, alert }) => {
// alert 为 true 时由本标签页负责提醒,避免多个标签页重复弹出通知
if (alert && document.hidden) showToast(`${message.sender} 发来一条新消息`);
});
im.on('connection.stateChanged', (state) => {
// ready:已连接并同步完成;waiting:等待重连;offline:网络不可用
render(state.kind === 'ready' ? '' : state.kind === 'offline' ? '网络不可用' : '连接中…');
});
im.on('auth.stateChanged', (state) => {
if (state.kind === 'ended') render('登录已失效,请重新登录', state.reason);
});When a new message arrives, the open conversation list and message list will be updated automatically, don't need you to deal with in the event;message.received Only used for pop-up notifications, play alert tones, etc. SeeEvent and error handling。
quit
await im.auth.logout();After exiting, the SDK clears the local login status, disconnects long connections, empties the open list and releases it, and exits all other tabs in the same browser. On computers shared by multiple users, you can use the logout({ clearLocalData: true }) At the same time, delete the user in the local chat.
complete example
Put the following two files into the Vite project created above, replace AppKey, service address and interface for obtaining credentials, and run npm run dev Use two browsers (or one normal window and one privacy window) to log in as two users and send messages to each other.
<!-- index.html -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>IM 示例</title>
</head>
<body>
<p id="status">未登录</p>
<button id="logout">退出</button>
<ul id="conversations"></ul>
<form id="open"><input id="peer" placeholder="对方用户名"> <button>开始聊天</button></form>
<div id="messages"></div>
<form id="send"><input id="text" placeholder="消息"> <button>发送</button></form>
<script type="module" src="/src/main.ts"></script>
</body>
</html>// src/main.ts
import { createDRClient, DRError, type LiveList, type ConversationView, type MessageList } from '@deeprespond/im-web';
import { conversationTitle, displayName, summarize } from '@deeprespond/im-web/render';
import { describeError } from '@deeprespond/im-web/locale/zh-CN';
const $ = (id: string) => document.getElementById(id) as HTMLElement;
const value = (id: string) => (document.getElementById(id) as HTMLInputElement).value.trim();
async function fetchTicket(): Promise<string> {
const res = await fetch('/api/im-ticket', { method: 'POST', credentials: 'include' });
return ((await res.json()) as { ticket: string }).ticket;
}
const im = createDRClient({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
ticketProvider: () => fetchTicket(),
});
// ---- 状态 ----
function showStatus() {
const user = im.auth.currentUser?.username ?? '未登录';
$('status').textContent = `${user} · ${im.connection.state.kind}`;
}
im.on('connection.stateChanged', showStatus);
im.on('auth.stateChanged', (state) => {
showStatus();
if (state.kind === 'signed_in') startConversationList();
if (state.kind === 'ended') alert(describeError({ code: state.code, reason: state.reason }));
});
// 另一个标签页登录了别的用户:列表已被清空释放,按新用户重新创建
im.on('auth.userSwitched', () => startConversationList());
// ---- 会话列表 ----
let conversations: LiveList<ConversationView> | null = null;
function startConversationList() {
conversations?.dispose();
conversations = im.conversations.list();
conversations.subscribe(renderConversations);
renderConversations();
}
function renderConversations() {
const me = im.auth.currentUser?.username;
const items = conversations?.getSnapshot().items ?? [];
$('conversations').replaceChildren(...items.map((c) => {
const li = document.createElement('li');
const last = c.last_message ? summarize(c.last_message, { nameOf: (u) => displayName(im, u), self: me }) : '';
li.textContent = `${conversationTitle(im, c)}${c.unread_count ? ` (${c.unread_count})` : ''}:${last}`;
li.onclick = () => openConversation(c.conversation_key);
return li;
}));
}
// ---- 消息列表 ----
let current: MessageList | null = null;
function openConversation(key: string) {
current?.dispose();
const list = im.messages.open(key);
current = list;
const renderMessages = () => {
$('messages').replaceChildren(...list.getSnapshot().items.map((m) => {
const p = document.createElement('p');
const text = m.type === 'text' ? String(m.body?.text ?? '') : summarize(m, { nameOf: (u) => displayName(im, u) });
p.textContent = `${displayName(im, m.sender)}:${text}${m._local.status === 'sent' ? '' : `(${m._local.status})`}`;
return p;
}));
};
list.subscribe(renderMessages);
renderMessages();
list.setViewing({ visible: true, atBottom: true });
list.markRead();
}
$('open').onsubmit = async (e) => {
e.preventDefault();
const view = await im.conversations.openSingle(value('peer'));
openConversation(view.conversation_key);
};
$('send').onsubmit = async (e) => {
e.preventDefault();
const text = value('text');
if (!current || !text) return;
(document.getElementById('text') as HTMLInputElement).value = '';
try {
await im.messages.send({ conversation_key: current.conversation_key }, { type: 'text', body: { text } });
} catch (err) {
alert(err instanceof DRError ? describeError(err) : String(err));
}
};
// ---- 新消息提醒 ----
im.on('message.received', ({ message, alert: isAlertTab }) => {
if (isAlertTab && document.hidden) document.title = `新消息 - ${displayName(im, message.sender)}`;
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) document.title = 'IM 示例';
});
// ---- 退出 ----
$('logout').onclick = async () => {
current?.dispose();
current = null;
$('messages').replaceChildren();
await im.auth.logout();
};
// ---- 启动:已登录的自动恢复,否则用凭证登录 ----
showStatus();
if (im.auth.currentUser) {
startConversationList();
} else {
try {
await im.auth.loginWithTicket({ ticket: await fetchTicket() });
} catch (err) {
$('status').textContent = err instanceof DRError ? describeError(err) : String(err);
}
}In this example:
- SDK issued when login is successful
auth.stateChanged(signed_in), examples create a conversation list accordingly; logged pages are created directly; - The display of conversation lists and message lists depends only on
getSnapshot(), receiving new messages, sending status changes, read on other devices, etc. will be automatically reflected; - Open a second tab on the same site and it will automatically be logged in, sharing a link with the first tab.
next step
- Initialization, login and connection: All initialization options, password login, reason for session end, connection status and multiple tabs;
- conversation、Messages: No reading, draft, picture and file messages, recall, read acknowledgement, etc.;
- Used in React and Vue: Manage lists and status with official bundles;
- Event and error handling: All events and error codes.
Access to independent channels
After client initialization and login, presschannel connectionJoin the channel created by the service server. Join requires application channel activation and target user authorization; default does not collect.
