比较提交
没有共同的提交。3d488250ee1c4539f6a957eeffe8dcff8e22e75a 和 765d8a03336761d8a49d910ff0414f1ed72adc70 的历史完全不同。
3d488250ee
...
765d8a0333
@ -154,7 +154,7 @@ export class ImClient {
|
|||||||
|
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
if (!this.activeToken) {
|
if (!this.activeToken) {
|
||||||
void _getToken().then((token: string | null) => {
|
void _getToken().then(token => {
|
||||||
this.activeToken = token
|
this.activeToken = token
|
||||||
if (token) {
|
if (token) {
|
||||||
this.sendFrame('CONNECT', {
|
this.sendFrame('CONNECT', {
|
||||||
@ -210,22 +210,11 @@ export class ImClient {
|
|||||||
|
|
||||||
if (frame.command === 'MESSAGE') {
|
if (frame.command === 'MESSAGE') {
|
||||||
const message: ImMessage = this.normalizeMessage(JSON.parse(frame.body))
|
const message: ImMessage = this.normalizeMessage(JSON.parse(frame.body))
|
||||||
if (message.status === 'READ') {
|
|
||||||
this.listeners.forEach(listener => listener.onRead?.(message))
|
|
||||||
}
|
|
||||||
if (message.revoked || message.status === 'REVOKED' || message.msgType === 'REVOKED') {
|
|
||||||
this.listeners.forEach(listener =>
|
|
||||||
listener.onRevoke?.({ msgId: message.id, operatorId: message.fromId ?? message.fromUserId }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (message.chatType === 'GROUP') {
|
if (message.chatType === 'GROUP') {
|
||||||
this.listeners.forEach(listener => listener.onGroupMessage?.(message))
|
this.listeners.forEach(listener => listener.onGroupMessage?.(message))
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
this.listeners.forEach(listener => listener.onMessage?.(message))
|
this.listeners.forEach(listener => listener.onMessage?.(message))
|
||||||
}
|
|
||||||
if (message.msgType === 'NOTIFY') {
|
|
||||||
this.listeners.forEach(listener => listener.onSystemMessage?.(message))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,8 +279,7 @@ export class ImClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateMessageId(): string {
|
private generateMessageId(): string {
|
||||||
const cryptoApi = globalThis as typeof globalThis & { crypto?: { randomUUID?: () => string } }
|
const cryptoId = globalThis.crypto?.randomUUID?.()
|
||||||
const cryptoId = cryptoApi.crypto?.randomUUID?.()
|
|
||||||
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,23 +12,15 @@ import type {
|
|||||||
ImGroup,
|
ImGroup,
|
||||||
ImMessage,
|
ImMessage,
|
||||||
MsgType,
|
MsgType,
|
||||||
PageResult,
|
|
||||||
UserProfile,
|
UserProfile,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { uploadFile } from './upload'
|
import { uploadFile } from './upload'
|
||||||
|
|
||||||
let client: ImClient | null = null
|
let client: ImClient | null = null
|
||||||
let _currentUserId: string | null = null
|
let _currentUserId: string | null = null
|
||||||
const draftStore = new Map<string, string>()
|
|
||||||
const listenerMap = new WeakMap<ImEventListener, ImEventListener>()
|
|
||||||
let conversationMemory: ConversationData[] = []
|
|
||||||
const conversationListeners = new Set<(conversations: ConversationData[]) => void>()
|
|
||||||
const messageConversationMemory = new Map<string, { targetId: string; chatType: ChatType }>()
|
|
||||||
const conversationLastMessageMemory = new Map<string, string>()
|
|
||||||
|
|
||||||
function generateMessageId(): string {
|
function generateMessageId(): string {
|
||||||
const cryptoApi = globalThis as typeof globalThis & { crypto?: { randomUUID?: () => string } }
|
const cryptoId = globalThis.crypto?.randomUUID?.()
|
||||||
const cryptoId = cryptoApi.crypto?.randomUUID?.()
|
|
||||||
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,142 +107,6 @@ function normalizeConversation(item: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortConversations(conversations: ConversationData[]): ConversationData[] {
|
|
||||||
return [...conversations].sort((a, b) => {
|
|
||||||
if (a.isPinned !== b.isPinned) return Number(b.isPinned) - Number(a.isPinned)
|
|
||||||
return b.lastMsgTime - a.lastMsgTime
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function conversationKey(targetId: string, chatType: ChatType): string {
|
|
||||||
return `${chatType}:${targetId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitConversationMemory(): void {
|
|
||||||
const snapshot = sortConversations(conversationMemory)
|
|
||||||
conversationMemory = snapshot
|
|
||||||
conversationListeners.forEach(listener => listener(snapshot))
|
|
||||||
}
|
|
||||||
|
|
||||||
function upsertConversationMemory(conversation: ConversationData): void {
|
|
||||||
const index = conversationMemory.findIndex(
|
|
||||||
item => item.targetId === conversation.targetId && item.chatType === conversation.chatType,
|
|
||||||
)
|
|
||||||
if (index >= 0) {
|
|
||||||
const existing = conversationMemory[index]
|
|
||||||
conversationMemory[index] = {
|
|
||||||
...existing,
|
|
||||||
...conversation,
|
|
||||||
unreadCount: conversation.unreadCount,
|
|
||||||
isMuted: conversation.isMuted,
|
|
||||||
isPinned: conversation.isPinned,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
conversationMemory = [...conversationMemory, conversation]
|
|
||||||
}
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function markConversationReadMemory(targetId: string, chatType: ChatType): void {
|
|
||||||
conversationMemory = conversationMemory.map(item =>
|
|
||||||
item.targetId === targetId && item.chatType === chatType
|
|
||||||
? { ...item, unreadCount: 0 }
|
|
||||||
: item,
|
|
||||||
)
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setConversationMutedMemory(targetId: string, chatType: ChatType, muted: boolean): void {
|
|
||||||
conversationMemory = conversationMemory.map(item =>
|
|
||||||
item.targetId === targetId && item.chatType === chatType
|
|
||||||
? { ...item, isMuted: muted }
|
|
||||||
: item,
|
|
||||||
)
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setConversationPinnedMemory(targetId: string, chatType: ChatType, pinned: boolean): void {
|
|
||||||
conversationMemory = conversationMemory.map(item =>
|
|
||||||
item.targetId === targetId && item.chatType === chatType
|
|
||||||
? { ...item, isPinned: pinned }
|
|
||||||
: item,
|
|
||||||
)
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteConversationMemory(targetId: string, chatType: ChatType): void {
|
|
||||||
conversationMemory = conversationMemory.filter(
|
|
||||||
item => !(item.targetId === targetId && item.chatType === chatType),
|
|
||||||
)
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMessageToMemory(message: ImMessage): void {
|
|
||||||
const normalized = normalizeMessage(message)
|
|
||||||
const key = conversationKey(normalized.toId, normalized.chatType)
|
|
||||||
messageConversationMemory.set(normalized.id, { targetId: normalized.toId, chatType: normalized.chatType })
|
|
||||||
conversationLastMessageMemory.set(key, normalized.id)
|
|
||||||
const index = conversationMemory.findIndex(
|
|
||||||
item => item.targetId === normalized.toId && item.chatType === normalized.chatType,
|
|
||||||
)
|
|
||||||
const revoked = normalized.revoked || normalized.status === 'REVOKED' || normalized.msgType === 'REVOKED'
|
|
||||||
const lastMsgContent = revoked ? '消息已撤回' : normalized.content
|
|
||||||
const unreadDelta = !revoked && normalized.fromId !== _currentUserId ? 1 : 0
|
|
||||||
const nextConversation: ConversationData = {
|
|
||||||
targetId: normalized.toId,
|
|
||||||
chatType: normalized.chatType,
|
|
||||||
lastMsgContent,
|
|
||||||
lastMsgType: revoked ? 'REVOKED' : normalized.msgType,
|
|
||||||
lastMsgTime: normalized.createdAt,
|
|
||||||
unreadCount: unreadDelta,
|
|
||||||
isMuted: index >= 0 ? conversationMemory[index].isMuted : false,
|
|
||||||
isPinned: index >= 0 ? conversationMemory[index].isPinned : false,
|
|
||||||
}
|
|
||||||
if (index >= 0) {
|
|
||||||
const current = conversationMemory[index]
|
|
||||||
conversationMemory[index] = {
|
|
||||||
...current,
|
|
||||||
...nextConversation,
|
|
||||||
unreadCount: unreadDelta > 0 ? current.unreadCount + unreadDelta : current.unreadCount,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
conversationMemory = [...conversationMemory, nextConversation]
|
|
||||||
}
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyRevokeToMemory(messageId: string): void {
|
|
||||||
const messageConversation = messageConversationMemory.get(messageId)
|
|
||||||
if (!messageConversation) return
|
|
||||||
const key = conversationKey(messageConversation.targetId, messageConversation.chatType)
|
|
||||||
if (conversationLastMessageMemory.get(key) !== messageId) return
|
|
||||||
conversationMemory = conversationMemory.map(item =>
|
|
||||||
item.targetId === messageConversation.targetId && item.chatType === messageConversation.chatType
|
|
||||||
? { ...item, lastMsgContent: '消息已撤回', lastMsgType: 'REVOKED' }
|
|
||||||
: item,
|
|
||||||
)
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyEditToMemory(message: ImMessage): void {
|
|
||||||
const normalized = normalizeMessage(message)
|
|
||||||
const messageConversation = messageConversationMemory.get(normalized.id)
|
|
||||||
if (!messageConversation) return
|
|
||||||
const key = conversationKey(messageConversation.targetId, messageConversation.chatType)
|
|
||||||
if (conversationLastMessageMemory.get(key) !== normalized.id) return
|
|
||||||
conversationMemory = conversationMemory.map(item =>
|
|
||||||
item.targetId === messageConversation.targetId && item.chatType === messageConversation.chatType
|
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
lastMsgContent: normalized.content,
|
|
||||||
lastMsgType: normalized.msgType,
|
|
||||||
lastMsgTime: normalized.createdAt,
|
|
||||||
}
|
|
||||||
: item,
|
|
||||||
)
|
|
||||||
emitConversationMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatQueryDateTime(value?: Date | string | number): string | undefined {
|
function formatQueryDateTime(value?: Date | string | number): string | undefined {
|
||||||
if (value === undefined || value === null) return undefined
|
if (value === undefined || value === null) return undefined
|
||||||
if (typeof value === 'string') return value
|
if (typeof value === 'string') return value
|
||||||
@ -377,9 +233,7 @@ export const ImSDK = {
|
|||||||
content: model.content,
|
content: model.content,
|
||||||
status: model.status as ImMessage['status'],
|
status: model.status as ImMessage['status'],
|
||||||
mentionedUserIds: model.mentionedUserIds ?? undefined,
|
mentionedUserIds: model.mentionedUserIds ?? undefined,
|
||||||
revoked: model.status === 'REVOKED',
|
|
||||||
createdAt: model.serverCreatedAt,
|
createdAt: model.serverCreatedAt,
|
||||||
editedAt: model.editedAt ?? undefined,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -438,9 +292,7 @@ export const ImSDK = {
|
|||||||
content: model.content,
|
content: model.content,
|
||||||
status: model.status as ImMessage['status'],
|
status: model.status as ImMessage['status'],
|
||||||
mentionedUserIds: model.mentionedUserIds ?? undefined,
|
mentionedUserIds: model.mentionedUserIds ?? undefined,
|
||||||
revoked: model.status === 'REVOKED',
|
|
||||||
createdAt: model.serverCreatedAt,
|
createdAt: model.serverCreatedAt,
|
||||||
editedAt: model.editedAt ?? undefined,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,38 +332,6 @@ export const ImSDK = {
|
|||||||
return messages
|
return messages
|
||||||
},
|
},
|
||||||
|
|
||||||
async locateHistoryPage(
|
|
||||||
toId: string,
|
|
||||||
messageId: string,
|
|
||||||
pageSize = 20,
|
|
||||||
maxPages = 20,
|
|
||||||
): Promise<ImMessage[] | null> {
|
|
||||||
for (let page = 0; page < Math.max(maxPages, 1); page += 1) {
|
|
||||||
const messages = await this.fetchHistory(toId, page, pageSize)
|
|
||||||
if (messages.some((item) => item.id === messageId)) {
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
if (messages.length < pageSize) return null
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
async locateGroupHistoryPage(
|
|
||||||
groupId: string,
|
|
||||||
messageId: string,
|
|
||||||
pageSize = 50,
|
|
||||||
maxPages = 20,
|
|
||||||
): Promise<ImMessage[] | null> {
|
|
||||||
for (let page = 0; page < Math.max(maxPages, 1); page += 1) {
|
|
||||||
const messages = await this.fetchGroupHistory(groupId, page, pageSize)
|
|
||||||
if (messages.some((item) => item.id === messageId)) {
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
if (messages.length < pageSize) return null
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
async sendMessage(
|
async sendMessage(
|
||||||
toId: string,
|
toId: string,
|
||||||
chatType: ChatType,
|
chatType: ChatType,
|
||||||
@ -543,16 +363,12 @@ export const ImSDK = {
|
|||||||
const finalMsg = normalizeMessage(msg, outgoing)
|
const finalMsg = normalizeMessage(msg, outgoing)
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||||
await ImDatabase.saveMessage(finalMsg, _currentUserId)
|
await ImDatabase.saveMessage(finalMsg, _currentUserId)
|
||||||
} else {
|
|
||||||
applyMessageToMemory(finalMsg)
|
|
||||||
}
|
}
|
||||||
return finalMsg
|
return finalMsg
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failed = { ...outgoing, status: 'FAILED' as const }
|
const failed = { ...outgoing, status: 'FAILED' as const }
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||||
await ImDatabase.saveMessage(failed, _currentUserId)
|
await ImDatabase.saveMessage(failed, _currentUserId)
|
||||||
} else {
|
|
||||||
applyMessageToMemory(failed)
|
|
||||||
}
|
}
|
||||||
return failed
|
return failed
|
||||||
}
|
}
|
||||||
@ -711,25 +527,6 @@ export const ImSDK = {
|
|||||||
})
|
})
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||||||
await ImDatabase.revokeMessage(config.appId, messageId)
|
|
||||||
} else {
|
|
||||||
applyMessageToMemory(msg)
|
|
||||||
applyRevokeToMemory(messageId)
|
|
||||||
}
|
|
||||||
return msg
|
|
||||||
},
|
|
||||||
|
|
||||||
async editMessage(messageId: string, content: string): Promise<ImMessage> {
|
|
||||||
const config = getConfig()
|
|
||||||
const msg = await apiRequest<ImMessage>(`/api/im/messages/${encodeURIComponent(messageId)}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
params: { appId: config.appId },
|
|
||||||
body: { content },
|
|
||||||
})
|
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
|
||||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
|
||||||
} else {
|
|
||||||
applyEditToMemory(msg)
|
|
||||||
}
|
}
|
||||||
return msg
|
return msg
|
||||||
},
|
},
|
||||||
@ -766,26 +563,6 @@ export const ImSDK = {
|
|||||||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}`)
|
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
async listGroupMembers(groupId: string): Promise<UserProfile[]> {
|
|
||||||
const config = getConfig()
|
|
||||||
const res = await apiRequest<UserProfile[] | { content?: UserProfile[] }>(`/api/im/groups/${encodeURIComponent(groupId)}/members`, {
|
|
||||||
params: { appId: config.appId },
|
|
||||||
})
|
|
||||||
return Array.isArray(res) ? res : (res.content ?? [])
|
|
||||||
},
|
|
||||||
|
|
||||||
async searchGroupMembers(groupId: string, keyword: string, size = 20): Promise<UserProfile[]> {
|
|
||||||
const config = getConfig()
|
|
||||||
const res = await apiRequest<UserProfile[] | { content?: UserProfile[] }>(`/api/im/groups/${encodeURIComponent(groupId)}/members/search`, {
|
|
||||||
params: {
|
|
||||||
appId: config.appId,
|
|
||||||
keyword,
|
|
||||||
size: String(size),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return Array.isArray(res) ? res : (res.content ?? [])
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateGroupInfo(groupId: string, name?: string, announcement?: string): Promise<ImGroup> {
|
async updateGroupInfo(groupId: string, name?: string, announcement?: string): Promise<ImGroup> {
|
||||||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}`, {
|
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@ -986,36 +763,12 @@ export const ImSDK = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async searchUsers(keyword: string, size = 20): Promise<UserProfile[]> {
|
|
||||||
const config = getConfig()
|
|
||||||
const res = await apiRequest<UserProfile[] | { content?: UserProfile[] }>('/api/im/accounts/search', {
|
|
||||||
params: {
|
|
||||||
appId: config.appId,
|
|
||||||
keyword,
|
|
||||||
size: String(size),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return Array.isArray(res) ? res : (res.content ?? [])
|
|
||||||
},
|
|
||||||
|
|
||||||
async searchGroups(keyword: string, size = 20): Promise<ImGroup[]> {
|
|
||||||
const config = getConfig()
|
|
||||||
const res = await apiRequest<ImGroup[] | { content?: ImGroup[] }>('/api/im/groups/search', {
|
|
||||||
params: {
|
|
||||||
appId: config.appId,
|
|
||||||
keyword,
|
|
||||||
size: String(size),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return Array.isArray(res) ? res : (res.content ?? [])
|
|
||||||
},
|
|
||||||
|
|
||||||
async listConversations(): Promise<ConversationData[]> {
|
async listConversations(): Promise<ConversationData[]> {
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
if (ImDatabase.isInitialized()) {
|
if (ImDatabase.isInitialized()) {
|
||||||
const models = await ImDatabase.getConversations(config.appId)
|
const models = await ImDatabase.getConversations(config.appId)
|
||||||
if (models.length > 0) {
|
if (models.length > 0) {
|
||||||
const conversations = models.map(model => normalizeConversation({
|
return models.map(model => normalizeConversation({
|
||||||
targetId: model.targetId,
|
targetId: model.targetId,
|
||||||
chatType: model.chatType,
|
chatType: model.chatType,
|
||||||
lastMsgContent: model.lastMsgContent,
|
lastMsgContent: model.lastMsgContent,
|
||||||
@ -1025,9 +778,6 @@ export const ImSDK = {
|
|||||||
isMuted: model.isMuted,
|
isMuted: model.isMuted,
|
||||||
isPinned: model.isPinned,
|
isPinned: model.isPinned,
|
||||||
}))
|
}))
|
||||||
conversationMemory = conversations
|
|
||||||
emitConversationMemory()
|
|
||||||
return conversations
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1035,19 +785,12 @@ export const ImSDK = {
|
|||||||
params: { appId: config.appId },
|
params: { appId: config.appId },
|
||||||
})
|
})
|
||||||
const conversations = Array.isArray(res) ? res : (res.content ?? [])
|
const conversations = Array.isArray(res) ? res : (res.content ?? [])
|
||||||
const normalized = conversations.map(normalizeConversation)
|
return conversations.map(normalizeConversation)
|
||||||
conversationMemory = normalized
|
|
||||||
emitConversationMemory()
|
|
||||||
return normalized
|
|
||||||
},
|
},
|
||||||
|
|
||||||
subscribeConversations(callback: (conversations: ConversationData[]) => void): () => void {
|
subscribeConversations(callback: (conversations: ConversationData[]) => void): () => void {
|
||||||
if (!ImDatabase.isInitialized()) {
|
if (!ImDatabase.isInitialized()) {
|
||||||
conversationListeners.add(callback)
|
return () => {}
|
||||||
callback(sortConversations(conversationMemory))
|
|
||||||
return () => {
|
|
||||||
conversationListeners.delete(callback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
return ImDatabase.subscribeConversations(config.appId, (models) => {
|
return ImDatabase.subscribeConversations(config.appId, (models) => {
|
||||||
@ -1073,8 +816,6 @@ export const ImSDK = {
|
|||||||
})
|
})
|
||||||
if (ImDatabase.isInitialized()) {
|
if (ImDatabase.isInitialized()) {
|
||||||
await ImDatabase.markRead(config.appId, targetId)
|
await ImDatabase.markRead(config.appId, targetId)
|
||||||
} else {
|
|
||||||
markConversationReadMemory(targetId, chatType)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1090,8 +831,6 @@ export const ImSDK = {
|
|||||||
})
|
})
|
||||||
if (ImDatabase.isInitialized()) {
|
if (ImDatabase.isInitialized()) {
|
||||||
await ImDatabase.setConversationMuted(config.appId, targetId, muted)
|
await ImDatabase.setConversationMuted(config.appId, targetId, muted)
|
||||||
} else {
|
|
||||||
setConversationMutedMemory(targetId, chatType, muted)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1107,30 +846,19 @@ export const ImSDK = {
|
|||||||
})
|
})
|
||||||
if (ImDatabase.isInitialized()) {
|
if (ImDatabase.isInitialized()) {
|
||||||
await ImDatabase.setConversationPinned(config.appId, targetId, pinned)
|
await ImDatabase.setConversationPinned(config.appId, targetId, pinned)
|
||||||
} else {
|
|
||||||
setConversationPinnedMemory(targetId, chatType, pinned)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
|
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
const draftKey = `${config.appId}:${chatType}:${targetId}`
|
await apiRequest(`/api/im/conversations/${encodeURIComponent(targetId)}/draft`, {
|
||||||
draftStore.set(draftKey, draft)
|
method: 'PUT',
|
||||||
if (ImDatabase.isInitialized()) {
|
params: {
|
||||||
await ImDatabase.setDraft(config.appId, targetId, chatType, draft)
|
appId: config.appId,
|
||||||
}
|
chatType,
|
||||||
|
draft,
|
||||||
},
|
},
|
||||||
|
})
|
||||||
async getDraft(targetId: string, chatType: ChatType): Promise<string> {
|
|
||||||
const config = getConfig()
|
|
||||||
if (ImDatabase.isInitialized()) {
|
|
||||||
const draft = await ImDatabase.getConversationDraft(config.appId, targetId, chatType)
|
|
||||||
if (draft !== null) {
|
|
||||||
return draft
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const draftKey = `${config.appId}:${chatType}:${targetId}`
|
|
||||||
return draftStore.get(draftKey) ?? ''
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
|
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
|
||||||
@ -1144,8 +872,6 @@ export const ImSDK = {
|
|||||||
})
|
})
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||||
await ImDatabase.deleteConversation(config.appId, targetId, chatType, _currentUserId)
|
await ImDatabase.deleteConversation(config.appId, targetId, chatType, _currentUserId)
|
||||||
} else {
|
|
||||||
deleteConversationMemory(targetId, chatType)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1166,57 +892,25 @@ export const ImSDK = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
addListener(listener: ImEventListener): void {
|
addListener(listener: ImEventListener): void {
|
||||||
const wrapped: ImEventListener = {
|
client?.addListener({
|
||||||
...listener,
|
...listener,
|
||||||
onMessage: async (msg) => {
|
onMessage: async (msg) => {
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||||||
} else {
|
|
||||||
applyMessageToMemory(normalizeMessage(msg))
|
|
||||||
}
|
}
|
||||||
listener.onMessage?.(normalizeMessage(msg))
|
listener.onMessage?.(normalizeMessage(msg))
|
||||||
},
|
},
|
||||||
onGroupMessage: async (msg) => {
|
onGroupMessage: async (msg) => {
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||||||
} else {
|
|
||||||
applyMessageToMemory(normalizeMessage(msg))
|
|
||||||
}
|
}
|
||||||
listener.onGroupMessage?.(normalizeMessage(msg))
|
listener.onGroupMessage?.(normalizeMessage(msg))
|
||||||
},
|
},
|
||||||
onSystemMessage: async (msg) => {
|
})
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
|
||||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
|
||||||
} else {
|
|
||||||
applyMessageToMemory(normalizeMessage(msg))
|
|
||||||
}
|
|
||||||
listener.onSystemMessage?.(normalizeMessage(msg))
|
|
||||||
},
|
|
||||||
onRead: async (msg) => {
|
|
||||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
|
||||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
|
||||||
} else {
|
|
||||||
applyMessageToMemory(normalizeMessage(msg))
|
|
||||||
}
|
|
||||||
listener.onRead?.(normalizeMessage(msg))
|
|
||||||
},
|
|
||||||
onRevoke: async (data) => {
|
|
||||||
if (ImDatabase.isInitialized()) {
|
|
||||||
await ImDatabase.revokeMessage(getConfig().appId, data.msgId)
|
|
||||||
} else {
|
|
||||||
applyRevokeToMemory(data.msgId)
|
|
||||||
}
|
|
||||||
listener.onRevoke?.(data)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
listenerMap.set(listener, wrapped)
|
|
||||||
client?.addListener(wrapped)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
removeListener(listener: ImEventListener): void {
|
removeListener(listener: ImEventListener): void {
|
||||||
const wrapped = listenerMap.get(listener)
|
client?.removeListener(listener)
|
||||||
client?.removeListener(wrapped ?? listener)
|
|
||||||
listenerMap.delete(listener)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
subscribeGroup(groupId: string): void {
|
subscribeGroup(groupId: string): void {
|
||||||
|
|||||||
@ -14,6 +14,5 @@ export class ConversationModel extends Model {
|
|||||||
@field('unread_count') unreadCount!: number
|
@field('unread_count') unreadCount!: number
|
||||||
@field('is_muted') isMuted!: boolean
|
@field('is_muted') isMuted!: boolean
|
||||||
@field('is_pinned') isPinned!: boolean
|
@field('is_pinned') isPinned!: boolean
|
||||||
@field('draft') draft!: string | null
|
|
||||||
@date('updated_at') updatedAt!: Date
|
@date('updated_at') updatedAt!: Date
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { MessageModel } from './MessageModel'
|
|||||||
import type { ImMessage } from '../types'
|
import type { ImMessage } from '../types'
|
||||||
|
|
||||||
let _db: Database | null = null
|
let _db: Database | null = null
|
||||||
const draftStore = new Map<string, string>()
|
|
||||||
|
|
||||||
function getDb(): Database {
|
function getDb(): Database {
|
||||||
if (!_db) throw new Error('[ImDatabase] Not initialized — call ImDatabase.init() first.')
|
if (!_db) throw new Error('[ImDatabase] Not initialized — call ImDatabase.init() first.')
|
||||||
@ -19,10 +18,6 @@ function conversationId(appId: string, userId: string, targetId: string, chatTyp
|
|||||||
return `${appId}:S:${a}:${b}`
|
return `${appId}:S:${a}:${b}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function draftKey(appId: string, targetId: string, chatType: string): string {
|
|
||||||
return `${appId}:${chatType}:${targetId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageSearchParams {
|
export interface MessageSearchParams {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
toId?: string
|
toId?: string
|
||||||
@ -74,14 +69,12 @@ export const ImDatabase = {
|
|||||||
m.status = msg.status
|
m.status = msg.status
|
||||||
m.mentionedUserIds = msg.mentionedUserIds ?? null
|
m.mentionedUserIds = msg.mentionedUserIds ?? null
|
||||||
m.serverCreatedAt = new Date(msg.createdAt).getTime()
|
m.serverCreatedAt = new Date(msg.createdAt).getTime()
|
||||||
m.editedAt = msg.editedAt ?? null
|
|
||||||
m.syncedAt = now
|
m.syncedAt = now
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await existing[0].update((m: MessageModel) => {
|
await existing[0].update((m: MessageModel) => {
|
||||||
m.status = msg.status
|
m.status = msg.status
|
||||||
m.content = msg.content
|
m.content = msg.content
|
||||||
m.editedAt = msg.editedAt ?? m.editedAt
|
|
||||||
m.syncedAt = now
|
m.syncedAt = now
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -105,7 +98,6 @@ export const ImDatabase = {
|
|||||||
c.unreadCount = msg.fromUserId !== currentUserId ? 1 : 0
|
c.unreadCount = msg.fromUserId !== currentUserId ? 1 : 0
|
||||||
c.isMuted = false
|
c.isMuted = false
|
||||||
c.isPinned = false
|
c.isPinned = false
|
||||||
c.draft = null
|
|
||||||
c.updatedAt = new Date()
|
c.updatedAt = new Date()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -126,42 +118,6 @@ export const ImDatabase = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async revokeMessage(appId: string, messageId: string): Promise<void> {
|
|
||||||
const db = getDb()
|
|
||||||
const messages = await db
|
|
||||||
.get<MessageModel>('im_messages')
|
|
||||||
.query(Q.where('app_id', appId), Q.where('server_id', messageId))
|
|
||||||
.fetch()
|
|
||||||
if (messages.length === 0) return
|
|
||||||
|
|
||||||
const message = messages[0]
|
|
||||||
const now = Date.now()
|
|
||||||
const revokedContent = '消息已撤回'
|
|
||||||
|
|
||||||
await db.write(async () => {
|
|
||||||
await message.update((m: MessageModel) => {
|
|
||||||
m.status = 'REVOKED'
|
|
||||||
m.content = revokedContent
|
|
||||||
m.syncedAt = now
|
|
||||||
})
|
|
||||||
|
|
||||||
const conversations = await db
|
|
||||||
.get<ConversationModel>('im_conversations')
|
|
||||||
.query(Q.where('app_id', appId), Q.where('target_id', message.toId))
|
|
||||||
.fetch()
|
|
||||||
|
|
||||||
if (conversations.length > 0) {
|
|
||||||
await conversations[0].update((c: ConversationModel) => {
|
|
||||||
if (c.lastMsgId === messageId) {
|
|
||||||
c.lastMsgContent = revokedContent
|
|
||||||
c.lastMsgType = 'REVOKED'
|
|
||||||
c.updatedAt = new Date()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
async getMessages(appId: string, targetId: string, chatType: string, currentUserId: string, limit = 50): Promise<MessageModel[]> {
|
async getMessages(appId: string, targetId: string, chatType: string, currentUserId: string, limit = 50): Promise<MessageModel[]> {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
const convId = conversationId(appId, currentUserId, targetId, chatType)
|
const convId = conversationId(appId, currentUserId, targetId, chatType)
|
||||||
@ -199,23 +155,6 @@ export const ImDatabase = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async setDraft(appId: string, targetId: string, chatType: string, draft: string): Promise<void> {
|
|
||||||
draftStore.set(draftKey(appId, targetId, chatType), draft)
|
|
||||||
const db = getDb()
|
|
||||||
const convs = await db
|
|
||||||
.get<ConversationModel>('im_conversations')
|
|
||||||
.query(Q.where('app_id', appId), Q.where('target_id', targetId))
|
|
||||||
.fetch()
|
|
||||||
if (convs.length > 0) {
|
|
||||||
await db.write(async () => {
|
|
||||||
await convs[0].update((c: ConversationModel) => {
|
|
||||||
c.draft = draft
|
|
||||||
c.updatedAt = new Date()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async bulkSave(messages: ImMessage[], currentUserId: string): Promise<void> {
|
async bulkSave(messages: ImMessage[], currentUserId: string): Promise<void> {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
await this.saveMessage(msg, currentUserId)
|
await this.saveMessage(msg, currentUserId)
|
||||||
@ -224,7 +163,7 @@ export const ImDatabase = {
|
|||||||
|
|
||||||
async searchMessages(appId: string, params: MessageSearchParams): Promise<MessageModel[]> {
|
async searchMessages(appId: string, params: MessageSearchParams): Promise<MessageModel[]> {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
const conditions: any[] = [
|
const conditions: Parameters<typeof Q.and>[0][] = [
|
||||||
Q.where('app_id', appId),
|
Q.where('app_id', appId),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -252,10 +191,10 @@ export const ImDatabase = {
|
|||||||
.query(...conditions)
|
.query(...conditions)
|
||||||
|
|
||||||
if (params.limit !== undefined) {
|
if (params.limit !== undefined) {
|
||||||
query = query.extend(Q.take(params.limit) as any)
|
query = query.extend(Q.take(params.limit))
|
||||||
}
|
}
|
||||||
if (params.offset !== undefined) {
|
if (params.offset !== undefined) {
|
||||||
query = query.extend(Q.skip(params.offset) as any)
|
query = query.extend(Q.skip(params.offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.fetch()
|
return query.fetch()
|
||||||
@ -304,20 +243,6 @@ export const ImDatabase = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getConversationDraft(appId: string, targetId: string, chatType: string): Promise<string | null> {
|
|
||||||
const memoryDraft = draftStore.get(draftKey(appId, targetId, chatType))
|
|
||||||
if (memoryDraft !== undefined) {
|
|
||||||
return memoryDraft
|
|
||||||
}
|
|
||||||
const db = getDb()
|
|
||||||
const convs = await db
|
|
||||||
.get<ConversationModel>('im_conversations')
|
|
||||||
.query(Q.where('app_id', appId), Q.where('target_id', targetId))
|
|
||||||
.fetch()
|
|
||||||
if (convs.length === 0) return null
|
|
||||||
return convs[0].draft
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteConversation(appId: string, targetId: string, chatType: string, currentUserId: string): Promise<void> {
|
async deleteConversation(appId: string, targetId: string, chatType: string, currentUserId: string): Promise<void> {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
const convId = conversationId(appId, currentUserId, targetId, chatType)
|
const convId = conversationId(appId, currentUserId, targetId, chatType)
|
||||||
|
|||||||
@ -15,6 +15,5 @@ export class MessageModel extends Model {
|
|||||||
@field('status') status!: string
|
@field('status') status!: string
|
||||||
@field('mentioned_user_ids') mentionedUserIds!: string | null
|
@field('mentioned_user_ids') mentionedUserIds!: string | null
|
||||||
@field('server_created_at') serverCreatedAt!: number
|
@field('server_created_at') serverCreatedAt!: number
|
||||||
@field('edited_at') editedAt!: number | null
|
|
||||||
@field('synced_at') syncedAt!: number
|
@field('synced_at') syncedAt!: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { appSchema, tableSchema } from '@nozbe/watermelondb'
|
import { appSchema, tableSchema } from '@nozbe/watermelondb'
|
||||||
|
|
||||||
export const imDbSchema = appSchema({
|
export const imDbSchema = appSchema({
|
||||||
version: 4,
|
version: 2,
|
||||||
tables: [
|
tables: [
|
||||||
tableSchema({
|
tableSchema({
|
||||||
name: 'im_conversations',
|
name: 'im_conversations',
|
||||||
@ -16,7 +16,6 @@ export const imDbSchema = appSchema({
|
|||||||
{ name: 'unread_count', type: 'number' },
|
{ name: 'unread_count', type: 'number' },
|
||||||
{ name: 'is_muted', type: 'boolean' },
|
{ name: 'is_muted', type: 'boolean' },
|
||||||
{ name: 'is_pinned', type: 'boolean' },
|
{ name: 'is_pinned', type: 'boolean' },
|
||||||
{ name: 'draft', type: 'string', isOptional: true },
|
|
||||||
{ name: 'updated_at', type: 'number' },
|
{ name: 'updated_at', type: 'number' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -34,7 +33,6 @@ export const imDbSchema = appSchema({
|
|||||||
{ name: 'status', type: 'string' },
|
{ name: 'status', type: 'string' },
|
||||||
{ name: 'mentioned_user_ids', type: 'string', isOptional: true },
|
{ name: 'mentioned_user_ids', type: 'string', isOptional: true },
|
||||||
{ name: 'server_created_at', type: 'number' },
|
{ name: 'server_created_at', type: 'number' },
|
||||||
{ name: 'edited_at', type: 'number', isOptional: true },
|
|
||||||
{ name: 'synced_at', type: 'number' },
|
{ name: 'synced_at', type: 'number' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -5,12 +5,6 @@ import { ImSDK as _ImSDK } from './ImSDK'
|
|||||||
export const listFriends = (): Promise<string[]> => _ImSDK.listFriends()
|
export const listFriends = (): Promise<string[]> => _ImSDK.listFriends()
|
||||||
export const addFriend = (friendId: string): Promise<void> => _ImSDK.addFriend(friendId)
|
export const addFriend = (friendId: string): Promise<void> => _ImSDK.addFriend(friendId)
|
||||||
export const removeFriend = (friendId: string): Promise<void> => _ImSDK.removeFriend(friendId)
|
export const removeFriend = (friendId: string): Promise<void> => _ImSDK.removeFriend(friendId)
|
||||||
export const searchUsers = (keyword: string, size?: number): ReturnType<typeof _ImSDK.searchUsers> => _ImSDK.searchUsers(keyword, size)
|
|
||||||
export const searchGroups = (keyword: string, size?: number): ReturnType<typeof _ImSDK.searchGroups> => _ImSDK.searchGroups(keyword, size)
|
|
||||||
export const searchMessages = (params: Parameters<typeof _ImSDK.searchMessages>[0]): ReturnType<typeof _ImSDK.searchMessages> => _ImSDK.searchMessages(params)
|
|
||||||
export const editMessage = (messageId: string, content: string): ReturnType<typeof _ImSDK.editMessage> => _ImSDK.editMessage(messageId, content)
|
|
||||||
export const locateHistoryPage = (toId: string, messageId: string, pageSize?: number, maxPages?: number): ReturnType<typeof _ImSDK.locateHistoryPage> => _ImSDK.locateHistoryPage(toId, messageId, pageSize, maxPages)
|
|
||||||
export const locateGroupHistoryPage = (groupId: string, messageId: string, pageSize?: number, maxPages?: number): ReturnType<typeof _ImSDK.locateGroupHistoryPage> => _ImSDK.locateGroupHistoryPage(groupId, messageId, pageSize, maxPages)
|
|
||||||
export { ImClient } from './ImClient'
|
export { ImClient } from './ImClient'
|
||||||
export { ImDatabase } from './db/ImDatabase'
|
export { ImDatabase } from './db/ImDatabase'
|
||||||
export type { MessageSearchParams } from './db/ImDatabase'
|
export type { MessageSearchParams } from './db/ImDatabase'
|
||||||
|
|||||||
@ -33,7 +33,6 @@ export interface ImMessage {
|
|||||||
groupReadCount?: number
|
groupReadCount?: number
|
||||||
revoked?: boolean
|
revoked?: boolean
|
||||||
createdAt: number
|
createdAt: number
|
||||||
editedAt?: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImEventListener {
|
export interface ImEventListener {
|
||||||
@ -41,9 +40,6 @@ export interface ImEventListener {
|
|||||||
onDisconnected?: (reason?: string) => void
|
onDisconnected?: (reason?: string) => void
|
||||||
onMessage?: (msg: ImMessage) => void
|
onMessage?: (msg: ImMessage) => void
|
||||||
onGroupMessage?: (msg: ImMessage) => void
|
onGroupMessage?: (msg: ImMessage) => void
|
||||||
onSystemMessage?: (msg: ImMessage) => void
|
|
||||||
onRead?: (msg: ImMessage) => void
|
|
||||||
onRevoke?: (data: { msgId: string; operatorId: string }) => void
|
|
||||||
onError?: (error: string) => void
|
onError?: (error: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,157 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* XuqmGroup Update Service — React Native Bundle Release Script
|
|
||||||
*
|
|
||||||
* Platform-native approach: runs via Node.js (always available in RN projects).
|
|
||||||
* Add to your app's package.json scripts:
|
|
||||||
* "xuqm:release": "node node_modules/@xuqm/update/scripts/xuqm_release.mjs"
|
|
||||||
*
|
|
||||||
* Config: xuqm.config.json in the project root
|
|
||||||
* {
|
|
||||||
* "serverUrl": "https://update.dev.xuqinmin.com",
|
|
||||||
* "appId": "your-app-id",
|
|
||||||
* "apiToken": "your-api-token",
|
|
||||||
* "rn": {
|
|
||||||
* "modules": [
|
|
||||||
* { "moduleId": "main", "entryFile": "index.js", "platforms": ["android", "ios"] }
|
|
||||||
* ],
|
|
||||||
* "bundleOutputDir": "/tmp/xuqm_rn_bundle"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Dependencies: none beyond Node.js built-ins (uses native fetch + node:child_process)
|
|
||||||
* Minimum Node.js: 18 (built-in fetch)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { execSync } from 'node:child_process'
|
|
||||||
import { createReadStream, mkdirSync, existsSync, readFileSync } from 'node:fs'
|
|
||||||
import { createInterface } from 'node:readline'
|
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
// ── Config ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const CONFIG_FILE = process.env.XUQM_CONFIG_FILE ?? 'xuqm.config.json'
|
|
||||||
if (!existsSync(CONFIG_FILE)) {
|
|
||||||
console.error(`[xuqm] Config not found: ${CONFIG_FILE}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
const cfg = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
|
|
||||||
const { serverUrl, appId, apiToken, rn = {} } = cfg
|
|
||||||
if (!serverUrl || !appId || !apiToken) {
|
|
||||||
console.error('[xuqm] serverUrl / appId / apiToken are required in config')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
const modules = rn.modules ?? [{ moduleId: 'main', entryFile: 'index.js', platforms: ['android', 'ios'] }]
|
|
||||||
const bundleOutputDir = rn.bundleOutputDir ?? '/tmp/xuqm_rn_bundle'
|
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
||||||
const ask = (q) => new Promise(res => rl.question(`\x1b[36m${q}\x1b[0m`, res))
|
|
||||||
const confirm = async (q) => /^y/i.test(((await ask(`${q} [y/N]: `)) ?? '').trim())
|
|
||||||
|
|
||||||
function apiHeaders() { return { Authorization: `Bearer ${apiToken}` } }
|
|
||||||
|
|
||||||
async function apiFetch(path, opts = {}) {
|
|
||||||
const res = await fetch(`${serverUrl}${path}`, { headers: apiHeaders(), ...opts })
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multipart upload using FormData (Node 18+ has built-in FormData)
|
|
||||||
async function uploadBundle(moduleId, platform, bundleFile, version, minVersion, note) {
|
|
||||||
const form = new FormData()
|
|
||||||
const blob = new Blob([readFileSync(bundleFile)], { type: 'application/octet-stream' })
|
|
||||||
form.append('appId', appId)
|
|
||||||
form.append('moduleId', moduleId)
|
|
||||||
form.append('platform', platform.toUpperCase())
|
|
||||||
form.append('version', version)
|
|
||||||
form.append('minCommonVersion', minVersion)
|
|
||||||
form.append('note', note)
|
|
||||||
form.append('bundle', blob, path.basename(bundleFile))
|
|
||||||
|
|
||||||
const res = await fetch(`${serverUrl}/api/v1/rn/upload`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: apiHeaders(),
|
|
||||||
body: form,
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`Upload failed: ${await res.text()}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('\x1b[36m=== XuqmGroup React Native Release ===\x1b[0m\n')
|
|
||||||
|
|
||||||
// ── 1. Local version from package.json ──────────────────────────────────
|
|
||||||
const pkg = JSON.parse(readFileSync('package.json', 'utf8'))
|
|
||||||
let localVersion = pkg.version ?? ''
|
|
||||||
console.log(`Local version (package.json): \x1b[32m${localVersion}\x1b[0m`)
|
|
||||||
|
|
||||||
// ── 2. Server latest ─────────────────────────────────────────────────────
|
|
||||||
let serverVersion = 'none'
|
|
||||||
try {
|
|
||||||
const resp = await apiFetch(`/api/v1/rn/list?appId=${appId}`)
|
|
||||||
const published = (resp.data ?? []).filter(x => x.publishStatus === 'PUBLISHED')
|
|
||||||
serverVersion = published[0]?.version ?? 'none'
|
|
||||||
} catch { /* no bundles yet */ }
|
|
||||||
console.log(`Server latest: \x1b[33m${serverVersion}\x1b[0m`)
|
|
||||||
|
|
||||||
// ── 3. Validate version ───────────────────────────────────────────────────
|
|
||||||
if (localVersion && serverVersion !== 'none' && localVersion <= serverVersion) {
|
|
||||||
console.warn(`\x1b[33m⚠ Local (${localVersion}) ≤ server (${serverVersion})\x1b[0m`)
|
|
||||||
const cont = await confirm('Continue anyway?')
|
|
||||||
if (!cont) { localVersion = await ask('New version string: ') }
|
|
||||||
}
|
|
||||||
if (!localVersion) localVersion = await ask('Version string (e.g. 1.2.3): ')
|
|
||||||
|
|
||||||
// ── 4. Options ────────────────────────────────────────────────────────────
|
|
||||||
const note = await ask('Release notes: ')
|
|
||||||
const minVersion = await ask('Min compatible native version (e.g. 1.0.0): ')
|
|
||||||
|
|
||||||
console.log('\n\x1b[36m--- Summary ---\x1b[0m')
|
|
||||||
console.log(` Version: ${localVersion}`)
|
|
||||||
console.log(` Modules: ${modules.map(m => m.moduleId).join(', ')}`)
|
|
||||||
const ok = await confirm('Proceed?')
|
|
||||||
if (!ok) { console.log('Aborted.'); rl.close(); return }
|
|
||||||
|
|
||||||
// ── 5. Build & upload each module/platform ────────────────────────────────
|
|
||||||
mkdirSync(bundleOutputDir, { recursive: true })
|
|
||||||
|
|
||||||
for (const mod of modules) {
|
|
||||||
for (const platform of mod.platforms) {
|
|
||||||
const outDir = path.join(bundleOutputDir, mod.moduleId, platform)
|
|
||||||
mkdirSync(outDir, { recursive: true })
|
|
||||||
const bundleFile = path.join(outDir, `${mod.moduleId}.${platform}.bundle`)
|
|
||||||
|
|
||||||
// Build using react-native CLI (platform-native, no extra deps)
|
|
||||||
console.log(`\n\x1b[36mBuilding ${mod.moduleId}/${platform}...\x1b[0m`)
|
|
||||||
execSync(
|
|
||||||
`npx react-native bundle \
|
|
||||||
--platform ${platform} \
|
|
||||||
--entry-file ${mod.entryFile ?? 'index.js'} \
|
|
||||||
--bundle-output ${bundleFile} \
|
|
||||||
--dev false \
|
|
||||||
--minify true`,
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Upload
|
|
||||||
console.log('\x1b[36mUploading...\x1b[0m')
|
|
||||||
const resp = await uploadBundle(mod.moduleId, platform, bundleFile, localVersion, minVersion, note)
|
|
||||||
const bundleId = resp.data?.id
|
|
||||||
console.log(`\x1b[32m✓ ${mod.moduleId}/${platform} uploaded, ID: ${bundleId}\x1b[0m`)
|
|
||||||
console.log(` Publish: POST ${serverUrl}/api/v1/rn/${bundleId}/publish`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.close()
|
|
||||||
console.log('\n\x1b[32m=== Release complete ===\x1b[0m')
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(e => {
|
|
||||||
console.error(`\n\x1b[31m❌ ${e.message}\x1b[0m`)
|
|
||||||
rl.close()
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@ -71,15 +71,6 @@ export const ImSDK = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async editMessage(messageId: string, content: string): Promise<ImMessage> {
|
|
||||||
const config = getConfig()
|
|
||||||
return apiRequest<ImMessage>(`/api/im/messages/${encodeURIComponent(messageId)}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
params: { appId: config.appId },
|
|
||||||
body: { content },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
addListener(listener: ImEventListener) {
|
addListener(listener: ImEventListener) {
|
||||||
client?.addListener(listener)
|
client?.addListener(listener)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,7 +28,6 @@ export interface ImMessage {
|
|||||||
status: MsgStatus
|
status: MsgStatus
|
||||||
mentionedUserIds?: string
|
mentionedUserIds?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
editedAt?: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImEventListener {
|
export interface ImEventListener {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export { ScaledImage } from '../packages/common/src'
|
|||||||
export { ImSDK } from '../packages/im/src'
|
export { ImSDK } from '../packages/im/src'
|
||||||
export { ImClient } from '../packages/im/src'
|
export { ImClient } from '../packages/im/src'
|
||||||
export { uploadFile } from '../packages/im/src'
|
export { uploadFile } from '../packages/im/src'
|
||||||
export { listFriends, addFriend, removeFriend, editMessage } from '../packages/im/src'
|
export { listFriends, addFriend, removeFriend } from '../packages/im/src'
|
||||||
export type {
|
export type {
|
||||||
ImMessage, ImGroup, ChatType, MsgType, MsgStatus,
|
ImMessage, ImGroup, ChatType, MsgType, MsgStatus,
|
||||||
ImEventListener, SendMessageParams,
|
ImEventListener, SendMessageParams,
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户