This commit is contained in:
2026-03-09 17:34:08 +08:00
parent c1ace5f53e
commit 0af7428353
55 changed files with 1630 additions and 665 deletions

View File

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

View File

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

View File

@@ -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 = @"MMdd HH:mm";
formatter.dateFormat = @"MM-dd HH:mm";
}
return [formatter stringFromDate:timestamp];

View File

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

View File

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

View File

@@ -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 commentIdKBAICommentModel/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];
//
// #0000000.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];

View File

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

View File

@@ -39,8 +39,8 @@
- (void)setup {
_state = KBAiRecordButtonStateNormal;
_normalTitle = @"按住说话";
_recordingTitle = @"松开结束";
_normalTitle = KBLocalized(@"Hold To Speak");
_recordingTitle = KBLocalized(@"Release To Finish");
_tintColor = [UIColor systemBlueColor];
//

View File

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

View File

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

View File

@@ -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")];
});
}];
}

View File

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

View File

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

View File

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

View File

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

View File

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