This commit is contained in:
2026-01-30 21:24:17 +08:00
parent 2ff8a7a4af
commit cc82396195
5 changed files with 222 additions and 45 deletions

View File

@@ -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

View File

@@ -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 {
//

View File

@@ -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;
}
}