添加消息长按弹窗
This commit is contained in:
33
keyBoard/Class/AiTalk/V/Chat/KBChatMessageActionPopView.h
Normal file
33
keyBoard/Class/AiTalk/V/Chat/KBChatMessageActionPopView.h
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// KBChatMessageActionPopView.h
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Codex on 2026/2/3.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBChatMessageActionType) {
|
||||
KBChatMessageActionTypeCopy = 0,
|
||||
KBChatMessageActionTypeDelete = 1,
|
||||
KBChatMessageActionTypeReport = 2,
|
||||
};
|
||||
|
||||
@class KBChatMessageActionPopView;
|
||||
|
||||
@protocol KBChatMessageActionPopViewDelegate <NSObject>
|
||||
@optional
|
||||
- (void)chatMessageActionPopView:(KBChatMessageActionPopView *)view
|
||||
didSelectAction:(KBChatMessageActionType)action;
|
||||
@end
|
||||
|
||||
/// 聊天消息长按操作弹窗(Copy / Delete / Report)
|
||||
@interface KBChatMessageActionPopView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<KBChatMessageActionPopViewDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
164
keyBoard/Class/AiTalk/V/Chat/KBChatMessageActionPopView.m
Normal file
164
keyBoard/Class/AiTalk/V/Chat/KBChatMessageActionPopView.m
Normal file
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// KBChatMessageActionPopView.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Codex on 2026/2/3.
|
||||
//
|
||||
|
||||
#import "KBChatMessageActionPopView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
static CGFloat const kKBChatActionRowHeight = 52.0;
|
||||
|
||||
@interface KBChatMessageActionPopView ()
|
||||
|
||||
@property (nonatomic, strong) UIControl *copyRow;
|
||||
@property (nonatomic, strong) UIControl *deleteRow;
|
||||
@property (nonatomic, strong) UIControl *reportRow;
|
||||
@property (nonatomic, strong) UIView *line1;
|
||||
@property (nonatomic, strong) UIView *line2;
|
||||
|
||||
@end
|
||||
|
||||
@implementation KBChatMessageActionPopView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
- (void)setupUI {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.1 alpha:0.92];
|
||||
self.layer.cornerRadius = 16.0;
|
||||
self.layer.masksToBounds = YES;
|
||||
|
||||
[self addSubview:self.copyRow];
|
||||
[self addSubview:self.line1];
|
||||
[self addSubview:self.deleteRow];
|
||||
[self addSubview:self.line2];
|
||||
[self addSubview:self.reportRow];
|
||||
|
||||
[self.copyRow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.left.right.equalTo(self);
|
||||
make.height.mas_equalTo(kKBChatActionRowHeight);
|
||||
}];
|
||||
|
||||
[self.line1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.copyRow.mas_bottom);
|
||||
make.left.right.equalTo(self);
|
||||
make.height.mas_equalTo(0.5);
|
||||
}];
|
||||
|
||||
[self.deleteRow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.line1.mas_bottom);
|
||||
make.left.right.equalTo(self);
|
||||
make.height.mas_equalTo(kKBChatActionRowHeight);
|
||||
}];
|
||||
|
||||
[self.line2 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.deleteRow.mas_bottom);
|
||||
make.left.right.equalTo(self);
|
||||
make.height.mas_equalTo(0.5);
|
||||
}];
|
||||
|
||||
[self.reportRow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.line2.mas_bottom);
|
||||
make.left.right.bottom.equalTo(self);
|
||||
make.height.mas_equalTo(kKBChatActionRowHeight);
|
||||
}];
|
||||
}
|
||||
|
||||
- (UIControl *)buildRowWithTitle:(NSString *)title
|
||||
iconName:(NSString *)iconName
|
||||
action:(KBChatMessageActionType)action {
|
||||
UIControl *row = [[UIControl alloc] init];
|
||||
row.tag = action;
|
||||
[row addTarget:self action:@selector(actionRowTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
UILabel *label = [[UILabel alloc] init];
|
||||
label.text = title;
|
||||
label.textColor = [UIColor whiteColor];
|
||||
label.font = [UIFont systemFontOfSize:18 weight:UIFontWeightRegular];
|
||||
|
||||
UIImageView *iconView = [[UIImageView alloc] init];
|
||||
UIImage *icon = [UIImage systemImageNamed:iconName];
|
||||
iconView.image = icon;
|
||||
iconView.tintColor = [UIColor whiteColor];
|
||||
iconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
|
||||
[row addSubview:label];
|
||||
[row addSubview:iconView];
|
||||
|
||||
[label mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(row).offset(16);
|
||||
make.centerY.equalTo(row);
|
||||
}];
|
||||
|
||||
[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(row).offset(-16);
|
||||
make.centerY.equalTo(row);
|
||||
make.width.height.mas_equalTo(18);
|
||||
}];
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)actionRowTapped:(UIControl *)sender {
|
||||
if ([self.delegate respondsToSelector:@selector(chatMessageActionPopView:didSelectAction:)]) {
|
||||
[self.delegate chatMessageActionPopView:self didSelectAction:(KBChatMessageActionType)sender.tag];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIControl *)copyRow {
|
||||
if (!_copyRow) {
|
||||
_copyRow = [self buildRowWithTitle:KBLocalized(@"Copy")
|
||||
iconName:@"doc.on.doc"
|
||||
action:KBChatMessageActionTypeCopy];
|
||||
}
|
||||
return _copyRow;
|
||||
}
|
||||
|
||||
- (UIControl *)deleteRow {
|
||||
if (!_deleteRow) {
|
||||
_deleteRow = [self buildRowWithTitle:KBLocalized(@"Delete")
|
||||
iconName:@"trash"
|
||||
action:KBChatMessageActionTypeDelete];
|
||||
}
|
||||
return _deleteRow;
|
||||
}
|
||||
|
||||
- (UIControl *)reportRow {
|
||||
if (!_reportRow) {
|
||||
_reportRow = [self buildRowWithTitle:KBLocalized(@"Report")
|
||||
iconName:@"exclamationmark.circle"
|
||||
action:KBChatMessageActionTypeReport];
|
||||
}
|
||||
return _reportRow;
|
||||
}
|
||||
|
||||
- (UIView *)line1 {
|
||||
if (!_line1) {
|
||||
_line1 = [[UIView alloc] init];
|
||||
_line1.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
|
||||
}
|
||||
return _line1;
|
||||
}
|
||||
|
||||
- (UIView *)line2 {
|
||||
if (!_line2) {
|
||||
_line2 = [[UIView alloc] init];
|
||||
_line2.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
|
||||
}
|
||||
return _line2;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -17,6 +17,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)chatTableViewDidScroll:(KBChatTableView *)chatView
|
||||
scrollView:(UIScrollView *)scrollView;
|
||||
- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView;
|
||||
/// 长按消息(用户/AI)
|
||||
- (void)chatTableView:(KBChatTableView *)chatView
|
||||
didLongPressMessage:(KBAiChatMessage *)message
|
||||
sourceRect:(CGRect)sourceRect;
|
||||
@end
|
||||
|
||||
/// 聊天列表视图(支持用户消息、AI 消息、时间戳、语音播放)
|
||||
|
||||
@@ -42,6 +42,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
@property (nonatomic, strong) UILabel *topStatusLabel;
|
||||
@property (nonatomic, assign) BOOL isTopLoading;
|
||||
@property (nonatomic, assign) BOOL isTopNoMore;
|
||||
@property (nonatomic, strong) UILongPressGestureRecognizer *messageLongPressGesture;
|
||||
|
||||
@end
|
||||
|
||||
@@ -101,6 +102,13 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
// 长按消息操作
|
||||
self.messageLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleMessageLongPress:)];
|
||||
self.messageLongPressGesture.minimumPressDuration = 0.4;
|
||||
self.messageLongPressGesture.cancelsTouchesInView = YES;
|
||||
[self.tableView addGestureRecognizer:self.messageLongPressGesture];
|
||||
|
||||
// 初始化 contentInset
|
||||
self.contentBottomInset = 0;
|
||||
[self updateContentBottomInset:self.contentBottomInset];
|
||||
@@ -136,6 +144,32 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
*/
|
||||
}
|
||||
|
||||
#pragma mark - Long Press
|
||||
|
||||
- (void)handleMessageLongPress:(UILongPressGestureRecognizer *)gesture {
|
||||
if (gesture.state != UIGestureRecognizerStateBegan) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGPoint point = [gesture locationInView:self.tableView];
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:point];
|
||||
if (!indexPath || indexPath.row >= self.messages.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
KBAiChatMessage *message = self.messages[indexPath.row];
|
||||
if (!message || message.isLoading || message.type == KBAiChatMessageTypeTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
|
||||
CGRect cellRect = cell ? [cell convertRect:cell.bounds toView:nil] : CGRectZero;
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(chatTableView:didLongPressMessage:sourceRect:)]) {
|
||||
[self.delegate chatTableView:self didLongPressMessage:message sourceRect:cellRect];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)setInverted:(BOOL)inverted {
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
#import "KBImagePositionButton.h"
|
||||
#import "KBAICommentView.h"
|
||||
#import "KBAIChatMessageCacheManager.h"
|
||||
#import "KBChatMessageActionPopView.h"
|
||||
#import "AIReportVC.h"
|
||||
#import "KBHUD.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <SDWebImage/SDWebImage.h>
|
||||
#import <LSTPopView/LSTPopView.h>
|
||||
@@ -19,7 +22,7 @@
|
||||
/// 聊天会话被重置的通知
|
||||
static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidResetNotification";
|
||||
|
||||
@interface KBPersonaChatCell () <KBChatTableViewDelegate>
|
||||
@interface KBPersonaChatCell () <KBChatTableViewDelegate, KBChatMessageActionPopViewDelegate>
|
||||
|
||||
/// 背景图
|
||||
@property (nonatomic, strong) UIImageView *backgroundImageView;
|
||||
@@ -65,6 +68,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
@property (nonatomic, assign) BOOL shouldAutoPlayPrologueAudio;
|
||||
@property (nonatomic, assign) BOOL hasPlayedPrologueAudio;
|
||||
@property (nonatomic, assign) BOOL shouldShowOpeningMessage;
|
||||
@property (nonatomic, weak) LSTPopView *messageActionPopView;
|
||||
@property (nonatomic, strong) KBAiChatMessage *selectedActionMessage;
|
||||
@property (nonatomic, strong) UIControl *messageActionMaskView;
|
||||
|
||||
@end
|
||||
|
||||
@@ -898,6 +904,126 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
[self loadMoreHistory];
|
||||
}
|
||||
|
||||
- (void)chatTableView:(KBChatTableView *)chatView
|
||||
didLongPressMessage:(KBAiChatMessage *)message
|
||||
sourceRect:(CGRect)sourceRect {
|
||||
[self showMessageActionPopForMessage:message sourceRect:sourceRect];
|
||||
}
|
||||
|
||||
#pragma mark - KBChatMessageActionPopViewDelegate
|
||||
|
||||
- (void)chatMessageActionPopView:(KBChatMessageActionPopView *)view
|
||||
didSelectAction:(KBChatMessageActionType)action {
|
||||
[self dismissMessageActionPop];
|
||||
|
||||
KBAiChatMessage *message = self.selectedActionMessage;
|
||||
self.selectedActionMessage = nil;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case KBChatMessageActionTypeCopy: {
|
||||
if (message.text.length > 0) {
|
||||
[UIPasteboard generalPasteboard].string = message.text;
|
||||
[KBHUD showSuccess:KBLocalized(@"复制成功")];
|
||||
}
|
||||
} break;
|
||||
case KBChatMessageActionTypeDelete: {
|
||||
NSInteger idx = [self.messages indexOfObjectIdenticalTo:message];
|
||||
if (idx != NSNotFound) {
|
||||
[self.messages removeObjectAtIndex:idx];
|
||||
[self.chatView reloadWithMessages:self.messages
|
||||
keepOffset:YES
|
||||
scrollToBottom:NO];
|
||||
if (self.persona.personaId > 0) {
|
||||
if (self.messages.count > 0) {
|
||||
[[KBAIChatMessageCacheManager shared] saveMessages:self.messages
|
||||
forCompanionId:self.persona.personaId];
|
||||
} else {
|
||||
[[KBAIChatMessageCacheManager shared] clearMessagesForCompanionId:self.persona.personaId];
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case KBChatMessageActionTypeReport: {
|
||||
if (self.persona.personaId <= 0) {
|
||||
return;
|
||||
}
|
||||
AIReportVC *vc = [[AIReportVC alloc] init];
|
||||
vc.personaId = self.persona.personaId;
|
||||
[KB_CURRENT_NAV pushViewController:vc animated:YES];
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Message Action Pop
|
||||
|
||||
- (void)showMessageActionPopForMessage:(KBAiChatMessage *)message
|
||||
sourceRect:(CGRect)sourceRect {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
[self dismissMessageActionPop];
|
||||
|
||||
self.selectedActionMessage = message;
|
||||
CGFloat width = 240;
|
||||
CGFloat height = 156;
|
||||
KBChatMessageActionPopView *content = [[KBChatMessageActionPopView alloc]
|
||||
initWithFrame:CGRectMake(0, 0, width, height)];
|
||||
content.delegate = self;
|
||||
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
if (!window) {
|
||||
window = [UIApplication sharedApplication].windows.firstObject;
|
||||
}
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIControl *mask = [[UIControl alloc] initWithFrame:window.bounds];
|
||||
[mask addTarget:self action:@selector(dismissMessageActionPop) forControlEvents:UIControlEventTouchUpInside];
|
||||
[window addSubview:mask];
|
||||
self.messageActionMaskView = mask;
|
||||
|
||||
BOOL isUserMessage = (message.type == KBAiChatMessageTypeUser);
|
||||
CGFloat margin = 12.0;
|
||||
CGFloat spacing = 8.0;
|
||||
CGFloat topSafe = 0.0;
|
||||
CGFloat bottomSafe = 0.0;
|
||||
if (@available(iOS 11.0, *)) {
|
||||
topSafe = window.safeAreaInsets.top;
|
||||
bottomSafe = window.safeAreaInsets.bottom;
|
||||
}
|
||||
|
||||
CGFloat x = isUserMessage ? CGRectGetMaxX(sourceRect) - width : CGRectGetMinX(sourceRect);
|
||||
x = MAX(margin, MIN(x, CGRectGetWidth(window.bounds) - width - margin));
|
||||
|
||||
CGFloat y = CGRectGetMinY(sourceRect) - height - spacing;
|
||||
if (y < topSafe + margin) {
|
||||
y = CGRectGetMaxY(sourceRect) + spacing;
|
||||
}
|
||||
if (y + height > CGRectGetHeight(window.bounds) - bottomSafe - margin) {
|
||||
y = MAX(topSafe + margin, CGRectGetHeight(window.bounds) - bottomSafe - margin - height);
|
||||
}
|
||||
|
||||
content.frame = CGRectMake(x, y, width, height);
|
||||
[mask addSubview:content];
|
||||
}
|
||||
|
||||
- (void)dismissMessageActionPop {
|
||||
if (self.messageActionPopView) {
|
||||
[self.messageActionPopView dismiss];
|
||||
self.messageActionPopView = nil;
|
||||
}
|
||||
if (self.messageActionMaskView) {
|
||||
[self.messageActionMaskView removeFromSuperview];
|
||||
self.messageActionMaskView = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Load
|
||||
|
||||
- (UIImageView *)backgroundImageView {
|
||||
|
||||
Reference in New Issue
Block a user