提交
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// TUITranslationConfig.h
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/4/7.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUITranslationConfig : NSObject
|
||||
|
||||
+ (TUITranslationConfig *)defaultConfig;
|
||||
|
||||
/**
|
||||
* Translation target language code.
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *targetLanguageCode;
|
||||
|
||||
/**
|
||||
* Translation target language name.
|
||||
*/
|
||||
@property(nonatomic, copy, readonly) NSString *targetLanguageName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// TUITranslationConfig.m
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/4/7.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITranslationConfig.h"
|
||||
|
||||
static NSString *const kTransaltionTargetLanguageCode = @"translation_target_language_code";
|
||||
|
||||
@interface TUITranslationConfig ()
|
||||
@property(nonatomic, copy, readwrite) NSString *targetLanguageName;
|
||||
@end
|
||||
|
||||
@implementation TUITranslationConfig
|
||||
|
||||
+ (TUITranslationConfig *)defaultConfig {
|
||||
static dispatch_once_t onceToken;
|
||||
static TUITranslationConfig *config;
|
||||
dispatch_once(&onceToken, ^{
|
||||
config = [[TUITranslationConfig 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:kTransaltionTargetLanguageCode];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (void)loadSavedLanguage {
|
||||
NSString *lang = [[NSUserDefaults standardUserDefaults] stringForKey:kTransaltionTargetLanguageCode];
|
||||
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
|
||||
36
TUIKit/TUITranslationPlugin/Resources/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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>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>
|
||||
|
After Width: | Height: | Size: 951 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 549 B |
|
After Width: | Height: | Size: 810 B |
|
After Width: | Height: | Size: 362 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,28 @@
|
||||
<?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>translation_view_bg_color</key>
|
||||
<string>#1F1F1F</string>
|
||||
<key>translation_view_text_color</key>
|
||||
<string>#D9D9D9</string>
|
||||
<key>translation_view_tips_color</key>
|
||||
<string>#D9D9D9</string>
|
||||
<key>translation_view_icon_loading_img</key>
|
||||
<string>translation_loading.png</string>
|
||||
<key>translation_view_icon_tips_img</key>
|
||||
<string>translation_tips.png</string>
|
||||
<key>translation_view_pop_menu_copy_img</key>
|
||||
<string>icon_copy.png</string>
|
||||
<key>translation_view_pop_menu_hide_img</key>
|
||||
<string>icon_hide.png</string>
|
||||
<key>translation_view_pop_menu_forward_img</key>
|
||||
<string>icon_forward.png</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 782 B |
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 733 B |
@@ -0,0 +1,28 @@
|
||||
<?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>translation_view_bg_color</key>
|
||||
<string>#F2F7FF</string>
|
||||
<key>translation_view_text_color</key>
|
||||
<string>#000000</string>
|
||||
<key>translation_view_tips_color</key>
|
||||
<string>#000000</string>
|
||||
<key>translation_view_icon_loading_img</key>
|
||||
<string>translation_loading.png</string>
|
||||
<key>translation_view_icon_tips_img</key>
|
||||
<string>translation_tips.png</string>
|
||||
<key>translation_view_pop_menu_copy_img</key>
|
||||
<string>icon_copy.png</string>
|
||||
<key>translation_view_pop_menu_hide_img</key>
|
||||
<string>icon_hide.png</string>
|
||||
<key>translation_view_pop_menu_forward_img</key>
|
||||
<string>icon_forward.png</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 810 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 672 B |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,28 @@
|
||||
<?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>translation_view_bg_color</key>
|
||||
<string>#F2F7FF</string>
|
||||
<key>translation_view_text_color</key>
|
||||
<string>#000000</string>
|
||||
<key>translation_view_tips_color</key>
|
||||
<string>#000000</string>
|
||||
<key>translation_view_icon_loading_img</key>
|
||||
<string>translation_loading.png</string>
|
||||
<key>translation_view_icon_tips_img</key>
|
||||
<string>translation_tips.png</string>
|
||||
<key>translation_view_pop_menu_copy_img</key>
|
||||
<string>icon_copy.png</string>
|
||||
<key>translation_view_pop_menu_hide_img</key>
|
||||
<string>icon_hide.png</string>
|
||||
<key>translation_view_pop_menu_forward_img</key>
|
||||
<string>icon_forward.png</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 810 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,28 @@
|
||||
<?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>translation_view_bg_color</key>
|
||||
<string>#F2F7FF</string>
|
||||
<key>translation_view_text_color</key>
|
||||
<string>#000000</string>
|
||||
<key>translation_view_tips_color</key>
|
||||
<string>#000000</string>
|
||||
<key>translation_view_icon_loading_img</key>
|
||||
<string>translation_loading.png</string>
|
||||
<key>translation_view_icon_tips_img</key>
|
||||
<string>translation_tips.png</string>
|
||||
<key>translation_view_pop_menu_copy_img</key>
|
||||
<string>icon_copy.png</string>
|
||||
<key>translation_view_pop_menu_hide_img</key>
|
||||
<string>icon_hide.png</string>
|
||||
<key>translation_view_pop_menu_forward_img</key>
|
||||
<string>icon_forward.png</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 810 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 435 B |
27
TUIKit/TUITranslationPlugin/TUITranslationPlugin.podspec
Normal file
@@ -0,0 +1,27 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = 'TUITranslationPlugin'
|
||||
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 = 'TUITranslationPlugin'
|
||||
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" => '**/TUITranslationPlugin/Resources/PrivacyInfo.xcprivacy'
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// TUITranslationDataProvider.h
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/3/21.
|
||||
// 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, TUITranslationViewStatus) {
|
||||
TUITranslationViewStatusUnknown = 0,
|
||||
TUITranslationViewStatusHidden = 1,
|
||||
TUITranslationViewStatusLoading = 2,
|
||||
TUITranslationViewStatusShown = 3,
|
||||
TUITranslationViewStatusSecurityStrike = 4,
|
||||
};
|
||||
|
||||
typedef void (^TUITranslateMessageCompletion)(NSInteger code, NSString *desc, TUIMessageCellData *data, NSInteger status, NSString *text);
|
||||
|
||||
@interface TUITranslationDataProvider : NSObject
|
||||
|
||||
+ (void)translateMessage:(TUIMessageCellData *)data completion:(TUITranslateMessageCompletion _Nullable)completion;
|
||||
|
||||
+ (void)saveTranslationResult:(V2TIMMessage *)message text:(NSString *)text status:(TUITranslationViewStatus)status;
|
||||
|
||||
+ (BOOL)shouldShowTranslation:(V2TIMMessage *)message;
|
||||
+ (NSString *)getTranslationText:(V2TIMMessage *)message;
|
||||
+ (TUITranslationViewStatus)getTranslationStatus:(V2TIMMessage *)message;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,225 @@
|
||||
//
|
||||
// TUITranslationDataProvider.m
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/3/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITranslationDataProvider.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUILogin.h>
|
||||
#import "TUITranslationConfig.h"
|
||||
|
||||
#pragma GCC diagnostic ignored "-Wundeclared-selector"
|
||||
|
||||
static NSString *const kKeyTranslationText = @"translation";
|
||||
static NSString *const kKeyTranslationViewStatus = @"translation_view_status";
|
||||
|
||||
@interface TUITranslationDataProvider () <TUINotificationProtocol, V2TIMAdvancedMsgListener>
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUITranslationDataProvider
|
||||
#pragma mark - Public
|
||||
+ (void)translateMessage:(TUIMessageCellData *)data completion:(TUITranslateMessageCompletion)completion {
|
||||
V2TIMMessage *msg = data.innerMessage;
|
||||
if (msg.elemType != V2TIM_ELEM_TYPE_TEXT) {
|
||||
return;
|
||||
}
|
||||
V2TIMTextElem *textElem = msg.textElem;
|
||||
if (textElem == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
/// Get at user's nickname by userID
|
||||
NSMutableArray *atUserIDs = [msg.groupAtUserList mutableCopy];
|
||||
if (atUserIDs.count == 0) {
|
||||
/// There's no any @user info.
|
||||
[self translateMessage:data atUsers:nil completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
/// Find @All info.
|
||||
NSMutableArray *atUserIDsExcludingAtAll = [NSMutableArray new];
|
||||
NSMutableIndexSet *atAllIndex = [NSMutableIndexSet new];
|
||||
for (int i = 0; i < atUserIDs.count; i++) {
|
||||
NSString *userID = atUserIDs[i];
|
||||
if (![userID isEqualToString:kImSDK_MesssageAtALL]) {
|
||||
/// Exclude @All.
|
||||
[atUserIDsExcludingAtAll addObject:userID];
|
||||
} else {
|
||||
/// Record @All's location for later restore.
|
||||
[atAllIndex addIndex:i];
|
||||
}
|
||||
}
|
||||
if (atUserIDsExcludingAtAll.count == 0) {
|
||||
/// There's only @All info.
|
||||
NSMutableArray *atAllNames = [NSMutableArray new];
|
||||
for (int i = 0; i < atAllIndex.count; i++) {
|
||||
[atAllNames addObject:TIMCommonLocalizableString(All)];
|
||||
}
|
||||
[self translateMessage:data atUsers:atAllNames completion:completion];
|
||||
return;
|
||||
}
|
||||
[[V2TIMManager sharedInstance] getUsersInfo:atUserIDsExcludingAtAll
|
||||
succ:^(NSArray<V2TIMUserFullInfo *> *infoList) {
|
||||
NSMutableArray *atUserNames = [NSMutableArray new];
|
||||
for (NSString *userID in atUserIDsExcludingAtAll) {
|
||||
for (V2TIMUserFullInfo *user in infoList) {
|
||||
if ([userID isEqualToString:user.userID]) {
|
||||
[atUserNames addObject:user.nickName ?: user.userID];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore @All.
|
||||
if (atAllIndex.count > 0) {
|
||||
[atAllIndex enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) {
|
||||
[atUserNames insertObject:TIMCommonLocalizableString(All) atIndex:idx];
|
||||
}];
|
||||
}
|
||||
[self translateMessage:data atUsers:atUserNames completion:completion];
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
[self translateMessage:data atUsers:atUserIDs completion:completion];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)translateMessage:(TUIMessageCellData *)data atUsers:(NSArray *)atUsers completion:(TUITranslateMessageCompletion)completion {
|
||||
V2TIMMessage *msg = data.innerMessage;
|
||||
V2TIMTextElem *textElem = msg.textElem;
|
||||
|
||||
NSString *target = [TUITranslationConfig defaultConfig].targetLanguageCode;
|
||||
NSDictionary *splitResult = [textElem.text splitTextByEmojiAndAtUsers:atUsers];
|
||||
NSArray *textArray = splitResult[kSplitStringTextKey];
|
||||
|
||||
if (textArray.count == 0) {
|
||||
/// Nothing need to be translated.
|
||||
[self saveTranslationResult:msg text:textElem.text status:TUITranslationViewStatusShown];
|
||||
if (completion) {
|
||||
completion(0, @"", data, TUITranslationViewStatusShown, textElem.text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *dict = [TUITool jsonData2Dictionary:msg.localCustomData];
|
||||
NSString *translatedText = nil;
|
||||
if ([dict.allKeys containsObject:kKeyTranslationText]) {
|
||||
translatedText = dict[kKeyTranslationText];
|
||||
}
|
||||
|
||||
if (translatedText.length > 0) {
|
||||
[self saveTranslationResult:msg text:translatedText status:TUITranslationViewStatusShown];
|
||||
if (completion) {
|
||||
completion(0, @"", data, TUITranslationViewStatusShown, translatedText);
|
||||
}
|
||||
} else {
|
||||
[self saveTranslationResult:msg text:@"" status:TUITranslationViewStatusLoading];
|
||||
if (completion) {
|
||||
completion(0, @"", data, TUITranslationViewStatusLoading, @"");
|
||||
}
|
||||
}
|
||||
|
||||
/// Send translate request.
|
||||
@weakify(self);
|
||||
[[V2TIMManager sharedInstance] translateText:textArray
|
||||
sourceLanguage:nil
|
||||
targetLanguage:target
|
||||
completion:^(int code, NSString *desc, NSDictionary<NSString *, NSString *> *result) {
|
||||
@strongify(self);
|
||||
/// Translate failed.
|
||||
if (code != 0 || result.count == 0) {
|
||||
if (code == 30007) {
|
||||
[TUITool makeToast:TIMCommonLocalizableString(TranslateLanguageNotSupport)];
|
||||
} else {
|
||||
[TUITool makeToastError:code msg:desc];
|
||||
}
|
||||
|
||||
[self saveTranslationResult:msg text:@"" status:TUITranslationViewStatusHidden];
|
||||
if (completion) {
|
||||
completion(code, desc, data, TUITranslationViewStatusHidden, @"");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/// Translate succeeded.
|
||||
NSString *text = [NSString replacedStringWithArray:splitResult[kSplitStringResultKey]
|
||||
index:splitResult[kSplitStringTextIndexKey]
|
||||
replaceDict:result];
|
||||
[self saveTranslationResult:msg text:text status:TUITranslationViewStatusShown];
|
||||
|
||||
if (completion) {
|
||||
completion(0, @"", data, TUITranslationViewStatusShown, text);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)saveTranslationResult:(V2TIMMessage *)message text:(NSString *)text status:(TUITranslationViewStatus)status {
|
||||
if (text.length > 0) {
|
||||
[self saveToLocalCustomDataOfMessage:message key:kKeyTranslationText value:text];
|
||||
}
|
||||
[self saveToLocalCustomDataOfMessage:message key:kKeyTranslationViewStatus 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)shouldShowTranslation:(V2TIMMessage *)message {
|
||||
if (message.localCustomData.length == 0) {
|
||||
return NO;
|
||||
}
|
||||
NSDictionary *dict = [TUITool jsonData2Dictionary:message.localCustomData];
|
||||
TUITranslationViewStatus status;
|
||||
if ([dict.allKeys containsObject:kKeyTranslationViewStatus]) {
|
||||
status = [dict[kKeyTranslationViewStatus] integerValue];
|
||||
} else {
|
||||
status = TUITranslationViewStatusHidden;
|
||||
}
|
||||
NSArray *hiddenStatus = @[ @(TUITranslationViewStatusUnknown), @(TUITranslationViewStatusHidden) ];
|
||||
return ![hiddenStatus containsObject:@(status)] || status == TUITranslationViewStatusLoading;
|
||||
}
|
||||
|
||||
+ (NSString *)getTranslationText:(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:kKeyTranslationText]) {
|
||||
return dict[kKeyTranslationText];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (TUITranslationViewStatus)getTranslationStatus:(V2TIMMessage *)message {
|
||||
BOOL hasRiskContent = message.hasRiskContent;
|
||||
if (hasRiskContent){
|
||||
return TUITranslationViewStatusSecurityStrike;
|
||||
}
|
||||
if (message.localCustomData.length == 0) {
|
||||
return TUITranslationViewStatusUnknown;
|
||||
}
|
||||
NSDictionary *dict = [TUITool jsonData2Dictionary:message.localCustomData];
|
||||
if ([dict.allKeys containsObject:kKeyTranslationViewStatus]) {
|
||||
return [dict[kKeyTranslationViewStatus] integerValue];
|
||||
}
|
||||
return TUITranslationViewStatusUnknown;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// TUITranslationExtensionObserver.h
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/4/4.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUITranslationExtensionObserver : NSObject
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,259 @@
|
||||
//
|
||||
// TUITranslationExtensionObserver.m
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/4/4.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITranslationExtensionObserver.h"
|
||||
|
||||
#import <TIMCommon/TIMPopActionProtocol.h>
|
||||
#import <TIMCommon/TUIMessageCell.h>
|
||||
#import <TUIChat/TUIChatConfig.h>
|
||||
#import <TUIChat/TUIReferenceMessageCell.h>
|
||||
#import <TUIChat/TUIReferenceMessageCell_Minimalist.h>
|
||||
#import <TUIChat/TUIReplyMessageCell.h>
|
||||
#import <TUIChat/TUIReplyMessageCell_Minimalist.h>
|
||||
#import <TUIChat/TUITextMessageCell.h>
|
||||
#import <TUIChat/TUITextMessageCell_Minimalist.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUIDefine.h>
|
||||
#import "TUITranslationConfig.h"
|
||||
#import "TUITranslationDataProvider.h"
|
||||
#import "TUITranslationLanguageController.h"
|
||||
#import "TUITranslationView.h"
|
||||
|
||||
@interface TUITranslationExtensionObserver () <TUIExtensionProtocol>
|
||||
|
||||
@property(nonatomic, weak) UINavigationController *navVC;
|
||||
@property(nonatomic, weak) TUICommonTextCellData *cellData;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUITranslationExtensionObserver
|
||||
|
||||
static id gShareInstance = nil;
|
||||
|
||||
+ (void)load {
|
||||
TUIRegisterThemeResourcePath(TUITranslationThemePath, TUIThemeModuleTranslation);
|
||||
|
||||
// UI extensions in pop menu when message is long pressed.
|
||||
[TUICore registerExtension:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID object:TUITranslationExtensionObserver.shareInstance];
|
||||
[TUICore registerExtension:TUICore_TUIChatExtension_PopMenuActionItem_MinimalistExtensionID object:TUITranslationExtensionObserver.shareInstance];
|
||||
|
||||
// UI extensions of setting.
|
||||
[TUICore registerExtension:TUICore_TUIContactExtension_MeSettingMenu_ClassicExtensionID object:TUITranslationExtensionObserver.shareInstance];
|
||||
[TUICore registerExtension:TUICore_TUIContactExtension_MeSettingMenu_MinimalistExtensionID object:TUITranslationExtensionObserver.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_TEXT) {
|
||||
return NO;
|
||||
}
|
||||
NSMutableDictionary *cacheMap = parentView.tui_extValueObj;
|
||||
TUITranslationView *cacheView = nil;
|
||||
if (!cacheMap){
|
||||
cacheMap = [NSMutableDictionary dictionaryWithCapacity:3];
|
||||
}
|
||||
else if ([cacheMap isKindOfClass:NSDictionary.class]) {
|
||||
cacheView = [cacheMap objectForKey:@"TUITranslationView"];
|
||||
}
|
||||
else {
|
||||
//cacheMap is not a dic ;
|
||||
}
|
||||
if (cacheView) {
|
||||
[cacheView removeFromSuperview];
|
||||
cacheView = nil;
|
||||
}
|
||||
TUITranslationView *view = [[TUITranslationView alloc] initWithData:cellData];
|
||||
[parentView addSubview:view];
|
||||
[cacheMap setObject:view forKey:@"TUITranslationView"];
|
||||
parentView.tui_extValueObj = cacheMap;
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSArray<TUIExtensionInfo *> *)onGetExtension:(NSString *)extensionID param:(NSDictionary *)param {
|
||||
if (![extensionID isKindOfClass:NSString.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID] ||
|
||||
[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]) {
|
||||
if (![cell isKindOfClass:TUITextMessageCell.class] && ![cell isKindOfClass:TUIReferenceMessageCell.class] &&
|
||||
![cell isKindOfClass:TUIReplyMessageCell.class]) {
|
||||
return nil;
|
||||
}
|
||||
} else if ([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_MinimalistExtensionID]) {
|
||||
if (![cell isKindOfClass:TUITextMessageCell_Minimalist.class] && ![cell isKindOfClass:TUIReferenceMessageCell_Minimalist.class] &&
|
||||
![cell isKindOfClass:TUIReplyMessageCell_Minimalist.class]) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
if (cell.messageData.innerMessage.elemType != V2TIM_ELEM_TYPE_TEXT) {
|
||||
return nil;
|
||||
}
|
||||
if ([TUITranslationDataProvider shouldShowTranslation:cell.messageData.innerMessage]) {
|
||||
return nil;
|
||||
}
|
||||
if (![self isSelectAllContentOfMessage:cell]) {
|
||||
return nil;
|
||||
}
|
||||
if (![TUIChatConfig defaultConfig].enablePopMenuTranslateAction) {
|
||||
return nil;
|
||||
}
|
||||
if (cell.messageData.innerMessage.hasRiskContent) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUIExtensionInfo *info = [[TUIExtensionInfo alloc] init];
|
||||
info.text = TIMCommonLocalizableString(TUIKitTranslate);
|
||||
if ([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_ClassicExtensionID]) {
|
||||
info.icon = TUIChatBundleThemeImage(@"chat_icon_translate_img", @"icon_translate");
|
||||
info.weight = 2000;
|
||||
} else if ([extensionID isEqualToString:TUICore_TUIChatExtension_PopMenuActionItem_MinimalistExtensionID]) {
|
||||
info.icon = [UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_translate")];
|
||||
info.weight = 800;
|
||||
|
||||
}
|
||||
info.onClicked = ^(NSDictionary *_Nonnull action) {
|
||||
TUIMessageCellData *cellData = cell.messageData;
|
||||
V2TIMMessage *message = cellData.innerMessage;
|
||||
if (message.elemType != V2TIM_ELEM_TYPE_TEXT) {
|
||||
return;
|
||||
}
|
||||
[TUITranslationDataProvider translateMessage:cellData
|
||||
completion:^(NSInteger code, NSString *_Nonnull desc,
|
||||
TUIMessageCellData *_Nonnull data, NSInteger status,
|
||||
NSString *_Nonnull text) {
|
||||
NSDictionary *param = @{TUICore_TUIPluginNotify_DidChangePluginViewSubKey_Data : cellData};
|
||||
[TUICore notifyEvent:TUICore_TUIPluginNotify
|
||||
subKey:TUICore_TUIPluginNotify_DidChangePluginViewSubKey
|
||||
object:nil
|
||||
param:param];
|
||||
}];
|
||||
};
|
||||
return @[ info ];
|
||||
} else if ([extensionID isEqualToString:TUICore_TUIContactExtension_MeSettingMenu_ClassicExtensionID] ||
|
||||
[extensionID isEqualToString:TUICore_TUIContactExtension_MeSettingMenu_MinimalistExtensionID]) {
|
||||
// Extension entrance in Me setting VC.
|
||||
if (![param isKindOfClass:NSDictionary.class]) {
|
||||
return nil;
|
||||
}
|
||||
if (param[TUICore_TUIContactExtension_MeSettingMenu_Nav]) {
|
||||
self.navVC = param[TUICore_TUIContactExtension_MeSettingMenu_Nav];
|
||||
}
|
||||
|
||||
TUICommonTextCellData *data = [TUICommonTextCellData new];
|
||||
data.key = TIMCommonLocalizableString(TranslateMessage);
|
||||
data.showAccessory = YES;
|
||||
data.value = [TUITranslationConfig defaultConfig].targetLanguageName;
|
||||
self.cellData = data;
|
||||
|
||||
TUICommonTextCell *cell = [[TUICommonTextCell alloc] init];
|
||||
[cell fillWithData:data];
|
||||
cell.mm_height(60).mm_width(Screen_Width);
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onClickedTargetLanguageCell:)];
|
||||
[cell addGestureRecognizer:tap];
|
||||
|
||||
TUIExtensionInfo *info = [[TUIExtensionInfo alloc] init];
|
||||
NSMutableDictionary *param = [NSMutableDictionary dictionary];
|
||||
[param setObject:@450 forKey:TUICore_TUIContactExtension_MeSettingMenu_Weight];
|
||||
if (cell) {
|
||||
[param setObject:cell forKey:TUICore_TUIContactExtension_MeSettingMenu_View];
|
||||
}
|
||||
if (data) {
|
||||
[param setObject:data forKey:TUICore_TUIContactExtension_MeSettingMenu_Data];
|
||||
}
|
||||
info.data = param;
|
||||
return @[ info ];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)onClickedTargetLanguageCell:(TUICommonTextCell *)cell {
|
||||
TUITranslationLanguageController *vc = [[TUITranslationLanguageController alloc] init];
|
||||
vc.onSelectedLanguage = ^(NSString *_Nonnull languageName) {
|
||||
self.cellData.value = languageName;
|
||||
};
|
||||
if (self.navVC) {
|
||||
[self.navVC pushViewController:vc animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isSelectAllContentOfMessage:(TUIMessageCell *)cell {
|
||||
if ([cell isKindOfClass:TUITextMessageCell.class]) {
|
||||
TUITextMessageCell *textCell = (TUITextMessageCell *)cell;
|
||||
if (textCell.selectContent.length == 0) {
|
||||
return YES;
|
||||
} else {
|
||||
NSAttributedString *selectedString = [textCell.textView.attributedText attributedSubstringFromRange:textCell.textView.selectedRange];
|
||||
if (selectedString.length == 0) {
|
||||
return YES;
|
||||
}
|
||||
return selectedString.length == textCell.textView.attributedText.length;
|
||||
}
|
||||
} else if ([cell isKindOfClass:TUIReferenceMessageCell.class]) {
|
||||
TUIReferenceMessageCell *refCell = (TUIReferenceMessageCell *)cell;
|
||||
if (refCell.selectContent.length == 0) {
|
||||
return YES;
|
||||
} else {
|
||||
NSAttributedString *selectedString = [refCell.textView.attributedText attributedSubstringFromRange:refCell.textView.selectedRange];
|
||||
if (selectedString.length == 0) {
|
||||
return YES;
|
||||
}
|
||||
return selectedString.length == refCell.textView.attributedText.length;
|
||||
}
|
||||
} else if ([cell isKindOfClass:TUIReplyMessageCell.class]) {
|
||||
TUIReplyMessageCell *replyCell = (TUIReplyMessageCell *)cell;
|
||||
if (replyCell.selectContent.length == 0) {
|
||||
return YES;
|
||||
} else {
|
||||
NSAttributedString *selectedString = [replyCell.textView.attributedText attributedSubstringFromRange:replyCell.textView.selectedRange];
|
||||
if (selectedString.length == 0) {
|
||||
return YES;
|
||||
}
|
||||
return selectedString.length == replyCell.textView.attributedText.length;
|
||||
}
|
||||
}
|
||||
if ([cell isKindOfClass:TUITextMessageCell_Minimalist.class] || [cell isKindOfClass:TUIReferenceMessageCell_Minimalist.class] ||
|
||||
[cell isKindOfClass:TUIReplyMessageCell_Minimalist.class]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// TUITranslationLanguageController.h
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/4/7.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUITranslationLanguageController : UIViewController
|
||||
|
||||
@property(nonatomic, copy) void (^onSelectedLanguage)(NSString *languageName);
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// TUITranslationLanguageController.m
|
||||
// TUITranslation
|
||||
//
|
||||
// Created by xia on 2023/4/7.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITranslationLanguageController.h"
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMConfig.h>
|
||||
#import "TUITranslationConfig.h"
|
||||
|
||||
@interface TUITranslationLanguageController () <UITableViewDelegate, UITableViewDataSource>
|
||||
|
||||
@property(nonatomic, copy) NSArray *languageCodeList;
|
||||
@property(nonatomic, copy) NSArray *languageNameList;
|
||||
@property(nonatomic, strong) UITableView *tableView;
|
||||
@property(nonatomic, strong) NSIndexPath *currentIndex;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUITranslationLanguageController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = TIMCommonLocalizableString(TranslateMessage);
|
||||
[self.view addSubview:self.tableView];
|
||||
}
|
||||
|
||||
#pragma mark - UITableView DataSource & Delegate
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.languageNameList.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.row >= self.languageNameList.count) {
|
||||
return nil;
|
||||
}
|
||||
NSString *language = self.languageNameList[indexPath.row];
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"textCell" forIndexPath:indexPath];
|
||||
cell.textLabel.text = language;
|
||||
if ([language isEqualToString:[TUITranslationConfig defaultConfig].targetLanguageName]) {
|
||||
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
||||
self.currentIndex = indexPath;
|
||||
} else {
|
||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.row >= self.languageNameList.count || indexPath.row >= self.languageCodeList.count) {
|
||||
return;
|
||||
}
|
||||
if (indexPath.row == self.currentIndex.row) {
|
||||
return;
|
||||
}
|
||||
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
||||
UITableViewCell *lastCell = [tableView cellForRowAtIndexPath:self.currentIndex];
|
||||
lastCell.accessoryType = UITableViewCellAccessoryNone;
|
||||
self.currentIndex = indexPath;
|
||||
|
||||
[TUITranslationConfig defaultConfig].targetLanguageCode = self.languageCodeList[indexPath.row];
|
||||
if (self.onSelectedLanguage) {
|
||||
self.onSelectedLanguage(self.languageNameList[indexPath.row]);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
- (UITableView *)tableView {
|
||||
if (!_tableView) {
|
||||
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
|
||||
_tableView.delaysContentTouches = NO;
|
||||
_tableView.tableFooterView = [[UIView alloc] init];
|
||||
_tableView.backgroundColor = TIMCommonDynamicColor(@"controller_bg_color", @"#F2F3F5");
|
||||
_tableView.delegate = self;
|
||||
_tableView.dataSource = self;
|
||||
_tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
|
||||
_tableView.rowHeight = 55;
|
||||
[_tableView registerClass:[TUICommonTextCell class] forCellReuseIdentifier:@"textCell"];
|
||||
if (@available(iOS 15.0, *)) {
|
||||
_tableView.sectionHeaderTopPadding = 0;
|
||||
}
|
||||
}
|
||||
return _tableView;
|
||||
}
|
||||
|
||||
/**
|
||||
Simplified Chinese, Traditional Chinese, English, Japanese, Korean, French, Spanish, Italian, German,
|
||||
Turkish, Russian, Portuguese, Vietnamese, Indonesian, Thai, Malaysian,
|
||||
Hindi language
|
||||
*/
|
||||
- (NSArray *)languageNameList {
|
||||
return @[
|
||||
@"简体中文", @"繁體中文", @"English", @"日本語", @"한국어", @"Français", @"Español", @"Italiano", @"Deutsch", @"Türkçe", @"Русский", @"Português",
|
||||
@"Tiếng Việt", @"Bahasa Indonesia", @"ภาษาไทย", @"Bahasa Melayu", @"हिन्दी"
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray *)languageCodeList {
|
||||
return @[ @"zh", @"zh-TW", @"en", @"ja", @"ko", @"fr", @"es", @"it", @"de", @"tr", @"ru", @"pt", @"vi", @"id", @"th", @"ms", @"hi" ];
|
||||
}
|
||||
|
||||
@end
|
||||
21
TUIKit/TUITranslationPlugin/UI/UI/TUITranslationView.h
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
* When you long press the text messages, you can choose to translate it.
|
||||
* Translation view will be displayed below the message bubble showing the translated text.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@class TUIMessageCellData;
|
||||
|
||||
@interface TUITranslationView : UIView
|
||||
|
||||
- (instancetype)initWithData:(TUIMessageCellData *)data;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
400
TUIKit/TUITranslationPlugin/UI/UI/TUITranslationView.m
Normal file
@@ -0,0 +1,400 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
// TUITranslationView.m
|
||||
// TUITranslation
|
||||
//
|
||||
|
||||
#import "TUITranslationView.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 "TUITranslationDataProvider.h"
|
||||
|
||||
@interface TUITranslationView ()<TUITextViewDelegate>
|
||||
|
||||
@property(nonatomic, copy) NSString *text;
|
||||
@property(nonatomic, copy) NSString *tips;
|
||||
@property(nonatomic, strong) UIColor *bgColor;
|
||||
|
||||
@property(nonatomic, strong) UIImageView *tipsIcon;
|
||||
@property(nonatomic, strong) UILabel *tipsLabel;
|
||||
@property(nonatomic, strong) UIImageView *loadingView;
|
||||
@property(nonatomic, strong) TUITextView *textView;
|
||||
@property(nonatomic, strong) UIImageView *retryView;
|
||||
|
||||
@property(nonatomic, strong) TUIMessageCellData *cellData;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUITranslationView
|
||||
|
||||
- (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 = [TUITranslationDataProvider shouldShowTranslation:data.innerMessage];
|
||||
if (shouldShow) {
|
||||
[self setupViews];
|
||||
[self setupGesture];
|
||||
[self refreshWithData:data];
|
||||
} else {
|
||||
if (!CGSizeEqualToSize(self.cellData.bottomContainerSize, CGSizeZero)) {
|
||||
[self notifyTranslationChanged];
|
||||
}
|
||||
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 = [TUITranslationDataProvider getTranslationText:cellData.innerMessage];
|
||||
TUITranslationViewStatus status = [TUITranslationDataProvider getTranslationStatus:cellData.innerMessage];
|
||||
|
||||
CGSize size = [self calcSizeOfStatus:status];
|
||||
if (!CGSizeEqualToSize(self.cellData.bottomContainerSize, size)) {
|
||||
[self notifyTranslationChanged];
|
||||
}
|
||||
self.cellData.bottomContainerSize = size;
|
||||
self.mm_top(0).mm_left(0).mm_width(size.width).mm_height(size.height);
|
||||
if (status == TUITranslationViewStatusLoading) {
|
||||
[self startLoading];
|
||||
} else if (status == TUITranslationViewStatusShown) {
|
||||
[self stopLoading];
|
||||
[self updateTransaltionViewByText:self.text translationViewStatus:status];
|
||||
} else if (status == TUITranslationViewStatusSecurityStrike) {
|
||||
[self stopLoading];
|
||||
[self updateTransaltionViewByText:self.text translationViewStatus:status];
|
||||
}
|
||||
// tell constraints they need updating
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
// update constraints now so we can animate the change
|
||||
[self updateConstraintsIfNeeded];
|
||||
|
||||
[self layoutIfNeeded];
|
||||
|
||||
}
|
||||
|
||||
- (CGSize)calcSizeOfStatus:(TUITranslationViewStatus)status {
|
||||
CGFloat minTextWidth = 164;
|
||||
CGFloat maxTextWidth = Screen_Width * 0.68;
|
||||
CGFloat actualTextWidth = 80 - 20; // 80 is the fixed container width.
|
||||
CGFloat tipsHeight = 20;
|
||||
CGFloat tipsBottomMargin = 10;
|
||||
CGFloat oneLineTextHeight = 22;
|
||||
CGFloat commonMargins = 10 * 2;
|
||||
|
||||
// Translation is processing, return the size of an empty cell including loading animation.
|
||||
if (status == TUITranslationViewStatusLoading) {
|
||||
return CGSizeMake(80, oneLineTextHeight + commonMargins);
|
||||
}
|
||||
|
||||
NSAttributedString *attrStr = [self.text getAdvancedFormatEmojiStringWithFont:[UIFont systemFontOfSize:16]
|
||||
textColor:[UIColor grayColor]
|
||||
emojiLocations:nil];
|
||||
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
|
||||
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
|
||||
[paragraphStyle setAlignment:NSTextAlignmentLeft];
|
||||
|
||||
// Translation is finished.
|
||||
// Calc the size according to the actual text width.
|
||||
CGRect textRect = [attrStr boundingRectWithSize:CGSizeMake(actualTextWidth, MAXFLOAT)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
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 + tipsHeight + tipsBottomMargin);
|
||||
}
|
||||
|
||||
// Result is more than one line, so recalc size using maxTextWidth.
|
||||
textRect = [attrStr boundingRectWithSize:CGSizeMake(maxTextWidth, MAXFLOAT)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
context:nil];
|
||||
CGSize result = CGSizeMake(MAX(textRect.size.width, minTextWidth) + commonMargins,
|
||||
MAX(textRect.size.height, oneLineTextHeight) + commonMargins + tipsHeight + tipsBottomMargin);
|
||||
return CGSizeMake(ceil(result.width), ceil(result.height));
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
- (void)setupViews {
|
||||
self.backgroundColor = self.bgColor ?: TUITranslationDynamicColor(@"translation_view_bg_color", @"#F2F7FF");
|
||||
self.layer.cornerRadius = 10.0;
|
||||
|
||||
self.loadingView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 15, 15)];
|
||||
[self.loadingView setImage:TUITranslationBundleThemeImage(@"translation_view_icon_loading_img", @"translation_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.tuiTextViewDelegate = self;
|
||||
[self.textView disableHighlightLink];
|
||||
[self addSubview:self.textView];
|
||||
self.textView.hidden = YES;
|
||||
self.textView.userInteractionEnabled = NO;
|
||||
|
||||
self.tipsIcon = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 13, 13)];
|
||||
[self.tipsIcon setImage:TUITranslationBundleThemeImage(@"translation_view_icon_tips_img", @"translation_tips")];
|
||||
self.tipsIcon.alpha = 0.4;
|
||||
[self addSubview:self.tipsIcon];
|
||||
self.tipsIcon.hidden = YES;
|
||||
|
||||
self.tipsLabel = [[UILabel alloc] init];
|
||||
self.tipsLabel.font = [UIFont systemFontOfSize:12];
|
||||
self.tipsLabel.text = TIMCommonLocalizableString(TUIKitTranslateDefaultTips);
|
||||
self.tipsLabel.textColor = TUITranslationDynamicColor(@"translation_view_tips_color", @"#000000");
|
||||
self.tipsLabel.alpha = 0.4;
|
||||
self.tipsLabel.numberOfLines = 0;
|
||||
self.tipsLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
|
||||
[self addSubview:self.tipsLabel];
|
||||
self.tipsLabel.hidden = YES;
|
||||
|
||||
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;
|
||||
}
|
||||
// this is Apple's recommended place for adding/updating constraints
|
||||
- (void)updateConstraints {
|
||||
[super updateConstraints];
|
||||
|
||||
if (self.text.length == 0) {
|
||||
[self.loadingView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.height.mas_equalTo(15);
|
||||
make.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.height.mas_equalTo(self.mm_h - 10 - 40 + 2);
|
||||
make.leading.mas_equalTo(10);
|
||||
make.trailing.mas_equalTo(-10);
|
||||
make.top.mas_equalTo(10);
|
||||
}];
|
||||
MASAttachKeys(self.textView);
|
||||
[self.tipsIcon mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.mas_equalTo(self.textView.mas_bottom).mas_offset(14);
|
||||
make.leading.mas_equalTo(10);
|
||||
make.height.mas_equalTo(13);
|
||||
make.width.mas_equalTo(13);
|
||||
}];
|
||||
MASAttachKeys(self.tipsIcon);
|
||||
[self.tipsLabel sizeToFit];
|
||||
[self.tipsLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerY.mas_equalTo(self.tipsIcon.mas_centerY);
|
||||
make.leading.mas_equalTo(self.tipsIcon.mas_trailing).mas_offset(4);
|
||||
make.trailing.mas_equalTo(self.textView.mas_trailing);
|
||||
}];
|
||||
MASAttachKeys(self.tipsLabel);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
- (void)updateTransaltionViewByText:(NSString *)text translationViewStatus:(TUITranslationViewStatus)status {
|
||||
BOOL isTranslated = text.length > 0;
|
||||
UIColor *textColor = TUITranslationDynamicColor(@"translation_view_text_color", @"#000000");
|
||||
UIColor *bgColor = TUITranslationDynamicColor(@"translation_view_bg_color", @"#F2F7FF");
|
||||
if (status == TUITranslationViewStatusSecurityStrike) {
|
||||
bgColor = [UIColor tui_colorWithHex:@"#FA5151" alpha:0.16];
|
||||
textColor = TUITranslationDynamicColor(@"", @"#DA2222");
|
||||
}
|
||||
self.bgColor = bgColor;
|
||||
self.backgroundColor = bgColor;
|
||||
|
||||
if (isTranslated) {
|
||||
NSAttributedString *originAttributedText = [text getAdvancedFormatEmojiStringWithFont:[UIFont systemFontOfSize:16]
|
||||
textColor:textColor
|
||||
emojiLocations:nil];
|
||||
if (isRTL()) {
|
||||
self.textView.attributedText = rtlAttributeString(originAttributedText,NSTextAlignmentRight);
|
||||
}
|
||||
else {
|
||||
self.textView.attributedText = originAttributedText;
|
||||
}
|
||||
}
|
||||
self.textView.hidden = !isTranslated;
|
||||
self.tipsIcon.hidden = !isTranslated;
|
||||
self.tipsLabel.hidden = !isTranslated;
|
||||
self.retryView.hidden = !(status == TUITranslationViewStatusSecurityStrike);
|
||||
|
||||
}
|
||||
|
||||
#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];
|
||||
TUITranslationViewStatus status = [TUITranslationDataProvider getTranslationStatus:self.cellData.innerMessage];
|
||||
BOOL hasRiskContent = (status == TUITranslationViewStatusSecurityStrike);
|
||||
|
||||
@weakify(self);
|
||||
TUIChatPopMenuAction *copy = [[TUIChatPopMenuAction alloc] initWithTitle:TIMCommonLocalizableString(Copy)
|
||||
image:TUITranslationBundleThemeImage(@"translation_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:TUITranslationBundleThemeImage(@"translation_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:TUITranslationBundleThemeImage(@"translation_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 notifyTranslationForward:text];
|
||||
}
|
||||
|
||||
- (void)onHide:(id)sender {
|
||||
self.cellData.bottomContainerSize = CGSizeZero;
|
||||
[TUITranslationDataProvider saveTranslationResult:self.cellData.innerMessage text:@"" status:TUITranslationViewStatusHidden];
|
||||
[self removeFromSuperview];
|
||||
[self notifyTranslationViewHidden];
|
||||
}
|
||||
|
||||
#pragma mark-- Notify
|
||||
- (void)notifyTranslationViewShown {
|
||||
[self notifyTranslationChanged];
|
||||
}
|
||||
|
||||
- (void)notifyTranslationViewHidden {
|
||||
[self notifyTranslationChanged];
|
||||
}
|
||||
|
||||
- (void)notifyTranslationForward:(NSString *)text {
|
||||
NSDictionary *param = @{TUICore_TUIPluginNotify_WillForwardTextSubKey_Text : text};
|
||||
[TUICore notifyEvent:TUICore_TUIPluginNotify
|
||||
subKey:TUICore_TUIPluginNotify_WillForwardTextSubKey
|
||||
object:nil
|
||||
param:param];
|
||||
}
|
||||
|
||||
- (void)notifyTranslationChanged {
|
||||
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
|
||||