mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
095b068f63
commit
de6836fa5d
@ -106,6 +106,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case experimentalCallMute(Bool)
|
||||
case liveStreamV2(Bool)
|
||||
case dynamicStreaming(Bool)
|
||||
case enableLocalTranslation(Bool)
|
||||
case preferredVideoCodec(Int, String, String?, Bool)
|
||||
case disableVideoAspectScaling(Bool)
|
||||
case enableNetworkFramework(Bool)
|
||||
@ -130,7 +131,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.web.rawValue
|
||||
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2, .dynamicStreaming:
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2, .dynamicStreaming, .enableLocalTranslation:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .logTranslationRecognition, .resetTranslationStates:
|
||||
return DebugControllerSection.translation.rawValue
|
||||
@ -251,8 +252,10 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 53
|
||||
case .dynamicStreaming:
|
||||
return 54
|
||||
case .enableLocalTranslation:
|
||||
return 55
|
||||
case let .preferredVideoCodec(index, _, _, _):
|
||||
return 55 + index
|
||||
return 56 + index
|
||||
case .disableVideoAspectScaling:
|
||||
return 100
|
||||
case .enableNetworkFramework:
|
||||
@ -1361,6 +1364,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .enableLocalTranslation(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Local Translation", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
settings.enableLocalTranslation = value
|
||||
return PreferencesEntry(settings)
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .preferredVideoCodec(_, title, value, isSelected):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
@ -1519,6 +1532,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute))
|
||||
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))
|
||||
entries.append(.dynamicStreaming(experimentalSettings.dynamicStreaming))
|
||||
entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation))
|
||||
}
|
||||
|
||||
/*let codecs: [(String, String?)] = [
|
||||
|
@ -246,12 +246,14 @@ public func galleryItemForEntry(
|
||||
} else {
|
||||
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||
var isHLS = false
|
||||
if NativeVideoContent.isHLSVideo(file: file) {
|
||||
isHLS = true
|
||||
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let disableHLS = data["video_ignore_alt_documents"] as? Double {
|
||||
if Int(disableHLS) != 0 {
|
||||
isHLS = false
|
||||
if #available(iOS 13.0, *) {
|
||||
if NativeVideoContent.isHLSVideo(file: file) {
|
||||
isHLS = true
|
||||
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let disableHLS = data["video_ignore_alt_documents"] as? Double {
|
||||
if Int(disableHLS) != 0 {
|
||||
isHLS = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,13 +119,15 @@ public final class ChunkMediaPlayerPart {
|
||||
public let startTime: Double
|
||||
public let endTime: Double
|
||||
public let file: TempBoxFile
|
||||
public let clippedStartTime: Double?
|
||||
|
||||
public var id: Id {
|
||||
return Id(rawValue: self.file.path)
|
||||
}
|
||||
|
||||
public init(startTime: Double, endTime: Double, file: TempBoxFile) {
|
||||
public init(startTime: Double, clippedStartTime: Double? = nil, endTime: Double, file: TempBoxFile) {
|
||||
self.startTime = startTime
|
||||
self.clippedStartTime = clippedStartTime
|
||||
self.endTime = endTime
|
||||
self.file = file
|
||||
}
|
||||
@ -620,59 +622,55 @@ private final class ChunkMediaPlayerContext {
|
||||
|
||||
var validParts: [ChunkMediaPlayerPart] = []
|
||||
|
||||
var minStartTime: Double = 0.0
|
||||
for i in 0 ..< self.partsState.parts.count {
|
||||
let part = self.partsState.parts[i]
|
||||
|
||||
let partStartTime = max(minStartTime, part.startTime)
|
||||
let partEndTime = max(partStartTime, part.endTime)
|
||||
if partStartTime >= partEndTime {
|
||||
continue
|
||||
}
|
||||
|
||||
var partMatches = false
|
||||
if timestamp >= part.startTime - 0.5 && timestamp < part.endTime + 0.5 {
|
||||
if timestamp >= partStartTime - 0.5 && timestamp < partEndTime + 0.5 {
|
||||
partMatches = true
|
||||
}
|
||||
|
||||
if partMatches {
|
||||
validParts.append(part)
|
||||
validParts.append(ChunkMediaPlayerPart(
|
||||
startTime: part.startTime,
|
||||
clippedStartTime: partStartTime == part.startTime ? nil : partStartTime,
|
||||
endTime: part.endTime,
|
||||
file: part.file
|
||||
))
|
||||
minStartTime = max(minStartTime, partEndTime)
|
||||
}
|
||||
}
|
||||
|
||||
if let lastValidPart = validParts.last {
|
||||
for i in 0 ..< self.partsState.parts.count {
|
||||
let part = self.partsState.parts[i]
|
||||
if lastValidPart !== part && part.startTime > lastValidPart.startTime && part.startTime <= lastValidPart.endTime + 0.5 {
|
||||
validParts.append(part)
|
||||
|
||||
let partStartTime = max(minStartTime, part.startTime)
|
||||
let partEndTime = max(partStartTime, part.endTime)
|
||||
if partStartTime >= partEndTime {
|
||||
continue
|
||||
}
|
||||
|
||||
if lastValidPart !== part && partStartTime > (lastValidPart.clippedStartTime ?? lastValidPart.startTime) && partStartTime <= lastValidPart.endTime + 0.5 {
|
||||
validParts.append(ChunkMediaPlayerPart(
|
||||
startTime: part.startTime,
|
||||
clippedStartTime: partStartTime == part.startTime ? nil : partStartTime,
|
||||
endTime: part.endTime,
|
||||
file: part.file
|
||||
))
|
||||
minStartTime = max(minStartTime, partEndTime)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*for i in 0 ..< self.partsState.parts.count {
|
||||
let part = self.partsState.parts[i]
|
||||
var partMatches = false
|
||||
if timestamp >= part.startTime - 0.001 && timestamp < part.endTime - 0.001 {
|
||||
partMatches = true
|
||||
} else if part.startTime < 0.2 && timestamp < part.endTime - 0.001 {
|
||||
partMatches = true
|
||||
}
|
||||
|
||||
if !partMatches, i != self.partsState.parts.count - 1, part.startTime >= 0.001, timestamp >= part.startTime {
|
||||
let nextPart = self.partsState.parts[i + 1]
|
||||
if timestamp < nextPart.endTime - 0.001 {
|
||||
if part.endTime >= nextPart.startTime - 0.1 {
|
||||
partMatches = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if partMatches {
|
||||
validParts.append(part)
|
||||
|
||||
inner: for lookaheadPart in self.partsState.parts {
|
||||
if lookaheadPart.startTime >= part.endTime - 0.001 && lookaheadPart.startTime - 0.1 < part.endTime {
|
||||
validParts.append(lookaheadPart)
|
||||
break inner
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}*/
|
||||
|
||||
if validParts.isEmpty, let initialSeekTimestamp = self.initialSeekTimestamp {
|
||||
for part in self.partsState.parts {
|
||||
if initialSeekTimestamp >= part.startTime - 0.2 && initialSeekTimestamp < part.endTime {
|
||||
@ -701,6 +699,8 @@ private final class ChunkMediaPlayerContext {
|
||||
self.initialSeekTimestamp = nil
|
||||
}
|
||||
|
||||
//print("validParts: \(validParts.map { "\($0.startTime) ... \($0.endTime)" })")
|
||||
|
||||
self.loadedState.partStates.removeAll(where: { partState in
|
||||
if !validParts.contains(where: { $0.id == partState.part.id }) {
|
||||
return true
|
||||
@ -742,7 +742,13 @@ private final class ChunkMediaPlayerContext {
|
||||
for i in 0 ..< self.loadedState.partStates.count {
|
||||
let partState = self.loadedState.partStates[i]
|
||||
if partState.mediaBuffersDisposable == nil {
|
||||
partState.mediaBuffersDisposable = (partState.frameSource.seek(timestamp: i == 0 ? timestamp : 0.0)
|
||||
let partSeekOffset: Double
|
||||
if let clippedStartTime = partState.part.clippedStartTime {
|
||||
partSeekOffset = clippedStartTime - partState.part.startTime
|
||||
} else {
|
||||
partSeekOffset = 0.0
|
||||
}
|
||||
partState.mediaBuffersDisposable = (partState.frameSource.seek(timestamp: i == 0 ? timestamp : partSeekOffset)
|
||||
|> deliverOn(self.queue)).startStrict(next: { [weak self, weak partState] result in
|
||||
guard let self, let partState else {
|
||||
return
|
||||
@ -921,13 +927,14 @@ private final class ChunkMediaPlayerContext {
|
||||
|
||||
for partState in self.loadedState.partStates {
|
||||
if let audioTrackFrameBuffer = partState.mediaBuffers?.audioBuffer {
|
||||
//print("Poll audio: part \(partState.part.startTime) frames: \(audioTrackFrameBuffer.frames.map(\.pts.seconds))")
|
||||
let frame = audioTrackFrameBuffer.takeFrame()
|
||||
switch frame {
|
||||
case .finished:
|
||||
continue
|
||||
default:
|
||||
/*if case let .frame(frame) = frame {
|
||||
print("audio: \(frame.position.seconds) \(frame.position.value) next: (\(frame.position.value + frame.duration.value))")
|
||||
print("audio: \(frame.position.seconds) \(frame.position.value) part \(partState.part.startTime) next: (\(frame.position.value + frame.duration.value))")
|
||||
}*/
|
||||
return frame
|
||||
}
|
||||
|
@ -461,6 +461,40 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
invite: nil,
|
||||
activeCall: EngineGroupCallDescription(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: groupCallPanelData.info.scheduleTimestamp, subscribedToScheduled: groupCallPanelData.info.subscribedToScheduled, isStream: groupCallPanelData.info.isStream)
|
||||
)
|
||||
}, notifyScheduledTapAction: { [weak self] in
|
||||
guard let self, let groupCallPanelData = self.groupCallPanelData else {
|
||||
return
|
||||
}
|
||||
if groupCallPanelData.info.scheduleTimestamp != nil && !groupCallPanelData.info.subscribedToScheduled {
|
||||
let _ = self.context.engine.calls.toggleScheduledGroupCallSubscription(peerId: groupCallPanelData.peerId, callId: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, subscribe: true).startStandalone()
|
||||
|
||||
//TODO:localize
|
||||
let controller = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universal(
|
||||
animation: "anim_profileunmute",
|
||||
scale: 0.075,
|
||||
colors: [
|
||||
"Middle.Group 1.Fill 1": UIColor.white,
|
||||
"Top.Group 1.Fill 1": UIColor.white,
|
||||
"Bottom.Group 1.Fill 1": UIColor.white,
|
||||
"EXAMPLE.Group 1.Fill 1": UIColor.white,
|
||||
"Line.Group 1.Stroke 1": UIColor.white
|
||||
],
|
||||
title: nil,
|
||||
text: "You will be notified when the liver stream starts.",
|
||||
customUndoText: nil,
|
||||
timeout: nil
|
||||
),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in
|
||||
return true
|
||||
}
|
||||
)
|
||||
self.audioRateTooltipController = controller
|
||||
self.present(controller, in: .current)
|
||||
}
|
||||
})
|
||||
if let accessoryPanelContainer = self.accessoryPanelContainer {
|
||||
accessoryPanelContainer.addSubnode(groupCallAccessoryPanel)
|
||||
|
@ -113,6 +113,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
private var dateTimeFormat: PresentationDateTimeFormat
|
||||
|
||||
private let tapAction: () -> Void
|
||||
private let notifyScheduledTapAction: () -> Void
|
||||
|
||||
private let contentNode: ASDisplayNode
|
||||
|
||||
@ -163,13 +164,14 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
private var currentData: GroupCallPanelData?
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat, Bool)?
|
||||
|
||||
public init(context: AccountContext, presentationData: PresentationData, tapAction: @escaping () -> Void) {
|
||||
public init(context: AccountContext, presentationData: PresentationData, tapAction: @escaping () -> Void, notifyScheduledTapAction: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.theme = presentationData.theme
|
||||
self.strings = presentationData.strings
|
||||
self.dateTimeFormat = presentationData.dateTimeFormat
|
||||
|
||||
self.tapAction = tapAction
|
||||
self.notifyScheduledTapAction = notifyScheduledTapAction
|
||||
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
@ -234,7 +236,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
self.joinButton.addSubnode(self.joinButtonBackgroundNode)
|
||||
self.joinButton.addSubnode(self.joinButtonTitleNode)
|
||||
self.contentNode.addSubnode(self.joinButton)
|
||||
self.joinButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside])
|
||||
self.joinButton.addTarget(self, action: #selector(self.joinTapped), forControlEvents: [.touchUpInside])
|
||||
|
||||
self.micButton.addSubnode(self.micButtonBackgroundNode)
|
||||
self.micButton.addSubnode(self.micButtonForegroundNode)
|
||||
@ -267,6 +269,14 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
self.tapAction()
|
||||
}
|
||||
|
||||
@objc private func joinTapped() {
|
||||
if let info = self.currentData?.info, let _ = info.scheduleTimestamp, !info.subscribedToScheduled {
|
||||
self.notifyScheduledTapAction()
|
||||
} else {
|
||||
self.tapAction()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func micTapped() {
|
||||
guard let call = self.currentData?.groupCall else {
|
||||
return
|
||||
@ -645,7 +655,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
var text = self.currentText
|
||||
var isScheduled = false
|
||||
var isLate = false
|
||||
if let scheduleTime = self.currentData?.info.scheduleTimestamp {
|
||||
if let info = self.currentData?.info, let scheduleTime = info.scheduleTimestamp {
|
||||
isScheduled = true
|
||||
if let voiceChatTitle = self.currentData?.info.title {
|
||||
title = voiceChatTitle
|
||||
@ -655,15 +665,20 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
text = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime, alwaysShowTime: true, format: HumanReadableStringFormat(dateFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsOnShort($0) }, tomorrowFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsTomorrowShort($0) }, todayFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsTodayShort($0) })).string
|
||||
}
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let elapsedTime = scheduleTime - currentTime
|
||||
if elapsedTime >= 86400 {
|
||||
joinText = scheduledTimeIntervalString(strings: strings, value: elapsedTime)
|
||||
} else if elapsedTime < 0 {
|
||||
joinText = "-\(textForTimeout(value: abs(elapsedTime)))"
|
||||
isLate = true
|
||||
if info.subscribedToScheduled {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let elapsedTime = scheduleTime - currentTime
|
||||
if elapsedTime >= 86400 {
|
||||
joinText = scheduledTimeIntervalString(strings: strings, value: elapsedTime).uppercased()
|
||||
} else if elapsedTime < 0 {
|
||||
joinText = "-\(textForTimeout(value: abs(elapsedTime)))".uppercased()
|
||||
isLate = true
|
||||
} else {
|
||||
joinText = textForTimeout(value: elapsedTime).uppercased()
|
||||
}
|
||||
} else {
|
||||
joinText = textForTimeout(value: elapsedTime)
|
||||
//TODO:localize
|
||||
joinText = "Notify Me"
|
||||
}
|
||||
|
||||
if self.updateTimer == nil {
|
||||
@ -691,7 +706,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
self.updateJoinButton()
|
||||
}
|
||||
|
||||
self.joinButtonTitleNode.attributedText = NSAttributedString(string: joinText.uppercased(), font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: isScheduled ? .white : self.theme.chat.inputPanel.actionControlForegroundColor)
|
||||
self.joinButtonTitleNode.attributedText = NSAttributedString(string: joinText, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: isScheduled ? .white : self.theme.chat.inputPanel.actionControlForegroundColor)
|
||||
|
||||
let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
|
||||
let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0)
|
||||
|
@ -944,7 +944,9 @@ private func boxedDecryptedMessage(transaction: Transaction, message: Message, g
|
||||
if let attribute = attribute as? ReplyMessageAttribute {
|
||||
if let message = message.associatedMessages[attribute.messageId] {
|
||||
replyGlobalId = message.globallyUniqueId
|
||||
flags |= (1 << 3)
|
||||
if replyGlobalId != nil {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -244,40 +244,54 @@ public enum ToggleScheduledGroupCallSubscriptionError {
|
||||
}
|
||||
|
||||
func _internal_toggleScheduledGroupCallSubscription(account: Account, peerId: PeerId, callId: Int64, accessHash: Int64, subscribe: Bool) -> Signal<Void, ToggleScheduledGroupCallSubscriptionError> {
|
||||
return account.network.request(Api.functions.phone.toggleGroupCallStartSubscription(call: .inputGroupCall(id: callId, accessHash: accessHash), subscribed: subscribe ? .boolTrue : .boolFalse))
|
||||
|> mapError { error -> ToggleScheduledGroupCallSubscriptionError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Void, ToggleScheduledGroupCallSubscriptionError> in
|
||||
var parsedCall: GroupCallInfo?
|
||||
loop: for update in result.allUpdates {
|
||||
switch update {
|
||||
case let .updateGroupCall(_, call):
|
||||
parsedCall = GroupCallInfo(call)
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||
if let cachedData = cachedData as? CachedChannelData, let activeCall = cachedData.activeCall {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribedToScheduled: true, isStream: activeCall.isStream))
|
||||
} else if let cachedData = cachedData as? CachedGroupData, let activeCall = cachedData.activeCall {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribedToScheduled: true, isStream: activeCall.isStream))
|
||||
} else {
|
||||
return cachedData
|
||||
}
|
||||
})
|
||||
}
|
||||
|> castError(ToggleScheduledGroupCallSubscriptionError.self)
|
||||
|> mapToSignal { _ -> Signal<Void, ToggleScheduledGroupCallSubscriptionError> in
|
||||
return account.network.request(Api.functions.phone.toggleGroupCallStartSubscription(call: .inputGroupCall(id: callId, accessHash: accessHash), subscribed: subscribe ? .boolTrue : .boolFalse))
|
||||
|> mapError { error -> ToggleScheduledGroupCallSubscriptionError in
|
||||
return .generic
|
||||
}
|
||||
|
||||
guard let callInfo = parsedCall else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled, isStream: callInfo.isStream))
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled, isStream: callInfo.isStream))
|
||||
} else {
|
||||
return cachedData
|
||||
|> mapToSignal { result -> Signal<Void, ToggleScheduledGroupCallSubscriptionError> in
|
||||
var parsedCall: GroupCallInfo?
|
||||
loop: for update in result.allUpdates {
|
||||
switch update {
|
||||
case let .updateGroupCall(_, call):
|
||||
parsedCall = GroupCallInfo(call)
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
account.stateManager.addUpdates(result)
|
||||
guard let callInfo = parsedCall else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled, isStream: callInfo.isStream))
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled, isStream: callInfo.isStream))
|
||||
} else {
|
||||
return cachedData
|
||||
}
|
||||
})
|
||||
|
||||
account.stateManager.addUpdates(result)
|
||||
}
|
||||
|> castError(ToggleScheduledGroupCallSubscriptionError.self)
|
||||
}
|
||||
|> castError(ToggleScheduledGroupCallSubscriptionError.self)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -536,8 +536,8 @@ public extension TelegramEngine {
|
||||
return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang)
|
||||
}
|
||||
|
||||
public func translateMessages(messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, TranslationError> {
|
||||
return _internal_translateMessages(account: self.account, messageIds: messageIds, toLang: toLang)
|
||||
public func translateMessages(messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Never, TranslationError> {
|
||||
return _internal_translateMessages(account: self.account, messageIds: messageIds, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: enableLocalIfPossible)
|
||||
}
|
||||
|
||||
public func togglePeerMessagesTranslationHidden(peerId: EnginePeer.Id, hidden: Bool) -> Signal<Never, NoError> {
|
||||
|
@ -84,16 +84,22 @@ func _internal_translate_texts(network: Network, texts: [(String, [MessageTextEn
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, TranslationError> {
|
||||
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Never, TranslationError> {
|
||||
var signals: [Signal<Void, TranslationError>] = []
|
||||
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
|
||||
signals.append(_internal_translateMessagesByPeerId(account: account, peerId: peerId, messageIds: messageIds, toLang: toLang))
|
||||
signals.append(_internal_translateMessagesByPeerId(account: account, peerId: peerId, messageIds: messageIds, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: enableLocalIfPossible))
|
||||
}
|
||||
return combineLatest(signals)
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
private func _internal_translateMessagesByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, TranslationError> {
|
||||
public protocol ExperimentalInternalTranslationService: AnyObject {
|
||||
func translate(texts: [AnyHashable: String], fromLang: String, toLang: String) -> Signal<[AnyHashable: String]?, NoError>
|
||||
}
|
||||
|
||||
public var engineExperimentalInternalTranslationService: ExperimentalInternalTranslationService?
|
||||
|
||||
private func _internal_translateMessagesByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Void, TranslationError> {
|
||||
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
|
||||
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
|
||||
}
|
||||
@ -132,21 +138,58 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
|
||||
if id.isEmpty {
|
||||
msgs = .single(nil)
|
||||
} else {
|
||||
msgs = account.network.request(Api.functions.messages.translateText(flags: flags, peer: inputPeer, id: id, text: nil, toLang: toLang))
|
||||
|> map(Optional.init)
|
||||
|> mapError { error -> TranslationError in
|
||||
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
|
||||
return .limitExceeded
|
||||
} else if error.errorDescription == "MSG_ID_INVALID" {
|
||||
return .invalidMessageId
|
||||
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
|
||||
return .textIsEmpty
|
||||
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
|
||||
return .textTooLong
|
||||
} else if error.errorDescription == "TO_LANG_INVALID" {
|
||||
return .invalidLanguage
|
||||
} else {
|
||||
return .generic
|
||||
if enableLocalIfPossible, let engineExperimentalInternalTranslationService, let fromLang {
|
||||
msgs = account.postbox.transaction { transaction -> [MessageId: String] in
|
||||
var texts: [MessageId: String] = [:]
|
||||
for messageId in messageIds {
|
||||
if let message = transaction.getMessage(messageId) {
|
||||
texts[message.id] = message.text
|
||||
}
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|> castError(TranslationError.self)
|
||||
|> mapToSignal { messageTexts -> Signal<Api.messages.TranslatedText?, TranslationError> in
|
||||
var mappedTexts: [AnyHashable: String] = [:]
|
||||
for (id, text) in messageTexts {
|
||||
mappedTexts[AnyHashable(id)] = text
|
||||
}
|
||||
return engineExperimentalInternalTranslationService.translate(texts: mappedTexts, fromLang: fromLang, toLang: toLang)
|
||||
|> castError(TranslationError.self)
|
||||
|> mapToSignal { resultTexts -> Signal<Api.messages.TranslatedText?, TranslationError> in
|
||||
guard let resultTexts else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
var result: [Api.TextWithEntities] = []
|
||||
for messageId in messageIds {
|
||||
if let text = resultTexts[AnyHashable(messageId)] {
|
||||
result.append(.textWithEntities(text: text, entities: []))
|
||||
} else if let text = messageTexts[messageId] {
|
||||
result.append(.textWithEntities(text: text, entities: []))
|
||||
} else {
|
||||
result.append(.textWithEntities(text: "", entities: []))
|
||||
}
|
||||
}
|
||||
return .single(.translateResult(result: result))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msgs = account.network.request(Api.functions.messages.translateText(flags: flags, peer: inputPeer, id: id, text: nil, toLang: toLang))
|
||||
|> map(Optional.init)
|
||||
|> mapError { error -> TranslationError in
|
||||
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
|
||||
return .limitExceeded
|
||||
} else if error.errorDescription == "MSG_ID_INVALID" {
|
||||
return .invalidMessageId
|
||||
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
|
||||
return .textIsEmpty
|
||||
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
|
||||
return .textTooLong
|
||||
} else if error.errorDescription == "TO_LANG_INVALID" {
|
||||
return .invalidLanguage
|
||||
} else {
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import MediaEditor
|
||||
import TelegramUIDeclareEncodables
|
||||
import ContextMenuScreen
|
||||
import MetalEngine
|
||||
import TranslateUI
|
||||
|
||||
#if canImport(AppCenter)
|
||||
import AppCenter
|
||||
@ -362,6 +363,12 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if #available(iOS 18.0, *) {
|
||||
let translationService = ExperimentalInternalTranslationServiceImpl(view: hostView.containerView)
|
||||
engineExperimentalInternalTranslationService = translationService
|
||||
}
|
||||
#endif
|
||||
|
||||
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
|
||||
if #available(iOS 10.0, *) {
|
||||
|
@ -719,7 +719,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
|
||||
private let clientId: Atomic<Int32>
|
||||
|
||||
private var toLang: String?
|
||||
private var translationLang: (fromLang: String?, toLang: String)?
|
||||
|
||||
private var allowDustEffect: Bool = true
|
||||
private var dustEffectLayer: DustEffectLayer?
|
||||
@ -838,13 +838,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
||||
}
|
||||
self.translationProcessingManager.process = { [weak self, weak context] messageIds in
|
||||
if let context = context, let toLang = self?.toLang {
|
||||
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang).startStandalone()
|
||||
if let context = context, let translationLang = self?.translationLang {
|
||||
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone()
|
||||
}
|
||||
}
|
||||
self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in
|
||||
if let context = context, let toLang = self?.toLang {
|
||||
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang).startStandalone()
|
||||
if let context = context, let translationLang = self?.translationLang {
|
||||
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1848,17 +1848,17 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
providedByGroupBoost: audioTranscriptionProvidedByBoost
|
||||
)
|
||||
|
||||
var translateToLanguage: String?
|
||||
var translateToLanguage: (fromLang: String, toLang: String)?
|
||||
if let translationState, isPremium && translationState.isEnabled {
|
||||
var languageCode = translationState.toLang ?? chatPresentationData.strings.baseLanguageCode
|
||||
let rawSuffix = "-raw"
|
||||
if languageCode.hasSuffix(rawSuffix) {
|
||||
languageCode = String(languageCode.dropLast(rawSuffix.count))
|
||||
}
|
||||
translateToLanguage = normalizeTranslationLanguage(languageCode)
|
||||
translateToLanguage = (normalizeTranslationLanguage(translationState.fromLang), normalizeTranslationLanguage(languageCode))
|
||||
}
|
||||
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"))
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage?.toLang, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"))
|
||||
|
||||
var includeEmbeddedSavedChatInfo = false
|
||||
if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated {
|
||||
@ -1958,7 +1958,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
|
||||
var scrollAnimationCurve: ListViewAnimationCurve? = nil
|
||||
if let strongSelf = self, case .default = source {
|
||||
strongSelf.toLang = translateToLanguage
|
||||
if let translateToLanguage {
|
||||
strongSelf.translationLang = (fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang)
|
||||
} else {
|
||||
strongSelf.translationLang = nil
|
||||
}
|
||||
if strongSelf.appliedScrollToMessageId == nil, let scrollToMessageId = scrollToMessageId {
|
||||
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(scrollToMessageId), quote: nil), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true, setupReply: false)
|
||||
scrollAnimationCurve = .Spring(duration: 0.4)
|
||||
|
@ -72,7 +72,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
private var currentLayout: (CGFloat, CGFloat, CGFloat)?
|
||||
private var currentMessage: ChatPinnedMessage?
|
||||
private var previousMediaReference: AnyMediaReference?
|
||||
private var currentTranslateToLanguage: String?
|
||||
private var currentTranslateToLanguage: (fromLang: String, toLang: String)?
|
||||
private let translationDisposable = MetaDisposable()
|
||||
|
||||
private var isReplyThread: Bool = false
|
||||
@ -496,21 +496,21 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
||||
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
||||
|
||||
var translateToLanguage: String?
|
||||
var translateToLanguage: (fromLang: String, toLang: String)?
|
||||
if let translationState = interfaceState.translationState, translationState.isEnabled {
|
||||
translateToLanguage = normalizeTranslationLanguage(translationState.toLang)
|
||||
translateToLanguage = (normalizeTranslationLanguage(translationState.fromLang), normalizeTranslationLanguage(translationState.toLang))
|
||||
}
|
||||
|
||||
var currentTranslateToLanguageUpdated = false
|
||||
if self.currentTranslateToLanguage != translateToLanguage {
|
||||
if self.currentTranslateToLanguage?.fromLang != translateToLanguage?.fromLang || self.currentTranslateToLanguage?.toLang != translateToLanguage?.toLang {
|
||||
self.currentTranslateToLanguage = translateToLanguage
|
||||
currentTranslateToLanguageUpdated = true
|
||||
}
|
||||
|
||||
if currentTranslateToLanguageUpdated || messageUpdated, let message = interfaceState.pinnedMessage?.message {
|
||||
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage {
|
||||
} else if let translateToLanguage {
|
||||
self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], toLang: translateToLanguage).startStrict())
|
||||
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage?.toLang {
|
||||
} else if let translateToLanguage {
|
||||
self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang).startStrict())
|
||||
}
|
||||
}
|
||||
|
||||
@ -522,7 +522,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
|
||||
if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout {
|
||||
self.dustNode?.update(revealed: false, animated: false)
|
||||
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread, translateToLanguage: translateToLanguage)
|
||||
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread, translateToLanguage: translateToLanguage?.toLang)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
public var disableReloginTokens: Bool
|
||||
public var liveStreamV2: Bool
|
||||
public var dynamicStreaming: Bool
|
||||
public var enableLocalTranslation: Bool
|
||||
|
||||
public static var defaultSettings: ExperimentalUISettings {
|
||||
return ExperimentalUISettings(
|
||||
@ -97,7 +98,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
allowWebViewInspection: false,
|
||||
disableReloginTokens: false,
|
||||
liveStreamV2: false,
|
||||
dynamicStreaming: false
|
||||
dynamicStreaming: false,
|
||||
enableLocalTranslation: false
|
||||
)
|
||||
}
|
||||
|
||||
@ -136,7 +138,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
allowWebViewInspection: Bool,
|
||||
disableReloginTokens: Bool,
|
||||
liveStreamV2: Bool,
|
||||
dynamicStreaming: Bool
|
||||
dynamicStreaming: Bool,
|
||||
enableLocalTranslation: Bool
|
||||
) {
|
||||
self.keepChatNavigationStack = keepChatNavigationStack
|
||||
self.skipReadHistory = skipReadHistory
|
||||
@ -173,6 +176,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.disableReloginTokens = disableReloginTokens
|
||||
self.liveStreamV2 = liveStreamV2
|
||||
self.dynamicStreaming = dynamicStreaming
|
||||
self.enableLocalTranslation = enableLocalTranslation
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -213,6 +217,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false
|
||||
self.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false
|
||||
self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming") ?? false
|
||||
self.enableLocalTranslation = try container.decodeIfPresent(Bool.self, forKey: "enableLocalTranslation") ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -253,6 +258,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens")
|
||||
try container.encode(self.liveStreamV2, forKey: "liveStreamV2")
|
||||
try container.encode(self.dynamicStreaming, forKey: "dynamicStreaming")
|
||||
try container.encode(self.enableLocalTranslation, forKey: "enableLocalTranslation")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ public func updateChatTranslationStateInteractively(engine: TelegramEngine, peer
|
||||
@available(iOS 12.0, *)
|
||||
private let languageRecognizer = NLLanguageRecognizer()
|
||||
|
||||
public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, NoError> {
|
||||
public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String) -> Signal<Never, NoError> {
|
||||
return context.account.postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||
var messageIdsToTranslate: [EngineMessage.Id] = []
|
||||
var messageIdsSet = Set<EngineMessage.Id>()
|
||||
@ -159,7 +159,7 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess
|
||||
}
|
||||
}
|
||||
}
|
||||
return context.engine.messages.translateMessages(messageIds: messageIdsToTranslate, toLang: toLang)
|
||||
return context.engine.messages.translateMessages(messageIds: messageIdsToTranslate, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: context.sharedContext.immediateExperimentalUISettings.enableLocalTranslation)
|
||||
|> `catch` { _ -> Signal<Never, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ import SwiftSignalKit
|
||||
import AccountContext
|
||||
import NaturalLanguage
|
||||
import TelegramCore
|
||||
import SwiftUI
|
||||
import Translation
|
||||
import Combine
|
||||
|
||||
// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker
|
||||
private final class LinkHelperClass: NSObject {
|
||||
@ -213,3 +216,190 @@ public func systemLanguageCodes() -> [String] {
|
||||
}
|
||||
return languages
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
class ExternalTranslationTrigger: ObservableObject {
|
||||
@Published var shouldInvalidate: Int = 0
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
private struct TranslationViewImpl: View {
|
||||
@State private var configuration: TranslationSession.Configuration?
|
||||
@ObservedObject var externalCondition: ExternalTranslationTrigger
|
||||
private let taskContainer: Atomic<ExperimentalInternalTranslationServiceImpl.TranslationTaskContainer>
|
||||
|
||||
init(externalCondition: ExternalTranslationTrigger, taskContainer: Atomic<ExperimentalInternalTranslationServiceImpl.TranslationTaskContainer>) {
|
||||
self.externalCondition = externalCondition
|
||||
self.taskContainer = taskContainer
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text("ABC")
|
||||
.onChange(of: self.externalCondition.shouldInvalidate) { _ in
|
||||
let firstTaskLanguagePair = self.taskContainer.with { taskContainer -> (String, String)? in
|
||||
if let firstTask = taskContainer.tasks.first {
|
||||
return (firstTask.fromLang, firstTask.toLang)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if let firstTaskLanguagePair {
|
||||
if let configuration = self.configuration, configuration.source?.languageCode?.identifier == firstTaskLanguagePair.0, configuration.target?.languageCode?.identifier == firstTaskLanguagePair.1 {
|
||||
self.configuration?.invalidate()
|
||||
} else {
|
||||
self.configuration = .init(
|
||||
source: Locale.Language(identifier: firstTaskLanguagePair.0),
|
||||
target: Locale.Language(identifier: firstTaskLanguagePair.1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.translationTask(self.configuration, action: { session in
|
||||
var task: ExperimentalInternalTranslationServiceImpl.TranslationTask?
|
||||
task = self.taskContainer.with { taskContainer -> ExperimentalInternalTranslationServiceImpl.TranslationTask? in
|
||||
if !taskContainer.tasks.isEmpty {
|
||||
return taskContainer.tasks.removeFirst()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
guard let task else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
var nextClientIdentifier: Int = 0
|
||||
var clientIdentifierMap: [String: AnyHashable] = [:]
|
||||
let translationRequests = task.texts.map { key, value in
|
||||
let id = nextClientIdentifier
|
||||
nextClientIdentifier += 1
|
||||
clientIdentifierMap["\(id)"] = key
|
||||
return TranslationSession.Request(sourceText: value, clientIdentifier: "\(id)")
|
||||
}
|
||||
|
||||
let responses = try await session.translations(from: translationRequests)
|
||||
var resultMap: [AnyHashable: String] = [:]
|
||||
for response in responses {
|
||||
if let clientIdentifier = response.clientIdentifier, let originalKey = clientIdentifierMap[clientIdentifier] {
|
||||
resultMap[originalKey] = "<L>\(response.targetText)"
|
||||
}
|
||||
}
|
||||
|
||||
task.completion(resultMap)
|
||||
} catch let e {
|
||||
print("Translation error: \(e)")
|
||||
task.completion(nil)
|
||||
}
|
||||
|
||||
let firstTaskLanguagePair = self.taskContainer.with { taskContainer -> (String, String)? in
|
||||
if let firstTask = taskContainer.tasks.first {
|
||||
return (firstTask.fromLang, firstTask.toLang)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if let firstTaskLanguagePair {
|
||||
if let configuration = self.configuration, configuration.source?.languageCode?.identifier == firstTaskLanguagePair.0, configuration.target?.languageCode?.identifier == firstTaskLanguagePair.1 {
|
||||
self.configuration?.invalidate()
|
||||
} else {
|
||||
self.configuration = .init(
|
||||
source: Locale.Language(identifier: firstTaskLanguagePair.0),
|
||||
target: Locale.Language(identifier: firstTaskLanguagePair.1)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
public final class ExperimentalInternalTranslationServiceImpl: ExperimentalInternalTranslationService {
|
||||
fileprivate final class TranslationTask {
|
||||
let id: Int
|
||||
let texts: [AnyHashable: String]
|
||||
let fromLang: String
|
||||
let toLang: String
|
||||
let completion: ([AnyHashable: String]?) -> Void
|
||||
|
||||
init(id: Int, texts: [AnyHashable: String], fromLang: String, toLang: String, completion: @escaping ([AnyHashable: String]?) -> Void) {
|
||||
self.id = id
|
||||
self.texts = texts
|
||||
self.fromLang = fromLang
|
||||
self.toLang = toLang
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final class TranslationTaskContainer {
|
||||
var tasks: [TranslationTask] = []
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
private let hostingController: UIViewController
|
||||
|
||||
private let taskContainer = Atomic(value: TranslationTaskContainer())
|
||||
private let taskTrigger = ExternalTranslationTrigger()
|
||||
|
||||
private var nextId: Int = 0
|
||||
|
||||
init(view: UIView) {
|
||||
self.hostingController = UIHostingController(rootView: TranslationViewImpl(
|
||||
externalCondition: self.taskTrigger,
|
||||
taskContainer: self.taskContainer
|
||||
))
|
||||
|
||||
view.addSubview(self.hostingController.view)
|
||||
}
|
||||
|
||||
func translate(texts: [AnyHashable: String], fromLang: String, toLang: String, onResult: @escaping ([AnyHashable: String]?) -> Void) -> Disposable {
|
||||
let id = self.nextId
|
||||
self.nextId += 1
|
||||
self.taskContainer.with { taskContainer in
|
||||
taskContainer.tasks.append(TranslationTask(
|
||||
id: id,
|
||||
texts: texts,
|
||||
fromLang: fromLang,
|
||||
toLang: toLang,
|
||||
completion: { result in
|
||||
onResult(result)
|
||||
}
|
||||
))
|
||||
}
|
||||
self.taskTrigger.shouldInvalidate += 1
|
||||
|
||||
return ActionDisposable { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.taskContainer.with { taskContainer in
|
||||
taskContainer.tasks.removeAll(where: { $0.id == id })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
public init(view: UIView) {
|
||||
self.impl = QueueLocalObject(queue: .mainQueue(), generate: {
|
||||
return Impl(view: view)
|
||||
})
|
||||
}
|
||||
|
||||
public func translate(texts: [AnyHashable: String], fromLang: String, toLang: String) -> Signal<[AnyHashable: String]?, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.translate(texts: texts, fromLang: fromLang, toLang: toLang, onResult: { result in
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user