// // GroupViewModel.swift // QuickLocation // import Foundation import RxSwift import RxCocoa import RxDataSources import OpenIMSDK import ObjectMapper typealias HotGroupListSectionModel = SectionModel typealias CircleListSectionModel = SectionModel /// 群聊 cell 数据:圈子集合以业务 `groups` 为准,IM 会话只补未读/预览/时间 struct GroupCellData { let group: GroupInfoModel let unreadCount: Int let lastMsgTime: Int // ms timestamp let lastMsgPreview: String var tags: [String] { Array(group.labels.prefix(2)) } var iconURL: String { group.groupIconURL } var localIcon: UIImage { group.groupIcon } } final class GroupViewModel { struct Output { let hotGroups: Observable<[HotGroupListSectionModel]> let createdSections: Observable<[CircleListSectionModel]> let joinedSections: Observable<[CircleListSectionModel]> let segmentTitles: Driver<[String]> } let output: Output private let hotGroupSectionedItems = PublishSubject<[HotGroupListSectionModel]>() private let createdSectionsSubject = PublishSubject<[CircleListSectionModel]>() private let joinedSectionsSubject = PublishSubject<[CircleListSectionModel]>() let segmentTitles = BehaviorRelay<[String]>(value: ["我创建的圈子", "我加入的圈子"]) private(set) var groupList: [GroupInfoModel] = [] private var cachedConversations: [OIMConversationInfo] = [] /// 热门酷圈 func loadHotGroupData(_ list: [GroupInfoModel]) { hotGroupSectionedItems.onNext(list.mapSection()) } lazy var hotGroupCellAction: Action = { this in return Action { model in AppRouter.push(Route.groupInfo, userInfo: ["groupInfo": model.toJSON()]) return .empty() } }(self) /// 业务圈子列表(与首页切圈弹层同一份 `groups`) func applyBusinessGroups(_ list: [GroupInfoModel]) { groupList = list reloadData() } /// 全量刷新 IM 会话(未读、最后一条、时间) func loadConversations(_ conversations: [OIMConversationInfo]) { cachedConversations = conversations reloadData() } /// 仅更新会话元数据(未读、时间)- 消息监听回调(只返回变更的会话,需合并) func updateConversations(_ conversations: [OIMConversationInfo]) { var merged: [String: OIMConversationInfo] = [:] for c in cachedConversations { if let gid = c.groupID { merged[gid] = c } } for c in conversations { if let gid = c.groupID { merged[gid] = c } } cachedConversations = Array(merged.values) reloadData() } private func reloadData() { let convMap: [String: OIMConversationInfo] = { var m: [String: OIMConversationInfo] = [:] for c in cachedConversations where c.groupID != nil && !(c.groupID?.isEmpty ?? true) { m[c.groupID!] = c } return m }() func cellData(for group: GroupInfoModel) -> GroupCellData { let conv = convMap[group.group_key] return GroupCellData( group: group, unreadCount: Int(conv?.unreadCount ?? 0), lastMsgTime: Int(conv?.latestMsgSendTime ?? 0), lastMsgPreview: Self.previewText(from: conv?.latestMsg) ) } func sortedByLastMsg(_ items: [GroupCellData]) -> [GroupCellData] { items.sorted { lhs, rhs in if lhs.lastMsgTime != rhs.lastMsgTime { return lhs.lastMsgTime > rhs.lastMsgTime } return lhs.group.group_key < rhs.group.group_key } } let all = sortedByLastMsg(groupList.map(cellData(for:))) let created = sortedByLastMsg(groupList.filter(\.is_owner).map(cellData(for:))) createdSectionsSubject.onNext(created.mapSection()) joinedSectionsSubject.onNext(all.mapSection()) } private static func previewText(from msg: OIMMessageInfo?) -> String { guard let msg else { return "" } let body: String switch msg.contentType { case .image: body = "[图片]" case .audio: body = "[语音]" case .video: body = "[视频]" case .file: body = "[文件]" case .location: body = "[位置]" case .at: body = Self.displayBody(msg.atTextElem?.text ?? "[消息]") case .quote: body = Self.displayBody(msg.quoteElem?.text ?? "[消息]") case .face: body = "[表情]" default: body = Self.displayBody(msg.textElem?.content ?? msg.content ?? "") } let name = msg.senderNickname ?? "" if name.isEmpty { return body } if body.isEmpty { return name } return "\(name): \(body)" } private static func displayBody(_ text: String) -> String { if text.hasPrefix("js_emoji:") { return "[表情]" } return text } // MARK: - Init init() { output = Output( hotGroups: hotGroupSectionedItems.asObservable(), createdSections: createdSectionsSubject.asObservable(), joinedSections: joinedSectionsSubject.asObservable(), segmentTitles: segmentTitles.asDriver() ) } }