千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(三)(下篇)

本文篇幅较长,预计阅读时长1-2h。

这是《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成》的第三篇-下篇:《APP 内部流程与逻辑》

系列文章可参考:

《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(一)》:Demo演示与IM设计

《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(二)》:UI设计与搭建

《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(三)》:APP 内部流程与逻辑(上)

《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(四)》:服务端搭建与总结

7. 会话窗口消息展示

进入会话窗口前,需要先准备好本地已保存的历史消息。然后显示窗口界面的同时,异步线程开始拉取最新的历史消息,并且检查之前的历史消息是否有缺失,有缺失就会持续填补,直到填补完成,或者用户退出等引发的填补中断。

编辑 IMCenter.swift,加入 showDialogueView() 函数:

    class func showDialogueView(contact: ContactInfo) {
        
        //-- 仅能在主线程中调用
        IMCenter.viewSharedInfo.targetContact = contact
        
        IMCenter.viewSharedInfo.strangerContacts.removeAll()
        
        IMCenter.prepareDialogueMesssageInfos(contact: contact)
        
        IMCenter.viewSharedInfo.lastPage = IMCenter.viewSharedInfo.currentPage
        
        IMCenter.viewSharedInfo.currentPage = .DialogueView
    }

并修改 ContactItemView.swift 中 onTapGesture 行为为:

        ... ...

        .onTapGesture {
            IMCenter.showDialogueView(contact: contactInfo)
        }

        ... ...

showDialogueView() 主要清理上一个(如果存在)会话的相关数据,并调用 prepareDialogueMesssageInfos() 函数,准备新的会话数据,然后显示会话窗口界面。

7.1 准备会话窗口数据

prepareDialogueMesssageInfos() 函数先加载本地保存的历史数据,并按时间排序;然后启动两个异步线程,一个清理未同步的未知联系人信息,一个检查并不断填充历史消息。

prepareDialogueMesssageInfos() 代码如下:

    private class func soreChatMessages(messages: [ChatMessage]) -> [ChatMessage] {
        if messages.count <= 1 {
            return messages
        }
        
        return messages.sorted(by: { (m1, m2) -> Bool in
            if m1.mtime < m2.mtime {
                return true
            }
            
            if m1.mtime == m2.mtime {
                if m1.mid < m2.mid {
                    return true
                }
            }
            return false
        })
    }


    class func prepareDialogueMesssageInfos(contact:ContactInfo) {
        
        if contact.kind == ContactKind.Room.rawValue {
            DispatchQueue.global(qos: .default).async {
                IMCenter.client!.enterRoom(withId: NSNumber(value: contact.xid), timeout: 0, success: {
                    
                    DispatchQueue.main.sync {
                        
                        sendCmd(contact: contact, message: "\(getSelfDispalyName()) 进入房间")
                    }

                }, fail: { _ in })
            }
        }
        
        let chatMessages = IMCenter.db.loadAllMessages(contact:contact)
        IMCenter.viewSharedInfo.dialogueMesssages = soreChatMessages(messages: chatMessages)
        
        DispatchQueue.global(qos: .default).async {
            let unknownContacts = pickupUnknownContacts(messages:chatMessages)
            cleanUnknownContacts(unknownContacts: unknownContacts)
        }
        
        DispatchQueue.global(qos: .default).async {
            refillHistoryMessage(contact:contact)
        }
    }

7.2 发送系统通知

当点击的联系人为房间类型时,为了简单起见,直接通过客户端进入房间。这时,我们需要告诉房间中的其他用户,谁进入房间了。因此,我们在这里用 RTM 预定义的 Cmd 类型消息发送系统通知:

    private class func sendGroupCmd(contact:ContactInfo, message:String) {
        IMCenter.client!.sendGroupCmdMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: { _ in
        }, fail: {
            _ in
        })
    }
    
    private class func sendRoomCmd(contact:ContactInfo, message:String) {
        IMCenter.client!.sendRoomCmdMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: { _ in
        }, fail: {
            _ in
        })
    }

    class func sendCmd(contact:ContactInfo, message:String) {
        if contact.kind == ContactKind.Group.rawValue {
            sendGroupCmd(contact:contact, message:message)
        }else if contact.kind == ContactKind.Room.rawValue {
            sendRoomCmd(contact:contact, message:message)
        } else { return }
        
        objc_sync_enter(locker)
        let mid = fakeMid
        fakeMid += 1
        objc_sync_exit(locker)
        
        let curr = Date().timeIntervalSince1970 * 1000
        
        let chatMessage = ChatMessage(sender: IMCenter.client!.userId, mid: mid, mtime: Int64(curr), message: message)
        chatMessage.isChat = false
        
        var chats = IMCenter.viewSharedInfo.dialogueMesssages
        chats.append(chatMessage)
        
        IMCenter.viewSharedInfo.dialogueMesssages = chats
    }

因为用户发送消息是网络操作,而网络操作可能会失败,比如断网时。因此我们无法等服务器确认后,再将用户发送的内容显示到界面上。但 RTM 只有在返回后,才能获取到消息的 messageId。但显示消息需要使用 MessageId 初始化 ChatMessage 对象。于是我们采用了当年QQ的方法:直接本地显示(这方法现在微信也还在用)。为此,我们引入了一个虚假的MessageId:fakeMid:Int64 进行代替。

但是在其他情况下,比如修改信息或者其他情况,也需要发送系统通知。而且这些系统通知往往是在网络操作完成后,在其他线程内异步触发的。那就存在着 fakeMid 被并发读写的情况。于是我们需要添加一个锁对象 class Locker,并用其进行同步。
于是在 IMCenter.swift 中,增加以下代码:

... ...

class Locker {}

... ...

class IMCenter {
    
    ... ...
    
    static var locker = Locker()
    
    ... ...
    
    static var fakeMid:Int64 = 1
    
    ... ...
}

7.3 同步未知联系人

在一个群组,或者房间中,RTM系统推送过来的消息,只有发送人唯一数字ID,而其它的展示信息,则须要我们自己获取。无论是从我们自己的服务器上获取,还是从RTM服务器上获取。
此外,在获取的过程中,可能因为用户退出等原因,获取过程被中断,此时,App本地的数据中也将存在未知联系人。所以 pickupUnknownContacts() 便是从本地消息中筛查出未知联系人,然后交给 cleanUnknownContacts() 进行信息更新。
相关代码如下:

    private class func syncQueryUsersInfos(type: Int, contacts:[ContactInfo]) {
        
        var queryIds = [NSNumber]()
        for contact in contacts {
            queryIds.append(NSNumber(value: contact.xid))
        }
        
        let attriAnswer = IMCenter.client!.getUserOpenInfo(queryIds, timeout: 0)
        let strangers = decodeAttributeAnswer(type: type, attriAnswer: attriAnswer)
        
        DispatchQueue.main.async {
            for stranger in strangers {
                if let contact = IMCenter.viewSharedInfo.strangerContacts[stranger.xid] {
                    contact.nickname = stranger.nickname
                    contact.imageUrl = stranger.imageUrl
                    contact.showInfo = stranger.showInfo
                } else {
                    IMCenter.viewSharedInfo.strangerContacts[stranger.xid] = stranger
                }
            }
        }
    }

    private class func pickupUnknownContacts(messages:[ChatMessage]) -> [ContactInfo] {
        
        var uids: Set<Int64> = []
        for msg in messages {
            if msg.sender != IMCenter.client!.userId {
                uids.insert(msg.sender)
            }
        }
        
        var contacts = [ContactInfo]()
        if uids.isEmpty == false {
            let allUsers = IMCenter.db.loadAllUserContactInfos()
            
            for uid in uids {
                if let contact = allUsers[uid] {
                    if contact.nickname.isEmpty || contact.imageUrl.isEmpty {
                        contacts.append(contact)
                    }
                } else {
                    contacts.append(ContactInfo(xid: uid))
                }
            }
        }
        
        return contacts
    }

    private class func cleanUnknownContacts(unknownContacts:[ContactInfo]) {
        
        //-- 查询 xname
        var uids = [Int64]()
        for contact in unknownContacts {
            uids.append(contact.xid)
        }
        BizClient.lookup(users: nil, groups: nil, rooms: nil, uids: uids, gids: nil, rids: nil, completedAction: {
            lookupData in
            
            var strangers = [ContactInfo]()
            for (key, value) in lookupData.users {
                let contact = ContactInfo(type: ContactKind.Stranger.rawValue, xid: value)
                contact.xname = key
                
                strangers.append(contact)
            }
            
            DispatchQueue.main.async {
                for stranger in strangers {
                    if let contact = IMCenter.viewSharedInfo.strangerContacts[stranger.xid] {
                        contact.xname = stranger.xname
                    } else {
                        IMCenter.viewSharedInfo.strangerContacts[stranger.xid] = stranger
                    }
                }
            }
        }, errorAction: { _ in })
        
        //-- 查询展示信息
        if unknownContacts.count < 100 {
            syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: unknownContacts)
        } else {
            var queryContacts = [ContactInfo]()
            
            for contact in unknownContacts {
                queryContacts.append(contact)
                if queryContacts.count == 99 {
                    syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: queryContacts)
                    queryContacts.removeAll()
                }
            }
            
            if queryContacts.count > 0 {
                syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: queryContacts)
            }
        }
    }

7.4 填补空缺的历史消息

最后一步,是填补历史消息空缺。填补历史消息空缺其实也很简单。先从数据库中获取已经保存的历史消息检查点,按从新到旧进行排序。然后从新到旧,以10条为单位(RTM的默认限制),降序拉取当前会话的历史消息,然后逐条保存。一旦需要保存的历史消息已经在数据库中存在,则意味着已经填补了历史消息的最新一段空缺。则检查对应范围内是否存在历史消息检查点,如果存在则删除,不存在则以最新的历史消息检查点开始,继续从新到旧,以降序拉取历史消息。
如果这之间发现有未知联系人,则启动新的异步线程进行同步。
填补空缺历史消息的 refillHistoryMessage() 函数及相关代码如下:

    private class func sortHistoryCheckpoint(checkpoints:[HistoryCheckpoint]) -> [HistoryCheckpoint] {
        if checkpoints.count < 2 {
            return checkpoints
        }
        
        return checkpoints.sorted(by: { (c1, c2) -> Bool in
            if c1.ts > c2.ts {
                return true
            }
            if c1.ts == c2.ts {
                return c1.desc
            }
            return false
        })
    }

    private class func refillHistoryMessage(contact:ContactInfo) {
        var historyAnswer: RTMHistoryMessageAnswer? = nil
        var begin: Int64 = 0
        var end: Int64 = 0
        var lastId: Int64 = 0
        
        let fetchCount = 10
        let nsXid = NSNumber(value: contact.xid)
        let nsCount = NSNumber(value: fetchCount)
        
        var checkpoints = db.loadAllHistoryMessageCheckpoints(contact:contact)
        checkpoints = sortHistoryCheckpoint(checkpoints: checkpoints)
        
        while (true)
        {
            if contact.kind == ContactKind.Friend.rawValue {
                historyAnswer = IMCenter.client!.getP2PHistoryMessageChat(withUserId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
            } else if contact.kind == ContactKind.Group.rawValue {
                historyAnswer = IMCenter.client!.getGroupHistoryMessageChat(withGroupId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
            } else if contact.kind == ContactKind.Room.rawValue {
                historyAnswer = IMCenter.client!.getRoomHistoryMessageChat(withRoomId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
            } else { return }
            
            if historyAnswer != nil && historyAnswer!.error.code == 0 {
                
                var chatMessages = [ChatMessage]()
                for message in historyAnswer!.history.messageArray {
                    
                    if message.messageType == 30 {
                        if IMCenter.db.insertChatMessage(type: contact.kind, xid: contact.xid, sender: message.fromUid, mid: message.messageId, message: message.stringMessage, mtime: message.modifiedTime, printError: false) == false {
                            break
                        }
                    } else {
                        if IMCenter.db.insertChatCmd(type: contact.kind, xid: contact.xid, sender: message.fromUid, mid: message.messageId, message: message.stringMessage, mtime: message.modifiedTime, printError: false) == false {
                            break
                        }
                    }
                    
                    let chatMsg = ChatMessage(sender: message.fromUid, mid: message.messageId, mtime: message.modifiedTime, message: message.stringMessage)
                    
                    if message.messageType != 30 {
                        chatMsg.isChat = false
                    }
                    
                    chatMessages.append(chatMsg)
                }
                
                if chatMessages.count > 0 {
                    
                    DispatchQueue.global(qos: .default).async {
                        
                        let unknownContacts = pickupUnknownContacts(messages:chatMessages)
                        cleanUnknownContacts(unknownContacts: unknownContacts)
                    }
                    
                    var continueLoading = true

                    DispatchQueue.main.sync {
                        if IMCenter.viewSharedInfo.targetContact == nil
                            || IMCenter.viewSharedInfo.targetContact!.xid != contact.xid
                            || IMCenter.viewSharedInfo.targetContact!.kind != contact.kind {
                            continueLoading = false
                        } else {
                            var oldDialogues = IMCenter.viewSharedInfo.dialogueMesssages
                            
                            for chatMsg in chatMessages {
                                oldDialogues.append(chatMsg)
                            }
                            
                            oldDialogues = soreChatMessages(messages: oldDialogues)
                            IMCenter.viewSharedInfo.dialogueMesssages = oldDialogues
                        }
                    }

                    if continueLoading == false {
                        IMCenter.db.insertCheckPoint(type: contact.kind, xid: contact.xid, ts:historyAnswer!.history.end, desc:true)
                        return
                    }
                }
                
                if historyAnswer!.history.messageArray.count < fetchCount {
                    IMCenter.db.clearAllHistoryMessageCheckpoints(contact: contact)
                    return
                }
                
                if chatMessages.count == fetchCount {
                    
                    begin = historyAnswer!.history.begin
                    end = historyAnswer!.history.end
                    lastId = historyAnswer!.history.lastid
                    
                } else {
                    while (true) {
                        if checkpoints.count == 0 { return }
                        
                        if checkpoints.first!.ts >= end {
                            checkpoints.removeFirst()
                        } else {
                            begin = 0
                            end = Int64(checkpoints.first!.ts)
                            lastId = 0
                            
                            break
                        }
                    }
                }
            } else if historyAnswer != nil && historyAnswer!.error.code == 200010 {
                var continueLoading = true
                DispatchQueue.main.sync {
                    if IMCenter.viewSharedInfo.targetContact == nil
                        || IMCenter.viewSharedInfo.targetContact!.xid != contact.xid
                        || IMCenter.viewSharedInfo.targetContact!.kind != contact.kind {
                        continueLoading = false
                    }
                }
                
                if continueLoading == false {
                    IMCenter.db.insertCheckPoint(type: contact.kind, xid: contact.xid, ts:end, desc:true)
                    return
                }
                
                sleep(2)
            } else {
                return
            }
        }
    }

至此,会话窗口的历史信息处理完毕。

7.5 消息的发送

在我们开始修改会话窗口的视图界面前,我们还的处理消息的发送行为。毕竟会话窗口不仅需要展示会话内容,还需要提供发送消息的能力。
sendMessage() 其实与 sendCmd() 高度类似。毕竟 sendMessge() 本质上是使用的 RTM 预定义的 chat 类型消息。chat 类型消息与 cmd 消息类型的不同之处在于,如果 chat 的内容是单纯的文本聊天内容,而非json、xml等结构化的信息,则可以直接开启RTM的文本自动翻译和文本自动审核两个功能。除此之外,其余没有差别。

    private class func sendP2PMessage(contact:ContactInfo, message:String) {
        IMCenter.client!.sendP2PMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {
            answer in
            _ = IMCenter.db.insertChatMessage(type: ContactKind.Friend.rawValue, xid: contact.xid, sender: IMCenter.client!.userId, mid: answer.messageId, message: message, mtime: answer.mtime)
        }, fail: {
            _ in
            //-- 这里应该在UI上显示红色圆形底叹号,但IMDemo为演示目的,这里从略
        })
    }
    
    private class func sendGroupMessage(contact:ContactInfo, message:String) {
        IMCenter.client!.sendGroupMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {
            answer in
            _ = IMCenter.db.insertChatMessage(type: ContactKind.Group.rawValue, xid: contact.xid, sender: IMCenter.client!.userId, mid: answer.messageId, message: message, mtime: answer.mtime)
        }, fail: {
            _ in
            //-- 这里应该在UI上显示红色圆形底叹号,但IMDemo为演示目的,这里从略
        })
    }
    
    private class func sendRoomMessage(contact:ContactInfo, message:String) {
        IMCenter.client!.sendRoomMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {
            answer in
            _ = IMCenter.db.insertChatMessage(type: ContactKind.Room.rawValue, xid: contact.xid, sender: IMCenter.client!.userId, mid: answer.messageId, message: message, mtime: answer.mtime)
        }, fail: {
            _ in
            //-- 这里应该在UI上显示红色圆形底叹号,但IMDemo为演示目的,这里从略
        })
    }

    class func sendMessage(contact:ContactInfo, message:String) {
        if contact.kind == ContactKind.Friend.rawValue {
            sendP2PMessage(contact:contact, message:message)
        } else if contact.kind == ContactKind.Group.rawValue {
            sendGroupMessage(contact:contact, message:message)
        }else if contact.kind == ContactKind.Room.rawValue {
            sendRoomMessage(contact:contact, message:message)
        } else { return }
        
        objc_sync_enter(locker)
        let mid = fakeMid
        fakeMid += 1
        objc_sync_exit(locker)
        
        let curr = Date().timeIntervalSince1970 * 1000
        
        let chatMessage = ChatMessage(sender: IMCenter.client!.userId, mid: mid, mtime: Int64(curr), message: message)
        
        var chats = IMCenter.viewSharedInfo.dialogueMesssages
        chats.append(chatMessage)
        
        IMCenter.viewSharedInfo.dialogueMesssages = chats
    }

7.6 实现之前占位的辅助功能

在最后和会话窗口页面关联起来之前,第二篇中,会话窗口需要的一个功能 findContact(),当时仅实现了一个空函数用于占位。此时,我们需要先将其完善。

函数的基本流程是,现在好友列表中查找,找到返回。如果不存在,则在记录的陌生人中查找。如果还找不到,检查是不是当前登录用户自身。如果不是,记录新的陌生人信息,并调用 cleanUnknownContacts() 同步联系人信息。

编辑 IMCenter.swift,修改 findContact() 函数代码如下:

class func findContact(chatMessage: ChatMessage) -> ContactInfo {
        //-- 好友列表中查找
        for contact in IMCenter.viewSharedInfo.contactList[ContactKind.Friend.rawValue]! {
            if contact.xid == chatMessage.sender {
                return contact
            }
        }
        
        //-- 陌生人中查找
        if let contact = IMCenter.viewSharedInfo.strangerContacts[chatMessage.sender] {
            return contact
        }
        
        let contact = ContactInfo(type: ContactKind.Stranger.rawValue, xid: chatMessage.sender)
        
        if chatMessage.sender == IMCenter.client!.userId {
            let username = IMCenter.fetchUserProfile(key: "username")
            contact.imageUrl = IMCenter.fetchUserProfile(key: "\(username)-image-url")
            contact.imagePath = IMCenter.fetchUserProfile(key: "\(username)-image")
        } else {
            IMCenter.viewSharedInfo.strangerContacts[chatMessage.sender] = contact
            
            DispatchQueue.global(qos: .default).async {
                
                var unknownContacts = [ContactInfo]()
                unknownContacts.append(contact)
                cleanUnknownContacts(unknownContacts: unknownContacts)
            }
        }
        
        return contact
    }

7.7 界面UI同步修改

最后,我们将功能和页面关联起来。
编辑 DialogueView.swift,首先是 DialogueHeaderView 视图,增加函数 updateSessionState(),并修改返回按钮的 onTapGesture 行为如下:

struct DialogueHeaderView: View {
    
    ... ...

    func updateSessionState() {
        for session in IMCenter.viewSharedInfo.sessions {
            if session.contact.kind == IMCenter.viewSharedInfo.targetContact!.kind && session.contact.xid == IMCenter.viewSharedInfo.targetContact!.xid {
                session.lastMessage.unread = false
                
                for idx in 0..<IMCenter.viewSharedInfo.dialogueMesssages.count {
                    let realIdx = IMCenter.viewSharedInfo.dialogueMesssages.count - 1 - idx
                    if IMCenter.viewSharedInfo.dialogueMesssages[realIdx].isChat {
                        
                        let message = IMCenter.viewSharedInfo.dialogueMesssages[realIdx]
                        session.lastMessage.message = message.message
                        session.lastMessage.mid = message.mid
                        session.lastMessage.timestamp = message.mtime
                        
                        let oldSessions = IMCenter.viewSharedInfo.sessions
                        IMCenter.viewSharedInfo.sessions = IMCenter.sortSessions(sessions: oldSessions)
                        
                        return
                    }
                }
                
                session.lastMessage.message = ""
                session.lastMessage.mid = 0
                session.lastMessage.timestamp = 0
                
                let oldSessions = IMCenter.viewSharedInfo.sessions
                IMCenter.viewSharedInfo.sessions = IMCenter.sortSessions(sessions: oldSessions)
                
                return
            }
        }
    }
    
    var body: some View {
        HStack {
            Image("button_back")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:IMDemoUIConfig.navigationIconEdgeLen, height: IMDemoUIConfig.navigationIconEdgeLen, alignment: .center)
                .padding((IMDemoUIConfig.topNavigationHight - IMDemoUIConfig.navigationIconEdgeLen)/2)
                .onTapGesture {
                    updateSessionState()
                    
                    IMCenter.viewSharedInfo.currentPage = IMCenter.viewSharedInfo.lastPage
                }
 
            ... ...
        }
    }
}

函数 updateSessionState() 用于在返回联系人列表或者会话列表界面前,去除会话页面中,当前会话可能的未读消息状态,以及更新联系人名称下显示的最新一条聊天消息。

之后是 DialogueFooterView 视图,添加对发送功能的调用:

struct DialogueFooterView: View {
 
    ... ...
    
    var body: some View {
        HStack {
            ... ...
            
            Image("button_send")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:IMDemoUIConfig.navigationIconEdgeLen, height: IMDemoUIConfig.navigationIconEdgeLen, alignment: .center)
                .padding((IMDemoUIConfig.topNavigationHight - IMDemoUIConfig.navigationIconEdgeLen)/2)
                .onTapGesture {
                    
                    IMCenter.viewSharedInfo.newMessageReceived = false
                    
                    if self.message.isEmpty {
                        viewInfo.newestMessage = IMCenter.viewSharedInfo.dialogueMesssages.last!
                        hideKeyboard()
                        return
                    }
                    
                    IMCenter.sendMessage(contact: contact, message: self.message)
                    self.message = ""
                    
                    hideKeyboard()
                    viewInfo.newestMessage = IMCenter.viewSharedInfo.dialogueMesssages.last!
                }
        }
    }
}

最后是 DialogueView 视图,修正对当前用户ID的引用:

struct DialogueView: View {
    
    ... ...
    
    init() {
        self.contact = IMCenter.viewSharedInfo.targetContact!
        self.selfId = IMCenter.client!.userId
        self.viewInfo = IMCenter.viewSharedInfo
        self.contactForInfoPage = self.contact
    }
    
    ... ...
}

到此,会话视图便开发完毕。

8. 菜单事件

接下来,便是对菜单的点击产生响应。

8.1 添加好友

添加好友的基本思路是:

  1. 如果查询本地联系人信息,已经是好友了,则直接打开会话页面;
  2. 如果本地将对方作为陌生人记录,则将陌生人标记修改为好友标记,并添加会话条目到会话列表页,然后打开会话页面;
  3. 否则查询业务服务器,确认对方是合法注册用户,然后保存联系人信息,并添加会话条目到会话列表页,打开会话页面。

编辑 IMCenter.swift,添加 入口函数 addFriendInMainThread() 及相关功能函数:

    class func addNewSessionByMenuActionInMainThread(contact: ContactInfo) {
        
        var contacts = [ContactInfo]()
        contacts.append(contact)
        
        IMCenter.appendContactsForSessionView(contacts: contacts)
        IMCenter.appendContactsForContactView(contacts: contacts)
        
        DispatchQueue.global(qos: .default).async {
            downloadImage(contactInfo: contact)
            checkContactsUpdate(type: contact.kind, contacts: contacts)
        }
    }

    class func addFriendInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {
        
        var contact = db.loadContentInfo(type: ContactKind.Friend.rawValue, xname: xname)
        if contact != nil {
            existAction(contact!)
            showDialogueView(contact: contact!)
            return
        } else {
            contact = db.loadContentInfo(type: ContactKind.Stranger.rawValue, xname: xname)
            if contact != nil {
                existAction(contact!)
                db.changeStrangerToFriend(xid: contact!.xid)
                addNewSessionByMenuActionInMainThread(contact: contact!)
                showDialogueView(contact: contact!)
                return
            }
        }
        
        var users = [String]()
        users.append(xname)
        
        BizClient.lookup(users: users, groups: nil, rooms: nil, uids: nil, gids: nil, rids: nil, completedAction: {
            respon in
            if let uid = respon.users[xname] {
                let contact = ContactInfo(type: ContactKind.Friend.rawValue, uniqueId: uid, uniqueName: xname, nickname: "")
                
                DispatchQueue.main.async {
                    successAction(contact)
                }
                return
            } else {
                var errInfo = ErrorInfo()
                errInfo.title = "用户不存在"
                errInfo.desc = "被添加的用户尚未注册!"
                
                DispatchQueue.main.async {
                    errorAction(errInfo)
                }
            }
        }, errorAction: { errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "添加好友失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {
                errorAction(errInfo)
            }
        })
    }

修改 MenuActionView.swift,编辑 AddFriendView 视图,添加 checkInput() 函数,并修改“添加”按钮响应如下:

struct AddFriendView: View {
    ... ...
    
    func checkInput() -> Bool {
        if username.isEmpty {
            let error = ErrorInfo(title: "无效的输入", desc: "用户名不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        ... ...
        
            Button("添加") {
                
                if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.addFriendInMainThread(xname: self.username, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
        
        ... ...
    }
}

此时,“添加好友”菜单功能完成。

8.2 创建群组

创建群组的基本思路:

  1. 查询本地联系人信息,如果已经是群组成员,则直接打开会话页面;
  2. 否则查询业务服务器,确认目标是合法群组,然后保存群组联系人信息,并添加会话条目到会话列表页,打开会话页面。

编辑 BizClient.swift,完成 createGroup() 函数:

... ...

struct CreateGroupResponse: Codable {
    var gid: Int64
}

... ...

class BizClient {
    
    ... ...
    
    class func createGroup(uniqueName: String, completedAction: @escaping (_ gid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {
        
        let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/createGroup?uid=\(IMCenter.client!.userId)&group=" + urlEncode(string: uniqueName))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{
            (data, response, error) in
            
            if error != nil {
                errorAction("连接错误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {
                    let json = try JSONDecoder().decode(CreateGroupResponse.self, from: data!)
                    completedAction(json.gid)
                } catch {
                    do {
                        let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {
                        errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()
    }
    
    ... ...
}

编辑 IMCenter.swift,添加入口函数 createGroupInMainThread():

    class func createGroupInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {
        
        let contact = db.loadContentInfo(type: ContactKind.Group.rawValue, xname: xname)
        if contact != nil {
            existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        BizClient.createGroup(uniqueName: xname, completedAction: {
            gid in
            
            let contact = ContactInfo(type: ContactKind.Group.rawValue, uniqueId: gid, uniqueName: xname, nickname: "")
            
            DispatchQueue.main.async {
                successAction(contact)
            }
            
        }, errorAction: {
            errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "创建群组失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {
                errorAction(errInfo)
            }
        })
    }

修改 MenuActionView.swift,编辑 JoinGroupView 视图,添加 checkInput() 函数,并修改“创建”按钮响应如下:

struct CreateGroupView: View {
    
    ... ...

    func checkInput() -> Bool {
        if groupname.isEmpty {
            let error = ErrorInfo(title: "无效的输入", desc: "群组唯一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("创建") {
                if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.createGroupInMainThread(xname: self.groupname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
        
        ... ...
    }   
}

此时,“创建群组”菜单功能完成。

8.3 加入群组

加入群组的基本思路是:

  1. 如果查询本地联系人信息,如果已经加入群组了,则直接打开会话页面;
  2. 否则查询业务服务器,确认目标是合法群组,然后保存群组联系人信息,并添加会话条目到会话列表页,打开会话页面。

编辑 BizClient.swift,完成 joinGroup() 函数:

    class func joinGroup(uniqueGroupName: String, completedAction: @escaping (_ gid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {
        
        let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/joinGroup?uid=\(IMCenter.client!.userId)&group=" + urlEncode(string: uniqueGroupName))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{
            (data, response, error) in
            
            if error != nil {
                errorAction("连接错误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {
                    let json = try JSONDecoder().decode(CreateGroupResponse.self, from: data!)
                    completedAction(json.gid)
                } catch {
                    do {
                        let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {
                        errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()
    }

编辑 IMCenter.swift,添加 入口函数 joinGroupInMainThread():

    class func joinGroupInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {
        
        let contact = db.loadContentInfo(type: ContactKind.Group.rawValue, xname: xname)
        if contact != nil {
            existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        BizClient.joinGroup(uniqueGroupName: xname, completedAction: {
            gid in
            
            let contact = ContactInfo(type: ContactKind.Group.rawValue, uniqueId: gid, uniqueName: xname, nickname: "")
            
            DispatchQueue.main.async {
                successAction(contact)
            }
            
        }, errorAction: {
            errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "加入群组失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {
                errorAction(errInfo)
            }
        })
    }

修改 MenuActionView.swift,编辑 JoinGroupView 视图,添加 checkInput() 函数,并修改“加入”按钮响应如下:

struct JoinGroupView: View {
    
    ... ...
    
    func checkInput() -> Bool {
        if groupname.isEmpty {
            let error = ErrorInfo(title: "无效的输入", desc: "群组唯一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("加入") {
                if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.joinGroupInMainThread(xname: self.groupname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
        
        ... ...
    }   
}

此时,“加入群组”菜单功能完成。

8.4 创建房间

创建房间的基本思路是:

  1. 查询本地联系人信息,如果本地保存有对应的房间联系人信息,则直接打开会话页面;
  2. 否则查询业务服务器,确认对方是合法房间,然后保存房间联系人信息,并添加会话条目到会话列表页,打开会话页面。

编辑 BizClient.swift,完成 createRoom() 函数:

... ...

struct CreateRoomResponse: Codable {
    var rid: Int64
}

... ...

class BizClient {
    
    ... ...

    class func createRoom(uniqueName: String, completedAction: @escaping (_ rid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {
        
        let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/createRoom?room=" + urlEncode(string: uniqueName))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{
            (data, response, error) in
            
            if error != nil {
                errorAction("连接错误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {
                    let json = try JSONDecoder().decode(CreateRoomResponse.self, from: data!)
                    completedAction(json.rid)
                } catch {
                    do {
                        let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {
                        errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()
    }
    
    ... ...
}

编辑 IMCenter.swift,添加 入口函数 createRoomInMainThread():

    class func createRoomInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {
        
        let contact = db.loadContentInfo(type: ContactKind.Room.rawValue, xname: xname)
        if contact != nil {
            existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        BizClient.createRoom(uniqueName: xname, completedAction: {
            rid in
            
            let contact = ContactInfo(type: ContactKind.Room.rawValue, uniqueId: rid, uniqueName: xname, nickname: "")
            
            DispatchQueue.main.async {
                successAction(contact)
            }
            
        }, errorAction: {
            errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "创建房间失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {
                errorAction(errInfo)
            }
        })
    }

修改 MenuActionView.swift,编辑 CreateRoomView 视图,添加 checkInput() 函数,并修改“创建”按钮响应如下:

struct CreateRoomView: View {
    
    ... ...
    
    func checkInput() -> Bool {
        if roomname.isEmpty {
            let error = ErrorInfo(title: "无效的输入", desc: "房间唯一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("创建") {
                if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.createRoomInMainThread(xname: self.roomname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
            
        ... ...
    }
}

此时,“创建房间”菜单功能完成。

8.5 加入房间

加入房间的基本思路是:

  1. 查询本地联系人信息,如果本地保存有对应的房间联系人信息,则直接打开会话页面;
  2. 否则查询业务服务器,确认对方是合法房间,然后保存房间联系人信息,并添加会话条目到会话列表页,打开会话页面。

编辑 IMCenter.swift,添加 入口函数 joinRoomInMainThread() 及相关功能函数:

    class func joinRoomInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {
        
        let contact = db.loadContentInfo(type: ContactKind.Room.rawValue, xname: xname)
        if contact != nil {
            existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        var rooms = [String]()
        rooms.append(xname)
        
        BizClient.lookup(users: nil, groups: nil, rooms: rooms, uids: nil, gids: nil, rids: nil, completedAction: {
            respon in
            if let rid = respon.rooms[xname] {
                let contact = ContactInfo(type: ContactKind.Room.rawValue, uniqueId: rid, uniqueName: xname, nickname: "")
                
                DispatchQueue.main.async {
                    successAction(contact)
                }
                return
            } else {
                var errInfo = ErrorInfo()
                errInfo.title = "房间不存在"
                errInfo.desc = "房间尚未被创建!"
                
                DispatchQueue.main.async {
                    errorAction(errInfo)
                }
            }
        }, errorAction: { errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "进入房间失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {
                errorAction(errInfo)
            }
        })
    }

修改 MenuActionView.swift,编辑 EnterRoomView 视图,添加 checkInput() 函数,并修改“加入”按钮响应如下:

struct EnterRoomView: View {
    
    ... ...
    
    func checkInput() -> Bool {
        if roomname.isEmpty {
            let error = ErrorInfo(title: "无效的输入", desc: "房间唯一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("加入") {
                if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.joinRoomInMainThread(xname: self.roomname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
            
        ... ...
    }
}

至此,菜单所有功能响应,添加完毕。

9. 接收消息

RTM消息种类其实非常丰富,目前我们可能收到的消息有五类:P2P聊天消息、群组聊天消息、房间聊天消息、群组系统通知、房间系统通知。

为了简化起见,聊天消息我们统一处理。
处理函数 receiveNewNessage() 先将收到的消息保存入数据库,然后发起一个主线程异步操作:

  1. 检查会话列表中是否已有对应会话
  2. 若当前不存在对应会话,则添加新会话和新联系人,然后更新联系人信息
  3. 若当前存在对应会话,则更新会话列表页中,对应条目的未读状态,以及最新聊天信息
  4. 若当前存在对应会话,且当前为对应的会话页面,则更新聊天信息列表,然后显示新消息标记

编辑 IMCenter.swift,添加代码如下:

    private class func addNewSession(contact: ContactInfo, message: RTMMessage?) {
        db.storeNewContact(contact: contact)
        downloadImage(contactInfo: contact)
        
        var contacts = [ContactInfo]()
        contacts.append(contact)
        
        DispatchQueue.main.async {
            IMCenter.appendContactsForContactView(contacts: contacts)
            
            var oldSessions = IMCenter.viewSharedInfo.sessions
            
            let sessionItem = SessionItem(contact: contact)
            if message != nil {
                sessionItem.lastMessage.message = extraChatMessage(rtmMessage: message!)
                sessionItem.lastMessage.mid = message!.messageId
                sessionItem.lastMessage.timestamp = message!.modifiedTime
                sessionItem.lastMessage.unread = true
            }
            oldSessions.append(sessionItem)
            
            IMCenter.viewSharedInfo.sessions = sortSessions(sessions: oldSessions)
            
            DispatchQueue.global(qos: .default).async {
                checkContactsUpdate(type: contact.kind, contacts: contacts)
            }
        }
    }

    private class func extraChatMessage(rtmMessage: RTMMessage) -> String {
        
        if rtmMessage.translatedInfo.targetText.isEmpty == false {
            return rtmMessage.translatedInfo.targetText
        }
        
        if rtmMessage.translatedInfo.sourceText.isEmpty == false {
            return rtmMessage.translatedInfo.sourceText
        }
        
        if rtmMessage.stringMessage.isEmpty == false {
            return rtmMessage.stringMessage
        }
        
        return ""
    }

    class func receiveNewNessage(type:Int, rtmMessage: RTMMessage) {
        
        _ = db.insertChatMessage(type: type, xid: rtmMessage.toId, sender: rtmMessage.fromUid, mid: rtmMessage.messageId, message: extraChatMessage(rtmMessage: rtmMessage), mtime: rtmMessage.modifiedTime)
        
        DispatchQueue.main.async {
            let sessions = IMCenter.viewSharedInfo.sessions
            
            DispatchQueue.global(qos: .default).async {
                for session in sessions {
                    let matchXid = (session.contact.kind == ContactKind.Friend.rawValue) ? rtmMessage.fromUid : rtmMessage.toId
                    if session.contact.kind == type && session.contact.xid == matchXid {
                        //-- 已有的 session
                        session.lastMessage.mid = rtmMessage.messageId
                        session.lastMessage.message = extraChatMessage(rtmMessage: rtmMessage)
                        session.lastMessage.timestamp = rtmMessage.modifiedTime
                        session.lastMessage.unread = true
                        
                        DispatchQueue.main.async {
                            let sessions2 = sortSessions(sessions: sessions)
                            IMCenter.viewSharedInfo.sessions = sessions2
                            
                            if let currContact = IMCenter.viewSharedInfo.targetContact {
                                if currContact.kind == type && currContact.xid == matchXid {
                                    
                                    var dialogueMesssages = IMCenter.viewSharedInfo.dialogueMesssages
                                    let chatMsg = ChatMessage(sender: rtmMessage.fromUid, mid: rtmMessage.messageId, mtime: rtmMessage.modifiedTime, message: extraChatMessage(rtmMessage: rtmMessage))
                                    dialogueMesssages.append(chatMsg)
                                    
                                    dialogueMesssages = soreChatMessages(messages: dialogueMesssages)
                                    
                                    IMCenter.viewSharedInfo.dialogueMesssages = dialogueMesssages
                                    IMCenter.viewSharedInfo.newMessageReceived = true
                                }
                            }
                        }
                        return
                    }
                }
                
                //-- new Session
                var newContact: ContactInfo? = nil
                if type == ContactKind.Group.rawValue || type == ContactKind.Room.rawValue {
                    newContact = ContactInfo(type: type, uniqueId: rtmMessage.toId, uniqueName: "", nickname: "")
                } else {
                    newContact = ContactInfo(type: type, uniqueId: rtmMessage.fromUid, uniqueName: "", nickname: "")
                }
                
                addNewSession(contact: newContact!, message: rtmMessage)
            }
        }
    }

同样为了简化起见,系统通知我们也统一处理。不过相对而言,系统通知要简单许多。

处理函数 receiveNewChatCmd() 先将收到的消息保存入数据库,然后发起一个主线程异步操作:

  1. 检查会话列表中是否已有对应会话
  2. 若当前不存在对应会话,则不做任何处理
  3. 若当前存在对应会话,且当前为对应的会话页面,则更新聊天信息列表,加入系统通知,但不显示新消息标记

编辑 IMCenter.swift,添加代码如下:

    class func receiveNewChatCmd(type:Int, rtmMessage: RTMMessage) {

        _ = db.insertChatCmd(type: type, xid: rtmMessage.toId, sender: rtmMessage.fromUid, mid: rtmMessage.messageId, message: rtmMessage.stringMessage, mtime: rtmMessage.modifiedTime)
        
        DispatchQueue.main.async {
            
            //-- 更新当前对话列表信息
            if let currContact = IMCenter.viewSharedInfo.targetContact {
                if currContact.kind == type && currContact.xid == rtmMessage.toId {
                    
                    var dialogueMesssages = IMCenter.viewSharedInfo.dialogueMesssages
                    let chatMsg = ChatMessage(sender: rtmMessage.fromUid, mid: rtmMessage.messageId, mtime: rtmMessage.modifiedTime, message: rtmMessage.stringMessage)
                    chatMsg.isChat = false
                    dialogueMesssages.append(chatMsg)
                    
                    dialogueMesssages = soreChatMessages(messages: dialogueMesssages)
                    
                    IMCenter.viewSharedInfo.dialogueMesssages = dialogueMesssages
                    return
                }
            }
        }
    }

最后,我们要和 RTMClient 的事件通知进行关联。打开 IMEventProcessor.swift,编辑代码如下:

@objcMembers public class IMEventProcessor: NSObject, RTMProtocol {
    
    ... ...

    public func rtmPushP2PChatMessage(_ client: RTMClient, message: RTMMessage?) {
        IMCenter.receiveNewNessage(type: ContactKind.Friend.rawValue, rtmMessage: message!)
    }
    
    public func rtmPushGroupChatMessage(_ client: RTMClient, message: RTMMessage?) {
        IMCenter.receiveNewNessage(type: ContactKind.Group.rawValue, rtmMessage: message!)
    }
    
    public func rtmPushRoomChatMessage(_ client: RTMClient, message: RTMMessage?) {
        IMCenter.receiveNewNessage(type: ContactKind.Room.rawValue, rtmMessage: message!)
    }
    
    public func rtmPushGroupChatCmd(_ client: RTMClient, message: RTMMessage?) {
        if let msg = message {
            IMCenter.receiveNewChatCmd(type: ContactKind.Group.rawValue, rtmMessage: msg)
        }
    }
    
    public func rtmPushRoomChatCmd(_ client: RTMClient, message: RTMMessage?) {
        if let msg = message {
            IMCenter.receiveNewChatCmd(type: ContactKind.Room.rawValue, rtmMessage: msg)
        }
    }
}

至此,RTMClient 相关事件关联完成,接收消息也同时处理完成。

10. 用户和群组、房间信息的查询和修改

用户信息页面在表现和功能上与联系人信息页面高度相同:不仅展现内容几乎相同,而且操作方式完全相同。但苦于基础数据来源形式不同,以及相似的数据性质不同,所以简单起见,我们分成两个独立的视图进行制作。

10.1 用户信息页面

用户信息页需要显示用户的头像、唯一数字ID、注册名称、昵称、签名,以及“退出登录”的按钮,而在编辑模式下,需要显示用户的头像、唯一数字ID、注册名称、昵称、头像的网络路径、签名。其中,在编辑模式时,“退出登录”的按钮将被隐藏,增加头像网络路径的显示,且仅有昵称、头像的网络路径、签名可以被编辑修改。
当用户在编辑模式下提交修改时,我们需要核对修改是否有效(不为空),然后向服务器提交。如果头像网络路径被修改,则还需要下载新的头像,并存储到本地,然后更新用户信息页面上的头像。
于是编辑 IMCenter.swift,增加处理函数 updateUserProfile():

    class func updateUserProfile(nickname: String, imgUrl: String, showInfo: String, completedAction: @escaping (_ path:String)->Void) {
        
        let info = OpenInfoData(nickname: nickname, imageUrl: imgUrl, showInfo: showInfo)
        let jsonEncoder = JSONEncoder()
        let jsonData = try? jsonEncoder.encode(info)
        let jsonStr = String(data: jsonData!, encoding: .utf8)
        
        DispatchQueue.global(qos: .default).async {
            
            //-- 暂时忽略错误处理
            IMCenter.client!.setUserInfoWithOpenInfo(jsonStr, privteinfo: nil, timeout: 0, success:{}, fail: { _ in })
            
            let contact = ContactInfo()
            contact.kind = ContactKind.Friend.rawValue
            contact.xid = IMCenter.client!.userId
            contact.xname = IMCenter.fetchUserProfile(key: "username")
            contact.imageUrl = imgUrl
            
            downloadImage(contactInfo: contact, completedAction: {
                (path, contactInfo) in
                
                let username = IMCenter.fetchUserProfile(key: "username")
                
                IMCenter.storeUserProfile(key: "\(username)-image-url", value: imgUrl)
                IMCenter.storeUserProfile(key: "\(username)-image", value: path)
                IMCenter.storeUserProfile(key: "nickname", value: nickname)
                IMCenter.storeUserProfile(key: "showInfo", value: showInfo)
                
                DispatchQueue.main.async {
                    completedAction(path)
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            }, failedAction:{
                
                IMCenter.storeUserProfile(key: "nickname", value: nickname)
                IMCenter.storeUserProfile(key: "showInfo", value: showInfo)

                DispatchQueue.main.async {
                    completedAction("")
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            })
        }
    }

然后编辑修改 ProfileView 的 body 部分为:

    var body: some View {
        ZStack {
        VStack {
            if self.editMode == false {
                TopNavigationView(title: "我的信息", icon: "button_edit", buttonAction: {
                    
                    self.editMode = true
                    
                }).frame(width: UIScreen.main.bounds.width, height: CGFloat(IMDemoUIConfig.topNavigationHight), alignment: .center)
            } else {
                TopNavigationView(title: "修改我的信息", icon: "button_ok", buttonAction: {
                    
                    self.editMode = false
                    self.viewInfo.inProcessing = true
                    
                    if self.newNickname.isEmpty {
                        self.newNickname = self.nickname
                    }
                    
                    if self.newImageUrl.isEmpty {
                        self.newImageUrl = self.userImageUrl
                    }
                    
                    if self.newShowInfo.isEmpty {
                        self.newShowInfo = self.showInfo
                    }
                    
                    IMCenter.updateUserProfile(nickname: self.newNickname, imgUrl: self.newImageUrl, showInfo: self.newShowInfo, completedAction: updateCallback)
                    
                }).frame(width: UIScreen.main.bounds.width, height: CGFloat(IMDemoUIConfig.topNavigationHight), alignment: .center)
            }
            
            
            Divider()
            
            Spacer()
            
            VStack {
                
                Spacer()
                
                if self.userImagePath.isEmpty {
                    Image(IMDemoUIConfig.defaultIcon)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width:UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/2, alignment: .center)
                        .padding()
                } else {
                    Image(uiImage: IMCenter.loadUIIMage(path: self.userImagePath))
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width:UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/2, alignment: .center)
                        .padding()
                }
                
                LazyVGrid(columns:[GridItem(.fixed(UIScreen.main.bounds.width * 0.4)), GridItem()]) {
                    HStack {
                        Spacer()
                        Text("用户ID:")
                            .padding()
                    }
                    HStack {
                        Text(String(IMCenter.client!.userId))
                            .padding()
                        Spacer()
                    }
                    
                    
                    HStack {
                        Spacer()
                        Text("用户名:")
                            .padding()
                    }
                    HStack {
                        Text(self.username)
                            .padding()
                        Spacer()
                    }
                    
                    HStack {
                        Spacer()
                        Text("用户昵称:")
                            .padding()
                    }
                    HStack {
                        if self.editMode == false {
                            if self.nickname.isEmpty {
                                Text(self.username)
                                    .padding()
                            } else {
                                Text(self.nickname)
                                    .padding()
                            }
                            
                        } else {
                            TextField(self.nickname.isEmpty ? "给自己取个昵称" : self.nickname, text: $newNickname)
                                .autocapitalization(.none)
                                .frame(width: UIScreen.main.bounds.width/3,
                                height: nil)
                                .padding()
                        }
                        
                        Spacer()
                    }
                    
                    if self.editMode {
                        HStack {
                            
                            Spacer()
                            Text("头像地址:")
                                .padding()
                        }
                        HStack {
                            TextField(self.userImageUrl.isEmpty ? "更改头像地址" : self.userImageUrl, text: $newImageUrl)
                                .autocapitalization(.none)
                                .frame(width: UIScreen.main.bounds.width/3,
                                height: nil)
                                .padding()
                            
                            Spacer()
                        }
                    }
                    
                    HStack {
                        Spacer()
                        Text("用户签名:")
                            .padding()
                    }
                    HStack {
                        if self.editMode == false {
                            TextEditor(text: $showInfo).disabled(true)
                                    .padding()
                        } else {
                            TextEditor(text: $showInfo)
                                .frame(width: UIScreen.main.bounds.width/3,
                                height: 80)
                                .ignoresSafeArea(.keyboard)
                                .padding()
                                .overlay(RoundedRectangle(cornerRadius: 8)
                                        .stroke(Color.secondary).opacity(0.5))
                        }
                        
                        Spacer()
                    }
                }
                
                if self.editMode == false {
                    Button("退出登录") {
                        
                        DispatchQueue.global(qos: .default).async {
                            IMCenter.client!.closeConnect()
                        }
                    }
                    .frame(width: UIScreen.main.bounds.width/4,
                    height: nil)
                    .padding(10)
                    .foregroundColor(.white)
                    .background(.blue)
                    .cornerRadius(10)
                }
                
                Spacer()
            }
            
            Spacer()
            
            Divider()
            
            BottomNavigationView().frame(width: UIScreen.main.bounds.width, height: CGFloat(IMDemoUIConfig.bottomNavigationHight), alignment: .center)
        }
        .onTapGesture {
            hideKeyboard()
        }
         
            if self.viewInfo.inProcessing {
                ProcessingView(info: "更新中,请等待……")
            }
        }
    }

到此,用户信息页面修改完成。

10.2 联系人信息页面

最后,是联系人信息页面。
本页面和用户信息页面比较大的区别有两点:

  1. 用户信息页默认就能编辑,而联系人信息页,只有群组和房间的信息才能编辑,其他用户的信息不可被编辑
  2. 联系人信息页不含“退出登录”按钮

与用户信息页面类似,第二篇其实已经将UI相关的功能准备的差不多了,我们在此只需补上UI之外的相关处理。X相关处理也与 updateUserProfile() 类似。编辑IMCenter.swift,增加功能函数 updateGroupOrRoomProfile() 及相关辅助函数:

    class func genGroupOrRoomProfileChangedNotifyMessage(type: Int) -> String {
        
        if type == ContactKind.Group.rawValue {
            return "\(getSelfDispalyName()) 修改了本群信息"
        } else if type == ContactKind.Room.rawValue {
            return "\(getSelfDispalyName()) 修改了本房间信息"
        } else {
            return "\(getSelfDispalyName()) 修改了信息"
        }
    }
    
    class func updateGroupOrRoomProfile(contact: ContactInfo, orgImageUrl: String, completedAction: @escaping (_ path:String)->Void) {
        
        let info = OpenInfoData(nickname: contact.nickname, imageUrl: contact.imageUrl, showInfo: contact.showInfo)
        let jsonEncoder = JSONEncoder()
        let jsonData = try? jsonEncoder.encode(info)
        let jsonStr = String(data: jsonData!, encoding: .utf8)
        
        DispatchQueue.global(qos: .default).async {
            
            //-- 暂时忽略错误处理
            if contact.kind == ContactKind.Group.rawValue {
                IMCenter.client!.setGroupInfoWithId(NSNumber(value: contact.xid), openInfo: jsonStr, privateInfo: nil, timeout: 0, success: {}, fail: { _ in })
            } else if contact.kind == ContactKind.Room.rawValue {
                IMCenter.client!.setRoomInfoWithId(NSNumber(value: contact.xid), openInfo: jsonStr, privateInfo: nil, timeout: 0, success: {}, fail: { _ in })
            } else { return }
        
            if contact.imageUrl == orgImageUrl {
                
                DispatchQueue.main.async {
                    completedAction("")
                    IMCenter.viewSharedInfo.inProcessing = false
                }
                return
            }
            
            downloadImage(contactInfo: contact, completedAction: {
                (path, contactInfo) in
                
                contact.imagePath = path
                IMCenter.db.updatePublicInfo(contact: contact)
                sendCmd(contact: contact, message: genGroupOrRoomProfileChangedNotifyMessage(type: contact.kind))
                
                DispatchQueue.main.async {
                    completedAction(path)
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            }, failedAction:{
                IMCenter.db.updatePublicInfo(contact: contact)
                sendCmd(contact: contact, message: genGroupOrRoomProfileChangedNotifyMessage(type: contact.kind))
                
                DispatchQueue.main.async {
                    completedAction("")
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            })
        }
    }

然后编辑修改 ContactInfoView.swift,修改 ContactInfoView 视图 ContactInfoHeaderView 组件的 editAction 为:

                editAction: {
                    inEditing in
                    
                    self.editMode = inEditing
                    
                    
                    if inEditing == false {
                        self.viewInfo.inProcessing = true
                        
                        let newContact = ContactInfo()
                        newContact.kind = contact.kind
                        newContact.xid = contact.xid
                        newContact.xname = contact.xname
                        newContact.nickname = self.newNickname.isEmpty ? contact.nickname : self.newNickname
                        newContact.showInfo = self.newShowInfo.isEmpty ? contact.showInfo : self.newShowInfo
                        
                        if self.newImageUrl.isEmpty {
                            newContact.imageUrl = self.contact.imageUrl
                        } else {
                            newContact.imageUrl = self.newImageUrl
                        }
    
                        IMCenter.updateGroupOrRoomProfile(contact: newContact, orgImageUrl:self.contact.imageUrl, completedAction: updateCallback)
                    }
                }

至此,整个Demo的iOS 端部分,便全部开发完成。完整代码可参见:
https://github.com/highras/rtm-teaching-demo/tree/main/rtm-imdemo/iOS/IMDemo

下篇,我们将进入服务端部分的开发。

若有收获,可以留下你的赞和收藏。

posted @   云上曲率  阅读(147)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示