This commit is contained in:
启星
2025-08-08 10:49:36 +08:00
parent 6400cf78bb
commit b5ce3d580a
8780 changed files with 978183 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
//
// TUIVoiceToTextConfig.h
// TTUIVoiceToText
//
// Created by xia on 2023/8/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <TIMCommon/TIMCommonModel.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIVoiceToTextConfig : NSObject
+ (TUIVoiceToTextConfig *)defaultConfig;
/**
* 识别目标语言码
* Recognize target language code.
*/
@property(nonatomic, copy) NSString *targetLanguageCode;
/**
* 识别目标语言名称。
* Recognize target language name.
*/
@property(nonatomic, copy, readonly) NSString *targetLanguageName;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,92 @@
//
// TUIVoiceToText.m
// TUIVoiceToText
//
// Created by xia on 2023/8/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVoiceToTextConfig.h"
static NSString *const kVoiceToTextTargetLanguageCode = @"voice_to_text_target_language_code";
@interface TUIVoiceToTextConfig ()
@property(nonatomic, copy, readwrite) NSString *targetLanguageName;
@end
@implementation TUIVoiceToTextConfig
+ (TUIVoiceToTextConfig *)defaultConfig {
static dispatch_once_t onceToken;
static TUIVoiceToTextConfig *config;
dispatch_once(&onceToken, ^{
config = [[TUIVoiceToTextConfig alloc] init];
});
return config;
}
- (id)init {
self = [super init];
if (self) {
[self loadSavedLanguage];
}
return self;
}
- (void)setTargetLanguageCode:(NSString *)targetLanguageCode {
if (targetLanguageCode.length == 0 || ![targetLanguageCode isKindOfClass:NSString.class]) {
return;
}
if (_targetLanguageCode == targetLanguageCode) {
return;
}
_targetLanguageCode = targetLanguageCode;
_targetLanguageName = self.languageDict[self.targetLanguageCode];
[[NSUserDefaults standardUserDefaults] setObject:targetLanguageCode forKey:kVoiceToTextTargetLanguageCode];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)loadSavedLanguage {
NSString *lang = [[NSUserDefaults standardUserDefaults] stringForKey:kVoiceToTextTargetLanguageCode];
if (lang.length == 0) {
[self setTargetLanguageCode:[self defalutTargetLanguageCode]];
self.targetLanguageName = self.languageDict[self.targetLanguageCode];
} else {
self.targetLanguageCode = lang;
}
}
- (NSString *)defalutTargetLanguageCode {
NSString *target = nil;
NSString *currentAppLanguage = [TUIGlobalization getPreferredLanguage];
if ([currentAppLanguage isEqualToString:@"zh-Hans"] || [currentAppLanguage isEqualToString:@"zh-Hant"]) {
target = @"zh";
} else {
target = @"en";
}
return target;
}
- (NSDictionary *)languageDict {
return @{
@"zh" : @"简体中文",
@"zh-TW" : @"繁體中文",
@"en" : @"English",
@"ja" : @"日本語",
@"ko" : @"한국어",
@"fr" : @"Français",
@"es" : @"Español",
@"it" : @"Italiano",
@"de" : @"Deutsch",
@"tr" : @"Türkçe",
@"ru" : @"Русский",
@"pt" : @"Português",
@"vi" : @"Tiếng Việt",
@"id" : @"Bahasa Indonesia",
@"th" : @"ภาษาไทย",
@"ms" : @"Bahasa Melayu",
@"hi" : @"हिन्दी"
};
}
@end

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeAudioData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeProductPersonalization</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeUserID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeProductPersonalization</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>id</key>
<string>dark</string>
<key>name</key>
<string>黑夜</string>
<key>name_en</key>
<string>Dark</string>
<key>convert_voice_text_view_bg_color</key>
<string>#1F1F1F</string>
<key>convert_voice_text_view_text_color</key>
<string>#D9D9D9</string>
<key>convert_voice_text_view_tips_color</key>
<string>#D9D9D9</string>
<key>convert_voice_text_view_icon_loading_img</key>
<string>convert_voice_text_loading.png</string>
<key>convert_voice_text_view_pop_menu_copy_img</key>
<string>icon_copy.png</string>
<key>convert_voice_text_view_pop_menu_hide_img</key>
<string>icon_hide.png</string>
<key>convert_voice_text_view_pop_menu_forward_img</key>
<string>icon_forward.png</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>id</key>
<string>light</string>
<key>name</key>
<string>轻量</string>
<key>name_en</key>
<string>Light</string>
<key>convert_voice_text_view_bg_color</key>
<string>#F2F7FF</string>
<key>convert_voice_text_view_text_color</key>
<string>#000000</string>
<key>convert_voice_text_view_tips_color</key>
<string>#000000</string>
<key>convert_voice_text_view_icon_loading_img</key>
<string>convert_voice_text_loading.png</string>
<key>convert_voice_text_view_pop_menu_copy_img</key>
<string>icon_copy.png</string>
<key>convert_voice_text_view_pop_menu_hide_img</key>
<string>icon_hide.png</string>
<key>convert_voice_text_view_pop_menu_forward_img</key>
<string>icon_forward.png</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>id</key>
<string>lively</string>
<key>name</key>
<string>活泼</string>
<key>name_en</key>
<string>Lively</string>
<key>convert_voice_text_view_bg_color</key>
<string>#F2F7FF</string>
<key>convert_voice_text_view_text_color</key>
<string>#000000</string>
<key>convert_voice_text_view_tips_color</key>
<string>#000000</string>
<key>convert_voice_text_view_icon_loading_img</key>
<string>convert_voice_text_loading.png</string>
<key>convert_voice_text_view_pop_menu_copy_img</key>
<string>icon_copy.png</string>
<key>convert_voice_text_view_pop_menu_hide_img</key>
<string>icon_hide.png</string>
<key>convert_voice_text_view_pop_menu_forward_img</key>
<string>icon_forward.png</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>id</key>
<string>serious</string>
<key>name</key>
<string>严肃</string>
<key>name_en</key>
<string>Business</string>
<key>convert_voice_text_view_bg_color</key>
<string>#F2F7FF</string>
<key>convert_voice_text_view_text_color</key>
<string>#000000</string>
<key>convert_voice_text_view_tips_color</key>
<string>#000000</string>
<key>convert_voice_text_view_icon_loading_img</key>
<string>convert_voice_text_loading.png</string>
<key>convert_voice_text_view_pop_menu_copy_img</key>
<string>icon_copy.png</string>
<key>convert_voice_text_view_pop_menu_hide_img</key>
<string>icon_hide.png</string>
<key>convert_voice_text_view_pop_menu_forward_img</key>
<string>icon_forward.png</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

View File

@@ -0,0 +1,27 @@
Pod::Spec.new do |spec|
spec.name = 'TUIVoiceToTextPlugin'
spec.version = '8.5.6864'
spec.platform = :ios
spec.ios.deployment_target = '9.0'
spec.license = { :type => 'Proprietary',
:text => <<-LICENSE
copyright 2017 tencent Ltd. All rights reserved.
LICENSE
}
spec.homepage = 'https://cloud.tencent.com/document/product/269/3794'
spec.documentation_url = 'https://cloud.tencent.com/document/product/269/9147'
spec.authors = 'tencent video cloud'
spec.summary = 'TUIVoiceToTextPlugin'
spec.dependency 'TUICore'
spec.dependency 'TIMCommon'
spec.dependency 'TUIChat'
spec.requires_arc = true
spec.source = { :git => 'https://git.woa.com/lynxzhang/tui-components.git', :tag => spec.version}
spec.source_files = '**/*.{h,m,mm,c}'
spec.resource = ['Resources/*.bundle']
spec.resource_bundle = {
"#{spec.module_name}_Privacy" => '**/TUIVoiceToTextPlugin/Resources/PrivacyInfo.xcprivacy'
}
end

View File

@@ -0,0 +1,37 @@
//
// TUIVoiceToTextDataProvider.h
// TUIVoiceToText
//
// Created by xia on 2023/8/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <TIMCommon/TUIMessageCellData.h>
#import <TUICore/TUIDefine.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, TUIVoiceToTextViewStatus) {
TUIVoiceToTextViewStatusUnknown = 0,
TUIVoiceToTextViewStatusHidden = 1,
TUIVoiceToTextViewStatusLoading = 2,
TUIVoiceToTextViewStatusShown = 3,
TUIVoiceToTextViewStatusSecurityStrike = 4,
};
typedef void (^TUIVoiceToTextCompletion)(NSInteger code, NSString *desc, TUIMessageCellData *data, NSInteger status, NSString *text);
@interface TUIVoiceToTextDataProvider : NSObject
+ (void)convertMessage:(TUIMessageCellData *)data completion:(TUIVoiceToTextCompletion _Nullable)completion;
+ (void)saveConvertedResult:(V2TIMMessage *)message text:(NSString *)text status:(TUIVoiceToTextViewStatus)status;
+ (BOOL)shouldShowConvertedText:(V2TIMMessage *)message;
+ (NSString *)getConvertedText:(V2TIMMessage *)message;
+ (TUIVoiceToTextViewStatus)getConvertedTextStatus:(V2TIMMessage *)message;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,146 @@
//
// TUIVoiceToTextDataProvider.m
// TUIVoiceToText
//
// Created by xia on 2023/8/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVoiceToTextDataProvider.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUILogin.h>
#import "TUIVoiceToTextConfig.h"
#pragma GCC diagnostic ignored "-Wundeclared-selector"
static NSString *const kKeyVoiceToText = @"voice_to_text";
static NSString *const kKeyVoiceToTextViewStatus = @"voice_to_text_view_status";
@interface TUIVoiceToTextDataProvider () <TUINotificationProtocol, V2TIMAdvancedMsgListener>
@end
@implementation TUIVoiceToTextDataProvider
#pragma mark - Public
+ (void)convertMessage:(TUIMessageCellData *)data completion:(TUIVoiceToTextCompletion _Nullable)completion {
V2TIMMessage *msg = data.innerMessage;
if (msg.elemType != V2TIM_ELEM_TYPE_SOUND) {
if (completion) {
completion(-1, @"element is not sound type", data, TUIVoiceToTextViewStatusHidden, @"");
}
return;
}
if (msg.status != V2TIM_MSG_STATUS_SEND_SUCC) {
if (completion) {
completion(-2, @"sound message is not sent successfully", data, TUIVoiceToTextViewStatusHidden, @"");
}
return;
}
V2TIMSoundElem *soundElem = msg.soundElem;
if (soundElem == nil) {
if (completion) {
completion(-3, @"soundElem is nil", data, TUIVoiceToTextViewStatusHidden, @"");
}
return;
}
// Loading converted text from localCustomData firstly.
NSString *convertedText = [self getConvertedText:msg];
if (convertedText.length > 0) {
[self saveConvertedResult:msg text:convertedText status:TUIVoiceToTextViewStatusShown];
if (completion) {
completion(0, @"", data, TUIVoiceToTextViewStatusShown, convertedText);
}
return;
}
// Try to request from server secondly.
[self saveConvertedResult:msg text:@"" status:TUIVoiceToTextViewStatusLoading];
if (completion) {
completion(0, @"", data, TUIVoiceToTextViewStatusLoading, @"");
}
[soundElem convertVoiceToText:@""
completion:^(int code, NSString *desc, NSString *result) {
TUIVoiceToTextViewStatus status;
if (code == 0 && result.length > 0) {
status = TUIVoiceToTextViewStatusShown;
} else {
status = TUIVoiceToTextViewStatusHidden;
}
[self saveConvertedResult:msg text:result status:status];
if (completion) {
completion(code, desc, data, status, result);
}
}];
}
+ (void)saveConvertedResult:(V2TIMMessage *)message text:(NSString *)text status:(TUIVoiceToTextViewStatus)status {
if (text.length > 0) {
[self saveToLocalCustomDataOfMessage:message key:kKeyVoiceToText value:text];
}
[self saveToLocalCustomDataOfMessage:message key:kKeyVoiceToTextViewStatus value:@(status)];
}
+ (void)saveToLocalCustomDataOfMessage:(V2TIMMessage *)message key:(NSString *)key value:(id)value {
if (key.length == 0 || value == nil) {
return;
}
NSData *customData = message.localCustomData;
NSMutableDictionary *dict = [[TUITool jsonData2Dictionary:customData] mutableCopy];
if (dict == nil) {
dict = [[NSMutableDictionary alloc] init];
}
dict[key] = value;
[message setLocalCustomData:[TUITool dictionary2JsonData:dict]];
}
+ (BOOL)shouldShowConvertedText:(V2TIMMessage *)message {
if (message.localCustomData.length == 0) {
return NO;
}
NSDictionary *dict = [TUITool jsonData2Dictionary:message.localCustomData];
TUIVoiceToTextViewStatus status;
if ([dict.allKeys containsObject:kKeyVoiceToTextViewStatus]) {
status = [dict[kKeyVoiceToTextViewStatus] integerValue];
} else {
status = TUIVoiceToTextViewStatusHidden;
}
NSArray *hiddenStatus = @[ @(TUIVoiceToTextViewStatusUnknown), @(TUIVoiceToTextViewStatusHidden) ];
return ![hiddenStatus containsObject:@(status)] || status == TUIVoiceToTextViewStatusLoading;
}
+ (NSString *)getConvertedText:(V2TIMMessage *)message {
BOOL hasRiskContent = message.hasRiskContent;
if (hasRiskContent){
return TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrikeTranslate);
}
if (message.localCustomData.length == 0) {
return nil;
}
NSDictionary *dict = [TUITool jsonData2Dictionary:message.localCustomData];
if ([dict.allKeys containsObject:kKeyVoiceToText]) {
return dict[kKeyVoiceToText];
}
return nil;
}
+ (TUIVoiceToTextViewStatus)getConvertedTextStatus:(V2TIMMessage *)message {
BOOL hasRiskContent = message.hasRiskContent;
if (hasRiskContent){
return TUIVoiceToTextViewStatusSecurityStrike;
}
if (message.localCustomData.length == 0) {
return TUIVoiceToTextViewStatusUnknown;
}
NSDictionary *dict = [TUITool jsonData2Dictionary:message.localCustomData];
if ([dict.allKeys containsObject:kKeyVoiceToTextViewStatus]) {
return [dict[kKeyVoiceToTextViewStatus] integerValue];
}
return TUIVoiceToTextViewStatusUnknown;
}
@end

View File

@@ -0,0 +1,17 @@
//
// TUIVoiceToTextExtensionObserver.h
// TUIVoiceToText
//
// Created by xia on 2023/8/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIVoiceToTextExtensionObserver : NSObject
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,205 @@
//
// TUIVoiceToTextExtensionObserver.m
// TUIVoiceToText
//
// Created by xia on 2023/8/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVoiceToTextExtensionObserver.h"
#import <TIMCommon/TIMPopActionProtocol.h>
#import <TIMCommon/TUIMessageCell.h>
#import <TUIChat/TUIVoiceMessageCell.h>
#import <TUIChat/TUIVoiceMessageCell_Minimalist.h>
#import <TUIChat/TUIChatConfig.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDefine.h>
#import "TUIVoiceToTextConfig.h"
#import "TUIVoiceToTextDataProvider.h"
#import "TUIVoiceToTextView.h"
@interface TUIVoiceToTextExtensionObserver () <TUIExtensionProtocol>
@property(nonatomic, weak) UINavigationController *navVC;
@property(nonatomic, weak) TUICommonTextCellData *cellData;
@end
@implementation TUIVoiceToTextExtensionObserver
static id gShareInstance = nil;
+ (void)load {
TUIRegisterThemeResourcePath(TUIVoiceToTextThemePath, TUIThemeModuleVoiceToText);
// UI extensions in pop menu when message is long pressed.
[TUICore registerExtension:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID object:TUIVoiceToTextExtensionObserver.shareInstance];
[TUICore registerExtension:TUICore_TUIChatExtension_PopMenuActionItem_MinimalistExtensionID object:TUIVoiceToTextExtensionObserver.shareInstance];
}
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gShareInstance = [[self alloc] init];
});
return gShareInstance;
}
- (instancetype)init {
if (self = [super init]) {
[TUICore registerExtension:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID object:self];
[TUICore registerExtension:TUICore_TUIChatExtension_BottomContainer_MinimalistExtensionID object:self];
}
return self;
}
#pragma mark - TUIExtensionProtocol
- (BOOL)onRaiseExtension:(NSString *)extensionID parentView:(UIView *)parentView param:(nullable NSDictionary *)param {
if ([extensionID isEqualToString:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID] ||
[extensionID isEqualToString:TUICore_TUIChatExtension_BottomContainer_MinimalistExtensionID]) {
NSObject *data = [param objectForKey:TUICore_TUIChatExtension_BottomContainer_CellData];
if (![parentView isKindOfClass:UIView.class] || ![data isKindOfClass:TUIMessageCellData.class]) {
return NO;
}
TUIMessageCellData *cellData = (TUIMessageCellData *)data;
if (cellData.innerMessage.elemType != V2TIM_ELEM_TYPE_SOUND ||
cellData.innerMessage.status != V2TIM_MSG_STATUS_SEND_SUCC) {
return NO;
}
NSMutableDictionary *cacheMap = parentView.tui_extValueObj;
TUIVoiceToTextView *cacheView = nil;
if (!cacheMap){
cacheMap = [NSMutableDictionary dictionaryWithCapacity:3];
}
else if ([cacheMap isKindOfClass:NSDictionary.class]) {
cacheView = [cacheMap objectForKey:@"TUIVoiceToTextView"];
}
else {
//cacheMap is not a dic ;
}
if (cacheView) {
[cacheView removeFromSuperview];
cacheView = nil;
}
TUIVoiceToTextView *view = [[TUIVoiceToTextView alloc] initWithData:cellData];
[parentView addSubview:view];
[cacheMap setObject:view forKey:@"TUIVoiceToTextView"];
parentView.tui_extValueObj = cacheMap;
return YES;
}
return NO;
}
- (NSArray<TUIExtensionInfo *> *)onGetExtension:(NSString *)extensionID param:(NSDictionary *)param {
if (![extensionID isKindOfClass:NSString.class]) {
return nil;
}
if (![TUIChatConfig defaultConfig].enablePopMenuConvertAction) {
return nil;
}
if ([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID]) {
// Extension entrance in pop menu when message is long pressed.
if (![param isKindOfClass:NSDictionary.class]) {
return nil;
}
TUIMessageCell *cell = param[TUICore_TUIChatExtension_PopMenuActionItem_ClickCell];
if (([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID] &&
![cell isKindOfClass:TUIVoiceMessageCell.class])) {
return nil;
}
if (cell.messageData.innerMessage.elemType != V2TIM_ELEM_TYPE_SOUND ||
cell.messageData.innerMessage.status != V2TIM_MSG_STATUS_SEND_SUCC) {
return nil;
}
if ([TUIVoiceToTextDataProvider shouldShowConvertedText:cell.messageData.innerMessage]) {
return nil;
}
if (cell.messageData.innerMessage.hasRiskContent) {
return nil;
}
TUIExtensionInfo *info = [[TUIExtensionInfo alloc] init];
info.weight = 2000;
info.text = TIMCommonLocalizableString(TUIKitConvertToText);
if ([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID]) {
info.icon = TUIChatBundleThemeImage(@"chat_icon_convert_voice_to_text_img", @"icon_convert_voice_to_text");
}
info.onClicked = ^(NSDictionary *_Nonnull action) {
TUIMessageCellData *cellData = cell.messageData;
V2TIMMessage *message = cellData.innerMessage;
if (message.elemType != V2TIM_ELEM_TYPE_SOUND) {
return;
}
[TUIVoiceToTextDataProvider convertMessage:cellData
completion:^(NSInteger code, NSString * _Nonnull desc,
TUIMessageCellData * _Nonnull data, NSInteger status,
NSString * _Nonnull text) {
if (code != 0 || (text.length == 0 && status == TUIVoiceToTextViewStatusHidden)) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitConvertToTextFailed)];
}
NSDictionary *param = @{TUICore_TUIPluginNotify_DidChangePluginViewSubKey_Data : cellData};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_DidChangePluginViewSubKey
object:nil
param:param];
}];
};
return @[ info ];
}
if ([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_MinimalistExtensionID]) {
// Extension entrance in pop menu when message is long pressed.
if (![param isKindOfClass:NSDictionary.class]) {
return nil;
}
TUIMessageCell *cell = param[TUICore_TUIChatExtension_PopMenuActionItem_ClickCell];
if (([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID] &&
![cell isKindOfClass:TUIVoiceMessageCell.class])) {
return nil;
}
if (cell.messageData.innerMessage.elemType != V2TIM_ELEM_TYPE_SOUND ||
cell.messageData.innerMessage.status != V2TIM_MSG_STATUS_SEND_SUCC) {
return nil;
}
if ([TUIVoiceToTextDataProvider shouldShowConvertedText:cell.messageData.innerMessage]) {
return nil;
}
TUIExtensionInfo *info = [[TUIExtensionInfo alloc] init];
info.weight = 2000;
info.text = TIMCommonLocalizableString(TUIKitConvertToText);
info.icon = TUIChatBundleThemeImage(@"chat_icon_convert_voice_to_text_img", @"icon_convert_voice_to_text");
info.onClicked = ^(NSDictionary *_Nonnull action) {
TUIMessageCellData *cellData = cell.messageData;
V2TIMMessage *message = cellData.innerMessage;
if (message.elemType != V2TIM_ELEM_TYPE_SOUND) {
return;
}
[TUIVoiceToTextDataProvider convertMessage:cellData
completion:^(NSInteger code, NSString * _Nonnull desc,
TUIMessageCellData * _Nonnull data, NSInteger status,
NSString * _Nonnull text) {
if (code != 0 || (text.length == 0 && status == TUIVoiceToTextViewStatusHidden)) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitConvertToTextFailed)];
}
NSDictionary *param = @{TUICore_TUIPluginNotify_DidChangePluginViewSubKey_Data : cellData};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_DidChangePluginViewSubKey
object:nil
param:param];
}];
};
return @[ info ];
}
return nil;
}
@end

View File

@@ -0,0 +1,25 @@
// Created by Tencent on 2023/08/17.
// Copyright © 2023 Tencent. All rights reserved.
/**
* 本文件声明了 TUIVoiceToTextView 类,负责实现语音转文字视图。
* 语音类消息支持长按后转文字,转文字后视图位于消息气泡下方,展示转文字后文本。
*
* When you long press the sound messages, you can choose to convert it to text.
* VoiceToText view will be displayed below the message bubble showing the converted text.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class TUIMessageCellData;
@interface TUIVoiceToTextView : UIView
- (instancetype)initWithData:(TUIMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,358 @@
// Created by Tencent on 2023/08/17.
// Copyright © 2023 Tencent. All rights reserved.
//
// TUIVoiceToTextView.m
// TUIVoiceToText
//
#import "TUIVoiceToTextView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TUIMessageCellData.h>
#import <TIMCommon/TUITextView.h>
#import <TUIChat/TUIChatPopMenu.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDefine.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIVoiceToTextDataProvider.h"
@interface TUIVoiceToTextView ()<TUITextViewDelegate>
@property(nonatomic, copy) NSString *text;
@property(nonatomic, copy) NSString *tips;
@property(nonatomic, strong) UIColor *bgColor;
@property(nonatomic, strong) UIImageView *loadingView;
@property(nonatomic, strong) TUITextView *textView;
@property(nonatomic, strong) UIImageView *retryView;
@property(nonatomic, strong) TUIMessageCellData *cellData;
@end
@implementation TUIVoiceToTextView
- (instancetype)initWithBackgroundColor:(UIColor *)color {
self.bgColor = color;
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithData:(TUIMessageCellData *)data {
self = [super init];
if (self) {
self.cellData = data;
BOOL shouldShow = [TUIVoiceToTextDataProvider shouldShowConvertedText:data.innerMessage];
if (shouldShow) {
[self setupViews];
[self setupGesture];
[self refreshWithData:data];
} else {
if (!CGSizeEqualToSize(self.cellData.bottomContainerSize, CGSizeZero)) {
[self notifyConversionChanged];
}
self.hidden = YES;
[self stopLoading];
self.cellData.bottomContainerSize = CGSizeZero;
}
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
[self setupGesture];
}
return self;
}
- (void)refreshWithData:(TUIMessageCellData *)cellData {
self.text = [TUIVoiceToTextDataProvider getConvertedText:cellData.innerMessage];
TUIVoiceToTextViewStatus status = [TUIVoiceToTextDataProvider getConvertedTextStatus:cellData.innerMessage];
CGSize size = [self calcSizeOfStatus:status];
if (!CGSizeEqualToSize(self.cellData.bottomContainerSize, size)) {
[self notifyConversionChanged];
}
self.cellData.bottomContainerSize = size;
self.mm_top(0).mm_left(0).mm_width(size.width).mm_height(size.height);
if (status == TUIVoiceToTextViewStatusLoading) {
[self startLoading];
} else if (status == TUIVoiceToTextViewStatusShown) {
[self stopLoading];
[self updateConversionViewByText:self.text translationViewStatus:TUIVoiceToTextViewStatusShown];
} else if (status == TUIVoiceToTextViewStatusSecurityStrike) {
[self stopLoading];
[self updateConversionViewByText:self.text translationViewStatus:TUIVoiceToTextViewStatusSecurityStrike];
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (CGSize)calcSizeOfStatus:(TUIVoiceToTextViewStatus)status {
CGFloat minTextWidth = 164;
CGFloat maxTextWidth = Screen_Width * 0.68;
CGFloat actualTextWidth = 80 - 20; // 80 is the fixed container width.
CGFloat oneLineTextHeight = 22;
CGFloat commonMargins = 11 * 2;
// Conversion is processing, return the size of an empty cell including loading animation.
if (status == TUIVoiceToTextViewStatusLoading) {
return CGSizeMake(80, oneLineTextHeight + commonMargins);
}
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
// Conversion is finished.
// Calc the size according to the actual text width.
NSString *rtlText = rtlString(self.text);
CGRect textRect = [rtlText boundingRectWithSize:CGSizeMake(actualTextWidth, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16],
NSParagraphStyleAttributeName: paragraphStyle}
context:nil];
if (textRect.size.height < 30) {
// Result is only one line text.
return CGSizeMake(MAX(textRect.size.width, minTextWidth) + commonMargins,
MAX(textRect.size.height, oneLineTextHeight) + commonMargins);
}
// Result is more than one line, so recalc size using maxTextWidth.
textRect = [rtlText boundingRectWithSize:CGSizeMake(maxTextWidth, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16],
NSParagraphStyleAttributeName: paragraphStyle}
context:nil];
CGSize result = CGSizeMake(MAX(textRect.size.width, minTextWidth) + commonMargins,
MAX(textRect.size.height, oneLineTextHeight) + commonMargins);
return CGSizeMake(ceil(result.width), ceil(result.height));
}
#pragma mark - UI
- (void)setupViews {
self.backgroundColor = self.bgColor ?: TUIVoiceToTextDynamicColor(@"convert_voice_text_view_bg_color", @"#F2F7FF");
self.layer.cornerRadius = 10.0;
self.loadingView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 15, 15)];
[self.loadingView setImage:TUIVoiceToTextBundleThemeImage(@"convert_voice_text_view_icon_loading_img", @"convert_voice_text_loading")];
self.loadingView.hidden = YES;
[self addSubview:self.loadingView];
self.textView = [[TUITextView alloc] init];
self.textView.backgroundColor = [UIColor clearColor];
self.textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
self.textView.textContainer.lineFragmentPadding = 0;
self.textView.scrollEnabled = NO;
self.textView.editable = NO;
self.textView.textAlignment = isRTL() ? NSTextAlignmentRight : NSTextAlignmentLeft;
self.textView.font = [UIFont systemFontOfSize:16];
self.textView.tuiTextViewDelegate = self;
[self.textView disableHighlightLink];
self.textView.textColor = TUIVoiceToTextDynamicColor(@"convert_voice_text_view_text_color", @"#000000");
[self addSubview:self.textView];
self.textView.hidden = YES;
self.textView.userInteractionEnabled = NO;
self.retryView = [[UIImageView alloc] init];
self.retryView.image = [UIImage imageNamed:TUIChatImagePath(@"msg_error")];
self.retryView.hidden = YES;
[self addSubview:self.retryView];
}
- (void)setupGesture {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] init];
[longPress addTarget:self action:@selector(onLongPressed:)];
[self addGestureRecognizer:longPress];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (void)updateConstraints {
[super updateConstraints];
if (self.text.length == 0) {
[self.loadingView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.height.width.mas_equalTo(15);
make.leading.mas_equalTo(10);
make.centerY.mas_equalTo(self.mas_centerY);
}];
MASAttachKeys(self.loadingView);
} else {
[self.retryView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.cellData.direction == MsgDirectionOutgoing){
make.leading.mas_equalTo(self.mas_leading).mas_offset(-27);
}
else {
make.trailing.mas_equalTo(self.mas_trailing).mas_offset(27);
}
make.centerY.mas_equalTo(self.mas_centerY);
make.width.mas_equalTo(20);
make.height.mas_equalTo(20);
}];
[self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(10);
make.trailing.mas_equalTo(-10);
make.top.bottom.mas_equalTo(10);
}];
MASAttachKeys(self.textView);
}
}
- (void)updateConversionViewByText:(NSString *)text translationViewStatus:(TUIVoiceToTextViewStatus)status {
BOOL isConverted = text.length > 0;
UIColor *textColor = TUIVoiceToTextDynamicColor(@"convert_voice_text_view_text_color", @"#000000");
UIColor *bgColor = TUIVoiceToTextDynamicColor(@"convert_voice_text_view_bg_color", @"#F2F7FF");
if (status == TUIVoiceToTextViewStatusSecurityStrike) {
bgColor = [UIColor tui_colorWithHex:@"#FA5151" alpha:0.16];
textColor = TUITranslationDynamicColor(@"", @"#DA2222");
}
self.bgColor = bgColor;
self.backgroundColor = bgColor;
self.textView.textColor = textColor;
if (isConverted) {
self.textView.text = rtlString(text);
}
self.textView.hidden = !isConverted;
self.retryView.hidden = !(status == TUIVoiceToTextViewStatusSecurityStrike);
}
#pragma mark - Public
- (void)startLoading {
if (!self.loadingView.hidden) {
return;
}
self.loadingView.hidden = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotate.toValue = @(M_PI * 2.0);
rotate.duration = 1;
rotate.repeatCount = HUGE_VALF;
[self.loadingView.layer addAnimation:rotate forKey:@"rotationAnimation"];
});
}
- (void)stopLoading {
if (self.loadingView.hidden) {
return;
}
self.loadingView.hidden = YES;
[self.loadingView.layer removeAllAnimations];
}
#pragma mark - Event response
- (void)onLongPressed:(UILongPressGestureRecognizer *)recognizer {
if (![recognizer isKindOfClass:[UILongPressGestureRecognizer class]] || recognizer.state != UIGestureRecognizerStateBegan) {
return;
}
TUIChatPopMenu *popMenu = [[TUIChatPopMenu alloc] init];
TUIVoiceToTextViewStatus status = [TUIVoiceToTextDataProvider getConvertedTextStatus:self.cellData.innerMessage];
BOOL hasRiskContent = (status == TUIVoiceToTextViewStatusSecurityStrike);
@weakify(self);
TUIChatPopMenuAction *copy = [[TUIChatPopMenuAction alloc] initWithTitle:TIMCommonLocalizableString(Copy)
image:TUIVoiceToTextBundleThemeImage(@"convert_voice_text_view_pop_menu_copy_img", @"icon_copy")
weight:1
callback:^{
@strongify(self);
[self onCopy:self.text];
}];
[popMenu addAction:copy];
TUIChatPopMenuAction *forward =
[[TUIChatPopMenuAction alloc] initWithTitle:TIMCommonLocalizableString(Forward)
image:TUIVoiceToTextBundleThemeImage(@"convert_voice_text_view_pop_menu_forward_img", @"icon_forward")
weight:2
callback:^{
@strongify(self);
[self onForward:self.text];
}];
if(!hasRiskContent) {
[popMenu addAction:forward];
}
TUIChatPopMenuAction *hide = [[TUIChatPopMenuAction alloc] initWithTitle:TIMCommonLocalizableString(Hide)
image:TUIVoiceToTextBundleThemeImage(@"convert_voice_text_view_pop_menu_hide_img", @"icon_hide")
weight:3
callback:^{
@strongify(self);
[self onHide:self];
}];
[popMenu addAction:hide];
CGRect frame = [UIApplication.sharedApplication.keyWindow convertRect:self.frame fromView:self.superview];
[popMenu setArrawPosition:CGPointMake(frame.origin.x + frame.size.width * 0.5, frame.origin.y + 66) adjustHeight:0];
[popMenu showInView:UIApplication.sharedApplication.keyWindow];
}
- (void)onCopy:(NSString *)text {
if (text.length == 0) {
return;
}
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = text;
[TUITool makeToast:TIMCommonLocalizableString(Copied)];
}
- (void)onForward:(NSString *)text {
[self notifyConversionForward:text];
}
- (void)onHide:(id)sender {
self.cellData.bottomContainerSize = CGSizeZero;
[TUIVoiceToTextDataProvider saveConvertedResult:self.cellData.innerMessage text:@"" status:TUIVoiceToTextViewStatusHidden];
[self removeFromSuperview];
[self notifyConversionViewHidden];
}
#pragma mark-- Notify
- (void)notifyConversionViewShown {
[self notifyConversionChanged];
}
- (void)notifyConversionViewHidden {
[self notifyConversionChanged];
}
- (void)notifyConversionForward:(NSString *)text {
NSDictionary *param = @{TUICore_TUIPluginNotify_WillForwardTextSubKey_Text : text};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_WillForwardTextSubKey
object:nil
param:param];
}
- (void)notifyConversionChanged {
NSDictionary *param = @{TUICore_TUIPluginNotify_DidChangePluginViewSubKey_Data : self.cellData,
TUICore_TUIPluginNotify_DidChangePluginViewSubKey_VC : self};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_DidChangePluginViewSubKey
object:nil
param:param];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
});
}
@end