Tencent IM Overview and Component Design for Instant Messaging Applications

This article provides a comprehensive technical guide on Tencent Cloud's instant messaging (IM) service, comparing UI‑integrated and non‑UI integration approaches, detailing the core chat and input components, their Vue/TypeScript implementations, rendering logic, event handling, and auxiliary features such as file upload simulation, scroll management, and voice/video calling.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Tencent IM Overview and Component Design for Instant Messaging Applications

Tencent IM Overview

Tencent is one of the earliest and largest instant‑messaging providers in China, offering QQ and WeChat. To support digital transformation, Tencent Cloud exposes high‑concurrency, high‑reliability IM capabilities via SDKs and REST APIs, allowing developers to embed chat functionality into their applications.

Access Methods

Tencent IM offers two integration modes:

UI‑integrated solution (quick to integrate, full feature set, but UI cannot be customized and component code may clash with existing projects).

Non‑UI solution (API‑only, lightweight code that matches project style, but developers must design the UI and implement basic chat logic).

IM Framework Design

The core of the chat UI consists of two components: the chat content area and the message input area.

<span style="line-height: 26px"><template></span>
<span style="line-height: 26px">  <div></span>
<span style="line-height: 26px">    <ChatContent /></span>
<span style="line-height: 26px">    <ChatFooter /></span>
<span style="line-height: 26px">  </div></span>
<span style="line-height: 26px"></template></span>

Component List

Chat box components include MessageLoadMore, MessageItem, MessageTimestamp, MessageTip, MessageBubble, MessageTool, MessageText, ProgressMessage, MessageImage, MessageFile, MessageVideo, MessageRevoked, and ScrollButton.

Message input components include MessageInputToolbar, EmojiPicker, ImageUpload, FileUpload, VideoUpload, VoiceCall, VideoCall, and MessageInputEditor.

Message Rendering Logic

The message list is fetched via getMessageList, which returns messageList, nextReqMessageID, and isCompleted. When isCompleted is false, a "Load more" button is displayed.

<span style="line-height: 26px">export interface IMResponseData {</span>
<span style="line-height: 26px">  /** 消息列表 */</span>
<span style="line-height: 26px">  messageList: any[]</span>
<span style="line-height: 26px">  /** 用于续拉,分页续拉时需传入该字段 */</span>
<span style="line-height: 26px">  nextReqMessageID: string</span>
<span style="line-height: 26px">  /** 表示是否已经拉完所有消息 */</span>
<span style="line-height: 26px">  isCompleted: boolean</span>
<span style="line-height: 26px">}</span>

Scrolling to a specific message after loading older messages is handled by scrollToPosition which uses element.scrollIntoView.

<span style="line-height: 26px">const scrollToPosition = async (config: ScrollConfig = {}): Promise<void> => {</span>
<span style="line-height: 26px">  return new Promise((resolve, reject) => {</span>
<span style="line-height: 26px">    requestAnimationFrame(() => {</span>
<span style="line-height: 26px">      const targetMessageDom = document.querySelector(`#tui-${config.scrollToMessage}`)</span>
<span style="line-height: 26px">      if (targetMessageDom?.scrollIntoView) {</span>
<span style="line-height: 26px">        targetMessageDom.scrollIntoView({ behavior: 'smooth' })</span>
<span style="line-height: 26px">      }</span>
<span style="line-height: 26px">      resolve()</span>
<span style="line-height: 26px">    })</span>
<span style="line-height: 26px">  })</span>
<span style="line-height: 26px">}</span>

Timestamp Formatting

Messages are grouped by time; the calculateTimestamp function formats timestamps as "hh:mm", "昨天 hh:mm", weekday, month/day, or year/month/day depending on recency.

<span style="line-height: 26px">function calculateTimestamp(timestamp: number): string {</span>
<span style="line-height: 26px">  const todayZero = new Date().setHours(0, 0, 0, 0)</span>
<span style="line-height: 26px">  const thisYear = new Date(new Date().getFullYear(), 0, 1).getTime()</span>
<span style="line-height: 26px">  const target = new Date(timestamp)</span>
<span style="line-height: 26px">  const oneDay = 24 * 60 * 60 * 1000</span>
<span style="line-height: 26px">  const oneWeek = 7 * oneDay</span>
<span style="line-height: 26px">  const diff = todayZero - target.getTime()</span>
<span style="line-height: 26px">  function formatNum(num: number): string { return num < 10 ? '0' + num : num.toString() }</span>
<span style="line-height: 26px">  if (diff <= 0) { return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}` }</span>
<span style="line-height: 26px">  else if (diff <= oneDay) { return `昨天 ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}` }</span>
<span style="line-height: 26px">  else if (diff <= oneWeek - oneDay) {</span>
<span style="line-height: 26px">    const weekdays = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']</span>
<span style="line-height: 26px">    const weekday = weekdays[target.getDay()]</span>
<span style="line-height: 26px">    return `${weekday} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`</span>
<span style="line-height: 26px">  } else if (target.getTime() >= thisYear) {</span>
<span style="line-height: 26px">    return `${target.getMonth() + 1}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`</span>
<span style="line-height: 26px">  } else {</span>
<span style="line-height: 26px">    return `${target.getFullYear()}/${target.getMonth() + 1}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`</span>
<span style="line-height: 26px">  }</span>
<span style="line-height: 26px">}</span>

System Tips, Bubbles, and Risk Content

System messages are rendered with MessageTip. Message bubbles are aligned left or right based on flow. Risky content (e.g., unsafe images) is replaced with a placeholder using the hasRiskContent flag.

<span style="line-height: 26px"><img v-if="item.type === TYPES.MSG_IMAGE && item.hasRiskContent" :src="riskImageReplaceUrl" alt="图片无法查看" /></span>
<span style="line-height: 26px"><div v-else>{{ riskContentText }}</div></span>

Loading Indicators and Failure Handling

When status === 'unSend', a loading icon is shown; when status === 'fail', a tooltip with a retry button appears.

<span style="line-height: 26px"><Icon v-if="item.status === 'unSend' && needLoadingIconMessageType.includes(item.type)" icon="eos-icons:three-dots-loading" :color="item.flow === 'in' ? '#38bdf8' : '#d4d4d8'" size="24" /></span>
<span style="line-height: 26px"><Tooltip v-if="item.status === 'fail' || item.hasRiskContent" @click="resendMessage"></span>
<span style="line-height: 26px">  <template #title>发送失败</template>!</Tooltip></span>

Right‑Click Menu

Message actions such as revoke, delete, and copy are provided via a Dropdown triggered on the context menu.

<span style="line-height: 26px"><Dropdown :dropMenuList="MessageDropMenuList" :trigger="['contextmenu']" placement="bottom" overlayClassName="message__dropdown" @menu-event="handleMenuEvent"></span>
<span style="line-height: 26px">  <slot></slot></span>
<span style="line-height: 26px"></Dropdown></span>

Text Rendering and Emoji Parsing

Plain text is rendered safely without v-html to avoid XSS. Emoji shortcuts like [调皮] are parsed and replaced with image URLs.

<span style="line-height: 26px">const text = computed(() => {</span>
<span style="line-height: 26px">  const brackets = parseBrackets(payload.value.text || '')</span>
<span style="line-height: 26px">  return brackets.map(item => {</span>
<span style="line-height: 26px">    if (item === '
') { return { name: 'br' } }</span>
<span style="line-height: 26px">    else if (item.startsWith('[') && item.endsWith(']') && basicEmojiMap[item]) {</span>
<span style="line-height: 26px">      return { name: 'img', src: basicEmojiUrl + basicEmojiMap[item] }</span>
<span style="line-height: 26px">    } else { return { name: 'text', text: item } }</span>
<span style="line-height: 26px">  })</span>
<span style="line-height: 26px">})</span>

File Upload Simulation

A simple upload‑progress simulator assumes a network speed of 512 KB/s and updates progress up to 99 %.

<span style="line-height: 26px">function simulateFileUpload(fileSize: number, callback: { (p: any): void }) {</span>
<span style="line-height: 26px">  const totalUploadTime = fileSize / (1024 * 512)</span>
<span style="line-height: 26px">  const totalIntervals = totalUploadTime * 10</span>
<span style="line-height: 26px">  let currentInterval = 0</span>
<span style="line-height: 26px">  timer = setInterval(() => {</span>
<span style="line-height: 26px">    currentInterval++</span>
<span style="line-height: 26px">    const progress = (currentInterval / totalIntervals) * 100</span>
<span style="line-height: 26px">    callback(parseInt(Math.min(progress, 99)))</span>
<span style="line-height: 26px">    if (currentInterval >= totalIntervals) { clearInterval(timer) }</span>
<span style="line-height: 26px">  }, 100)</span>
<span style="line-height: 26px">}</span>

Image Component

Image payloads contain multiple resolutions; the component selects a compressed version for display and the original for preview, while also providing explicit height and width to avoid layout shifts.

<span style="line-height: 26px">export interface ImagePayload { uuid: string; imageFormat: 1|2|3|4|255; imageInfoArray: ImageInfo[] }</span>
<span style="line-height: 26px">export interface ImageInfo { type: 0|1|2; width: number; height: number; size: number; url: string }</span>

File and Video Components

File messages show name and size with a download click; video messages use the video tag with a poster image.

<span style="line-height: 26px"><div title="单击下载" @click="download"></span>
<span style="line-height: 26px">  <Icon icon="ant-design:file-twotone" :size="40" /></span>
<span style="line-height: 26px">  <div>{{ item.payload.fileName }}</div></span>
<span style="line-height: 26px">  <div>{{ fileSize }}</div></span>
<span style="line-height: 26px"></div></span>

Scroll Button Logic

The button appears when the scroll offset exceeds one screen height and scrolls the view to the latest message.

<span style="line-height: 26px">function judgeScrollOverOneScreen(e: Event) {</span>
<span style="line-height: 26px">  const scrollListDom = e.target as HTMLElement</span>
<span style="line-height: 26px">  const { height } = scrollListDom.getBoundingClientRect()</span>
<span style="line-height: 26px">  const { scrollHeight, scrollTop } = scrollListDom</span>
<span style="line-height: 26px">  if (height && scrollHeight) { isScrollOverOneScreen.value = scrollTop < scrollHeight - 2 * height }</span>
<span style="line-height: 26px">}</span>

Message Input Features

The input toolbar integrates emoji picker, image/file/video upload, voice/video call, and a rich‑text editor based on @tiptap/vue-3. Sending is handled by sendMessages, which creates and dispatches text, image, file, or video messages via the Tencent Cloud Chat SDK.

<span style="line-height: 26px">export const sendMessages = async (chat: ChatSDK, messageList: ITipTapEditorContent[], currentConversationID: string, beforeSend?: (msg: Message) => void) => {</span>
<span style="line-height: 26px">  for (const content of messageList) {</span>
<span style="line-height: 26px">    const options: MESSAGE_OPTIONS = { to: currentConversationID, conversationType: TencentCloudChat.TYPES.CONV_GROUP, payload: {}, needReadReceipt: false, onProgress: () => {} }</span>
<span style="line-height: 26px">    switch (content?.type) {</span>
<span style="line-height: 26px">      case 'text': /* create and send text */ break;</span>
<span style="line-height: 26px">      case 'image': /* create and send image */ break;</span>
<span style="line-height: 26px">      case 'file': /* create and send file */ break;</span>
<span style="line-height: 26px">      case 'video': /* create and send video */ break;</span>
<span style="line-height: 26px">    }</span>
<span style="line-height: 26px">  }</span>
<span style="line-height: 26px">}</span>

Toolbar Sub‑Features

Emoji picker uses a predefined list (e.g., [龇牙]) mapped to image URLs.

Image, file, and video uploads are triggered by hidden input elements and emitted to the parent.

Voice and video calls leverage @tencentcloud/call-uikit-vue with dynamic sizing and minimization support.

Editor Keyboard Handling

Enter sends the message; Shift + Enter inserts a new paragraph.

<span style="line-height: 26px">const handleEnter = (e: any) => {</span>
<span style="line-height: 26px">  e?.preventDefault(); e?.stopPropagation();</span>
<span style="line-height: 26px">  if (e.keyCode === 13 && e.shiftKey) { editor?.commands?.insertContent('<p></p>') }</span>
<span style="line-height: 26px">  else if (e.keyCode === 13) { emits('sendMessage') }</span>
<span style="line-height: 26px">}</span>

Paste Handling for Images and Files

Paste events are intercepted; images are inserted as custom nodes, while files are rendered as canvas‑generated thumbnails.

<span style="line-height: 26px">const handleFilePaste = async (e: any) => {</span>
<span style="line-height: 26px">  e.preventDefault(); e.stopPropagation();</span>
<span style="line-height: 26px">  const files = e?.clipboardData?.files;</span>
<span style="line-height: 26px">  for (let i = 0; i < files.length; i++) {</span>
<span style="line-height: 26px">    const file = files[i];</span>
<span style="line-height: 26px">    if (file.type.startsWith('image/')) {</span>
<span style="line-height: 26px">      const fileSrc = URL.createObjectURL(file);</span>
<span style="line-height: 26px">      editor?.commands?.insertContent({ type: 'custom-image', attrs: { src: fileSrc, alt: file?.name, title: file?.name, class: 'normal' } })</span>
<span style="line-height: 26px">    }</span>
<span style="line-height: 26px">  }</span>
<span style="line-height: 26px">}</span>

Exposed Editor Methods

The component exposes getEditorContent, addEmoji, resetEditor, and setEditorContent for external control.

<span style="line-height: 26px">defineExpose({ getEditorContent, addEmoji, resetEditor, setEditorContent })</span>

Conclusion

The article demonstrates a full‑stack instant‑messaging UI built with Vue 3, TypeScript, and Tencent Cloud's IM SDK, covering component architecture, rendering logic, user interaction, and auxiliary features such as file handling and voice/video calling, providing a solid reference for developers building similar chat solutions.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptVueComponent DesignInstant Messaging
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.