基本ok
This commit is contained in:
@@ -81,6 +81,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
keepOffset:(BOOL)keepOffset
|
||||
scrollToBottom:(BOOL)scrollToBottom;
|
||||
|
||||
- (void)prependHistoryMessages:(NSArray<KBAiChatMessage *> *)messages
|
||||
openingMessage:(nullable KBAiChatMessage *)openingMessage;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -364,6 +364,39 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
scrollToBottom:(BOOL)scrollToBottom {
|
||||
CGFloat oldContentHeight = self.tableView.contentSize.height;
|
||||
CGFloat oldOffsetY = self.tableView.contentOffset.y;
|
||||
KBAiChatMessage *anchorMessage = nil;
|
||||
CGFloat anchorOffset = 0;
|
||||
if (keepOffset) {
|
||||
NSArray<NSIndexPath *> *visibleRows = self.tableView.indexPathsForVisibleRows;
|
||||
if (visibleRows.count > 0) {
|
||||
NSArray<NSIndexPath *> *sortedRows = [visibleRows sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *obj1, NSIndexPath *obj2) {
|
||||
if (obj1.row < obj2.row) {
|
||||
return NSOrderedAscending;
|
||||
} else if (obj1.row > obj2.row) {
|
||||
return NSOrderedDescending;
|
||||
}
|
||||
return NSOrderedSame;
|
||||
}];
|
||||
NSIndexPath *anchorIndexPath = nil;
|
||||
for (NSIndexPath *indexPath in sortedRows) {
|
||||
if (indexPath.row < self.messages.count) {
|
||||
KBAiChatMessage *message = self.messages[indexPath.row];
|
||||
if (message.type != KBAiChatMessageTypeTime) {
|
||||
anchorIndexPath = indexPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!anchorIndexPath) {
|
||||
anchorIndexPath = sortedRows.firstObject;
|
||||
}
|
||||
if (anchorIndexPath && anchorIndexPath.row < self.messages.count) {
|
||||
anchorMessage = self.messages[anchorIndexPath.row];
|
||||
CGRect anchorRect = [self.tableView rectForRowAtIndexPath:anchorIndexPath];
|
||||
anchorOffset = oldOffsetY - anchorRect.origin.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[self.messages removeAllObjects];
|
||||
if (messages.count > 0) {
|
||||
@@ -399,10 +432,18 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
NSLog(@"[KBChatTableView] ========== reloadWithMessages 结束 ==========");
|
||||
|
||||
if (keepOffset) {
|
||||
CGFloat newContentHeight = self.tableView.contentSize.height;
|
||||
CGFloat delta = newContentHeight - oldContentHeight;
|
||||
CGFloat offsetY = oldOffsetY + delta;
|
||||
// 关键修复:使用非动画方式设置 offset,避免抖动
|
||||
CGFloat offsetY = oldOffsetY;
|
||||
if (anchorMessage) {
|
||||
NSInteger newIndex = [self.messages indexOfObjectIdenticalTo:anchorMessage];
|
||||
if (newIndex != NSNotFound) {
|
||||
CGRect newRect = [self.tableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:newIndex inSection:0]];
|
||||
offsetY = newRect.origin.y + anchorOffset;
|
||||
}
|
||||
} else {
|
||||
CGFloat newContentHeight = self.tableView.contentSize.height;
|
||||
CGFloat delta = newContentHeight - oldContentHeight;
|
||||
offsetY = oldOffsetY + delta;
|
||||
}
|
||||
[self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
|
||||
return;
|
||||
}
|
||||
@@ -439,6 +480,131 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
[self.messages addObject:message];
|
||||
}
|
||||
|
||||
- (NSInteger)firstVisibleNonTimeRowExcludingMessage:(KBAiChatMessage *)excludedMessage {
|
||||
NSArray<NSIndexPath *> *visible = self.tableView.indexPathsForVisibleRows;
|
||||
if (visible.count == 0) { return NSNotFound; }
|
||||
NSArray<NSIndexPath *> *sorted = [visible sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath * _Nonnull obj1, NSIndexPath * _Nonnull obj2) {
|
||||
if (obj1.row < obj2.row) return NSOrderedAscending;
|
||||
if (obj1.row > obj2.row) return NSOrderedDescending;
|
||||
return NSOrderedSame;
|
||||
}];
|
||||
for (NSIndexPath *ip in sorted) {
|
||||
if (ip.row < self.messages.count) {
|
||||
KBAiChatMessage *msg = self.messages[ip.row];
|
||||
if (msg.type != KBAiChatMessageTypeTime && msg != excludedMessage) {
|
||||
return ip.row;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sorted.firstObject.row;
|
||||
}
|
||||
|
||||
- (CGFloat)offsetForRow:(NSInteger)row {
|
||||
if (row == NSNotFound) { return self.tableView.contentOffset.y; }
|
||||
CGRect rect = [self.tableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:0]];
|
||||
return self.tableView.contentOffset.y - rect.origin.y;
|
||||
}
|
||||
|
||||
- (void)restoreOffsetWithMessage:(KBAiChatMessage *)message anchorOffset:(CGFloat)anchorOffset fallbackDelta:(CGFloat)fallbackDelta {
|
||||
CGFloat offsetY = self.tableView.contentOffset.y + fallbackDelta;
|
||||
if (message) {
|
||||
NSInteger newIndex = [self.messages indexOfObjectIdenticalTo:message];
|
||||
if (newIndex != NSNotFound) {
|
||||
CGRect newRect = [self.tableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:newIndex inSection:0]];
|
||||
offsetY = newRect.origin.y + anchorOffset;
|
||||
}
|
||||
}
|
||||
[self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
|
||||
}
|
||||
|
||||
- (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message
|
||||
inMessages:(NSArray<KBAiChatMessage *> *)messages {
|
||||
if (messages.count == 0) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
KBAiChatMessage *lastMessage = nil;
|
||||
for (NSInteger i = messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *msg = messages[i];
|
||||
if (msg.type != KBAiChatMessageTypeTime) {
|
||||
lastMessage = msg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastMessage) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSTimeInterval interval = [message.timestamp timeIntervalSinceDate:lastMessage.timestamp];
|
||||
if (interval >= kTimestampInterval) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSCalendar *calendar = [NSCalendar currentCalendar];
|
||||
NSDateComponents *lastComponents = [calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear
|
||||
fromDate:lastMessage.timestamp];
|
||||
NSDateComponents *currentComponents = [calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear
|
||||
fromDate:message.timestamp];
|
||||
return ![lastComponents isEqual:currentComponents];
|
||||
}
|
||||
|
||||
- (NSArray<KBAiChatMessage *> *)messagesByInsertingTimestamps:(NSArray<KBAiChatMessage *> *)messages {
|
||||
NSMutableArray<KBAiChatMessage *> *result = [NSMutableArray array];
|
||||
for (KBAiChatMessage *msg in messages) {
|
||||
if ([self shouldInsertTimestampForMessage:msg inMessages:result]) {
|
||||
KBAiChatMessage *timeMessage = [KBAiChatMessage timeMessageWithTimestamp:msg.timestamp];
|
||||
[result addObject:timeMessage];
|
||||
}
|
||||
[result addObject:msg];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)prependHistoryMessages:(NSArray<KBAiChatMessage *> *)messages
|
||||
openingMessage:(nullable KBAiChatMessage *)openingMessage {
|
||||
CGFloat oldContentHeight = self.tableView.contentSize.height;
|
||||
NSInteger anchorRow = [self firstVisibleNonTimeRowExcludingMessage:openingMessage];
|
||||
KBAiChatMessage *anchorMsg = nil;
|
||||
CGFloat anchorOffset = 0;
|
||||
if (anchorRow != NSNotFound && anchorRow < self.messages.count) {
|
||||
anchorMsg = self.messages[anchorRow];
|
||||
anchorOffset = [self offsetForRow:anchorRow];
|
||||
}
|
||||
|
||||
NSArray<KBAiChatMessage *> *toInsert = [self messagesByInsertingTimestamps:messages];
|
||||
|
||||
[UIView performWithoutAnimation:^{
|
||||
if (toInsert.count > 0) {
|
||||
NSInteger insertIndex = 0;
|
||||
if (openingMessage) {
|
||||
NSInteger openingIndex = [self.messages indexOfObjectIdenticalTo:openingMessage];
|
||||
if (openingIndex != NSNotFound) {
|
||||
insertIndex = openingIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
NSRange range = NSMakeRange(insertIndex, toInsert.count);
|
||||
NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:range];
|
||||
[self.messages insertObjects:toInsert atIndexes:set];
|
||||
|
||||
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray array];
|
||||
for (NSInteger i = 0; i < toInsert.count; i++) {
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:insertIndex + i inSection:0]];
|
||||
}
|
||||
|
||||
[self.tableView beginUpdates];
|
||||
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView endUpdates];
|
||||
}
|
||||
|
||||
[self.tableView layoutIfNeeded];
|
||||
CGFloat newContentHeight = self.tableView.contentSize.height;
|
||||
CGFloat delta = newContentHeight - oldContentHeight;
|
||||
[self restoreOffsetWithMessage:anchorMsg anchorOffset:anchorOffset fallbackDelta:delta];
|
||||
}];
|
||||
}
|
||||
|
||||
/// 判断是否需要插入时间戳
|
||||
- (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message {
|
||||
// 第一条消息总是显示时间
|
||||
|
||||
@@ -44,6 +44,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
/// 是否正在加载
|
||||
@property (nonatomic, assign) BOOL isLoading;
|
||||
|
||||
@property (nonatomic, assign) BOOL canTriggerLoadMore;
|
||||
|
||||
/// 当前页码
|
||||
@property (nonatomic, assign) NSInteger currentPage;
|
||||
|
||||
@@ -94,6 +96,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
|
||||
// 重置加载状态标志(但不清空 hasLoadedData)
|
||||
self.isLoading = NO;
|
||||
self.canTriggerLoadMore = YES;
|
||||
|
||||
// ✅ 移除了 self.hasLoadedData = NO;
|
||||
// 这样 Cell 复用时不会重复请求数据
|
||||
@@ -174,6 +177,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
|
||||
// 重置状态
|
||||
self.isLoading = NO;
|
||||
self.canTriggerLoadMore = YES;
|
||||
self.currentPage = 1;
|
||||
self.hasMoreHistory = YES;
|
||||
|
||||
@@ -258,21 +262,25 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
pageNum:self.currentPage
|
||||
pageSize:20
|
||||
completion:^(KBChatHistoryPageModel *pageModel, NSError *error) {
|
||||
weakSelf.isLoading = NO;
|
||||
|
||||
if (error) {
|
||||
NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription);
|
||||
[weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory];
|
||||
|
||||
// 如果是第一次加载失败,显示开场白
|
||||
if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) {
|
||||
[weakSelf showOpeningMessage];
|
||||
}
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
weakSelf.hasLoadedData = YES;
|
||||
weakSelf.hasMoreHistory = pageModel.hasMore;
|
||||
if (error) {
|
||||
NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
strongSelf.isLoading = NO;
|
||||
[strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory];
|
||||
if (strongSelf.currentPage == 1 && strongSelf.persona.introText.length > 0) {
|
||||
[strongSelf showOpeningMessage];
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
strongSelf.hasLoadedData = YES;
|
||||
strongSelf.hasMoreHistory = pageModel.hasMore;
|
||||
|
||||
// 转换为 KBAiChatMessage
|
||||
NSMutableArray *newMessages = [NSMutableArray array];
|
||||
@@ -296,56 +304,54 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
|
||||
message.isComplete = YES;
|
||||
message.needsTypewriterEffect = NO;
|
||||
[newMessages addObject:message];
|
||||
// [newMessages addObject:message];
|
||||
[newMessages insertObject:message atIndex:0];
|
||||
}
|
||||
|
||||
// 插入历史消息(确保开场白始终是第一条)
|
||||
// 关键修复:在 dispatch_async 之前保存当前页码,避免异步执行时 currentPage 已经被递增
|
||||
NSInteger loadedPage = weakSelf.currentPage;
|
||||
NSInteger loadedPage = strongSelf.currentPage;
|
||||
|
||||
if (loadedPage == 1) {
|
||||
// 第一页,直接赋值
|
||||
weakSelf.messages = newMessages;
|
||||
[weakSelf ensureOpeningMessageAtTop];
|
||||
strongSelf.messages = newMessages;
|
||||
[strongSelf ensureOpeningMessageAtTop];
|
||||
} else {
|
||||
// 后续页,插入到开场白之后
|
||||
[weakSelf ensureOpeningMessageAtTop];
|
||||
[strongSelf ensureOpeningMessageAtTop];
|
||||
if (newMessages.count > 0) {
|
||||
NSUInteger insertIndex = [weakSelf hasOpeningMessageAtTop] ? 1 : 0;
|
||||
NSUInteger insertIndex = [strongSelf hasOpeningMessageAtTop] ? 1 : 0;
|
||||
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, newMessages.count)];
|
||||
[weakSelf.messages insertObjects:newMessages atIndexes:indexSet];
|
||||
[strongSelf.messages insertObjects:newMessages atIndexes:indexSet];
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新 UI
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
NSLog(@"[KBPersonaChatCell] ⚠️ strongSelf 为空,跳过刷新");
|
||||
return;
|
||||
if (loadedPage == 1) {
|
||||
NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, keepOffset: 0, scrollToBottom: 1",
|
||||
(long)loadedPage);
|
||||
[strongSelf.chatView reloadWithMessages:strongSelf.messages
|
||||
keepOffset:NO
|
||||
scrollToBottom:YES];
|
||||
} else {
|
||||
NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, prependHistory",
|
||||
(long)loadedPage);
|
||||
KBAiChatMessage *openingMessage = [strongSelf hasOpeningMessageAtTop] ? strongSelf.messages.firstObject : nil;
|
||||
[strongSelf.chatView prependHistoryMessages:newMessages openingMessage:openingMessage];
|
||||
}
|
||||
|
||||
// 使用保存的页码判断,而不是 currentPage(可能已被递增)
|
||||
BOOL keepOffset = (loadedPage != 1);
|
||||
BOOL scrollToBottom = (loadedPage == 1);
|
||||
|
||||
NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, keepOffset: %d, scrollToBottom: %d",
|
||||
(long)loadedPage, keepOffset, scrollToBottom);
|
||||
|
||||
[strongSelf.chatView reloadWithMessages:strongSelf.messages
|
||||
keepOffset:keepOffset
|
||||
scrollToBottom:scrollToBottom];
|
||||
[strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory];
|
||||
|
||||
// ✅ 保存到缓存(包含开场白)
|
||||
[[KBAIChatMessageCacheManager shared] saveMessages:strongSelf.messages
|
||||
forCompanionId:companionId];
|
||||
strongSelf.isLoading = NO;
|
||||
});
|
||||
|
||||
weakSelf.currentPage++;
|
||||
strongSelf.currentPage++;
|
||||
|
||||
NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@",
|
||||
(long)weakSelf.currentPage - 1,
|
||||
(long)strongSelf.currentPage - 1,
|
||||
(long)newMessages.count,
|
||||
pageModel.hasMore ? @"是" : @"否");
|
||||
}];
|
||||
@@ -357,7 +363,6 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
return;
|
||||
}
|
||||
|
||||
self.currentPage++;
|
||||
[self loadChatHistory];
|
||||
}
|
||||
|
||||
@@ -552,8 +557,11 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
CGFloat offsetY = scrollView.contentOffset.y;
|
||||
|
||||
// 下拉到顶部,加载历史消息
|
||||
if (offsetY <= -50 && !self.isLoading) {
|
||||
if (offsetY <= 50 && !self.isLoading && self.canTriggerLoadMore && self.hasMoreHistory) {
|
||||
self.canTriggerLoadMore = NO;
|
||||
[self loadMoreHistory];
|
||||
} else if (offsetY > -20) {
|
||||
self.canTriggerLoadMore = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user