// // ChatMessageModel.m // AIIM // // Created by qitewei on 2025/5/14. // #import "ChatMessageModel.h" #import "CryptoAES.h" #import "ChatsStore.h" #import "AVFoundation/AVFoundation.h" #import "FileNetApi.h" #import @implementation ChatMessageModel + (instancetype)modelWithDictionary:(NSDictionary *)dict { ChatMessageModel *model = [[ChatMessageModel alloc] init]; [model setupWithDictionary:dict]; return model; } - (void)setupWithDictionary:(NSDictionary *)dict { NSMutableDictionary * mutableDict = [NSMutableDictionary dictionaryWithDictionary:dict]; [mutableDict jk_each:^(id k, id v) { if ([v isKindOfClass:[NSNull class]]) { [mutableDict setObject:@"" forKey:k]; } }]; //原消息 self.formerMessage = dict; self.chatId = mutableDict[@"chatId"]; // 消息类型 NSInteger type = [mutableDict[@"messageType"] integerValue]; self.messageType = (ChatMessageType)type; self.type = [mutableDict[@"type"] integerValue]; self.nickName = mutableDict[@"fromName"]?:@""; self.avatar = mutableDict[@"fromAvatar"]?:@""; // 通用内容 // self.content = mutableDict[@"content"] ?: @"";//本地明文存储 self.content =[CryptoAES.shareInstance decryptDataL:mutableDict[@"content"] ?: @""];//本地加密存储 self.msgId = mutableDict[@"id"]; self.fromId = mutableDict[@"fromId"]; // 发送方标识 NSDictionary *userinfo = [UDManager.shareInstance getDDManager:dkuserinfo]; NSString *userId = userinfo[@"id"]; if([self.fromId isEqualToString:userId]){ self.isSender=true; } else{ self.isSender=false; } // 处理媒体尺寸 CGFloat width = [mutableDict[@"width"] floatValue]; CGFloat height = [mutableDict[@"height"] floatValue]; if (width > 0 && height > 0) { self.mediaSize = CGSizeMake(width, height); } else { // 默认尺寸 self.mediaSize = CGSizeMake(200, 150); } self.placeholderImage = kImageMake(@"pictrue_placeholder"); // 文件消息 NSDictionary * extend = [mutableDict[@"extend"] isKindOfClass:NSDictionary.class]?mutableDict[@"extend"]:@{}; self.fileName = extend[@"fileName"] ?: @"未知文件"; self.fileSize = extend[@"size"] ?: @"0KB"; self.url = extend[@"url"] ?: @""; // NSCharacterSet *allowedCharacters = [NSCharacterSet URLQueryAllowedCharacterSet]; // self.url = [self.url stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; self.localurl = extend[@"localurl"] ?: @""; self.quoteMessage = ![extend jk_hasKey:@"quoteMessage"] ?NULL: [ChatMessageModel modelWithDictionary:extend[@"quoteMessage"]]; // 语音消息 NSInteger duration = [extend[@"time"] integerValue]; if (duration<=60) { self.voiceDuration = duration; }else{ self.voiceDuration = duration/1000; } self.voiceWidth = [self voiceBubbleWidth]; //文件传输成功或失败 self.fileError = [extend[@"fileError"] integerValue]; //合并转发 self.forwardMsgArray = extend[@"messageList"] ?:@[]; // 通话记录 self.callDuration = [mutableDict[@"duration"] integerValue]; self.isCallSuccess = [mutableDict[@"result"] boolValue]; self.isVideo = [mutableDict[@"video"] boolValue]; //@相关 self.atAll = extend[@"atAll"]?[extend[@"atAll"] boolValue]:NO; self.atUserIds = extend[@"atUserIds"]?:@[]; // 时间戳(如果需要) self.timestamp = [mutableDict[@"timestamp"] longLongValue]; self.localtime = [mutableDict[@"localtime"] longLongValue]; self.formatterTime = [self timeF:self.timestamp]; // 其他自定义字段... self.readStatus = [self getReadStatus]; BOOL contain = [[NSFileManager defaultManager] fileExistsAtPath:self.localurl]; NSString *localPath = [self checkHasLocalPath]; if (!contain && localPath.length) { self.localurl = localPath; } } - (void)exchangeModelWithModel:(ChatMessageModel *)model{ self.msgId = model.msgId; self.formerMessage = model.formerMessage; if(self.messageType!=model.messageType){ self.cellHeight=0; } self.messageType = model.messageType; self.type = model.type; self.isSender = model.isSender; self.content = model.content; self.msgId = model.msgId; self.mediaSize = model.mediaSize; self.placeholderImage = model.placeholderImage; self.fileName = model.fileName; self.fileSize = model.fileSize; self.url = model.url; self.localurl = model.localurl; //文件传输成功或失败 self.fileError = model.fileError; // 通话记录 self.callDuration = model.callDuration; self.isCallSuccess = model.isCallSuccess; self.isVideo = model.isVideo; self.avatar = model.avatar; self.nickName = model.nickName; //@相关 self.atAll = model.atAll; self.atUserIds = model.atUserIds; // 时间戳(如果需要) self.timestamp = model.timestamp; self.localtime = model.localtime; self.formatterTime = model.formatterTime; // // 其他自定义字段... // 其他自定义字段... self.readStatus = [self getReadStatus]; } -(NSString *)timeF:(NSInteger)time{ //NSLog(@"time:%ld",(long)time); // 创建日期格式器 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"dd"]; // 设置你想要的日期格式 long long timestamp =(long)time/1000; // 例如:2021-10-01 00:00:00 UTC // 转换为NSDate NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp]; NSString *dateString = [formatter stringFromDate:date]; NSDate *now = [NSDate date]; NSTimeInterval trt = [now timeIntervalSince1970]; date = [NSDate dateWithTimeIntervalSince1970:trt]; NSString *tdateString = [formatter stringFromDate:date]; NSInteger shijiancha = trt-timestamp; if(shijiancha<60){ return NSLocalizedString(@"time-oneminint", @""); } if(shijiancha>24*3600||dateString.intValue!=tdateString.intValue){ // 假设这是你的时间戳(以秒为单位) long long timestamp =(long)time/1000; // 例如:2021-10-01 00:00:00 UTC // 转换为NSDate NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp]; // 创建日期格式器 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"yyyy-MM-dd HH:mm"]; // 设置你想要的日期格式 // 转换为字符串 NSString *dateString = [formatter stringFromDate:date]; return dateString; } else{ NSInteger xiaoshi = shijiancha/3600; NSInteger fenzhong = shijiancha/60; if(xiaoshi>0){ // 假设这是你的时间戳(以秒为单位) long long timestamp =(long)time/1000; // 例如:2021-10-01 00:00:00 UTC // 转换为NSDate NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp]; // 创建日期格式器 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"HH:mm"]; // 设置你想要的日期格式 // 转换为字符串 NSString *dateString = [formatter stringFromDate:date]; return dateString; } else{ return [NSString stringWithFormat:@"%ld%@",(long)fenzhong,NSLocalizedString(@"time-outmin", @"")]; } } return @""; } - (NSString *)getReadStatus{ if(self.isSent){ //判断已读未读 if(self.timestamp>ChatsStore.shareInstance.lastreadTime){ return @"未读"; } else{ return @"已读"; } } return @""; } - (void)generateThumbnailWithCompletion:(void(^)(UIImage *thumbnail))completion { // 如果内存中已有缩略图,直接返回 if (self.videoThumbnailImage) { if (completion) completion(self.videoThumbnailImage); return; } // 生成缓存 key,优先使用 msgId,如果没有则使用 URL 的 hash NSString *cacheKey = nil; if (self.msgId.length > 0) { cacheKey = [NSString stringWithFormat:@"video_thumbnail_%@", self.msgId]; } else if (self.url.length > 0) { cacheKey = [NSString stringWithFormat:@"video_thumbnail_%lu", (unsigned long)[self.url hash]]; } // 从 SDWebImage 缓存中查找 if (cacheKey) { SDImageCache *imageCache = [SDImageCache sharedImageCache]; UIImage *cachedImage = [imageCache imageFromCacheForKey:cacheKey]; if (cachedImage) { NSLog(@"从缓存加载缩略图: %@", cacheKey); self.videoThumbnailImage = cachedImage; if (completion) completion(cachedImage); return; } } // 没有缓存,需要生成缩略图 NSURL *videoURL = nil; if (self.localurl.length != 0 && [[NSFileManager defaultManager] fileExistsAtPath:self.localurl]) { // 从本地文件生成缩略图 NSLog(@"generateThumbnailWithCompletion localurl:%@",self.localurl); videoURL = [NSURL fileURLWithPath:self.localurl]; NSString *finalCacheKey = cacheKey; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:videoURL options:nil]; AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; generator.appliesPreferredTrackTransform = YES; CMTime acttime; NSError *error = nil; CGImageRef imageRef = [generator copyCGImageAtTime:CMTimeMake(0, 600) actualTime:&acttime error:&error]; NSLog(@"error:%@",error); if (!error && imageRef) { UIImage *thumbnail = [[UIImage alloc] initWithCGImage:imageRef]; CGImageRelease(imageRef); // 缓存到内存 self.videoThumbnailImage = thumbnail; // 使用 SDWebImage 缓存到磁盘和内存 if (finalCacheKey) { SDImageCache *imageCache = [SDImageCache sharedImageCache]; [imageCache storeImage:thumbnail forKey:finalCacheKey completion:^{ NSLog(@"缩略图已缓存: %@", finalCacheKey); }]; } if (completion){ completion(thumbnail); } } else { if (completion) completion(nil); } }); } else { // 从远程 URL 生成缩略图 NSLog(@"generateThumbnailWithCompletion url"); if (self.url.length == 0){ if (completion) completion(nil); return; } NSDictionary *options = @{AVURLAssetAllowsCellularAccessKey: @YES, AVURLAssetPreferPreciseDurationAndTimingKey: @YES}; AVURLAsset *asset = [AVURLAsset URLAssetWithURL:getURL(self.url) options:options]; // 创建图像生成器 AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; generator.appliesPreferredTrackTransform = YES; // 应用视频的方向 generator.maximumSize = CGSizeMake(320, 0); // 设置最大尺寸,保持宽高比 NSString *finalCacheKey = cacheKey; // 异步生成缩略图 [generator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:kCMTimeZero]] completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) { __block UIImage *thumbnail = nil; // 处理生成结果 if (result == AVAssetImageGeneratorSucceeded && image) { // 创建UIImage thumbnail = [UIImage imageWithCGImage:image]; self.videoThumbnailImage = thumbnail; // 使用 SDWebImage 缓存到磁盘和内存 if (finalCacheKey) { SDImageCache *imageCache = [SDImageCache sharedImageCache]; [imageCache storeImage:thumbnail forKey:finalCacheKey completion:^{ NSLog(@"缩略图已缓存: %@", finalCacheKey); }]; } } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(thumbnail); }); }]; } } //下载视频文件 -(void)downloadFileIfNeed:(void(^)(NSInteger persent))loading{ if ([[NSFileManager defaultManager] fileExistsAtPath:self.localurl]){ self.customFileSize = [self getFileSize]; if (loading) { loading(100); } NSLog(@"downloadFileIfNeed 0: %@", self.localurl); return; } NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil]; NSURL *tempUrl = [NSURL URLWithString:self.url]; NSString *fileName = tempUrl.lastPathComponent; NSString *fileExtension = [fileName pathExtension]; NSString *newfileName = [NSString stringWithFormat:@"%@.%@", self.msgId,fileExtension]; documentsDirectoryURL = [documentsDirectoryURL URLByAppendingPathComponent:newfileName]; if ([[NSFileManager defaultManager] fileExistsAtPath:documentsDirectoryURL.path]){ NSLog(@"downloadFileIfNeed 1: %@", documentsDirectoryURL.path); NSMutableDictionary *mutableDict = [self.formerMessage mutableCopy]; NSMutableDictionary *extend = [self.formerMessage[@"extend"] mutableCopy]; [extend setObject:documentsDirectoryURL.path forKey:@"localurl"]; [mutableDict setObject:extend forKey:@"extend"]; self.localurl = documentsDirectoryURL.path; NSDictionary *NewMsg = [mutableDict copy]; [ChatsStore.shareInstance reciveMsg:NewMsg]; self.customFileSize = [self getFileSize]; if (loading) { loading(100); } return; } if (![self.url hasPrefix:@"http"]) { NSLog(@"文件URL异常:%@", self.formerMessage); return; } [FileNetApi downLoadWToken:getURL(self.url) thrid:self.msgId succ:^(int code, NSDictionary * res) { if(res!=nil){ NSMutableDictionary *mutableDict = [self.formerMessage mutableCopy]; NSMutableDictionary *extend = [self.formerMessage[@"extend"] mutableCopy]; [extend setObject:res[@"filePath"] forKey:@"localurl"]; [mutableDict setObject:extend forKey:@"extend"]; self.localurl = res[@"filePath"]; NSDictionary *NewMsg = [mutableDict copy]; [ChatsStore.shareInstance reciveMsg:NewMsg]; self.customFileSize = [self getFileSize]; if (loading) { loading(100); } } else{ if (code>=0&&code<200) { if (loading) { loading(code); } } } } fail:^(NSError * _Nonnull error) { NSLog(@"downLoadWToken error:%@",error); }]; } // 根据语音时长计算气泡宽度 - (CGFloat)voiceBubbleWidth { // 基础宽度 CGFloat minWidth = 60.0f; CGFloat maxWidth = 200.0f; // 最大限制时长(如60秒) NSInteger maxDuration = 60; NSInteger duration = MIN(self.voiceDuration, maxDuration); // 线性增长计算 CGFloat width = minWidth + (maxWidth - minWidth) * (duration / (CGFloat)maxDuration); return width; } #pragma mark - 消息状态管理 - (NSString *)getFileSize { NSString *filePath = self.localurl; NSFileManager *fileManager = [NSFileManager defaultManager]; // 获取文件属性 NSError *error = nil; NSDictionary *attributes = [fileManager attributesOfItemAtPath:filePath error:&error]; NSString *string = @"0KB"; if (error) { NSLog(@"获取文件属性失败: %@", error); } else { // 获取文件大小(单位:字节) unsigned long long fileSize = [attributes fileSize]; NSLog(@"文件大小: %llu 字节", fileSize); // 可选:转换为 KB、MB double fileSizeKB = fileSize / 1024.0; double fileSizeMB = fileSizeKB / 1024.0; NSLog(@"文件大小: %.2f KB (%.2f MB)", fileSizeKB, fileSizeMB); string = [NSString stringWithFormat:@"%.2fMB", fileSizeMB]; } return string; } - (NSString *)customFileSize { if (!_customFileSize) { _customFileSize = [self getFileSize]; } return _customFileSize; } - (NSString *)checkHasLocalPath { if ([[NSFileManager defaultManager] fileExistsAtPath:self.localurl]){ self.customFileSize = [self getFileSize]; NSLog(@"checkHasLocalPath 0: %@", self.localurl); return self.localurl; } NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil]; NSURL *tempUrl = [NSURL URLWithString:self.url]; NSString *fileName = tempUrl.lastPathComponent; NSString *fileExtension = [fileName pathExtension]; NSString *newfileName = [NSString stringWithFormat:@"%@.%@", self.msgId,fileExtension]; NSString *path = [documentsDirectoryURL URLByAppendingPathComponent:newfileName].path; if ([[NSFileManager defaultManager] fileExistsAtPath:path]){ NSLog(@"checkHasLocalPath 0: %@", path); return path; } return nil; } - (BOOL)isUploadFile { return self.messageType == ChatMessageTypeFile || self.messageType == ChatMessageTypeVoice || self.messageType == ChatMessageTypeImage || self.messageType == ChatMessageTypeVideo; } - (BOOL)isSent { return ![self.msgId isEqualToString:[NSString stringWithFormat:@"%ld", self.localtime]]; } - (BOOL)checkIsSendFail { // NSDate *now = [NSDate date]; // NSTimeInterval trt = [now timeIntervalSince1970]; // NSInteger time = trt*1000; // NSInteger outTime =time-self.localtime; // if(outTime>33000){ // return YES; // } return NO; } @end