3
This commit is contained in:
@@ -85,13 +85,16 @@
|
||||
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date];
|
||||
|
||||
if (interval < 60) {
|
||||
return @"刚刚";
|
||||
return KBLocalized(@"Just now");
|
||||
} else if (interval < 3600) {
|
||||
return [NSString stringWithFormat:@"%.0f分钟前", interval / 60];
|
||||
return [NSString stringWithFormat:KBLocalized(@"%.0f minutes ago"),
|
||||
interval / 60];
|
||||
} else if (interval < 86400) {
|
||||
return [NSString stringWithFormat:@"%.0f小时前", interval / 3600];
|
||||
return [NSString stringWithFormat:KBLocalized(@"%.0f hours ago"),
|
||||
interval / 3600];
|
||||
} else if (interval < 86400 * 30) {
|
||||
return [NSString stringWithFormat:@"%.0f天前", interval / 86400];
|
||||
return [NSString stringWithFormat:KBLocalized(@"%.0f days ago"),
|
||||
interval / 86400];
|
||||
} else {
|
||||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||||
formatter.dateFormat = @"MM-dd";
|
||||
|
||||
@@ -68,13 +68,16 @@
|
||||
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date];
|
||||
|
||||
if (interval < 60) {
|
||||
return @"刚刚";
|
||||
return KBLocalized(@"Just now");
|
||||
} else if (interval < 3600) {
|
||||
return [NSString stringWithFormat:@"%.0f分钟前", interval / 60];
|
||||
return [NSString stringWithFormat:KBLocalized(@"%.0f minutes ago"),
|
||||
interval / 60];
|
||||
} else if (interval < 86400) {
|
||||
return [NSString stringWithFormat:@"%.0f小时前", interval / 3600];
|
||||
return [NSString stringWithFormat:KBLocalized(@"%.0f hours ago"),
|
||||
interval / 3600];
|
||||
} else if (interval < 86400 * 30) {
|
||||
return [NSString stringWithFormat:@"%.0f天前", interval / 86400];
|
||||
return [NSString stringWithFormat:KBLocalized(@"%.0f days ago"),
|
||||
interval / 86400];
|
||||
} else {
|
||||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||||
formatter.dateFormat = @"MM-dd";
|
||||
@@ -95,7 +98,8 @@
|
||||
// 用户名高度(可能包含 "回复 @xxx")
|
||||
NSMutableString *userNameText = [NSMutableString stringWithString:self.userName ?: @""];
|
||||
if (self.replyToUserName.length > 0) {
|
||||
[userNameText appendFormat:@" 回复 @%@", self.replyToUserName];
|
||||
[userNameText appendFormat:@" %@ @%@", KBLocalized(@"Reply"),
|
||||
self.replyToUserName];
|
||||
}
|
||||
UIFont *userNameFont = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
|
||||
CGRect userNameRect = [userNameText boundingRectWithSize:CGSizeMake(contentWidth, CGFLOAT_MAX)
|
||||
|
||||
@@ -58,10 +58,12 @@
|
||||
formatter.dateFormat = @"HH:mm";
|
||||
} else if ([calendar isDateInYesterday:timestamp]) {
|
||||
// 昨天
|
||||
formatter.dateFormat = @"'昨天' HH:mm";
|
||||
formatter.dateFormat = @"HH:mm";
|
||||
NSString *timeText = [formatter stringFromDate:timestamp];
|
||||
return [NSString stringWithFormat:@"%@ %@", KBLocalized(@"Yesterday"), timeText];
|
||||
} else {
|
||||
// 其他:显示日期 + 时间
|
||||
formatter.dateFormat = @"MM月dd日 HH:mm";
|
||||
formatter.dateFormat = @"MM-dd HH:mm";
|
||||
}
|
||||
|
||||
return [formatter stringFromDate:timestamp];
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
}
|
||||
case KBAIReplyFooterStateExpand: {
|
||||
self.actionButton.hidden = NO;
|
||||
title = [NSString
|
||||
stringWithFormat:@"展开%ld条回复", (long)comment.totalReplyCount];
|
||||
title = [NSString stringWithFormat:KBLocalized(@"View %ld replies"),
|
||||
(long)comment.totalReplyCount];
|
||||
[self.actionButton setImage:[UIImage systemImageNamed:@"chevron.down"]
|
||||
forState:UIControlStateNormal];
|
||||
break;
|
||||
@@ -71,15 +71,15 @@
|
||||
self.actionButton.hidden = NO;
|
||||
NSInteger remaining =
|
||||
comment.totalReplyCount - comment.displayedReplies.count;
|
||||
title =
|
||||
[NSString stringWithFormat:@"展开更多回复(%ld条)", (long)remaining];
|
||||
title = [NSString stringWithFormat:KBLocalized(@"View more replies (%ld)"),
|
||||
(long)remaining];
|
||||
[self.actionButton setImage:[UIImage systemImageNamed:@"chevron.down"]
|
||||
forState:UIControlStateNormal];
|
||||
break;
|
||||
}
|
||||
case KBAIReplyFooterStateCollapse: {
|
||||
self.actionButton.hidden = NO;
|
||||
title = @"收起";
|
||||
title = KBLocalized(@"Collapse");
|
||||
[self.actionButton setImage:[UIImage systemImageNamed:@"chevron.up"]
|
||||
forState:UIControlStateNormal];
|
||||
break;
|
||||
|
||||
@@ -94,8 +94,9 @@
|
||||
self.timeLabel.text = [comment formattedTime];
|
||||
|
||||
// 点赞按钮
|
||||
NSString *likeText =
|
||||
comment.likeCount > 0 ? [self formatLikeCount:comment.likeCount] : @"赞";
|
||||
NSString *likeText = comment.likeCount > 0
|
||||
? [self formatLikeCount:comment.likeCount]
|
||||
: KBLocalized(@"Like");
|
||||
self.likeButton.textLabel.text = likeText;
|
||||
|
||||
UIImage *likeImage = comment.liked
|
||||
@@ -174,7 +175,7 @@
|
||||
if (!_replyButton) {
|
||||
_replyButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_replyButton.titleLabel.font = [UIFont systemFontOfSize:12];
|
||||
[_replyButton setTitle:@"回复" forState:UIControlStateNormal];
|
||||
[_replyButton setTitle:KBLocalized(@"Reply") forState:UIControlStateNormal];
|
||||
[_replyButton setTitleColor:[UIColor colorWithHex:0x9F9F9F] forState:UIControlStateNormal];
|
||||
[_replyButton addTarget:self
|
||||
action:@selector(replyButtonTapped)
|
||||
|
||||
@@ -35,23 +35,23 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
|
||||
@property(nonatomic, strong) NSMutableArray<KBAICommentModel *> *comments;
|
||||
|
||||
/// 分页参数
|
||||
/// Pagination params
|
||||
@property(nonatomic, assign) NSInteger currentPage;
|
||||
@property(nonatomic, assign) NSInteger pageSize;
|
||||
@property(nonatomic, assign) BOOL isLoading;
|
||||
@property(nonatomic, assign) BOOL hasMoreData;
|
||||
|
||||
/// 键盘高度
|
||||
/// Keyboard height
|
||||
@property(nonatomic, assign) CGFloat keyboardHeight;
|
||||
/// 输入框底部约束
|
||||
/// Bottom constraint for input view
|
||||
@property(nonatomic, strong) MASConstraint *inputBottomConstraint;
|
||||
|
||||
/// 当前回复的目标(一级评论)
|
||||
/// Current reply target (top-level comment)
|
||||
@property(nonatomic, weak) KBAICommentModel *replyToComment;
|
||||
/// 当前回复的目标(二级评论)
|
||||
/// Current reply target (reply)
|
||||
@property(nonatomic, weak) KBAIReplyModel *replyToReply;
|
||||
|
||||
/// AiVM 实例
|
||||
/// AiVM instance
|
||||
@property(nonatomic, strong) AiVM *aiVM;
|
||||
|
||||
@end
|
||||
@@ -65,7 +65,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
if (user.nickName.length > 0) {
|
||||
return user.nickName;
|
||||
}
|
||||
return @"我";
|
||||
return KBLocalized(@"Me");
|
||||
}
|
||||
|
||||
- (NSString *)currentUserId {
|
||||
@@ -80,7 +80,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
|
||||
- (NSString *)generateTempIdString {
|
||||
long long ms = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
|
||||
// 使用负数避免与后端 ID 冲突
|
||||
// Use negative values to avoid colliding with server IDs
|
||||
long long tmp = -ms;
|
||||
return [NSString stringWithFormat:@"%lld", tmp];
|
||||
}
|
||||
@@ -155,13 +155,13 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
#pragma mark - UI Setup
|
||||
|
||||
- (void)setupUI {
|
||||
// 设置背景为透明,让模糊效果可见
|
||||
// Make background transparent so blur effect is visible
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
self.layer.cornerRadius = 12;
|
||||
self.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
|
||||
self.clipsToBounds = YES;
|
||||
|
||||
// 添加模糊背景(最底层)
|
||||
// Add blur background (bottom-most layer)
|
||||
[self addSubview:self.blurBackgroundView];
|
||||
[self.blurBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
@@ -201,7 +201,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
make.bottom.equalTo(self.inputView.mas_top);
|
||||
}];
|
||||
|
||||
// 上拉加载更多
|
||||
// Load more on pull-up
|
||||
__weak typeof(self) weakSelf = self;
|
||||
MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
@@ -302,7 +302,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
|
||||
- (void)fetchCommentsAtPage:(NSInteger)page append:(BOOL)append {
|
||||
if (self.companionId <= 0) {
|
||||
NSLog(@"[KBAICommentView] companionId 未设置,无法加载评论");
|
||||
NSLog(@"[KBAICommentView] companionId is not set, cannot load comments");
|
||||
[self showEmptyState];
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
return;
|
||||
@@ -323,7 +323,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
strongSelf.isLoading = NO;
|
||||
|
||||
if (error) {
|
||||
NSLog(@"[KBAICommentView] 加载评论失败:%@", error.localizedDescription);
|
||||
NSLog(@"[KBAICommentView] Failed to load comments: %@", error.localizedDescription);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (append) {
|
||||
[strongSelf.tableView.mj_footer endRefreshing];
|
||||
@@ -340,11 +340,11 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
}];
|
||||
}
|
||||
|
||||
/// 更新评论数据(从后端返回的 KBCommentPageModel 转换为 UI 层的 KBAICommentModel)
|
||||
/// Update comments (convert backend KBCommentPageModel to UI KBAICommentModel)
|
||||
- (void)updateCommentsWithPageModel:(KBCommentPageModel *)pageModel append:(BOOL)append {
|
||||
if (!pageModel) {
|
||||
NSLog(@"[KBAICommentView] pageModel 为空");
|
||||
// 数据为空,显示空态
|
||||
NSLog(@"[KBAICommentView] pageModel is nil");
|
||||
// Data is empty, show empty state
|
||||
[self showEmptyState];
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
return;
|
||||
@@ -356,19 +356,20 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
[self.comments removeAllObjects];
|
||||
}
|
||||
|
||||
// 获取 tableView 宽度用于计算高度
|
||||
// Get tableView width for height calculation
|
||||
CGFloat tableWidth = self.tableView.bounds.size.width;
|
||||
if (tableWidth <= 0) {
|
||||
tableWidth = [UIScreen mainScreen].bounds.size.width;
|
||||
}
|
||||
|
||||
NSLog(@"[KBAICommentView] 加载到 %ld 条评论,共 %ld 条,页码:%ld/%ld", (long)pageModel.records.count, (long)pageModel.total, (long)pageModel.current, (long)pageModel.pages);
|
||||
NSLog(@"[KBAICommentView] Loaded %ld comments, total %ld, page: %ld/%ld", (long)pageModel.records.count, (long)pageModel.total, (long)pageModel.current, (long)pageModel.pages);
|
||||
|
||||
for (KBCommentItem *item in pageModel.records) {
|
||||
// 转换为 KBAICommentModel(使用 MJExtension)
|
||||
// 注意:KBCommentItem 通过 MJExtension 将后端字段 id 映射为了 commentId。
|
||||
// 这里如果直接用 mj_keyValues,会导致字典里只有 commentId,KBAICommentModel/KBAIReplyModel
|
||||
// 的映射(commentId/replyId -> id)拿不到值,最终 commentId/replyId 为空,进而影响发送回复时的 parentId/rootId。
|
||||
// Convert to KBAICommentModel (via MJExtension)
|
||||
// Note: KBCommentItem maps backend field id to commentId via MJExtension.
|
||||
// If we directly use mj_keyValues, the dictionary only has commentId and
|
||||
// KBAICommentModel/KBAIReplyModel mapping (commentId/replyId -> id) misses it.
|
||||
// That would make commentId/replyId empty and break parentId/rootId when replying.
|
||||
NSMutableDictionary *itemKV = [[item mj_keyValues] mutableCopy];
|
||||
id commentIdVal = itemKV[@"commentId"];
|
||||
if (commentIdVal) {
|
||||
@@ -397,10 +398,10 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
|
||||
KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[itemKV copy]];
|
||||
|
||||
// 预先计算并缓存 Header 高度
|
||||
// Precompute and cache header height
|
||||
comment.cachedHeaderHeight = [comment calculateHeaderHeightWithMaxWidth:tableWidth];
|
||||
|
||||
// 预先计算并缓存所有 Reply 高度
|
||||
// Precompute and cache all reply heights
|
||||
for (KBAIReplyModel *reply in comment.replies) {
|
||||
reply.cachedCellHeight = [reply calculateCellHeightWithMaxWidth:tableWidth];
|
||||
}
|
||||
@@ -411,7 +412,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
[self updateTitle];
|
||||
[self.tableView reloadData];
|
||||
|
||||
// 更新分页状态
|
||||
// Update pagination state
|
||||
self.currentPage = pageModel.current > 0 ? pageModel.current : self.currentPage;
|
||||
if (pageModel.pages > 0) {
|
||||
self.hasMoreData = pageModel.current < pageModel.pages;
|
||||
@@ -425,7 +426,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
[self.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
}
|
||||
|
||||
// 根据数据是否为空,动态控制空态显示
|
||||
// Toggle empty state based on data
|
||||
if (self.comments.count == 0) {
|
||||
[self showEmptyState];
|
||||
} else {
|
||||
@@ -433,25 +434,25 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示空态视图
|
||||
/// Show empty state view
|
||||
- (void)showEmptyState {
|
||||
self.tableView.useEmptyDataSet = YES;
|
||||
self.tableView.emptyTitleText = @"暂无评论";
|
||||
self.tableView.emptyDescriptionText = @"快来抢沙发吧~";
|
||||
self.tableView.emptyImage = nil; // 可选:设置空态图片
|
||||
self.tableView.emptyVerticalOffset = -50; // 向上偏移一点
|
||||
self.tableView.emptyTitleText = KBLocalized(@"No comments yet");
|
||||
self.tableView.emptyDescriptionText = KBLocalized(@"Be the first to comment");
|
||||
self.tableView.emptyImage = nil; // Optional: set empty-state image
|
||||
self.tableView.emptyVerticalOffset = -50; // Slight upward offset
|
||||
[self.tableView kb_reloadEmptyDataSet];
|
||||
}
|
||||
|
||||
/// 显示错误空态视图
|
||||
/// Show error empty state view
|
||||
- (void)showEmptyStateWithError {
|
||||
self.tableView.useEmptyDataSet = YES;
|
||||
self.tableView.emptyTitleText = @"加载失败";
|
||||
self.tableView.emptyDescriptionText = @"点击重新加载";
|
||||
self.tableView.emptyTitleText = KBLocalized(@"Load failed");
|
||||
self.tableView.emptyDescriptionText = KBLocalized(@"Tap to retry");
|
||||
self.tableView.emptyImage = nil;
|
||||
self.tableView.emptyVerticalOffset = -50;
|
||||
|
||||
// 点击重新加载
|
||||
// Tap to reload
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.tableView.emptyDidTapView = ^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
@@ -463,7 +464,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
[self.tableView kb_reloadEmptyDataSet];
|
||||
}
|
||||
|
||||
/// 隐藏空态视图
|
||||
/// Hide empty state view
|
||||
- (void)hideEmptyState {
|
||||
self.tableView.useEmptyDataSet = NO;
|
||||
[self.tableView kb_reloadEmptyDataSet];
|
||||
@@ -473,10 +474,10 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
NSString *countText;
|
||||
if (self.totalCommentCount >= 10000) {
|
||||
countText = [NSString
|
||||
stringWithFormat:@"%.1fw条评论", self.totalCommentCount / 10000.0];
|
||||
stringWithFormat:KBLocalized(@"%.1fw comments"), self.totalCommentCount / 10000.0];
|
||||
} else {
|
||||
countText =
|
||||
[NSString stringWithFormat:@"%ld条评论", (long)self.totalCommentCount];
|
||||
[NSString stringWithFormat:KBLocalized(@"%ld comments"), (long)self.totalCommentCount];
|
||||
}
|
||||
self.titleLabel.text = countText;
|
||||
}
|
||||
@@ -510,41 +511,41 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取评论 ID(需要转换为 NSInteger)
|
||||
// Get comment ID (convert to NSInteger)
|
||||
NSInteger commentId = [reply.replyId integerValue];
|
||||
|
||||
// 调用点赞接口
|
||||
// Call like API
|
||||
[strongSelf.aiVM likeCommentWithCommentId:commentId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error) {
|
||||
NSLog(@"[KBAICommentView] 二级评论点赞失败:%@", error.localizedDescription);
|
||||
// TODO: 显示错误提示
|
||||
NSLog(@"[KBAICommentView] Failed to like reply: %@", error.localizedDescription);
|
||||
// TODO: Show error message
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.code == 0) {
|
||||
// data = true: 点赞成功,data = false: 取消点赞成功
|
||||
// data = true: liked, data = false: unliked
|
||||
BOOL isNowLiked = response.data;
|
||||
|
||||
// 更新模型状态
|
||||
// Update model state
|
||||
if (isNowLiked) {
|
||||
// 点赞成功:喜欢数+1
|
||||
// Like succeeded: like count +1
|
||||
reply.liked = YES;
|
||||
reply.likeCount = MAX(0, reply.likeCount + 1);
|
||||
NSLog(@"[KBAICommentView] 二级评论点赞成功,ID: %ld", (long)commentId);
|
||||
NSLog(@"[KBAICommentView] Reply liked successfully, ID: %ld", (long)commentId);
|
||||
} else {
|
||||
// 取消点赞成功:喜欢数-1
|
||||
// Unlike succeeded: like count -1
|
||||
reply.liked = NO;
|
||||
reply.likeCount = MAX(0, reply.likeCount - 1);
|
||||
NSLog(@"[KBAICommentView] 二级评论取消点赞成功,ID: %ld", (long)commentId);
|
||||
NSLog(@"[KBAICommentView] Reply unliked successfully, ID: %ld", (long)commentId);
|
||||
}
|
||||
|
||||
// 刷新对应的行
|
||||
// Refresh target row
|
||||
[strongSelf.tableView reloadRowsAtIndexPaths:@[ indexPath ]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
} else {
|
||||
NSLog(@"[KBAICommentView] 二级评论点赞失败:%@", response.message ?: @"未知错误");
|
||||
// TODO: 显示错误提示
|
||||
NSLog(@"[KBAICommentView] Failed to like reply: %@", response.message ?: @"Unknown error");
|
||||
// TODO: Show error message
|
||||
}
|
||||
});
|
||||
}];
|
||||
@@ -574,41 +575,41 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取评论 ID(需要转换为 NSInteger)
|
||||
// Get comment ID (convert to NSInteger)
|
||||
NSInteger commentId = [comment.commentId integerValue];
|
||||
|
||||
// 调用点赞接口
|
||||
// Call like API
|
||||
[strongSelf.aiVM likeCommentWithCommentId:commentId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error) {
|
||||
NSLog(@"[KBAICommentView] 一级评论点赞失败:%@", error.localizedDescription);
|
||||
// TODO: 显示错误提示
|
||||
NSLog(@"[KBAICommentView] Failed to like top-level comment: %@", error.localizedDescription);
|
||||
// TODO: Show error message
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.code == 0) {
|
||||
// data = true: 点赞成功,data = false: 取消点赞成功
|
||||
// data = true: liked, data = false: unliked
|
||||
BOOL isNowLiked = response.data;
|
||||
|
||||
// 更新模型状态
|
||||
// Update model state
|
||||
if (isNowLiked) {
|
||||
// 点赞成功:喜欢数+1
|
||||
// Like succeeded: like count +1
|
||||
comment.liked = YES;
|
||||
comment.likeCount = MAX(0, comment.likeCount + 1);
|
||||
NSLog(@"[KBAICommentView] 一级评论点赞成功,ID: %ld", (long)commentId);
|
||||
NSLog(@"[KBAICommentView] Top-level comment liked successfully, ID: %ld", (long)commentId);
|
||||
} else {
|
||||
// 取消点赞成功:喜欢数-1
|
||||
// Unlike succeeded: like count -1
|
||||
comment.liked = NO;
|
||||
comment.likeCount = MAX(0, comment.likeCount - 1);
|
||||
NSLog(@"[KBAICommentView] 一级评论取消点赞成功,ID: %ld", (long)commentId);
|
||||
NSLog(@"[KBAICommentView] Top-level comment unliked successfully, ID: %ld", (long)commentId);
|
||||
}
|
||||
|
||||
// 刷新对应的 section
|
||||
// Refresh target section
|
||||
[strongSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
} else {
|
||||
NSLog(@"[KBAICommentView] 一级评论点赞失败:%@", response.message ?: @"未知错误");
|
||||
// TODO: 显示错误提示
|
||||
NSLog(@"[KBAICommentView] Failed to like top-level comment: %@", response.message ?: @"Unknown error");
|
||||
// TODO: Show error message
|
||||
}
|
||||
});
|
||||
}];
|
||||
@@ -626,7 +627,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
KBAICommentModel *comment = self.comments[section];
|
||||
KBAIReplyFooterState state = [comment footerState];
|
||||
|
||||
// 无二级评论时返回空视图
|
||||
// Return empty view when there are no replies
|
||||
if (state == KBAIReplyFooterStateHidden) {
|
||||
return nil;
|
||||
}
|
||||
@@ -669,7 +670,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
|
||||
#pragma mark - Footer Actions
|
||||
|
||||
/// 每次加载的回复数量
|
||||
/// Number of replies loaded each time
|
||||
static NSInteger const kRepliesLoadCount = 5;
|
||||
|
||||
- (void)handleFooterActionForSection:(NSInteger)section {
|
||||
@@ -695,10 +696,10 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
KBAICommentModel *comment = self.comments[section];
|
||||
NSInteger currentCount = comment.displayedReplies.count;
|
||||
|
||||
// 加载更多回复
|
||||
// Load more replies
|
||||
[comment loadMoreReplies:kRepliesLoadCount];
|
||||
|
||||
// 计算新增的行
|
||||
// Calculate newly inserted rows
|
||||
NSInteger newCount = comment.displayedReplies.count;
|
||||
NSMutableArray *insertIndexPaths = [NSMutableArray array];
|
||||
for (NSInteger i = currentCount; i < newCount; i++) {
|
||||
@@ -706,7 +707,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
inSection:section]];
|
||||
}
|
||||
|
||||
// 插入行(不刷新 Header,避免头像闪烁)
|
||||
// Insert rows (do not refresh header to avoid avatar flicker)
|
||||
[self.tableView beginUpdates];
|
||||
if (insertIndexPaths.count > 0) {
|
||||
[self.tableView insertRowsAtIndexPaths:insertIndexPaths
|
||||
@@ -714,7 +715,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
}
|
||||
[self.tableView endUpdates];
|
||||
|
||||
// 手动刷新 Footer
|
||||
// Manually refresh footer
|
||||
KBAICommentFooterView *footerView =
|
||||
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
|
||||
if (footerView) {
|
||||
@@ -726,17 +727,17 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
KBAICommentModel *comment = self.comments[section];
|
||||
NSInteger rowCount = comment.displayedReplies.count;
|
||||
|
||||
// 计算要删除的行
|
||||
// Calculate rows to delete
|
||||
NSMutableArray *deleteIndexPaths = [NSMutableArray array];
|
||||
for (NSInteger i = 0; i < rowCount; i++) {
|
||||
[deleteIndexPaths addObject:[NSIndexPath indexPathForRow:i
|
||||
inSection:section]];
|
||||
}
|
||||
|
||||
// 收起全部回复
|
||||
// Collapse all replies
|
||||
[comment collapseReplies];
|
||||
|
||||
// 删除行(不刷新 Header,避免头像闪烁)
|
||||
// Delete rows (do not refresh header to avoid avatar flicker)
|
||||
[self.tableView beginUpdates];
|
||||
if (deleteIndexPaths.count > 0) {
|
||||
[self.tableView deleteRowsAtIndexPaths:deleteIndexPaths
|
||||
@@ -744,7 +745,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
}
|
||||
[self.tableView endUpdates];
|
||||
|
||||
// 手动刷新 Footer
|
||||
// Manually refresh footer
|
||||
KBAICommentFooterView *footerView =
|
||||
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
|
||||
if (footerView) {
|
||||
@@ -756,7 +757,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
|
||||
- (void)closeButtonTapped {
|
||||
[self.popView dismiss];
|
||||
// 关闭评论视图(由外部处理)
|
||||
// Close comment view (handled by outside)
|
||||
// [[NSNotificationCenter defaultCenter]
|
||||
// postNotificationName:@"KBAICommentViewCloseNotification"
|
||||
// object:nil];
|
||||
@@ -766,13 +767,13 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
|
||||
- (UIVisualEffectView *)blurBackgroundView {
|
||||
if (!_blurBackgroundView) {
|
||||
// 创建模糊效果(43pt 的模糊半径)
|
||||
// iOS 的 UIBlurEffect 没有直接设置模糊半径的 API,使用系统预设的 dark 效果
|
||||
// Create blur effect (43pt blur radius in design)
|
||||
// iOS UIBlurEffect has no direct blur-radius API; use system dark style
|
||||
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
|
||||
_blurBackgroundView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
||||
|
||||
// 在模糊效果上叠加一个半透明黑色遮罩来调整透明度和颜色
|
||||
// 颜色:#000000,透明度:0.31
|
||||
// Overlay a semi-transparent black layer to tune tone and opacity
|
||||
// Color: #000000, alpha: 0.31
|
||||
UIView *darkOverlay = [[UIView alloc] init];
|
||||
darkOverlay.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.31];
|
||||
[_blurBackgroundView.contentView addSubview:darkOverlay];
|
||||
@@ -796,7 +797,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
|
||||
_titleLabel.textColor = [UIColor whiteColor];
|
||||
_titleLabel.text = @"0条评论";
|
||||
_titleLabel.text = [NSString stringWithFormat:KBLocalized(@"%ld comments"), (long)0];
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
@@ -824,10 +825,10 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
_tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
_tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 0.01)];
|
||||
|
||||
// 关闭空数据占位,避免加载时显示"暂无数据"
|
||||
// Disable empty placeholder by default to avoid showing "No data" while loading
|
||||
_tableView.useEmptyDataSet = NO;
|
||||
|
||||
// 注册 Header/Cell/Footer
|
||||
// Register Header/Cell/Footer
|
||||
[_tableView registerClass:[KBAICommentHeaderView class]
|
||||
forHeaderFooterViewReuseIdentifier:kCommentHeaderIdentifier];
|
||||
[_tableView registerClass:[KBAIReplyCell class]
|
||||
@@ -835,7 +836,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
[_tableView registerClass:[KBAICommentFooterView class]
|
||||
forHeaderFooterViewReuseIdentifier:kCommentFooterIdentifier];
|
||||
|
||||
// 去掉顶部间距
|
||||
// Remove top padding
|
||||
if (@available(iOS 15.0, *)) {
|
||||
_tableView.sectionHeaderTopPadding = 0;
|
||||
}
|
||||
@@ -846,7 +847,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
- (KBAICommentInputView *)inputView {
|
||||
if (!_inputView) {
|
||||
_inputView = [[KBAICommentInputView alloc] init];
|
||||
_inputView.placeholder = @"Send A Message";
|
||||
_inputView.placeholder = KBLocalized(@"Send A Message");
|
||||
_inputView.layer.cornerRadius = 26;
|
||||
_inputView.clipsToBounds = true;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
@@ -865,26 +866,26 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
self.replyToReply = reply;
|
||||
|
||||
if (reply) {
|
||||
// 回复二级评论
|
||||
// Reply to a second-level comment
|
||||
self.inputView.placeholder =
|
||||
[NSString stringWithFormat:@"回复 @%@", reply.userName];
|
||||
[NSString stringWithFormat:KBLocalized(@"Reply to @%@"), reply.userName];
|
||||
} else if (comment) {
|
||||
// 回复一级评论
|
||||
// Reply to a top-level comment
|
||||
self.inputView.placeholder =
|
||||
[NSString stringWithFormat:@"回复 @%@", comment.userName];
|
||||
[NSString stringWithFormat:KBLocalized(@"Reply to @%@"), comment.userName];
|
||||
} else {
|
||||
// 普通评论
|
||||
self.inputView.placeholder = @"说点什么...";
|
||||
// New comment
|
||||
self.inputView.placeholder = KBLocalized(@"Say something...");
|
||||
}
|
||||
|
||||
// 弹起键盘
|
||||
// Show keyboard
|
||||
[self.inputView showKeyboard];
|
||||
}
|
||||
|
||||
- (void)clearReplyTarget {
|
||||
self.replyToComment = nil;
|
||||
self.replyToReply = nil;
|
||||
self.inputView.placeholder = @"说点什么...";
|
||||
self.inputView.placeholder = KBLocalized(@"Say something...");
|
||||
}
|
||||
|
||||
#pragma mark - Send Comment
|
||||
@@ -899,20 +900,20 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
}
|
||||
|
||||
if (self.replyToComment) {
|
||||
// 回复评论(添加二级评论)
|
||||
// Send reply (add second-level comment)
|
||||
[self sendReplyWithText:text tableWidth:tableWidth];
|
||||
} else {
|
||||
// 发送一级评论
|
||||
// Send top-level comment
|
||||
[self sendNewCommentWithText:text tableWidth:tableWidth];
|
||||
}
|
||||
|
||||
// 清空输入框和回复目标
|
||||
// Clear input and reply target
|
||||
[self.inputView clearText];
|
||||
[self clearReplyTarget];
|
||||
}
|
||||
|
||||
- (void)sendNewCommentWithText:(NSString *)text tableWidth:(CGFloat)tableWidth {
|
||||
NSLog(@"[KBAICommentView] 发送一级评论:%@", text);
|
||||
NSLog(@"[KBAICommentView] Send top-level comment: %@", text);
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM addCommentWithCompanionId:self.companionId
|
||||
@@ -927,11 +928,11 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error || code != 0) {
|
||||
NSLog(@"[KBAICommentView] 发送一级评论失败:%@", error.localizedDescription ?: @"");
|
||||
NSLog(@"[KBAICommentView] Failed to send top-level comment: %@", error.localizedDescription ?: @"");
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地插入新评论到第一条,不再全量刷新
|
||||
// Insert new comment locally at first position; avoid full reload
|
||||
KBAICommentModel *localComment =
|
||||
[strongSelf buildLocalNewCommentWithText:text
|
||||
serverItem:newItem
|
||||
@@ -954,29 +955,29 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
});
|
||||
}];
|
||||
|
||||
// 示例代码:
|
||||
// Example code:
|
||||
// [self.aiVM sendCommentWithCompanionId:self.companionId
|
||||
// content:text
|
||||
// completion:^(KBCommentItem *newItem, NSError *error) {
|
||||
// if (error) {
|
||||
// NSLog(@"[KBAICommentView] 发送评论失败:%@", error.localizedDescription);
|
||||
// NSLog(@"[KBAICommentView] Failed to send comment: %@", error.localizedDescription);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 转换为 KBAICommentModel
|
||||
// // Convert to KBAICommentModel
|
||||
// KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[newItem mj_keyValues]];
|
||||
// comment.cachedHeaderHeight = [comment calculateHeaderHeightWithMaxWidth:tableWidth];
|
||||
//
|
||||
// // 插入到数组第一个
|
||||
// // Insert into array at index 0
|
||||
// [self.comments insertObject:comment atIndex:0];
|
||||
// self.totalCommentCount++;
|
||||
// [self updateTitle];
|
||||
//
|
||||
// // 插入新 section
|
||||
// // Insert new section
|
||||
// [self.tableView insertSections:[NSIndexSet indexSetWithIndex:0]
|
||||
// withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
//
|
||||
// // 滚动到顶部
|
||||
// // Scroll to top
|
||||
// [self.tableView setContentOffset:CGPointZero animated:YES];
|
||||
// }];
|
||||
}
|
||||
@@ -986,7 +987,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
if (!comment)
|
||||
return;
|
||||
|
||||
NSLog(@"[KBAICommentView] 回复评论 %@:%@", comment.commentId, text);
|
||||
NSLog(@"[KBAICommentView] Reply to comment %@: %@", comment.commentId, text);
|
||||
|
||||
NSInteger root = [comment.commentId integerValue];
|
||||
NSNumber *rootId = @(root);
|
||||
@@ -1011,7 +1012,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error || code != 0) {
|
||||
NSLog(@"[KBAICommentView] 回复评论失败:%@", error.localizedDescription ?: @"");
|
||||
NSLog(@"[KBAICommentView] Failed to send reply: %@", error.localizedDescription ?: @"");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1051,7 +1052,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
[strongSelf.delegate commentView:strongSelf didUpdateTotalCommentCount:strongSelf.totalCommentCount];
|
||||
}
|
||||
|
||||
// 若当前已完整展开,则直接插入新行;否则保持 displayedReplies 为前缀,避免破坏 loadMoreReplies 逻辑
|
||||
// If fully expanded, insert new row directly; otherwise keep displayedReplies as prefix to preserve loadMoreReplies behavior
|
||||
if (wasFullyExpanded) {
|
||||
[comment.displayedReplies addObject:localReply];
|
||||
NSInteger newRowIndex = comment.displayedReplies.count - 1;
|
||||
@@ -1085,31 +1086,31 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
});
|
||||
}];
|
||||
|
||||
// 示例代码:
|
||||
// Example code:
|
||||
// NSInteger parentId = [comment.commentId integerValue];
|
||||
// [self.aiVM replyCommentWithParentId:parentId
|
||||
// content:text
|
||||
// completion:^(KBCommentItem *newItem, NSError *error) {
|
||||
// if (error) {
|
||||
// NSLog(@"[KBAICommentView] 回复评论失败:%@", error.localizedDescription);
|
||||
// NSLog(@"[KBAICommentView] Failed to reply comment: %@", error.localizedDescription);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 转换为 KBAIReplyModel
|
||||
// // Convert to KBAIReplyModel
|
||||
// KBAIReplyModel *newReply = [KBAIReplyModel mj_objectWithKeyValues:[newItem mj_keyValues]];
|
||||
// newReply.cachedCellHeight = [newReply calculateCellHeightWithMaxWidth:tableWidth];
|
||||
//
|
||||
// // 添加到 replies 数组
|
||||
// // Append to replies array
|
||||
// NSMutableArray *newReplies = [NSMutableArray arrayWithArray:comment.replies];
|
||||
// [newReplies addObject:newReply];
|
||||
// comment.replies = newReplies;
|
||||
// comment.totalReplyCount = newReplies.count;
|
||||
//
|
||||
// // 找到该评论的 section
|
||||
// // Find section for this comment
|
||||
// NSInteger section = [self.comments indexOfObject:comment];
|
||||
// if (section == NSNotFound) return;
|
||||
//
|
||||
// // 如果已展开,添加到 displayedReplies 并插入行
|
||||
// // If expanded, append to displayedReplies and insert row
|
||||
// if (comment.isRepliesExpanded) {
|
||||
// NSInteger newRowIndex = comment.displayedReplies.count;
|
||||
// [comment.displayedReplies addObject:newReply];
|
||||
@@ -1118,18 +1119,18 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
// [self.tableView insertRowsAtIndexPaths:@[indexPath]
|
||||
// withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
//
|
||||
// // 刷新 Footer
|
||||
// // Refresh footer
|
||||
// KBAICommentFooterView *footerView = (KBAICommentFooterView *)[self.tableView footerViewForSection:section];
|
||||
// if (footerView) {
|
||||
// [footerView configureWithComment:comment];
|
||||
// }
|
||||
//
|
||||
// // 滚动到新回复
|
||||
// // Scroll to new reply
|
||||
// [self.tableView scrollToRowAtIndexPath:indexPath
|
||||
// atScrollPosition:UITableViewScrollPositionBottom
|
||||
// animated:YES];
|
||||
// } else {
|
||||
// // 未展开,刷新 Footer 显示新的回复数
|
||||
// // If not expanded, refresh footer to show updated reply count
|
||||
// KBAICommentFooterView *footerView = (KBAICommentFooterView *)[self.tableView footerViewForSection:section];
|
||||
// if (footerView) {
|
||||
// [footerView configureWithComment:comment];
|
||||
|
||||
@@ -108,8 +108,10 @@
|
||||
NSFontAttributeName : [UIFont systemFontOfSize:13],
|
||||
NSForegroundColorAttributeName : [UIColor whiteColor]
|
||||
};
|
||||
NSString *replyText =
|
||||
[NSString stringWithFormat:@" %@ ", KBLocalized(@"Reply")];
|
||||
[attrName appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:@" 回复 "
|
||||
initWithString:replyText
|
||||
attributes:replyAttrs]];
|
||||
|
||||
NSDictionary *toUserAttrs = @{
|
||||
@@ -133,8 +135,9 @@
|
||||
self.timeLabel.text = [reply formattedTime];
|
||||
|
||||
// 点赞
|
||||
NSString *likeText =
|
||||
reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @"赞";
|
||||
NSString *likeText = reply.likeCount > 0
|
||||
? [self formatLikeCount:reply.likeCount]
|
||||
: KBLocalized(@"Like");
|
||||
self.likeButton.textLabel.text = likeText;
|
||||
|
||||
UIImage *likeImage = reply.liked
|
||||
@@ -212,7 +215,7 @@
|
||||
if (!_replyButton) {
|
||||
_replyButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_replyButton.titleLabel.font = [UIFont systemFontOfSize:11];
|
||||
[_replyButton setTitle:@"回复" forState:UIControlStateNormal];
|
||||
[_replyButton setTitle:KBLocalized(@"Reply") forState:UIControlStateNormal];
|
||||
[_replyButton setTitleColor:[UIColor colorWithHex:0x9F9F9F] forState:UIControlStateNormal];
|
||||
[_replyButton addTarget:self
|
||||
action:@selector(replyButtonTapped)
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
|
||||
- (void)setup {
|
||||
_state = KBAiRecordButtonStateNormal;
|
||||
_normalTitle = @"按住说话";
|
||||
_recordingTitle = @"松开结束";
|
||||
_normalTitle = KBLocalized(@"Hold To Speak");
|
||||
_recordingTitle = KBLocalized(@"Release To Finish");
|
||||
_tintColor = [UIColor systemBlueColor];
|
||||
|
||||
// 背景视图
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
- (UILabel *)statusLabel {
|
||||
if (!_statusLabel) {
|
||||
_statusLabel = [[UILabel alloc] init];
|
||||
_statusLabel.text = @"按住按钮开始对话";
|
||||
_statusLabel.text = KBLocalized(@"Hold To Start Talking");
|
||||
_statusLabel.font = [UIFont systemFontOfSize:14];
|
||||
_statusLabel.textColor = [UIColor secondaryLabelColor];
|
||||
_statusLabel.textAlignment = NSTextAlignmentCenter;
|
||||
@@ -310,8 +310,8 @@
|
||||
if (!_recordButton) {
|
||||
_recordButton = [[KBAiRecordButton alloc] init];
|
||||
_recordButton.delegate = self;
|
||||
_recordButton.normalTitle = @"按住说话";
|
||||
_recordButton.recordingTitle = @"松开结束";
|
||||
_recordButton.normalTitle = KBLocalized(@"Hold To Speak");
|
||||
_recordButton.recordingTitle = KBLocalized(@"Release To Finish");
|
||||
_recordButton.normalIconImage = [UIImage imageNamed:@"ai_jianpan_icon"];
|
||||
_recordButton.recordingIconImage = [UIImage imageNamed:@"ai_luyining_icon"];
|
||||
_recordButton.hidden = YES;
|
||||
@@ -340,7 +340,7 @@
|
||||
- (UIButton *)textCenterButton {
|
||||
if (!_textCenterButton) {
|
||||
_textCenterButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
|
||||
[_textCenterButton setTitle:KBLocalized(@"Send A Message To Her") forState:UIControlStateNormal];
|
||||
_textCenterButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
[_textCenterButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_textCenterButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
|
||||
@@ -362,7 +362,7 @@
|
||||
- (UILabel *)voiceCenterLabel {
|
||||
if (!_voiceCenterLabel) {
|
||||
_voiceCenterLabel = [[UILabel alloc] init];
|
||||
_voiceCenterLabel.text = @"按住说话";
|
||||
_voiceCenterLabel.text = KBLocalized(@"Hold To Speak");
|
||||
_voiceCenterLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
_voiceCenterLabel.textColor = [UIColor whiteColor];
|
||||
_voiceCenterLabel.textAlignment = NSTextAlignmentCenter;
|
||||
@@ -526,9 +526,9 @@
|
||||
|
||||
- (void)updateCenterTextIfNeeded {
|
||||
if (self.inputState == KBVoiceInputBarStateText) {
|
||||
[self.textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
|
||||
[self.textCenterButton setTitle:KBLocalized(@"Send A Message To Her") forState:UIControlStateNormal];
|
||||
} else if (self.inputState == KBVoiceInputBarStateVoice) {
|
||||
self.voiceCenterLabel.text = @"按住说话";
|
||||
self.voiceCenterLabel.text = KBLocalized(@"Hold To Speak");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -936,7 +936,7 @@ static void KBChatUpdatedDarwinCallback(CFNotificationCenterRef center,
|
||||
- (KBVoiceInputBar *)voiceInputBar {
|
||||
if (!_voiceInputBar) {
|
||||
_voiceInputBar = [[KBVoiceInputBar alloc] init];
|
||||
_voiceInputBar.statusText = @"按住按钮开始对话";
|
||||
_voiceInputBar.statusText = KBLocalized(@"Hold To Start Talking");
|
||||
}
|
||||
return _voiceInputBar;
|
||||
}
|
||||
@@ -1372,7 +1372,7 @@ static void KBChatUpdatedDarwinCallback(CFNotificationCenterRef center,
|
||||
if (cell) {
|
||||
[cell removeLoadingAssistantMessageWithRequestId:requestId];
|
||||
}
|
||||
NSString *message = response.message ?: @"聊天响应为空";
|
||||
NSString *message = response.message ?: KBLocalized(@"Chat response is empty");
|
||||
NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message);
|
||||
if (message.length > 0) {
|
||||
[KBHUD showError:message];
|
||||
|
||||
@@ -144,7 +144,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
// 创建删除按钮
|
||||
if (!self.deleteButton) {
|
||||
self.deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[self.deleteButton setTitle:@"删除此记录" forState:UIControlStateNormal];
|
||||
[self.deleteButton setTitle:KBLocalized(@"Delete This Record") forState:UIControlStateNormal];
|
||||
[self.deleteButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
self.deleteButton.titleLabel.font = [UIFont systemFontOfSize:14];
|
||||
self.deleteButton.backgroundColor = [UIColor colorWithRed:244/255.0 green:67/255.0 blue:54/255.0 alpha:1.0]; // #F44336
|
||||
@@ -317,7 +317,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
|
||||
if (error) {
|
||||
NSLog(@"[KBAIMessageChatingVC] 删除失败:%@", error.localizedDescription);
|
||||
[KBHUD showError:@"删除失败,请重试"];
|
||||
[KBHUD showError:KBLocalized(@"Delete failed, please try again")];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
NSLog(@"[KBAIMessageChatingVC] ✅ 已发送重置通知:companionId=%ld", (long)companionId);
|
||||
|
||||
// 5. 显示成功提示
|
||||
[KBHUD showSuccess:@"已删除"];
|
||||
[KBHUD showSuccess:KBLocalized(@"Deleted")];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ autoShowBusinessError:NO
|
||||
// 解析响应
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
@@ -356,7 +356,7 @@ autoShowBusinessError:NO
|
||||
} else {
|
||||
NSError *parseError = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
|
||||
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
|
||||
if (completion) {
|
||||
completion(nil, parseError);
|
||||
}
|
||||
@@ -398,7 +398,7 @@ autoShowBusinessError:NO
|
||||
// 解析响应
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
@@ -418,7 +418,7 @@ autoShowBusinessError:NO
|
||||
} else {
|
||||
NSError *parseError = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
|
||||
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
|
||||
if (completion) {
|
||||
completion(nil, parseError);
|
||||
}
|
||||
@@ -463,7 +463,7 @@ autoShowBusinessError:NO
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
NSError *parseError = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"数据格式错误"}];
|
||||
userInfo:@{NSLocalizedDescriptionKey : KBLocalized(@"Invalid data format")}];
|
||||
if (completion) {
|
||||
completion(NO, parseError);
|
||||
}
|
||||
@@ -472,7 +472,7 @@ autoShowBusinessError:NO
|
||||
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey : message}];
|
||||
@@ -498,7 +498,7 @@ autoShowBusinessError:NO
|
||||
if (content.length == 0) {
|
||||
NSError *error = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"评论内容不能为空"}];
|
||||
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Comment content cannot be empty")}];
|
||||
if (completion) {
|
||||
completion(nil, -1, error);
|
||||
}
|
||||
@@ -535,7 +535,7 @@ autoShowBusinessError:NO
|
||||
NSLog(@"[AiVM] /ai-companion/comment/add response: %@", json);
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
@@ -596,7 +596,7 @@ autoShowBusinessError:NO
|
||||
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
@@ -615,7 +615,7 @@ autoShowBusinessError:NO
|
||||
} else {
|
||||
NSError *parseError = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
|
||||
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
|
||||
if (completion) {
|
||||
completion(nil, parseError);
|
||||
}
|
||||
@@ -711,7 +711,7 @@ autoShowBusinessError:NO
|
||||
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
@@ -755,7 +755,7 @@ autoShowBusinessError:NO
|
||||
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
@@ -838,7 +838,7 @@ autoShowBusinessError:NO
|
||||
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
@@ -857,7 +857,7 @@ autoShowBusinessError:NO
|
||||
} else {
|
||||
NSError *parseError = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
|
||||
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
|
||||
if (completion) {
|
||||
completion(nil, parseError);
|
||||
}
|
||||
@@ -936,7 +936,7 @@ autoShowBusinessError:NO
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
NSError *parseError = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"数据格式错误"}];
|
||||
userInfo:@{NSLocalizedDescriptionKey : KBLocalized(@"Invalid data format")}];
|
||||
if (completion) {
|
||||
completion(NO, parseError);
|
||||
}
|
||||
@@ -945,7 +945,7 @@ autoShowBusinessError:NO
|
||||
|
||||
NSInteger code = [json[@"code"] integerValue];
|
||||
if (code != 0) {
|
||||
NSString *message = json[@"message"] ?: @"请求失败";
|
||||
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
|
||||
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey : message}];
|
||||
|
||||
@@ -513,7 +513,11 @@ typedef void(^KBInputProfileSelectHandler)(NSString *languageCode, NSString *lay
|
||||
@"profileId": layout.profileId
|
||||
}];
|
||||
}
|
||||
|
||||
// STTODO 繁体拼音下个版本更新
|
||||
if ([profile.code isEqualToString:@"zh-Hant-Pinyin"]) {
|
||||
// NSLog(@"===");
|
||||
continue;
|
||||
}
|
||||
[configs addObject:@{
|
||||
@"code": profile.code,
|
||||
@"name": profile.name,
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
@{ @"title": KBLocalized(@"Agreement"), @"icon": @"my_agreement_icon", @"color": @(0x4CD964),@"id":@"5" },
|
||||
@{ @"title": KBLocalized(@"Privacy Policy"), @"icon": @"my_privacy_icon", @"color": @(0x5AC8FA),@"id":@"6" },
|
||||
#if DEBUG
|
||||
@{ @"title": KBLocalized(@"Test"), @"icon": @"", @"color": @(0x5AC8FA),@"id":@"7" },
|
||||
// @{ @"title": KBLocalized(@"Test"), @"icon": @"", @"color": @(0x5AC8FA),@"id":@"7" },
|
||||
#endif
|
||||
|
||||
]
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
if (remoteURL.length > 0) {
|
||||
vc.url = remoteURL;
|
||||
} else {
|
||||
vc.htmlString = [self kb_htmlForLegalDocumentType:type];
|
||||
// vc.htmlString = [self kb_htmlForLegalDocumentType:type];
|
||||
}
|
||||
return vc;
|
||||
}
|
||||
@@ -265,25 +265,6 @@ didFailProvisionalNavigation:(WKNavigation *)navigation
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString *)kb_htmlForLegalDocumentType:(KBLegalDocumentType)type {
|
||||
NSString *title = [self kb_titleForLegalDocumentType:type];
|
||||
NSString *body = @"";
|
||||
switch (type) {
|
||||
case KBLegalDocumentTypePrivacyPolicy:
|
||||
body = @"<section><h2>Overview</h2><p>This in-app privacy disclosure explains how the app and the custom keyboard handle data when you use account, AI, subscription, sync, and voice features.</p></section><section><h2>Full Access</h2><p>Network-based features inside the keyboard require Full Access. If you do not enable Full Access, those features stay unavailable.</p></section><section><h2>Data Used For Features You Trigger</h2><p>When you actively use AI reply, cloud sync, account, purchase verification, or voice input, the content required for that feature may be transmitted to the service provider to complete your request.</p><p>This may include typed text you choose to send, voice audio you record, account identifiers, email address, subscription status, and limited diagnostics needed for app functionality and fraud prevention.</p></section><section><h2>Keyboard Boundaries</h2><p>The custom keyboard does not operate in secure text fields and cannot access content in contexts where iOS blocks third-party keyboards.</p></section><section><h2>Retention And Deletion</h2><p>Account-related data is retained only as needed for app functionality, purchases, support, and legal compliance. Use the in-app account deletion flow to request account removal and associated cleanup.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published privacy policy URL before App Store submission so that the wording exactly matches App Store Connect privacy labels and your backend behavior.</p></section>";
|
||||
break;
|
||||
case KBLegalDocumentTypeMembershipAgreement:
|
||||
body = @"<section><h2>Subscription Terms</h2><p>Paid membership unlocks subscription benefits for eligible premium features. Pricing, billing period, and any trial details are shown on the purchase sheet before you confirm payment.</p></section><section><h2>Auto-Renewal</h2><p>Subscriptions renew automatically unless cancelled at least 24 hours before the end of the current billing period. Renewal charges are handled by Apple through your App Store account.</p></section><section><h2>Managing Your Subscription</h2><p>You can restore purchases inside the app and manage or cancel subscriptions in Apple ID subscription settings after purchase.</p></section><section><h2>Feature Availability</h2><p>Some premium actions started from the custom keyboard may open the main app to complete login, purchase, or subscription management.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published membership agreement URL before App Store submission.</p></section>";
|
||||
break;
|
||||
case KBLegalDocumentTypeTermsOfService:
|
||||
default:
|
||||
body = @"<section><h2>Service Scope</h2><p>This app provides a custom keyboard, account features, premium subscriptions, and optional AI-assisted and voice features. Some capabilities require network access and may open the main app to complete the flow.</p></section><section><h2>Acceptable Use</h2><p>You must not use the service to violate law, harass others, infringe rights, or generate abusive, sexual, hateful, or otherwise prohibited content.</p></section><section><h2>AI And Voice Features</h2><p>AI-generated or transcribed content may be inaccurate, incomplete, or inappropriate. You remain responsible for reviewing content before sending or relying on it.</p></section><section><h2>Accounts And Purchases</h2><p>You are responsible for activity performed through your account. Paid features are subject to Apple billing rules and any product limitations shown in the app.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published terms URL before App Store submission.</p></section>";
|
||||
break;
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1,maximum-scale=1'><style>body{font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;margin:0;padding:24px 18px 48px;color:#1f2937;background:#ffffff;line-height:1.6;}h1{font-size:28px;line-height:1.2;margin:0 0 20px;color:#111827;}h2{font-size:18px;line-height:1.3;margin:24px 0 10px;color:#111827;}p{font-size:15px;margin:0 0 10px;color:#4b5563;}section{padding-bottom:4px;border-bottom:1px solid #eef2f7;}section:last-child{border-bottom:none;} .note{margin-top:18px;font-size:13px;color:#6b7280;}</style></head><body><h1>%@</h1>%@<p class='note'>Built-in fallback document. Configure the final public URL in KBConfig.h when release content is ready.</p></body></html>", title, body];
|
||||
}
|
||||
|
||||
+ (NSString *)kb_fallbackErrorHTML {
|
||||
return @"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><style>body{font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;padding:32px;color:#1f2937;}h1{font-size:22px;margin:0 0 12px;}p{font-size:15px;line-height:1.6;color:#4b5563;}</style></head><body><h1>Page unavailable</h1><p>The requested document could not be loaded.</p></body></html>";
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
@@ -44,6 +45,42 @@
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeAudioData</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<true/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<true/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
|
||||
Reference in New Issue
Block a user