tableview倒置
This commit is contained in:
@@ -127,6 +127,60 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)setInverted:(BOOL)inverted {
|
||||
if (_inverted == inverted) {
|
||||
return;
|
||||
}
|
||||
_inverted = inverted;
|
||||
|
||||
self.tableView.transform = inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
||||
[self updateContentBottomInset:self.contentBottomInset];
|
||||
[self.tableView reloadData];
|
||||
[self.tableView layoutIfNeeded];
|
||||
}
|
||||
|
||||
static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
|
||||
return interval < 0 ? -interval : interval;
|
||||
}
|
||||
|
||||
- (BOOL)shouldInsertTimestampBetweenMessage:(KBAiChatMessage *)message
|
||||
andReference:(KBAiChatMessage *)reference {
|
||||
if (!message.timestamp || !reference.timestamp) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSTimeInterval interval = KBChatAbsTimeInterval([message.timestamp timeIntervalSinceDate:reference.timestamp]);
|
||||
if (interval >= kTimestampInterval) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSCalendar *calendar = [NSCalendar currentCalendar];
|
||||
NSDateComponents *a = [calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear
|
||||
fromDate:reference.timestamp];
|
||||
NSDateComponents *b = [calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear
|
||||
fromDate:message.timestamp];
|
||||
return ![a isEqual:b];
|
||||
}
|
||||
|
||||
- (NSInteger)firstNonTimeIndex {
|
||||
for (NSInteger i = 0; i < self.messages.count; i++) {
|
||||
if (self.messages[i].type != KBAiChatMessageTypeTime) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
- (NSInteger)lastNonTimeIndexBeforeIndex:(NSInteger)index {
|
||||
NSInteger maxIndex = MIN(index, self.messages.count);
|
||||
for (NSInteger i = maxIndex - 1; i >= 0; i--) {
|
||||
if (self.messages[i].type != KBAiChatMessageTypeTime) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
- (void)addUserMessage:(NSString *)text {
|
||||
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
|
||||
[self addMessage:message autoScroll:YES];
|
||||
@@ -138,17 +192,31 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
}
|
||||
|
||||
- (void)updateLastUserMessage:(NSString *)text {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
|
||||
message.text = text;
|
||||
message.isLoading = NO;
|
||||
message.isComplete = YES;
|
||||
|
||||
// 刷新该行
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||
return;
|
||||
if (self.inverted) {
|
||||
for (NSInteger i = 0; i < self.messages.count; i++) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
|
||||
message.text = text;
|
||||
message.isLoading = NO;
|
||||
message.isComplete = YES;
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
|
||||
message.text = text;
|
||||
message.isLoading = NO;
|
||||
message.isComplete = YES;
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,24 +240,44 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
|
||||
- (void)updateLastAssistantMessage:(NSString *)text {
|
||||
// 查找最后一条未完成的 AI 消息
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant && !message.isComplete) {
|
||||
NSLog(@"[KBChatTableView] 更新最后一条 AI 消息 - 索引: %ld, 文本长度: %lu, needsTypewriter: %d",
|
||||
(long)i, (unsigned long)text.length, message.needsTypewriterEffect);
|
||||
message.text = text;
|
||||
|
||||
// 直接更新 Cell 的文本,不刷新整个 Cell(避免打断打字机效果)
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
KBChatAssistantMessageCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
|
||||
if ([cell isKindOfClass:[KBChatAssistantMessageCell class]]) {
|
||||
NSLog(@"[KBChatTableView] 找到 Cell,直接配置消息");
|
||||
// 直接调用 configureWithMessage,让 Cell 自己决定是否使用打字机效果
|
||||
[cell configureWithMessage:message];
|
||||
} else {
|
||||
NSLog(@"[KBChatTableView] 未找到 Cell 或类型不匹配");
|
||||
if (self.inverted) {
|
||||
for (NSInteger i = 0; i < self.messages.count; i++) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant && !message.isComplete) {
|
||||
NSLog(@"[KBChatTableView] 更新最后一条 AI 消息 - 索引: %ld, 文本长度: %lu, needsTypewriter: %d",
|
||||
(long)i, (unsigned long)text.length, message.needsTypewriterEffect);
|
||||
message.text = text;
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
KBChatAssistantMessageCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
|
||||
if ([cell isKindOfClass:[KBChatAssistantMessageCell class]]) {
|
||||
NSLog(@"[KBChatTableView] 找到 Cell,直接配置消息");
|
||||
[cell configureWithMessage:message];
|
||||
cell.contentView.transform = self.inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
||||
} else {
|
||||
NSLog(@"[KBChatTableView] 未找到 Cell 或类型不匹配");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant && !message.isComplete) {
|
||||
NSLog(@"[KBChatTableView] 更新最后一条 AI 消息 - 索引: %ld, 文本长度: %lu, needsTypewriter: %d",
|
||||
(long)i, (unsigned long)text.length, message.needsTypewriterEffect);
|
||||
message.text = text;
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
KBChatAssistantMessageCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
|
||||
if ([cell isKindOfClass:[KBChatAssistantMessageCell class]]) {
|
||||
NSLog(@"[KBChatTableView] 找到 Cell,直接配置消息");
|
||||
[cell configureWithMessage:message];
|
||||
} else {
|
||||
NSLog(@"[KBChatTableView] 未找到 Cell 或类型不匹配");
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,33 +287,63 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
}
|
||||
|
||||
- (void)markLastAssistantMessageComplete {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant) {
|
||||
NSLog(@"[KBChatTableView] 标记消息完成 - 索引: %ld, 文本: %@", (long)i, message.text);
|
||||
message.isComplete = YES;
|
||||
message.needsTypewriterEffect = NO; // 完成后不再需要打字机效果
|
||||
|
||||
// 刷新 Cell 以显示完整文本
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
return;
|
||||
if (self.inverted) {
|
||||
for (NSInteger i = 0; i < self.messages.count; i++) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant) {
|
||||
NSLog(@"[KBChatTableView] 标记消息完成 - 索引: %ld, 文本: %@", (long)i, message.text);
|
||||
message.isComplete = YES;
|
||||
message.needsTypewriterEffect = NO;
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant) {
|
||||
NSLog(@"[KBChatTableView] 标记消息完成 - 索引: %ld, 文本: %@", (long)i, message.text);
|
||||
message.isComplete = YES;
|
||||
message.needsTypewriterEffect = NO; // 完成后不再需要打字机效果
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)markLastUserMessageLoadingComplete {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeUser) {
|
||||
message.isLoading = NO;
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView layoutIfNeeded];
|
||||
[self scrollToBottomAnimated:NO];
|
||||
return;
|
||||
if (self.inverted) {
|
||||
for (NSInteger i = 0; i < self.messages.count; i++) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeUser) {
|
||||
message.isLoading = NO;
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView layoutIfNeeded];
|
||||
[self scrollToBottomAnimated:NO];
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeUser) {
|
||||
message.isLoading = NO;
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView layoutIfNeeded];
|
||||
[self scrollToBottomAnimated:NO];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,14 +355,41 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
}
|
||||
|
||||
- (void)removeLoadingAssistantMessage {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant && message.isLoading) {
|
||||
[self.messages removeObjectAtIndex:i];
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||
NSLog(@"[KBChatTableView] 移除 loading AI 消息,索引: %ld", (long)i);
|
||||
break;
|
||||
if (self.inverted) {
|
||||
for (NSInteger i = 0; i < self.messages.count; i++) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant && message.isLoading) {
|
||||
if (self.playingCellIndexPath) {
|
||||
if (self.playingCellIndexPath.row == i) {
|
||||
[self stopPlayingAudio];
|
||||
} else if (self.playingCellIndexPath.row > i) {
|
||||
self.playingCellIndexPath = [NSIndexPath indexPathForRow:self.playingCellIndexPath.row - 1 inSection:0];
|
||||
}
|
||||
}
|
||||
[self.messages removeObjectAtIndex:i];
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||
NSLog(@"[KBChatTableView] 移除 loading AI 消息,索引: %ld", (long)i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeAssistant && message.isLoading) {
|
||||
if (self.playingCellIndexPath) {
|
||||
if (self.playingCellIndexPath.row == i) {
|
||||
[self stopPlayingAudio];
|
||||
} else if (self.playingCellIndexPath.row > i) {
|
||||
self.playingCellIndexPath = [NSIndexPath indexPathForRow:self.playingCellIndexPath.row - 1 inSection:0];
|
||||
}
|
||||
}
|
||||
[self.messages removeObjectAtIndex:i];
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||
NSLog(@"[KBChatTableView] 移除 loading AI 消息,索引: %ld", (long)i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,6 +403,12 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
|
||||
// 关键修复:使用 layoutIfNeeded 确保布局完成后再滚动
|
||||
[self.tableView layoutIfNeeded];
|
||||
|
||||
if (self.inverted) {
|
||||
CGFloat minOffsetY = -self.tableView.contentInset.top;
|
||||
[self.tableView setContentOffset:CGPointMake(0, minOffsetY) animated:animated];
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算需要滚动到的位置
|
||||
CGFloat contentHeight = self.tableView.contentSize.height;
|
||||
@@ -299,7 +450,11 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
|
||||
// 直接设置 contentInset
|
||||
UIEdgeInsets insets = UIEdgeInsetsZero;
|
||||
insets.bottom = bottomInset;
|
||||
if (self.inverted) {
|
||||
insets.top = bottomInset;
|
||||
} else {
|
||||
insets.bottom = bottomInset;
|
||||
}
|
||||
self.tableView.contentInset = insets;
|
||||
self.tableView.scrollIndicatorInsets = insets;
|
||||
|
||||
@@ -319,22 +474,46 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
}
|
||||
|
||||
NSInteger oldCount = self.messages.count;
|
||||
[self insertMessageWithTimestamp:message];
|
||||
NSArray<KBAiChatMessage *> *itemsToInsert = nil;
|
||||
NSInteger insertIndex = oldCount;
|
||||
|
||||
if (self.inverted) {
|
||||
itemsToInsert = [self invertedItemsForNewMessageAtBeginning:message];
|
||||
insertIndex = 0;
|
||||
if (itemsToInsert.count > 0) {
|
||||
NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, itemsToInsert.count)];
|
||||
[self.messages insertObjects:itemsToInsert atIndexes:set];
|
||||
}
|
||||
} else {
|
||||
[self insertMessageWithTimestamp:message];
|
||||
itemsToInsert = nil;
|
||||
insertIndex = oldCount;
|
||||
}
|
||||
|
||||
NSInteger newCount = self.messages.count;
|
||||
NSInteger insertedCount = newCount - oldCount;
|
||||
|
||||
if (insertedCount > 0) {
|
||||
NSMutableArray *indexPaths = [NSMutableArray array];
|
||||
for (NSInteger i = oldCount; i < newCount; i++) {
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
|
||||
if (self.inverted && self.playingCellIndexPath) {
|
||||
self.playingCellIndexPath = [NSIndexPath indexPathForRow:self.playingCellIndexPath.row + insertedCount inSection:0];
|
||||
}
|
||||
|
||||
NSMutableArray *indexPaths = [NSMutableArray array];
|
||||
if (self.inverted) {
|
||||
for (NSInteger i = 0; i < insertedCount; i++) {
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
|
||||
}
|
||||
} else {
|
||||
for (NSInteger i = insertIndex; i < newCount; i++) {
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
|
||||
}
|
||||
}
|
||||
|
||||
// 关键修复:批量插入前先布局,避免高度计算不准确
|
||||
[self.tableView layoutIfNeeded];
|
||||
|
||||
[self.tableView insertRowsAtIndexPaths:indexPaths
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView beginUpdates];
|
||||
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView endUpdates];
|
||||
}
|
||||
|
||||
[self updateFooterVisibility];
|
||||
@@ -362,6 +541,9 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
- (void)reloadWithMessages:(NSArray<KBAiChatMessage *> *)messages
|
||||
keepOffset:(BOOL)keepOffset
|
||||
scrollToBottom:(BOOL)scrollToBottom {
|
||||
if (self.inverted) {
|
||||
keepOffset = NO;
|
||||
}
|
||||
CGFloat oldContentHeight = self.tableView.contentSize.height;
|
||||
CGFloat oldOffsetY = self.tableView.contentOffset.y;
|
||||
KBAiChatMessage *anchorMessage = nil;
|
||||
@@ -400,8 +582,14 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
|
||||
[self.messages removeAllObjects];
|
||||
if (messages.count > 0) {
|
||||
for (KBAiChatMessage *message in messages) {
|
||||
[self insertMessageWithTimestamp:message];
|
||||
if (self.inverted) {
|
||||
NSArray<KBAiChatMessage *> *rebuilt = [self invertedMessagesByInsertingTimestamps:messages
|
||||
startingReference:nil];
|
||||
[self.messages addObjectsFromArray:rebuilt];
|
||||
} else {
|
||||
for (KBAiChatMessage *message in messages) {
|
||||
[self insertMessageWithTimestamp:message];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +620,9 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
NSLog(@"[KBChatTableView] ========== reloadWithMessages 结束 ==========");
|
||||
|
||||
if (keepOffset) {
|
||||
if (self.inverted) {
|
||||
keepOffset = NO;
|
||||
}
|
||||
CGFloat offsetY = oldOffsetY;
|
||||
if (anchorMessage) {
|
||||
NSInteger newIndex = [self.messages indexOfObjectIdenticalTo:anchorMessage];
|
||||
@@ -480,6 +671,36 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
[self.messages addObject:message];
|
||||
}
|
||||
|
||||
- (NSArray<KBAiChatMessage *> *)invertedItemsForNewMessageAtBeginning:(KBAiChatMessage *)message {
|
||||
NSMutableArray<KBAiChatMessage *> *result = [NSMutableArray array];
|
||||
NSInteger firstIndex = [self firstNonTimeIndex];
|
||||
KBAiChatMessage *reference = (firstIndex != NSNotFound) ? self.messages[firstIndex] : nil;
|
||||
|
||||
[result addObject:message];
|
||||
if (!reference || [self shouldInsertTimestampBetweenMessage:message andReference:reference]) {
|
||||
KBAiChatMessage *timeMessage = [KBAiChatMessage timeMessageWithTimestamp:message.timestamp];
|
||||
[result addObject:timeMessage];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray<KBAiChatMessage *> *)invertedMessagesByInsertingTimestamps:(NSArray<KBAiChatMessage *> *)messages
|
||||
startingReference:(nullable KBAiChatMessage *)reference {
|
||||
NSMutableArray<KBAiChatMessage *> *result = [NSMutableArray array];
|
||||
KBAiChatMessage *ref = reference;
|
||||
for (KBAiChatMessage *msg in messages) {
|
||||
if (ref && [self shouldInsertTimestampBetweenMessage:msg andReference:ref]) {
|
||||
KBAiChatMessage *timeMessage = [KBAiChatMessage timeMessageWithTimestamp:msg.timestamp];
|
||||
[result addObject:timeMessage];
|
||||
} else if (!ref) {
|
||||
// 第一条消息不强制插时间,避免底部多一条时间戳
|
||||
}
|
||||
[result addObject:msg];
|
||||
ref = msg;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSInteger)firstVisibleNonTimeRowExcludingMessage:(KBAiChatMessage *)excludedMessage {
|
||||
NSArray<NSIndexPath *> *visible = self.tableView.indexPathsForVisibleRows;
|
||||
if (visible.count == 0) { return NSNotFound; }
|
||||
@@ -605,6 +826,48 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)appendHistoryMessages:(NSArray<KBAiChatMessage *> *)messages
|
||||
openingMessage:(nullable KBAiChatMessage *)openingMessage {
|
||||
if (messages.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSInteger insertIndex = self.messages.count;
|
||||
if (openingMessage) {
|
||||
NSInteger openingIndex = [self.messages indexOfObjectIdenticalTo:openingMessage];
|
||||
if (openingIndex != NSNotFound) {
|
||||
insertIndex = openingIndex;
|
||||
}
|
||||
}
|
||||
|
||||
NSInteger referenceIndex = [self lastNonTimeIndexBeforeIndex:insertIndex];
|
||||
KBAiChatMessage *reference = (referenceIndex != NSNotFound) ? self.messages[referenceIndex] : nil;
|
||||
|
||||
NSArray<KBAiChatMessage *> *toInsert = nil;
|
||||
if (self.inverted) {
|
||||
toInsert = [self invertedMessagesByInsertingTimestamps:messages startingReference:reference];
|
||||
} else {
|
||||
toInsert = [self messagesByInsertingTimestamps:messages];
|
||||
}
|
||||
|
||||
[UIView performWithoutAnimation:^{
|
||||
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];
|
||||
}];
|
||||
}
|
||||
|
||||
/// 判断是否需要插入时间戳
|
||||
- (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message {
|
||||
// 第一条消息总是显示时间
|
||||
@@ -659,19 +922,37 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
}
|
||||
|
||||
- (void)stopPreviousIncompleteAssistantMessageIfNeeded {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *msg = self.messages[i];
|
||||
if (msg.type == KBAiChatMessageTypeAssistant && !msg.isComplete) {
|
||||
msg.isComplete = YES;
|
||||
msg.needsTypewriterEffect = NO;
|
||||
|
||||
NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
KBChatAssistantMessageCell *oldCell = [self.tableView cellForRowAtIndexPath:oldIndexPath];
|
||||
if ([oldCell isKindOfClass:[KBChatAssistantMessageCell class]]) {
|
||||
[oldCell stopTypewriterEffect];
|
||||
oldCell.messageLabel.text = msg.text;
|
||||
if (self.inverted) {
|
||||
for (NSInteger i = 0; i < self.messages.count; i++) {
|
||||
KBAiChatMessage *msg = self.messages[i];
|
||||
if (msg.type == KBAiChatMessageTypeAssistant && !msg.isComplete) {
|
||||
msg.isComplete = YES;
|
||||
msg.needsTypewriterEffect = NO;
|
||||
|
||||
NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
KBChatAssistantMessageCell *oldCell = [self.tableView cellForRowAtIndexPath:oldIndexPath];
|
||||
if ([oldCell isKindOfClass:[KBChatAssistantMessageCell class]]) {
|
||||
[oldCell stopTypewriterEffect];
|
||||
oldCell.messageLabel.text = msg.text;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *msg = self.messages[i];
|
||||
if (msg.type == KBAiChatMessageTypeAssistant && !msg.isComplete) {
|
||||
msg.isComplete = YES;
|
||||
msg.needsTypewriterEffect = NO;
|
||||
|
||||
NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
KBChatAssistantMessageCell *oldCell = [self.tableView cellForRowAtIndexPath:oldIndexPath];
|
||||
if ([oldCell isKindOfClass:[KBChatAssistantMessageCell class]]) {
|
||||
[oldCell stopTypewriterEffect];
|
||||
oldCell.messageLabel.text = msg.text;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -700,6 +981,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
KBChatUserMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:kUserCellIdentifier
|
||||
forIndexPath:indexPath];
|
||||
[cell configureWithMessage:message];
|
||||
cell.contentView.transform = self.inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
||||
return cell;
|
||||
}
|
||||
|
||||
@@ -713,6 +995,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
BOOL isPlaying = [indexPath isEqual:self.playingCellIndexPath];
|
||||
[cell updateVoicePlayingState:isPlaying];
|
||||
|
||||
cell.contentView.transform = self.inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
||||
return cell;
|
||||
}
|
||||
|
||||
@@ -720,6 +1003,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
KBChatTimeCell *cell = [tableView dequeueReusableCellWithIdentifier:kTimeCellIdentifier
|
||||
forIndexPath:indexPath];
|
||||
[cell configureWithMessage:message];
|
||||
cell.contentView.transform = self.inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
@@ -732,6 +1016,42 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (UICollectionView *)nearestCollectionView {
|
||||
UIView *view = self;
|
||||
while (view) {
|
||||
if ([view isKindOfClass:[UICollectionView class]]) {
|
||||
return (UICollectionView *)view;
|
||||
}
|
||||
view = view.superview;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)setNearestCollectionViewScrollEnabled:(BOOL)enabled {
|
||||
UICollectionView *collectionView = [self nearestCollectionView];
|
||||
if (!collectionView) {
|
||||
return;
|
||||
}
|
||||
if (collectionView.scrollEnabled == enabled) {
|
||||
return;
|
||||
}
|
||||
collectionView.scrollEnabled = enabled;
|
||||
}
|
||||
|
||||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
||||
[self setNearestCollectionViewScrollEnabled:NO];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
||||
if (!decelerate) {
|
||||
[self setNearestCollectionViewScrollEnabled:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||||
[self setNearestCollectionViewScrollEnabled:YES];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
if ([self.delegate respondsToSelector:@selector(chatTableViewDidScroll:scrollView:)]) {
|
||||
[self.delegate chatTableViewDidScroll:self scrollView:scrollView];
|
||||
@@ -743,23 +1063,18 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
withVelocity:(CGPoint)velocity
|
||||
targetContentOffset:(inout CGPoint *)targetContentOffset {
|
||||
|
||||
CGFloat offsetY = scrollView.contentOffset.y;
|
||||
CGFloat contentHeight = scrollView.contentSize.height;
|
||||
CGFloat scrollViewHeight = scrollView.bounds.size.height;
|
||||
CGFloat minOffset = -scrollView.contentInset.top;
|
||||
CGFloat maxOffset = contentHeight - scrollViewHeight + scrollView.contentInset.bottom;
|
||||
if (maxOffset < minOffset) {
|
||||
maxOffset = minOffset;
|
||||
}
|
||||
|
||||
// 如果内容不够长,禁用弹簧效果
|
||||
if (contentHeight <= scrollViewHeight) {
|
||||
scrollView.bounces = NO;
|
||||
} else {
|
||||
scrollView.bounces = YES;
|
||||
|
||||
// 在快速滑动到底部时,避免过度弹簧导致抖动
|
||||
if (velocity.y < 0) { // 向上滑动(到底部)
|
||||
CGFloat maxOffset = contentHeight - scrollViewHeight + scrollView.contentInset.bottom;
|
||||
if (targetContentOffset->y > maxOffset) {
|
||||
targetContentOffset->y = maxOffset;
|
||||
}
|
||||
}
|
||||
if (targetContentOffset->y < minOffset) {
|
||||
targetContentOffset->y = minOffset;
|
||||
} else if (targetContentOffset->y > maxOffset) {
|
||||
targetContentOffset->y = maxOffset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user