Skip to content

Compose 界面套件 ​

talkwire-ui 是建在 Android SDK 的 ChatEngine 之上的整套 Compose 聊天界面:会话列表(带搜索)、单聊与群聊、通讯录与选人、群信息与成员管理、消息操作、按住说话、表情与附件面板、已读名单、前台横幅。宿主唯一要实现的接口是 ImUiHost。

挂载 ​

界面放在 ImTheme 里,状态由 rememberImChatState 创建,ImChatApp 画出整套界面。下面摘自 android/QUICKSTART.md:

kotlin
setContent {
    ImTheme(locale = appLocale /* null 跟随系统 */) {
        val state = rememberImChatState(engine, MyHost(media))
        ImChatApp(state)
    }
}

engine 与 media 的建立见 Android SDK。依赖 talkwire-ui 会连带引入 talkwire-android 与 talkwire-core。

ImTheme ​

ImTheme 给套件提供颜色、尺寸与语言,每个组件都从它读取,没有写死的颜色。

参数默认说明
darkThemeisSystemInDarkTheme()是否深色
colors深色时 ImColors.dark(),否则 ImColors.light()颜色
dimensImDimens()尺寸与形状
localenull套件文案的语言;null 跟随 App 的 context
content必填

ImColors.light(accent) 的默认强调色是 0xFFD67744,ImColors.dark(accent) 是 0xFFB8623A;自己的气泡、已读勾与强调色同色。换品牌色只要传 accent,例如 ImColors.light(accent = …);也可以直接构造 ImColors,它的颜色项是:

颜色项用途
accent、onAccent品牌色:自己的气泡、链接、按钮、发送;其上的文字
background、chatBackground页面与顶栏;消息列表背后
surface、inputBackground卡片、弹层;输入框
bubbleMine、bubbleTheirs、onBubbleMine、onBubbleTheirs气泡与其中的文字
textPrimary、textSecondary、textHint文字;次要文字在 background 上对比度不低于 4.5:1
divider分隔线
online在线圆点,刻意不用品牌色
read已读勾
error未读角标、发送失败、危险操作
mentionOnMine、mentionOnTheirs提及
recording、recordingCancel录音浮层的正常色与取消色
avatarPalette没有头像图片时按名字挑的背景色

ImDimens 有媒体预览的宽度与高度范围(mediaWidth、mediaMinHeight、mediaMaxHeight、mediaFallbackHeight)、列表与聊天里的头像大小(avatarInList、avatarInChat)以及气泡与媒体的形状。组件里用 ImTheme.colors、ImTheme.dimens 读取当前值。

状态与导航 ​

rememberImChatState(engine, host, types, config, start) 创建界面状态。start 只用一次;之后返回栈在旋转与进程被杀后都能恢复。

参数默认说明
engine必填ChatEngine
host必填ImUiHost
typesMessageTypeRegistry.Default消息类型,见自己的消息类型
configImUiConfig()界面的限制与开关
startImDeepLink.Conversations起始位置
ImUiConfig默认说明
messageMaxLength20000一条消息的最大长度
typingThrottleMs3000发送“正在输入”的最小间隔
maxAdmins10群管理员上限,按宿主启动时报告的服务端配置传
groupMembersCanOpenProfilesfalse群成员能否互看资料
chatsFromContactstrue协议不列出单聊,新设备或换账号后单聊会一直看不到,直到有人发消息。打开时宿主的每个联系人都有一个单聊会话并同步一次,列表只显示有消息的

ImChatState 管返回栈:current、backStack、navigate(destination)、replace(destination)、back()、open(link)。目的地 ImDestination 有 Conversations、Chat(id)、Info(id)、Contacts、PickMembers(topic)。openConversation 是当前打开的会话的 StateFlow,交给通知控制器用(见下文)。

ImChatApp(state, modifier, onExit) 按返回栈画出当前页面,处理系统返回键;每个目的地有自己的保存状态,所以列表保留滚动位置。onExit 不为 null 时会话列表带一个返回箭头,用于聊天界面不是根页面的情况。

ImUiHost ​

框架只认 uid,人的名字、头像、谁是好友都归宿主。下面摘自 android/QUICKSTART.md:

kotlin
class MyHost(override val media: MediaSender) : ImUiHost {
    override val contacts = MutableStateFlow<List<ImProfile>>(emptyList())
    override suspend fun refreshContacts() { contacts.value = api.contacts() }        // 发起单聊、建群时用
    override suspend fun profiles(uids: Collection<String>) = api.profiles(uids)       // 名字与头像,批量
    override fun resolveMediaUrl(url: String?) = url?.let { baseUrl.toHttpUrl().resolve(it).toString() }
}

用 Node 宿主 SDK(@talkwire/host-node)时,/api/v1/chat/contacts 与 /api/v1/chat/profiles 已经现成,sample-app 的 SampleHost 就是这样接的。

成员默认说明
profiles(uids)必须实现返回 uid 到 ImProfile(uid, name, avatarUrl) 的映射;缺的人显示缩短的 uid
contacts必须实现联系人 StateFlow;没有联系人的 App 给空列表
refreshContacts()什么都不做重新拉联系人,例如通讯录打开时
medianullMediaSender;null 表示 App 不支持媒体
resolveMediaUrl(url)原样(空白返回 null)把存下的媒体地址变成可加载的地址
attachmentActions(conversation)空附件面板里在照片、拍摄、文件之后加的入口(AttachmentAction)
topBarActions(conversation)空会话页顶栏的额外按钮(TopBarAction)
topBarExtrasnull顶栏里需要组合的内容,例如先要权限的通话按钮
groupInfoExtrasnull群信息页里成员之后、聊天设置之前的内容,带本人角色
onOpenProfile(uid)什么都不做点了某人的头像;默认群成员之间不能互看资料

topBarExtras 与 groupInfoExtras 是可选模块接进聊天界面的两个槽位,SDK 本身不依赖这些模块。接法见音视频通话与扫码入群,sample-app 两个都接了。

通知与横幅 ​

通知由 talkwire-android 的 ImNotifier 与 NotificationController 负责,前台横幅由 talkwire-ui 的 ImBannerState 与 ImBannerHost 负责。下面摘自 android/QUICKSTART.md:

kotlin
val notifier = ImNotifier(context, ImNotifier.Channels("Messages", "Calls", R.drawable.ic_notification))
val banners = ImBannerState()
val notifications = NotificationController(engine, notifier, SoundPoolAlertPlayer(context, R.raw.ding),
    { m, group -> MessageNotification(m.conversation, group ?: name(m.from), types.preview(m, context), openIntent(m.conversation)) },
    banners.sink, scope)
notifier.createChannels(); notifications.start()
  • 把 state.openConversation 同步到 notifications.openConversation,Activity 的 onStart/onStop 设 appInForeground:正在看的会话不弹通知,App 在前台时弹横幅(ImBannerHost)。
  • 渠道是静音的,提示音由 App 播(红线 C6),因为有的厂商系统无论渠道怎么设都不放 App 渠道的声音。
  • 通知点开用链接:ImDeepLink.Conversation(id).uri,在 Activity 里 state.open(ImDeepLink.parse(intent.data))。

NotificationController 的决定规则(NotificationDecider):

情况结果
宿主设了 suppressAll(隐私屏、勿扰)不提醒
免打扰的会话不提醒
App 在前台且正在看这个会话不提醒
App 在前台,在别处横幅
App 在后台系统通知,每个会话一条,新的替换旧的

提示音节流:一串消息只响一次,单聊 2 秒内、群 10 秒内不重复。

类要点
ImNotifier(context, Channels)Channels(messagesName, callsName, smallIcon),渠道 id 默认 talkwire_messages、talkwire_calls。createChannels() 在启动时调一次。Android 13 起要 POST_NOTIFICATIONS 权限,没有时发通知的方法返回 false
NotificationController属性 appInForeground、openConversation(设置时同时撤掉该会话的通知)、suppressAll;start()、stop();onMessage(message, groupName) 也可以直接调用,例如处理离线推送
SoundPoolAlertPlayer(context, sound, vibrateMs = 0)在通知音量流上播放 res/raw 里的短音,静音模式下不响;要振动时 App 清单里要有 VIBRATE
MessageNotificationconversation、title、text、contentIntent、largeIcon;名字由宿主给
ImBannerStatesink 交给 NotificationController,dismiss() 收起;Composable 里用 rememberImBannerState()
ImBannerHost(banners, chat, onOpen, modifier, visibleMs = 4000)画在 App 内容之上;点一下打开会话,上滑收起

离线推送见 Android SDK。

深链 ​

ImDeepLink 是进入聊天界面的链接,供通知、横幅和 App 自己的页面使用。scheme 是 talkwire。

链接对应打开后
talkwire://chatsImDeepLink.Conversations会话列表
talkwire://chat/{会话编号}ImDeepLink.Conversation(id)列表之上打开该会话
talkwire://group/{grp_…}ImDeepLink.GroupInfo(topic)列表、该群的聊天,再打开群信息

ImDeepLink.parse(uri) 对不是 Talkwire 的链接返回 null;group 只接受 grp_ 开头的 topic。sample-app 在清单里给 Activity 加了 android:scheme="talkwire" 的 VIEW intent-filter 来接这些链接。

语言 ​

套件的文案自带英文、简体中文、越南文(res/values、values-zh、values-vi)。ImTheme(locale = …) 让套件说指定的语言,null 跟随 App 的 context。

底部弹层与对话框是独立窗口,会从窗口重新取 context,丢掉 ImTheme 设的语言(红线 C8)。套件自己的弹层用 LocalizedSheet、LocalizedDialog 保住语言与主题;App 在聊天界面里加自己的弹层时也应当用它们,而不是直接用 ModalBottomSheet。

核心以 Apache-2.0 发布;端到端加密模块为 AGPLv3。