chore: sync local changes

这个提交包含在:
XuqmGroup 2026-05-07 19:39:38 +08:00
父节点 84221ff6b2
当前提交 74d9566554
共有 25 个文件被更改,包括 566 次插入89 次删除

查看文件

@ -10,6 +10,7 @@ XuqmGroup-AndroidSDK/
├── sdk-im/ # IMWebSocket 实时通信
├── sdk-push/ # 推送:设备 Token 注册
├── sdk-update/ # 版本管理:检查更新、下载安装
├── sdk-webview/ # WebView嵌入式组件 / 独立页面
└── sample-app/ # 示例 AppJetpack Compose
```
@ -45,6 +46,7 @@ dependencies {
implementation("com.xuqm:sdk-im:0.4.0") // 可选
implementation("com.xuqm:sdk-push:0.4.0") // 可选
implementation("com.xuqm:sdk-update:0.4.0") // 可选
implementation("com.xuqm:sdk-webview:0.4.0") // 可选
}
```
@ -74,6 +76,34 @@ XuqmSDK.login(
// SDK 会自动完成对应模块的登录与初始化
```
### 3. WebView 独立模块
`sdk-webview` 不需要额外初始化,直接依赖 `sdk-core` 即可使用。
```kotlin
import com.xuqm.sdk.webview.XWebViewConfig
import com.xuqm.sdk.webview.XWebViewView
import com.xuqm.sdk.webview.XWebViewScreen
import com.xuqm.sdk.webview.openXWebView
// 嵌入式组件:直接放到页面中,不包含导航栏 / 状态栏
XWebViewView(
config = XWebViewConfig(
url = "https://example.com",
title = "嵌入式网页",
),
)
// 独立页面:先设置配置,再跳转到页面
openXWebView(
XWebViewConfig(
url = "https://example.com",
title = "独立页面",
)
)
// navigate("xwebview") 后使用 XWebViewScreen()
```
### 3. 切换联调环境
默认使用外网域名。若要本地联调,可在 `Application.onCreate()` 里切换:
@ -157,6 +187,7 @@ val service = RetrofitFactory.create(MyApiService::class.java)
- 支持引用回复、群聊已读人数展示
- 支持 `LOCATION` / `CUSTOM` / `RICH_TEXT` / `FORWARD` / `QUOTE` / `MERGE` / `CALL_AUDIO` / `CALL_VIDEO` 等通用消息类型发送
- 单聊支持已读回执,服务端会把 `READ` 状态推回发送者
- `sdk-webview` 作为独立模块提供嵌入式组件和独立页面两种形态,可与 IM / Push / Update 任意组合
### ImClient

查看文件

@ -52,6 +52,7 @@ dependencies {
implementation(project(":sdk-im"))
implementation(project(":sdk-push"))
implementation(project(":sdk-update"))
implementation(project(":sdk-webview"))
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose)

查看文件

@ -21,14 +21,14 @@ data class DemoResponse<T>(
val message: String? = null,
)
data class LoginRequest(val appId: String, val userId: String, val password: String)
data class LoginRequest(val appKey: String, val userId: String, val password: String)
data class RegisterRequest(val appId: String, val userId: String, val password: String, val nickname: String)
data class RegisterRequest(val appKey: String, val userId: String, val password: String, val nickname: String)
data class ResetPasswordRequest(val appId: String, val userId: String, val newPassword: String)
data class ResetPasswordRequest(val appKey: String, val userId: String, val newPassword: String)
data class AuthProfile(
val appId: String,
val appKey: String,
val userId: String,
val nickname: String,
val avatar: String? = null,
@ -68,10 +68,10 @@ interface DemoApi {
suspend fun resetPassword(@Body request: ResetPasswordRequest): DemoResponse<Unit>
@GET("api/demo/user/profile")
suspend fun getProfile(@Query("appId") appId: String = DEMO_APP_ID): DemoResponse<UserData>
suspend fun getProfile(@Query("appKey") appKey: String = DEMO_APP_ID): DemoResponse<UserData>
@GET("api/demo/users/members")
suspend fun listMembers(@Query("appId") appId: String = DEMO_APP_ID): DemoResponse<List<UserData>>
suspend fun listMembers(@Query("appKey") appKey: String = DEMO_APP_ID): DemoResponse<List<UserData>>
@PUT("api/demo/user/profile")
suspend fun updateProfile(@Body request: UpdateProfileRequest): DemoResponse<UserData>
@ -81,7 +81,7 @@ interface DemoApi {
@GET("api/demo/users/search")
suspend fun searchUsers(
@Query("appId") appId: String = DEMO_APP_ID,
@Query("appKey") appKey: String = DEMO_APP_ID,
@Query("keyword") keyword: String,
): DemoResponse<List<UserData>>
}

查看文件

@ -136,7 +136,7 @@ class LocalImCache(context: Context) {
add(
ImMessage(
id = obj.optString("id"),
appId = obj.optString("appId"),
appKey = obj.optString("appKey"),
fromId = obj.optString("fromId"),
toId = obj.optString("toId"),
chatType = obj.optString("chatType"),
@ -180,7 +180,7 @@ class LocalImCache(context: Context) {
array.put(
JSONObject().apply {
put("id", message.id)
put("appId", message.appId)
put("appKey", message.appKey)
put("fromId", message.fromId)
put("toId", message.toId)
put("chatType", message.chatType)

查看文件

@ -13,6 +13,7 @@ import com.xuqm.sdk.sample.ui.chat.ChatScreen
import com.xuqm.sdk.sample.ui.environment.EnvironmentScreen
import com.xuqm.sdk.sample.ui.group.GroupSettingsScreen
import com.xuqm.sdk.sample.ui.main.MainScreen
import com.xuqm.sdk.webview.XWebViewScreen
import java.net.URLDecoder
import java.net.URLEncoder
@ -58,6 +59,9 @@ fun AppNavGraph(
onGroupSettings = { groupId ->
navController.navigate("group_settings/$groupId")
},
onOpenWebView = {
navController.navigate("xwebview")
},
onOpenEnvironment = { navController.navigate("environment") },
onLogout = {
navController.navigate("auth") {
@ -67,6 +71,13 @@ fun AppNavGraph(
)
}
composable("xwebview") {
XWebViewScreen(
onBack = { navController.popBackStack() },
onClose = { navController.popBackStack() },
)
}
composable("chat/{chatType}/{targetId}/{targetName}") { backStackEntry ->
val chatType = backStackEntry.arguments?.getString("chatType") ?: "SINGLE"
val targetId = backStackEntry.arguments?.getString("targetId") ?: ""

查看文件

@ -653,7 +653,7 @@ class ChatViewModel : ViewModel() {
private fun mergeMessageRecord(existing: ImMessage, incoming: ImMessage): ImMessage {
return existing.copy(
appId = incoming.appId.ifBlank { existing.appId },
appKey = incoming.appKey.ifBlank { existing.appKey },
fromId = existing.fromId,
toId = existing.toId,
chatType = existing.chatType,

查看文件

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
@ -40,6 +41,7 @@ import com.xuqm.sdk.sample.ui.contact.ContactScreen
import com.xuqm.sdk.sample.ui.conversation.ConversationScreen
import com.xuqm.sdk.sample.ui.group.GroupListScreen
import com.xuqm.sdk.sample.ui.profile.ProfileScreen
import com.xuqm.sdk.sample.ui.webview.WebViewEntryScreen
import com.xuqm.sdk.sample.ui.update.UpdateScreen
import kotlinx.coroutines.launch
import com.xuqm.sdk.sample.ui.common.ConnectionStatusBanner
@ -51,6 +53,7 @@ private val tabs = listOf(
BottomTab("群组", Icons.Default.Group),
BottomTab("联系人", Icons.Default.People),
BottomTab("更新", Icons.Default.SystemUpdate),
BottomTab("网页", Icons.Default.Language),
BottomTab("", Icons.Default.Person),
)
@ -59,6 +62,7 @@ private val tabs = listOf(
fun MainScreen(
onOpenChat: (targetId: String, chatType: String, targetName: String) -> Unit,
onGroupSettings: (groupId: String) -> Unit,
onOpenWebView: () -> Unit,
onOpenEnvironment: () -> Unit,
onLogout: () -> Unit,
) {
@ -116,7 +120,8 @@ fun MainScreen(
)
2 -> ContactScreen(onOpenChat = { userId -> onOpenChat(userId, "SINGLE", userId) })
3 -> UpdateScreen()
4 -> ProfileScreen(onLogout = onLogout)
4 -> WebViewEntryScreen(onOpenWebView = onOpenWebView)
5 -> ProfileScreen(onLogout = onLogout)
}
}
}

查看文件

@ -0,0 +1,147 @@
package com.xuqm.sdk.sample.ui.webview
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberSaveable
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.AnnotatedString
import com.xuqm.sdk.webview.XWebViewConfig
import com.xuqm.sdk.webview.XWebViewControl
import com.xuqm.sdk.webview.XWebViewView
import com.xuqm.sdk.webview.openXWebView
private enum class WebViewMode { Embedded, Page }
@Composable
fun WebViewEntryScreen(
onOpenWebView: () -> Unit,
) {
var url by rememberSaveable { mutableStateOf("https://example.com") }
var title by rememberSaveable { mutableStateOf("示例网页") }
var mode by rememberSaveable { mutableIntStateOf(WebViewMode.Embedded.ordinal) }
var statusVersion by rememberSaveable { mutableIntStateOf(0) }
val selectedMode = WebViewMode.entries[mode]
val config = XWebViewConfig(url = url.trim(), title = title.trim())
val clipboard = LocalClipboardManager.current
val currentUrl = remember(statusVersion, config.url) {
XWebViewControl.currentUrl() ?: config.url
}
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text("XWebView", style = MaterialTheme.typography.headlineMedium)
Text(
"可在下面输入 URL,切换嵌入式组件或独立页面模式。",
style = MaterialTheme.typography.bodyMedium,
)
OutlinedTextField(
value = url,
onValueChange = { url = it },
label = { Text("URL") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("标题") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = { mode = WebViewMode.Embedded.ordinal }) {
Text(if (selectedMode == WebViewMode.Embedded) "嵌入式" else "切换到嵌入式")
}
Button(onClick = { mode = WebViewMode.Page.ordinal }) {
Text(if (selectedMode == WebViewMode.Page) "页面模式" else "切换到页面")
}
}
if (selectedMode == WebViewMode.Embedded) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(12.dp)) {
Text("嵌入式组件", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(12.dp))
XWebViewView(
config = config,
modifier = Modifier
.fillMaxWidth()
.height(320.dp),
)
}
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("页面控制", style = MaterialTheme.typography.titleMedium)
Text("当前 URL: ${if (currentUrl.isBlank()) "未加载" else currentUrl}")
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = {
XWebViewControl.goBack()
statusVersion++
},
enabled = XWebViewControl.canGoBack(),
) { Text("后退") }
Button(
onClick = {
XWebViewControl.goForward()
statusVersion++
},
enabled = XWebViewControl.canGoForward(),
) { Text("前进") }
Button(onClick = {
XWebViewControl.reload()
statusVersion++
}) { Text("刷新") }
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = {
clipboard.setText(AnnotatedString(currentUrl.ifBlank { config.url }))
statusVersion++
}) { Text("复制 URL") }
Button(onClick = {
XWebViewControl.loadUrl(url.trim())
statusVersion++
}) { Text("重新加载输入 URL") }
}
}
}
Button(onClick = {
openXWebView(config)
onOpenWebView()
}) {
Text("打开页面模式")
}
}
}

查看文件

@ -23,6 +23,8 @@ object XuqmSDK {
private var initialized = false
@Volatile
private var initializedAppKey: String? = null
@Volatile
private var loginSession: XuqmLoginSession? = null
fun initialize(
@ -30,11 +32,22 @@ object XuqmSDK {
appKey: String,
logLevel: LogLevel = LogLevel.WARN,
) {
config = SDKConfig(appKey, logLevel)
appContext = context.applicationContext
tokenStore = TokenStore(context.applicationContext)
ApiClient.init(config, tokenStore)
initialized = true
val applicationContext = context.applicationContext
synchronized(this) {
if (initialized) {
check(initializedAppKey == appKey) {
"XuqmSDK already initialized with appKey=$initializedAppKey"
}
appContext = applicationContext
return
}
config = SDKConfig(appKey, logLevel)
appContext = applicationContext
tokenStore = TokenStore(applicationContext)
ApiClient.init(config, tokenStore)
initializedAppKey = appKey
initialized = true
}
}
fun configureServiceEndpoints(endpoints: ServiceEndpoints) {
@ -65,6 +78,9 @@ object XuqmSDK {
userSig: String,
): XuqmLoginSession = withContext(Dispatchers.IO) {
requireInit()
loginSession?.takeIf {
it.appKey == appKey && it.userId == userId && it.userSig == userSig
}?.let { return@withContext it }
val session = XuqmLoginSession(
appKey = appKey,
userId = userId,

查看文件

@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit
class ImClient(
private val wsUrl: String,
private val token: String,
private val appId: String,
private val appKey: String,
) {
companion object {
private const val TAG = "XuqmImClient"
@ -36,7 +36,7 @@ class ImClient(
.build()
fun connect() {
Log.d(TAG, "connect() wsUrl=$wsUrl appId=$appId")
Log.d(TAG, "connect() wsUrl=$wsUrl appKey=$appKey")
disconnect(closeSocket = false)
val request = Request.Builder()
.url(wsUrl)
@ -95,7 +95,7 @@ class ImClient(
): Boolean {
Log.d(TAG, "sendMessage messageId=$messageId toId=$toId chatType=$chatType msgType=$msgType contentLength=${content.length} mentioned=${mentionedUserIds.orEmpty()}")
val payload = linkedMapOf(
"appId" to appId,
"appKey" to appKey,
"messageId" to messageId,
"toId" to toId,
"chatType" to chatType,
@ -124,7 +124,7 @@ class ImClient(
),
gson.toJson(
mapOf(
"appId" to appId,
"appKey" to appKey,
"messageId" to messageId,
)
),
@ -138,7 +138,7 @@ class ImClient(
"destination" to "/app/chat.sync",
"content-type" to "application/json",
),
gson.toJson(mapOf("appId" to appId)),
gson.toJson(mapOf("appKey" to appKey)),
)
}

查看文件

@ -93,6 +93,8 @@ object ImSDK {
var currentUserId: String = ""
private set
@Volatile
private var currentUserSig: String? = null
interface ConversationListener {
fun onConversationsChanged(conversations: List<ConversationData>)
@ -111,7 +113,11 @@ object ImSDK {
suspend fun login(userId: String, userSig: String) = withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
if (currentUserId == userId && currentUserSig == userSig) {
return@withContext
}
currentUserId = userId
currentUserSig = userSig
connectWithToken(userSig)
}
@ -801,7 +807,9 @@ object ImSDK {
fun onSdkLogin(session: XuqmLoginSession) {
XuqmSDK.requireInit()
if (currentUserId == session.userId && currentUserSig == session.userSig) return
currentUserId = session.userId
currentUserSig = session.userSig
connectWithToken(session.userSig)
}
@ -838,6 +846,7 @@ object ImSDK {
client?.disconnect()
client = null
currentUserId = ""
currentUserSig = null
currentToken = ""
synchronized(activeGroupSubscriptions) {
activeGroupSubscriptions.clear()
@ -896,7 +905,7 @@ object ImSDK {
): ImMessage {
return ImMessage(
id = messageId,
appId = XuqmSDK.appKey,
appKey = XuqmSDK.appKey,
fromId = currentUserId,
toId = toId,
chatType = chatType,

查看文件

@ -32,7 +32,7 @@ data class ApiResponse<T>(
)
data class LoginRequest(
val appId: String,
val appKey: String,
val userId: String,
val nickname: String? = null,
val avatar: String? = null,
@ -64,7 +64,7 @@ interface ImApi {
@GET("api/im/messages/history/{toId}")
suspend fun fetchHistory(
@Path("toId") toId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("msgType") msgType: String? = null,
@Query("keyword") keyword: String? = null,
@Query("startTime") startTime: String? = null,
@ -76,7 +76,7 @@ interface ImApi {
@GET("api/im/messages/group-history/{groupId}")
suspend fun fetchGroupHistory(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("msgType") msgType: String? = null,
@Query("keyword") keyword: String? = null,
@Query("startTime") startTime: String? = null,
@ -86,31 +86,31 @@ interface ImApi {
): ApiResponse<PageResult<ImMessage>>
@GET("api/im/groups")
suspend fun listGroups(@Query("appId") appId: String): ApiResponse<List<ImGroup>>
suspend fun listGroups(("appKey") appKey: String): ApiResponse<List<ImGroup>>
@GET("api/im/groups/public")
suspend fun listPublicGroups(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("keyword") keyword: String? = null,
): ApiResponse<List<ImGroup>>
@GET("api/im/admin/users/search")
suspend fun searchUsers(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("keyword") keyword: String,
@Query("size") size: Int = 20,
): ApiResponse<List<UserProfile>>
@GET("api/im/admin/groups/search")
suspend fun searchGroups(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("keyword") keyword: String,
@Query("size") size: Int = 20,
): ApiResponse<List<ImGroup>>
@GET("api/im/admin/messages/search")
suspend fun searchMessages(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("keyword") keyword: String? = null,
@Query("chatType") chatType: String? = null,
@Query("msgType") msgType: String? = null,
@ -122,7 +122,7 @@ interface ImApi {
@POST("api/im/groups")
suspend fun createGroup(
@Query("appId") appId: String,
("appKey") appKey: String,
@Body request: CreateGroupRequest,
): ApiResponse<ImGroup>
@ -132,13 +132,13 @@ interface ImApi {
@GET("api/im/groups/{groupId}/members")
suspend fun listGroupMembers(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<List<UserProfile>>
@GET("api/im/groups/{groupId}/members/search")
suspend fun searchGroupMembers(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("keyword") keyword: String,
@Query("size") size: Int = 20,
): ApiResponse<List<UserProfile>>
@ -200,73 +200,73 @@ interface ImApi {
@POST("api/im/groups/{groupId}/join-requests")
suspend fun sendGroupJoinRequest(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("remark") remark: String? = null,
): ApiResponse<GroupJoinRequest>
@GET("api/im/groups/{groupId}/join-requests")
suspend fun listGroupJoinRequests(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<List<GroupJoinRequest>>
@POST("api/im/groups/{groupId}/join-requests/{requestId}/accept")
suspend fun acceptGroupJoinRequest(
@Path("groupId") groupId: String,
@Path("requestId") requestId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<GroupJoinRequest>
@POST("api/im/groups/{groupId}/join-requests/{requestId}/reject")
suspend fun rejectGroupJoinRequest(
@Path("groupId") groupId: String,
@Path("requestId") requestId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<GroupJoinRequest>
@GET("api/im/friends")
suspend fun listFriends(@Query("appId") appId: String): ApiResponse<List<String>>
suspend fun listFriends(("appKey") appKey: String): ApiResponse<List<String>>
@POST("api/im/friends")
suspend fun addFriend(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("friendId") friendId: String,
): ApiResponse<Unit>
@DELETE("api/im/friends")
suspend fun removeAllFriends(@Query("appId") appId: String): ApiResponse<Unit>
suspend fun removeAllFriends(("appKey") appKey: String): ApiResponse<Unit>
@DELETE("api/im/friends/{friendId}")
suspend fun removeFriend(
@Path("friendId") friendId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<Unit>
@PUT("api/im/friends/{friendId}/group")
suspend fun setFriendGroup(
@Path("friendId") friendId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("groupName") groupName: String? = null,
): ApiResponse<Unit>
@GET("api/im/friends/groups")
suspend fun listFriendGroups(@Query("appId") appId: String): ApiResponse<List<String>>
suspend fun listFriendGroups(("appKey") appKey: String): ApiResponse<List<String>>
@GET("api/im/friends/groups/{groupName}")
suspend fun listFriendsByGroup(
@Path("groupName") groupName: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<List<String>>
@GET("api/im/friend-requests")
suspend fun listFriendRequests(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("direction") direction: String = "incoming",
): ApiResponse<List<FriendRequest>>
@POST("api/im/friend-requests")
suspend fun sendFriendRequest(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("toUserId") toUserId: String,
@Query("remark") remark: String? = null,
): ApiResponse<FriendRequest>
@ -274,58 +274,58 @@ interface ImApi {
@POST("api/im/friend-requests/{requestId}/accept")
suspend fun acceptFriendRequest(
@Path("requestId") requestId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<FriendRequest>
@POST("api/im/friend-requests/{requestId}/reject")
suspend fun rejectFriendRequest(
@Path("requestId") requestId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<FriendRequest>
@GET("api/im/blacklist")
suspend fun listBlacklist(@Query("appId") appId: String): ApiResponse<List<BlacklistEntry>>
suspend fun listBlacklist(("appKey") appKey: String): ApiResponse<List<BlacklistEntry>>
@POST("api/im/blacklist")
suspend fun addToBlacklist(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("blockedUserId") blockedUserId: String,
): ApiResponse<BlacklistEntry>
@DELETE("api/im/blacklist")
suspend fun removeFromBlacklist(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("blockedUserId") blockedUserId: String,
): ApiResponse<Unit>
@GET("api/im/blacklist/check")
suspend fun checkBlacklist(
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("targetUserId") targetUserId: String,
): ApiResponse<BlacklistCheckResult>
@GET("api/im/accounts/{userId}")
suspend fun getProfile(
@Path("userId") userId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<UserProfile>
@PUT("api/im/accounts/{userId}")
suspend fun updateProfile(
@Path("userId") userId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("nickname") nickname: String? = null,
@Query("avatar") avatar: String? = null,
@Query("gender") gender: String? = null,
): ApiResponse<UserProfile>
@GET("api/im/conversations")
suspend fun listConversations(@Query("appId") appId: String): ApiResponse<List<ConversationData>>
suspend fun listConversations(("appKey") appKey: String): ApiResponse<List<ConversationData>>
@PUT("api/im/conversations/{targetId}/pinned")
suspend fun setConversationPinned(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("chatType") chatType: String,
@Query("pinned") pinned: Boolean,
): ApiResponse<Unit>
@ -333,7 +333,7 @@ interface ImApi {
@PUT("api/im/conversations/{targetId}/muted")
suspend fun setConversationMuted(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("chatType") chatType: String,
@Query("muted") muted: Boolean,
): ApiResponse<Unit>
@ -341,7 +341,7 @@ interface ImApi {
@PUT("api/im/conversations/{targetId}/hidden")
suspend fun setConversationHidden(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("chatType") chatType: String,
@Query("hidden") hidden: Boolean,
): ApiResponse<Unit>
@ -349,44 +349,44 @@ interface ImApi {
@PUT("api/im/conversations/{targetId}/group")
suspend fun setConversationGroup(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("chatType") chatType: String,
@Query("groupName") groupName: String? = null,
): ApiResponse<Unit>
@GET("api/im/conversation-groups")
suspend fun listConversationGroups(@Query("appId") appId: String): ApiResponse<List<String>>
suspend fun listConversationGroups(("appKey") appKey: String): ApiResponse<List<String>>
@GET("api/im/conversation-groups/{groupName}")
suspend fun listConversationGroupItems(
@Path("groupName") groupName: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<List<ConversationGroupItem>>
@PUT("api/im/conversations/{targetId}/read")
suspend fun markRead(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("chatType") chatType: String,
): ApiResponse<Unit>
@PUT("api/im/messages/{messageId}")
suspend fun editMessage(
@Path("messageId") messageId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Body request: EditMessageRequest,
): ApiResponse<ImMessage>
@POST("api/im/messages/{messageId}/revoke")
suspend fun revokeMessage(
@Path("messageId") messageId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
): ApiResponse<ImMessage>
@PUT("api/im/conversations/{targetId}/draft")
suspend fun setDraft(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("chatType") chatType: String,
@Query("draft") draft: String,
): ApiResponse<Unit>
@ -394,47 +394,47 @@ interface ImApi {
@DELETE("api/im/conversations/{targetId}")
suspend fun deleteConversation(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Query("chatType") chatType: String,
): ApiResponse<Unit>
@POST("api/im/admin/groups/{groupId}/read-receipts")
suspend fun adminGroupReadReceipts(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
("appKey") appKey: String,
@Body request: GroupReadReceiptRequest,
): ApiResponse<List<GroupReadReceiptSummary>>
@POST("api/im/friends/batch")
suspend fun batchAddFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse<Unit>
suspend fun batchAddFriends(("appKey") appKey: String, @Body request: BatchFriendRequest): ApiResponse<Unit>
@POST("api/im/friends/batch/remove")
suspend fun batchRemoveFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse<Unit>
suspend fun batchRemoveFriends(("appKey") appKey: String, @Body request: BatchFriendRequest): ApiResponse<Unit>
@POST("api/im/friend-requests/batch/accept")
suspend fun batchAcceptFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
suspend fun batchAcceptFriendRequests(("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@POST("api/im/friend-requests/batch/reject")
suspend fun batchRejectFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
suspend fun batchRejectFriendRequests(("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/members/batch")
suspend fun batchAddGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse<Unit>
suspend fun batchAddGroupMembers(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchUserIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/members/batch/remove")
suspend fun batchRemoveGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse<Unit>
suspend fun batchRemoveGroupMembers(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchUserIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/join-requests/batch/accept")
suspend fun batchAcceptGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
suspend fun batchAcceptGroupJoinRequests(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/join-requests/batch/reject")
suspend fun batchRejectGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
suspend fun batchRejectGroupJoinRequests(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@PUT("api/im/groups/{groupId}/members/{userId}/info")
suspend fun modifyGroupMemberInfo(@Path("groupId") groupId: String, @Path("userId") userId: String, @Query("appId") appId: String, @Body request: ModifyMemberInfoRequest): ApiResponse<Unit>
suspend fun modifyGroupMemberInfo(@Path("groupId") groupId: String, @Path("userId") userId: String, ("appKey") appKey: String, @Body request: ModifyMemberInfoRequest): ApiResponse<Unit>
@GET("api/im/messages/offline/count")
suspend fun offlineMessageCount(@Query("appId") appId: String): ApiResponse<Map<String, Int>>
suspend fun offlineMessageCount(("appKey") appKey: String): ApiResponse<Map<String, Int>>
@POST("api/im/messages/offline")
suspend fun syncOfflineMessages(@Query("appId") appId: String): ApiResponse<List<ImMessage>>
suspend fun syncOfflineMessages(("appKey") appKey: String): ApiResponse<List<ImMessage>>
}

查看文件

@ -20,7 +20,7 @@ data class EditMessageRequest(
data class ImMessage(
val id: String,
val appId: String,
val appKey: String,
@SerializedName(value = "fromId", alternate = ["fromUserId"])
val fromId: String,
val toId: String,
@ -74,7 +74,7 @@ data class UserProfile(
data class FriendRequest(
val id: String,
val appId: String,
val appKey: String,
val fromUserId: String,
val toUserId: String,
val remark: String? = null,
@ -85,7 +85,7 @@ data class FriendRequest(
data class GroupJoinRequest(
val id: String,
val appId: String,
val appKey: String,
val groupId: String,
val requesterId: String,
val remark: String? = null,
@ -96,7 +96,7 @@ data class GroupJoinRequest(
data class BlacklistEntry(
val id: String,
val appId: String,
val appKey: String,
val userId: String,
val blockedUserId: String,
val createdAt: Long,

查看文件

@ -113,7 +113,7 @@ object PushSDK {
scope.launch {
runCatching {
api.setReceivePush(
appId = XuqmSDK.appKey,
appKey = XuqmSDK.appKey,
userId = resolvedUserId,
deviceId = DeviceUtils.getDeviceId(context),
enabled = enabled,
@ -155,7 +155,7 @@ object PushSDK {
scope.launch {
runCatching {
api.registerDevice(
appId = XuqmSDK.appKey,
appKey = XuqmSDK.appKey,
userId = userId,
vendor = vendor.name,
token = pushToken,

查看文件

@ -8,7 +8,7 @@ interface PushApi {
@POST("api/push/register")
suspend fun registerDevice(
@Query("appId") appId: String,
@Query("appKey") appKey: String,
@Query("userId") userId: String,
@Query("vendor") vendor: String,
@Query("token") token: String,
@ -22,7 +22,7 @@ interface PushApi {
@DELETE("api/push/unregister")
suspend fun unregisterDevice(
@Query("appId") appId: String,
@Query("appKey") appKey: String,
@Query("userId") userId: String,
@Query("vendor") vendor: String,
@Query("deviceId") deviceId: String? = null,
@ -30,7 +30,7 @@ interface PushApi {
@POST("api/push/receive-push")
suspend fun setReceivePush(
@Query("appId") appId: String,
@Query("appKey") appKey: String,
@Query("userId") userId: String,
@Query("deviceId") deviceId: String? = null,
@Query("enabled") enabled: Boolean,

查看文件

@ -7,7 +7,7 @@ import retrofit2.http.Query
interface PushConfigApi {
@GET("api/sdk/config")
suspend fun sdkConfig(
@Query("appId") appId: String,
@Query("appKey") appKey: String,
@Query("platform") platform: String = "ANDROID",
): SdkConfigResponse
}

查看文件

@ -9,7 +9,7 @@ data class ApiResponse<T>(val code: Int, val data: T?, val message: String)
interface UpdateApi {
@GET("api/v1/updates/app/check")
suspend fun checkUpdate(
@Query("appId") appId: String,
@Query("appKey") appKey: String,
@Query("platform") platform: String,
@Query("currentVersionCode") currentVersionCode: Int,
): ApiResponse<UpdateInfo>

查看文件

@ -0,0 +1,41 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
}
apply(from = rootProject.file("gradle/publish.gradle"))
android {
namespace = "com.xuqm.sdk.webview"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
publishing {
singleVariant("release")
}
}
dependencies {
api(project(":sdk-core"))
api(platform(libs.androidx.compose.bom))
api(libs.androidx.ui)
api(libs.androidx.ui.graphics)
api(libs.androidx.ui.tooling.preview)
api(libs.androidx.material3)
api(libs.androidx.material.icons.extended)
api(libs.androidx.webkit)
implementation(libs.androidx.activity.compose)
}

查看文件

@ -0,0 +1 @@
# Keep module rules intentionally empty for now.

查看文件

@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

查看文件

@ -0,0 +1,39 @@
package com.xuqm.sdk.webview
import androidx.compose.runtime.mutableStateOf
private val currentConfig = mutableStateOf(XWebViewConfig())
private var currentController: XWebViewController? = null
fun openXWebView(config: XWebViewConfig) {
currentConfig.value = config
}
fun getXWebViewConfig(): XWebViewConfig = currentConfig.value
fun setXWebViewController(controller: XWebViewController?) {
currentController = controller
}
fun getXWebViewController(): XWebViewController? = currentController
object XWebViewControl : XWebViewController {
override fun canGoBack(): Boolean = currentController?.canGoBack() ?: false
override fun canGoForward(): Boolean = currentController?.canGoForward() ?: false
override fun currentUrl(): String? = currentController?.currentUrl()
override fun goBack() {
currentController?.goBack()
}
override fun goForward() {
currentController?.goForward()
}
override fun reload() {
currentController?.reload()
}
override fun loadUrl(url: String) {
currentController?.loadUrl(url)
}
}

查看文件

@ -0,0 +1,69 @@
package com.xuqm.sdk.webview
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun XWebViewScreen(
modifier: Modifier = Modifier,
config: XWebViewConfig = getXWebViewConfig(),
onBack: (() -> Unit)? = null,
onClose: (() -> Unit)? = null,
) {
val controller = XWebViewControl
val canGoBack = controller.canGoBack()
BackHandler(enabled = canGoBack) {
controller.goBack()
}
Scaffold(
modifier = modifier,
topBar = {
if (!config.hideToolbar) {
TopAppBar(
title = { Text(text = config.title.ifBlank { "WebView" }) },
navigationIcon = {
IconButton(onClick = {
if (canGoBack) {
controller.goBack()
} else {
onBack?.invoke() ?: onClose?.invoke()
}
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
if (onClose != null) {
IconButton(onClick = onClose) {
Icon(Icons.Default.Close, contentDescription = "Close")
}
}
},
)
}
},
) { paddingValues ->
Column(modifier = Modifier.fillMaxSize()) {
XWebViewView(
modifier = Modifier.fillMaxSize(),
config = config,
)
}
}
}

查看文件

@ -0,0 +1,19 @@
package com.xuqm.sdk.webview
data class XWebViewConfig(
val url: String = "",
val title: String = "",
val hideToolbar: Boolean = false,
val hideStatusBar: Boolean = false,
val userAgent: String? = null,
)
interface XWebViewController {
fun canGoBack(): Boolean
fun canGoForward(): Boolean
fun currentUrl(): String?
fun goBack()
fun goForward()
fun reload()
fun loadUrl(url: String)
}

查看文件

@ -0,0 +1,86 @@
package com.xuqm.sdk.webview
import android.annotation.SuppressLint
import android.webkit.WebResourceRequest
import android.webkit.WebViewClient
import android.webkit.WebView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun XWebViewView(
modifier: Modifier = Modifier,
config: XWebViewConfig = getXWebViewConfig(),
) {
var webView by remember { mutableStateOf<WebView?>(null) }
var currentUrl by remember { mutableStateOf<String?>(config.url.ifBlank { null }) }
DisposableEffect(Unit) {
onDispose {
if (getXWebViewController() != null) {
setXWebViewController(null)
}
webView?.destroy()
webView = null
}
}
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
config.userAgent?.let { settings.userAgentString = it }
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
currentUrl = url ?: view?.url?.toString()
super.onPageFinished(view, url)
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return false
}
}
if (config.url.isNotBlank()) {
loadUrl(config.url)
}
webView = this
}
},
update = { view ->
if (view.url.isNullOrBlank() && config.url.isNotBlank()) {
view.loadUrl(config.url)
}
},
)
SideEffect {
val view = webView ?: return@SideEffect
setXWebViewController(object : XWebViewController {
override fun canGoBack(): Boolean = view.canGoBack()
override fun canGoForward(): Boolean = view.canGoForward()
override fun currentUrl(): String? = currentUrl ?: view.url
override fun goBack() {
if (view.canGoBack()) view.goBack()
}
override fun goForward() {
if (view.canGoForward()) view.goForward()
}
override fun reload() {
view.reload()
}
override fun loadUrl(url: String) {
view.loadUrl(url)
}
})
}
}

查看文件

@ -24,4 +24,5 @@ include(":sdk-core")
include(":sdk-im")
include(":sdk-push")
include(":sdk-update")
include(":sdk-webview")
include(":sample-app")