This commit is contained in:
isaac 2026-04-29 16:55:40 +04:00
commit 3188c793bc
30 changed files with 391 additions and 174 deletions

View file

@ -16176,6 +16176,7 @@ Error: %8$@";
"CreatePoll.OptionsNeededOne" = "Add at least one option";
"CreatePoll.QuizCorrectOptionNeeded" = "Select a correct option";
"CreatePoll.QuizCorrectOptionNeededMultiple" = "Select at least one correct option";
"CreatePoll.QuizCountryNeeded" = "Select at least one country";
"Stars.Intro.Transaction.Commission.Title" = "%@ commission";
@ -16192,7 +16193,7 @@ Error: %8$@";
"CreatePoll.AllowedCountries.Countries_1" = "%@ country";
"CreatePoll.AllowedCountries.Countries_any" = "%@ countries";
"Chat.Poll.Restriction.Subscribers" = "Only subscribers of **%@** can vote";
"Chat.Poll.Restriction.Subscribers" = "Only subscribers of **%@** can vote.";
"Chat.Poll.Restriction.Subscribers.TimeLimit" = "Only subscribers who joined more than **24 hours** ago can vote.";
"Chat.Poll.Restriction.Country" = "Only users from %@ can vote.";
"Chat.Poll.Restriction.SubscribersCountry" = "Only subscribers of **%@** from %@ can vote.";
@ -16232,3 +16233,26 @@ Error: %8$@";
"Login.Fee.GetPremiumForDays" = "Get Telegram Premium for %@";
"Login.Fee.GetPremiumForDays.Days_1" = "%@ day";
"Login.Fee.GetPremiumForDays.Days_any" = "%@ days";
"PeerInfo.DeleteReaction" = "Delete Reaction";
"Chat.DeleteReactionInfo" = "Tap and hold to delete reaction.";
"Chat.AdminActionSheet.DeleteReactionTitle" = "Delete 1 Reaction";
"Chat.AdminActionSheet.DeleteAllMessages" = "Delete All Messages";
"Chat.AdminActionSheet.DeleteAllReactions" = "Delete All Reactions";
"Conversation.CalendarSearch.Title" = "Search";
"Conversation.CalendarSearch.Done" = "Done";
"ScheduleMessage.SilentPosting.YouEnabled" = "You will receive a silent notification";
"ScheduleMessage.SilentPosting.YouDisabled" = "You will be notified";
"ScheduleMessage.SilentPosting.UserEnabled" = "%@ will receive a silent notification";
"ScheduleMessage.SilentPosting.UserDisabled" = "%@ will be notified";
"ScheduleMessage.SilentPosting.GroupEnabled" = "Members will receive a silent notification";
"ScheduleMessage.SilentPosting.GroupDisabled" = "Members will be notified";
"ScheduleMessage.SilentPosting.ChannelEnabled" = "Subscribers will receive a silent notification";
"ScheduleMessage.SilentPosting.ChannelDisabled" = "Subscribers will be notified";
"Settings.ChatAutomation" = "Chat Automation";
"Settings.ChatAutomationInfo" = "Add a bot to reply to messages on your behalf.";
"Settings.ChatAutomationOff" = "Off";

View file

@ -20,6 +20,7 @@ private let markdownInlineHTMLInlineIntent = InlinePresentationIntent(rawValue:
private let markdownDefaultBlockImageDimensions = PixelDimensions(width: 1200, height: 900)
private let markdownDefaultInlineImageDimensions = PixelDimensions(width: 18, height: 18)
private let markdownImageParsingEnabled = false
private let markdownTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
private let markdownTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
private let markdownRawHTMLTagRegex = try! NSRegularExpression(pattern: #"</?([A-Za-z][A-Za-z0-9:-]*)\b[^>]*?>"#)
@ -353,6 +354,9 @@ private final class MarkdownConversionContext {
}
func resolveImage(attributes: [NSAttributedString.Key: Any]) -> MarkdownResolvedImage? {
guard markdownImageParsingEnabled else {
return nil
}
guard let imageUrl = markdownImageURL(attributes: attributes) else {
return nil
}
@ -1166,10 +1170,8 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
switch level {
case Int.min ... 1:
return [.title(text)]
case 2:
return [.header(text)]
default:
return [.heading(text: text, level: Int32(max(3, min(level, 6))))]
return [.heading(text: text, level: Int32(max(2, min(level, 6))))]
}
case .paragraph:
guard let inlineContent = markdownInlineContent(from: node.attributedText, context: context) else {

View file

@ -146,7 +146,7 @@ private final class CameraContext {
transform = CGAffineTransformTranslate(transform, 0.0, -size.height)
ciImage = ciImage.transformed(by: transform)
}
ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 100.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size))
ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 60.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size))
if let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) {
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
if front {
@ -189,6 +189,7 @@ private final class CameraContext {
deinit {
Logger.shared.log("CameraContext", "deinit")
NotificationCenter.default.removeObserver(self)
}
private var isSessionRunning = false
@ -202,7 +203,7 @@ private final class CameraContext {
}
func stopCapture(invalidate: Bool = false) {
Logger.shared.log("CameraContext", "startCapture(invalidate: \(invalidate))")
Logger.shared.log("CameraContext", "stopCapture(invalidate: \(invalidate))")
if invalidate {
self.mainDeviceContext?.device.resetZoom()
@ -212,6 +213,7 @@ private final class CameraContext {
}
self.session.session.stopRunning()
self.isSessionRunning = false
}
func focus(at point: CGPoint, autoFocus: Bool) {
@ -228,7 +230,7 @@ private final class CameraContext {
}
func setFps(_ fps: Float64) {
self.mainDeviceContext?.device.fps = fps
self.mainDeviceContext?.device.setFps(fps)
}
private var modeChange: Camera.ModeChange = .none {
@ -1170,6 +1172,7 @@ public struct CameraRecordingData {
}
public enum CameraRecordingError {
case videoRecorderInitializationError
case audioInitializationError
}

View file

@ -141,10 +141,12 @@ final class CameraDevice {
Logger.shared.log("Camera", "No format selected")
}
#if DEBUG
Logger.shared.log("Camera", "Available formats:")
for format in device.formats {
Logger.shared.log("Camera", format.description)
}
#endif
if let targetFPS = device.actualFPS(maxFramerate) {
device.activeVideoMinFrameDuration = targetFPS.duration
@ -180,18 +182,16 @@ final class CameraDevice {
self.setFocusPoint(CGPoint(x: 0.5, y: 0.5), focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: false)
}
var fps: Double = defaultFPS {
didSet {
guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else {
return
}
self.fps = targetFPS.fps
self.transaction(device) { device in
device.activeVideoMinFrameDuration = targetFPS.duration
device.activeVideoMaxFrameDuration = targetFPS.duration
}
private(set) var fps: Double = defaultFPS
func setFps(_ fps: Double) {
guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else {
return
}
self.fps = targetFPS.fps
self.transaction(device) { device in
device.activeVideoMinFrameDuration = targetFPS.duration
device.activeVideoMaxFrameDuration = targetFPS.duration
}
}

View file

@ -93,6 +93,11 @@ public struct CameraCode: Equatable {
}
final class CameraOutput: NSObject {
private struct RoundVideoFormatDescriptionCacheEntry {
let sourceFormatDescription: CMFormatDescription
let outputFormatDescription: CMFormatDescription
}
let exclusive: Bool
let ciContext: CIContext
let colorSpace: CGColorSpace
@ -111,13 +116,14 @@ final class CameraOutput: NSObject {
private var roundVideoFilter: CameraRoundLegacyVideoFilter?
private let semaphore = DispatchSemaphore(value: 1)
private var roundVideoFormatDescriptionCache: [RoundVideoFormatDescriptionCacheEntry] = []
private let videoQueue = DispatchQueue(label: "", qos: .userInitiated)
private let audioQueue = DispatchQueue(label: "")
private let metadataQueue = DispatchQueue(label: "")
private var photoCaptureRequests: [Int64: PhotoCaptureContext] = [:]
private var photoCaptureRequests = Atomic<[Int64: PhotoCaptureContext]>(value: [:])
private var videoRecorder: VideoRecorder?
private var captureOrientation: AVCaptureVideoOrientation = .portrait
@ -268,8 +274,8 @@ final class CameraOutput: NSObject {
return EmptyDisposable
}
subscriber.putNext(self.photoOutput.isFlashScene)
let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { device, _ in
subscriber.putNext(self.photoOutput.isFlashScene)
let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { output, _ in
subscriber.putNext(output.isFlashScene)
})
return ActionDisposable {
observer.invalidate()
@ -316,12 +322,20 @@ final class CameraOutput: NSObject {
#else
let uniqueId = settings.uniqueID
let photoCapture = PhotoCaptureContext(ciContext: self.ciContext, settings: settings, orientation: orientation, mirror: mirror)
self.photoCaptureRequests[uniqueId] = photoCapture
let _ = self.photoCaptureRequests.modify { dict in
var dict = dict
dict[uniqueId] = photoCapture
return dict
}
self.photoOutput.capturePhoto(with: settings, delegate: photoCapture)
return photoCapture.signal
|> afterDisposed { [weak self] in
self?.photoCaptureRequests.removeValue(forKey: uniqueId)
let _ = self?.photoCaptureRequests.modify { dict in
var dict = dict
dict.removeValue(forKey: uniqueId)
return dict
}
}
#endif
}
@ -419,18 +433,21 @@ final class CameraOutput: NSObject {
}
}
)
guard let videoRecorder else {
return .fail(.videoRecorderInitializationError)
}
videoRecorder?.start()
videoRecorder.start()
self.videoRecorder = videoRecorder
if case .dualCamera = mode, let position {
videoRecorder?.markPositionChange(position: position, time: .zero)
videoRecorder.markPositionChange(position: position, time: .zero)
} else if case .roundVideo = mode {
additionalOutput?.masterOutput = self
}
return Signal { subscriber in
let timer = SwiftSignalKit.Timer(timeout: 0.033, repeat: true, completion: { [weak videoRecorder] in
let timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { [weak videoRecorder] in
let recordingData = CameraRecordingData(duration: videoRecorder?.duration ?? 0.0, filePath: outputFilePath)
subscriber.putNext(recordingData)
}, queue: Queue.mainQueue())
@ -456,13 +473,54 @@ final class CameraOutput: NSObject {
}
var transitionImage: UIImage? {
return self.videoRecorder?.transitionImage
var result: UIImage?
self.videoQueue.sync {
result = self.videoRecorder?.transitionImage
}
return result
}
private weak var masterOutput: CameraOutput?
private var lastSampleTimestamp: CMTime?
private func roundVideoFormatDescription(for sourceFormatDescription: CMFormatDescription) -> CMFormatDescription? {
if let entry = self.roundVideoFormatDescriptionCache.first(where: { CFEqual($0.sourceFormatDescription, sourceFormatDescription) }) {
return entry.outputFormatDescription
}
guard let extensions = CMFormatDescriptionGetExtensions(sourceFormatDescription) as? [String: Any] else {
return nil
}
let mediaSubType = CMFormatDescriptionGetMediaSubType(sourceFormatDescription)
var updatedExtensions = extensions
updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4
var outputFormatDescription: CMFormatDescription?
let status = CMVideoFormatDescriptionCreate(
allocator: nil,
codecType: mediaSubType,
width: videoMessageDimensions.width,
height: videoMessageDimensions.height,
extensions: updatedExtensions as CFDictionary,
formatDescriptionOut: &outputFormatDescription
)
guard status == noErr, let outputFormatDescription else {
return nil
}
self.roundVideoFormatDescriptionCache.append(RoundVideoFormatDescriptionCacheEntry(
sourceFormatDescription: sourceFormatDescription,
outputFormatDescription: outputFormatDescription
))
if self.roundVideoFormatDescriptionCache.count > 4 {
self.roundVideoFormatDescriptionCache.removeFirst(self.roundVideoFormatDescriptionCache.count - 4)
}
return outputFormatDescription
}
private var needsCrossfadeTransition = false
private var crossfadeTransitionStart: Double = 0.0
@ -564,17 +622,11 @@ final class CameraOutput: NSObject {
return nil
}
self.semaphore.wait()
let mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription)
let extensions = CMFormatDescriptionGetExtensions(formatDescription) as! [String: Any]
var updatedExtensions = extensions
updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4
var newFormatDescription: CMFormatDescription?
var status = CMVideoFormatDescriptionCreate(allocator: nil, codecType: mediaSubType, width: videoMessageDimensions.width, height: videoMessageDimensions.height, extensions: updatedExtensions as CFDictionary, formatDescriptionOut: &newFormatDescription)
guard status == noErr, let newFormatDescription else {
defer {
self.semaphore.signal()
}
guard let newFormatDescription = self.roundVideoFormatDescription(for: formatDescription) else {
return nil
}
@ -585,12 +637,11 @@ final class CameraOutput: NSObject {
filter = CameraRoundLegacyVideoFilter(ciContext: self.ciContext, colorSpace: self.colorSpace, simple: self.exclusive)
self.roundVideoFilter = filter
}
if !filter.isPrepared {
if !filter.isPrepared || filter.inputFormatDescription.map({ !CFEqual($0, newFormatDescription) }) ?? true {
filter.prepare(with: newFormatDescription, outputRetainedBufferCountHint: 4)
}
guard let newPixelBuffer = filter.render(pixelBuffer: videoPixelBuffer, additional: additional, captureOrientation: self.captureOrientation, transitionFactor: transitionFactor) else {
self.semaphore.signal()
return nil
}
@ -603,7 +654,7 @@ final class CameraOutput: NSObject {
}
var newSampleBuffer: CMSampleBuffer?
status = CMSampleBufferCreateForImageBuffer(
let status = CMSampleBufferCreateForImageBuffer(
allocator: kCFAllocatorDefault,
imageBuffer: newPixelBuffer,
dataReady: true,
@ -615,10 +666,8 @@ final class CameraOutput: NSObject {
)
if status == noErr, let newSampleBuffer {
self.semaphore.signal()
return newSampleBuffer
}
self.semaphore.signal()
return nil
}
@ -640,18 +689,18 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA
guard CMSampleBufferDataIsReady(sampleBuffer) else {
return
}
if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection)
} else if sampleBuffer.type == kCMMediaType_Audio {
self.processAudioBuffer?(sampleBuffer)
}
if let masterOutput = self.masterOutput {
masterOutput.processVideoRecording(sampleBuffer, fromAdditionalOutput: true)
} else {
self.processVideoRecording(sampleBuffer, fromAdditionalOutput: false)
}
if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection)
} else if sampleBuffer.type == kCMMediaType_Audio {
self.processAudioBuffer?(sampleBuffer)
}
}
func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {

View file

@ -56,6 +56,7 @@ final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate {
} else {
guard let photoPixelBuffer = photo.pixelBuffer else {
print("Error occurred while capturing photo: Missing pixel buffer (\(String(describing: error)))")
self.pipe.putNext(.failed)
return
}

View file

@ -204,9 +204,7 @@ private final class VideoRecorderImpl {
let maxDate = Date(timeIntervalSinceNow: 0.05)
RunLoop.current.run(until: maxDate)
}
}
if let videoInput = self.videoInput {
let time = CACurrentMediaTime()
// if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
// print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
@ -366,7 +364,7 @@ private final class VideoRecorderImpl {
private func maybeFinish() {
dispatchPrecondition(condition: .onQueue(self.queue))
guard self.hasAllVideoBuffers && self.hasAllVideoBuffers && !self.stopped else {
guard self.hasAllVideoBuffers && (!self.configuration.hasAudio || self.hasAllAudioBuffers) && !self.stopped else {
return
}
let _ = self._stopped.modify { _ in return true }

View file

@ -7389,3 +7389,25 @@ private final class AdsInfoContextReferenceContentSource: ContextReferenceConten
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets)
}
}
public struct ChatListNavigationTarget {
public let chatListController: ChatListControllerImpl
public let popToController: ViewController?
}
public func resolveChatListNavigationTarget(navigationController: NavigationController, excluding excludedController: ViewController? = nil) -> ChatListNavigationTarget? {
for case let controller as ViewController in navigationController.viewControllers.reversed() {
if let excludedController, controller === excludedController {
continue
}
if let chatListController = controller as? ChatListControllerImpl, !chatListController.previewing {
return ChatListNavigationTarget(chatListController: chatListController, popToController: controller)
}
}
if let controller = navigationController.viewControllers.first as? TabBarController, let chatListController = controller.currentController as? ChatListControllerImpl {
return ChatListNavigationTarget(chatListController: chatListController, popToController: nil)
}
return nil
}

View file

@ -92,5 +92,18 @@ public func chatTextLinkEditController(
dismissImpl = { [weak alertController] in
alertController?.dismiss(completion: nil)
}
if link == nil {
Queue.mainQueue().after(0.1, {
let pasteboard = UIPasteboard.general
if pasteboard.hasURLs {
if inputState.value.string.isEmpty, let url = pasteboard.url?.absoluteString, !url.isEmpty {
let value = NSAttributedString(string: url)
inputState.setValue(value, selectionRange: 0 ..< value.length)
}
}
})
}
return alertController
}

View file

@ -138,6 +138,10 @@
- (void)createDismissViewIfNeeded
{
if (_dismissView != nil) {
return;
}
UIView *parentView = [self _parentView];
_dismissView = [[UIView alloc] initWithFrame:parentView.bounds];
@ -231,6 +235,12 @@
- (void)finishEditing {
if ([self.inputPanel dismissInput]) {
_editing = false;
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_dismissView.alpha = 0.0f;
} completion:^(BOOL finished) {
}];
if (self.finishedWithCaption != nil)
self.finishedWithCaption([_inputPanel caption]);

View file

@ -260,7 +260,11 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P
entries.append(.serversHeader(theme, strings.SocksProxySetup_SavedProxies))
entries.append(.addServer(theme, strings.SocksProxySetup_AddProxy, state.editing))
var index = 0
var existingServers = Set<ProxyServerSettings>()
for server in proxySettings.servers {
if !existingServers.insert(server).inserted {
continue
}
let status: ProxyServerStatus = statuses[server] ?? .checking
let displayStatus: DisplayProxyServerStatus
if proxySettings.enabled && server == proxySettings.activeServer {
@ -301,7 +305,7 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P
entries.append(.server(index, theme, strings, server, server == proxySettings.activeServer, displayStatus, ProxySettingsServerItemEditing(editable: true, editing: state.editing, revealed: state.revealedServer == server), proxySettings.enabled))
index += 1
}
if !proxySettings.servers.isEmpty {
if !existingServers.isEmpty {
entries.append(.shareProxyList(theme, strings.SocksProxySetup_ShareProxyList))
}

View file

@ -347,8 +347,11 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context:
}
}
} else {
settings.servers.append(proxyServerSettings)
if settings.servers.count == 1 {
let wasEmpty = settings.servers.isEmpty
if !settings.servers.contains(proxyServerSettings) {
settings.servers.append(proxyServerSettings)
}
if wasEmpty && settings.servers.count == 1 {
settings.activeServer = proxyServerSettings
}
}
@ -388,4 +391,3 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context:
return controller
}

View file

@ -3318,7 +3318,7 @@ func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Network, pe
resetForumTopics.insert(peerId)
}
for message in messages {
var peerIsForum = false
if let peerId = message.peerId {
@ -3952,6 +3952,7 @@ func replayFinalState(
var updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:]
var updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:]
var updatedEmojiGameInfo: EmojiGameInfo?
var recentlyUsedGuestChatBots = Set<PeerId>()
var holesFromPreviousStateMessageIds: [MessageId] = []
var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:]
@ -4350,6 +4351,15 @@ func replayFinalState(
}
}
}
if message.flags.contains(.Incoming), let authorId = message.authorId {
for attribute in message.attributes {
if let attribute = attribute as? GuestChatMessageAttribute, attribute.peerId == accountPeerId {
recentlyUsedGuestChatBots.insert(authorId)
break
}
}
}
}
if !message.flags.contains(.Incoming) && !message.flags.contains(.Unsent) {
if message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
@ -4359,7 +4369,7 @@ func replayFinalState(
if !message.flags.contains(.Incoming), message.forwardInfo == nil {
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), let peer = transaction.getPeer(message.id.peerId), peer.isCopyProtectionEnabled {
} else if message.id.peerId.namespace == Namespaces.Peer.CloudUser, let cachedUserData = transaction.getPeerCachedData(peerId: message.id.peerId) as? CachedUserData, cachedUserData.flags.contains(.copyProtectionEnabled) || cachedUserData.flags.contains(.myCopyProtectionEnabled) {
} else {
@ -5876,6 +5886,10 @@ func replayFinalState(
}
}
for peerId in recentlyUsedGuestChatBots {
_internal_addRecentlyUsedInlineBot(transaction: transaction, peerId: peerId)
}
if syncAttachMenuBots {
// addSynchronizeAttachMenuBotsOperation(transaction: transaction)
}

View file

@ -211,17 +211,21 @@ func _internal_managedRecentlyUsedInlineBots(postbox: Postbox, network: Network,
return updatedRemotePeers
}
func _internal_addRecentlyUsedInlineBot(transaction: Transaction, peerId: PeerId) {
var maxRating = 1.0
for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots) {
if let contents = entry.contents.get(RecentPeerItem.self) {
maxRating = max(maxRating, contents.rating)
}
}
if let entry = CodableEntry(RecentPeerItem(rating: maxRating)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, item: OrderedItemListEntry(id: RecentPeerItemId(peerId).rawValue, contents: entry), removeTailIfCountExceeds: 20)
}
}
func _internal_addRecentlyUsedInlineBot(postbox: Postbox, peerId: PeerId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
var maxRating = 1.0
for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots) {
if let contents = entry.contents.get(RecentPeerItem.self) {
maxRating = max(maxRating, contents.rating)
}
}
if let entry = CodableEntry(RecentPeerItem(rating: maxRating)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, item: OrderedItemListEntry(id: RecentPeerItemId(peerId).rawValue, contents: entry), removeTailIfCountExceeds: 20)
}
_internal_addRecentlyUsedInlineBot(transaction: transaction, peerId: peerId)
}
}

View file

@ -302,9 +302,8 @@ private func adminUserActionsTitle(
} else {
return strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount))
}
case .chatReaction(_):
//TODO:localize
return "Delete 1 Reaction"
case .chatReaction:
return strings.Chat_AdminActionSheet_DeleteReactionTitle
}
}
@ -535,14 +534,13 @@ private final class AdminUserActionsContentComponent: Component {
))))
}
} else {
//TODO:localize
subItems.append(
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: component.theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Delete all Messages",
string: component.strings.Chat_AdminActionSheet_DeleteAllMessages,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
@ -568,7 +566,7 @@ private final class AdminUserActionsContentComponent: Component {
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Delete all Reactions",
string: component.strings.Chat_AdminActionSheet_DeleteAllReactions,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),

View file

@ -19,6 +19,7 @@ public final class AlertMultilineInputFieldComponent: Component {
public fileprivate(set) var value: NSAttributedString = NSAttributedString()
public fileprivate(set) var animateError: () -> Void = {}
public fileprivate(set) var activateInput: () -> Void = {}
fileprivate var setValueImpl: ((NSAttributedString, Range<Int>) -> Void)?
fileprivate let valuePromise = ValuePromise<NSAttributedString>(NSAttributedString())
public var valueSignal: Signal<NSAttributedString, NoError> {
return self.valuePromise.get()
@ -32,6 +33,13 @@ public final class AlertMultilineInputFieldComponent: Component {
public init() {
}
public func setValue(_ value: NSAttributedString, selectionRange: Range<Int>? = nil) {
let selectionRange = selectionRange ?? (value.length ..< value.length)
self.value = value
self.valuePromise.set(value)
self.setValueImpl?(value, selectionRange)
}
}
public enum FormatMenuAvailability: Equatable {
@ -226,13 +234,18 @@ public final class AlertMultilineInputFieldComponent: Component {
func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
var resetText: NSAttributedString?
if self.component == nil {
resetText = component.initialValue
resetText = component.initialValue ?? (component.externalState.value.length == 0 ? nil : component.externalState.value)
component.externalState.animateError = { [weak self] in
self?.animateError()
}
component.externalState.activateInput = { [weak self] in
self?.activateInput()
}
component.externalState.setValueImpl = { [weak self] value, selectionRange in
if let textFieldView = self?.textField.view as? TextFieldComponent.View {
textFieldView.updateText(value, selectionRange: selectionRange)
}
}
}
let isFirstTime = self.component == nil

View file

@ -34,7 +34,7 @@ final class CameraVideoSource: VideoSource {
let index = self.onUpdatedListeners.add(f)
return ActionDisposable { [weak self] in
DispatchQueue.main.async {
Queue.mainQueue().async {
guard let self else {
return
}
@ -252,7 +252,7 @@ final class LiveStreamMediaSource {
let index = self.onVideoUpdatedListeners.add(f)
return ActionDisposable { [weak self] in
DispatchQueue.main.async {
Queue.mainQueue().async {
guard let self else {
return
}

View file

@ -2918,31 +2918,44 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if case .public = poll.publicity {
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
} else if isRestricted {
let locale = localeWithStrings(item.presentationData.strings)
let countryNames = poll.countries.map { id in
if let countryName = locale.localizedString(forRegionCode: id) {
return countryName
} else {
return id
}
}
var countries: String = ""
if countryNames.count == 1, let country = countryNames.first {
countries = "**\(country)**"
} else {
for i in 0 ..< countryNames.count {
countries.append("**\(countryNames[i])**")
if i == countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter)
} else if i < countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter)
let text: String
let peerName = item.message.peers[item.message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? ""
if !poll.countries.isEmpty {
let locale = localeWithStrings(item.presentationData.strings)
let countryNames = poll.countries.map { id in
if id == "FT" {
return "Fragment"
} else if let countryName = locale.localizedString(forRegionCode: id) {
return countryName
} else {
return id
}
}
var countries: String = ""
if countryNames.count == 1, let country = countryNames.first {
countries = "**\(country)**"
} else {
for i in 0 ..< countryNames.count {
countries.append("**\(countryNames[i])**")
if i == countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter)
} else if i < countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter)
}
}
}
if poll.restrictToSubscribers {
text = item.presentationData.strings.Chat_Poll_Restriction_SubscribersCountry(peerName, countries).string
} else {
text = item.presentationData.strings.Chat_Poll_Restriction_Country(countries).string
}
} else {
text = item.presentationData.strings.Chat_Poll_Restriction_Subscribers(peerName).string
}
//TODO:localize
let controller = UndoOverlayController(
presentationData: item.context.sharedContext.currentPresentationData.with { $0 },
content: .banned(text: "Only users from \(countries) can vote."),
content: .banned(text: text),
elevatedLayout: true,
position: .bottom,
action: { _ in return true }

View file

@ -3427,6 +3427,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
}
if let mediaDraftState = interfaceState.interfaceState.mediaDraftState, case .audio = mediaDraftState.contentType {
viewOnceIsVisible = true
recordMoreIsVisible = true
}
@ -3438,13 +3439,29 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta))*/
let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme)
let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -152.0), size: viewOnceSize)
var viewOnceButtonY: CGFloat = -105.0
if isRecording {
if accessoryPanel == nil {
viewOnceButtonY -= 49.0
}
}
let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: viewOnceButtonY), size: viewOnceSize)
self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size)
transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center)
if self.viewOnceButton.alpha.isZero && viewOnceIsVisible {
self.viewOnceButton.update(isSelected: self.viewOnce, animated: false)
}
transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01)
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil {
self.viewOnceButton.isHidden = false
} else {
self.viewOnceButton.isHidden = true
}
let recordMoreSize = self.recordMoreButton.update(theme: interfaceState.theme)
let recordMoreButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -52.0), size: recordMoreSize)
@ -3455,14 +3472,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.recordMoreButton.update(isSelected: false, animated: false)
}
transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01)
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil {
self.viewOnceButton.isHidden = false
} else {
self.viewOnceButton.isHidden = true
}
transition.updateAlpha(node: self.recordMoreButton, alpha: recordMoreIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.recordMoreButton, scale: recordMoreIsVisible ? 1.0 : 0.01)
@ -5601,9 +5610,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
public func frameForInputActionButton() -> CGRect? {
if !self.sendActionButtons.alpha.isZero && self.sendActionButtons.frame.minX < self.bounds.width {
return self.sendActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0)
}
if !self.mediaActionButtons.alpha.isZero && self.mediaActionButtons.frame.minX < self.bounds.width {
return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0)
}

View file

@ -174,42 +174,39 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
let isSilentPosting = self.isSilentPosting
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer, let controller = self.environment?.controller() else {
guard let self, let peer, let environment = self.environment, let controller = self.environment?.controller() else {
return
}
var isChannel = false
if case let .channel(channel) = peer, case .broadcast = channel.info {
isChannel = true
}
//TODO:localize
let text: String
if case let .user(user) = peer {
if user.id == component.context.account.peerId {
if isSilentPosting {
text = "You will receive a silent notification"
text = environment.strings.ScheduleMessage_SilentPosting_YouEnabled
} else {
text = "You will be notified"
text = environment.strings.ScheduleMessage_SilentPosting_YouDisabled
}
} else {
if isSilentPosting {
text = "\(peer.compactDisplayTitle) will receive a silent notification"
text = environment.strings.ScheduleMessage_SilentPosting_UserEnabled(peer.compactDisplayTitle).string
} else {
text = "\(peer.compactDisplayTitle) will be notified"
text = environment.strings.ScheduleMessage_SilentPosting_UserDisabled(peer.compactDisplayTitle).string
}
}
} else if isChannel {
if isSilentPosting {
text = "Subscribers will receive a silent notification"
text = environment.strings.ScheduleMessage_SilentPosting_ChannelEnabled
} else {
text = "Subscribers will be notified"
text = environment.strings.ScheduleMessage_SilentPosting_ChannelDisabled
}
} else {
if isSilentPosting {
text = "Members will receive a silent notification"
text = environment.strings.ScheduleMessage_SilentPosting_GroupEnabled
} else {
text = "Members will be notified"
text = environment.strings.ScheduleMessage_SilentPosting_GroupDisabled
}
}
@ -272,8 +269,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
case .poll:
title = strings.CreatePoll_Deadline_Title
case .search:
//TODO:localize
title = "Search"
title = strings.Conversation_CalendarSearch_Title
}
let titleSize = self.title.update(
transition: transition,
@ -554,8 +550,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
case .poll:
buttonTitle = strings.CreatePoll_Deadline_SetDeadline
case .search:
//TODO:localize
buttonTitle = "Done"
buttonTitle = strings.Conversation_CalendarSearch_Done
}
let buttonSideInset: CGFloat = 30.0

View file

@ -1245,10 +1245,19 @@ final class ComposePollScreenComponent: Component {
return
}
let maxCount: Int32
if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["poll_countries_max"] as? Double {
maxCount = Int32(value)
} else {
maxCount = 12
}
let stateContext = CountriesMultiselectionScreen.StateContext(
context: component.context,
subject: .countries,
initialSelectedCountries: self.limitToCountries
maxCount: maxCount,
initialSelectedCountries: self.limitToCountries,
showFragment: true
)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
let controller = CountriesMultiselectionScreen(
@ -2395,7 +2404,11 @@ final class ComposePollScreenComponent: Component {
if self.limitToCountries.count > 1 {
value = environment.strings.CreatePoll_AllowedCountries_Countries(Int32(self.limitToCountries.count))
} else if self.limitToCountries.count == 1, let countryCode = self.limitToCountries.first {
value = self.currentLocale?.localizedString(forRegionCode: countryCode) ?? countryCode
if countryCode == "FT" {
value = "Fragment"
} else {
value = self.currentLocale?.localizedString(forRegionCode: countryCode) ?? countryCode
}
} else {
value = ""
}
@ -3099,9 +3112,8 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
text = presentationData.strings.CreatePoll_QuizCorrectOptionNeeded
}
case .countriesNeeded:
//TODO:localize
title = nil
text = "Select at least one country"
text = presentationData.strings.CreatePoll_QuizCountryNeeded
}
let controller = UndoOverlayController(

View file

@ -426,8 +426,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
case .deleteReaction:
self.action = nil
//TODO:localize
self.text = "Tap and hold to delete reaction."
self.text = self.presentationData.strings.Chat_DeleteReactionInfo
self.targetSelectionIndex = nil
icon = nil
isUserInteractionEnabled = false

View file

@ -378,8 +378,7 @@ func infoItems(
if let reactionSourceMessageId = reactionSourceMessageId {
if canDeleteReaction {
//TODO:localize
items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemDeleteReaction, text: "Delete Reaction", color: .destructive, action: {
items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemDeleteReaction, text: presentationData.strings.PeerInfo_DeleteReaction, color: .destructive, action: {
interaction.openDeleteReaction(reactionSourceMessageId)
}))
}

View file

@ -4592,20 +4592,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
guard let tabController = navigationController.viewControllers.first as? TabBarController else {
guard let navigationTarget = resolveChatListNavigationTarget(navigationController: navigationController, excluding: controller) else {
return
}
for childController in tabController.controllers {
if let chatListController = childController as? ChatListController {
chatListController.maybeAskForPeerChatRemoval(peer: EngineRenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in
if deleted {
navigationController?.popToRoot(animated: true)
}
}, removed: {
})
break
navigationTarget.chatListController.maybeAskForPeerChatRemoval(peer: EngineRenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in
guard deleted, let navigationController = navigationController else {
return
}
}
if let popToController = navigationTarget.popToController {
let _ = navigationController.popToViewController(popToController, animated: true)
} else {
navigationController.popToRoot(animated: true)
}
}, removed: {
})
}
func deleteProfilePhoto(_ item: PeerInfoAvatarListItem) {

View file

@ -496,18 +496,17 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
}
}
//TODO:localize
let automationBotTitle: String
if let botPeer = data.businessConnectedBot {
let _ = botPeer
automationBotTitle = "@\(botPeer.compactDisplayTitle)"
} else {
automationBotTitle = "Off"
automationBotTitle = presentationData.strings.Settings_ChatAutomationOff
}
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerChatAutomation, label: .text(automationBotTitle), additionalBadgeLabel: nil, text: "Chat Automation", icon: PresentationResourcesSettings.aiTools, action: {
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerChatAutomation, label: .text(automationBotTitle), additionalBadgeLabel: nil, text: presentationData.strings.Settings_ChatAutomation, icon: PresentationResourcesSettings.aiTools, action: {
interaction.editingOpenBusinessChatBots()
}))
items[.info]!.append(PeerInfoScreenCommentItem(id: ItemPeerChatAutomationHelp, text: "Add a bot to reply to messages on your behalf."))
items[.info]!.append(PeerInfoScreenCommentItem(id: ItemPeerChatAutomationHelp, text: presentationData.strings.Settings_ChatAutomationInfo))
items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: {
interaction.openSettings(.addAccount)

View file

@ -49,7 +49,7 @@ public func quickReplyNameAlertController(context: AccountContext, updatedPresen
component: AnyComponent(
AlertInputFieldComponent(
context: context,
initialValue: nil,
initialValue: value,
placeholder: strings.QuickReply_ShortcutPlaceholder,
characterLimit: characterLimit,
hasClearButton: false,
@ -58,6 +58,21 @@ public func quickReplyNameAlertController(context: AccountContext, updatedPresen
autocorrectionType: .no,
isInitiallyFocused: true,
externalState: inputState,
shouldChangeText: { updatedText in
if updatedText.isEmpty {
return true
}
for scalar in updatedText.unicodeScalars {
if scalar.value == 0x5f || scalar.value == 0x200c || scalar.value == 0xb7 || (scalar.value >= 0xd80 && scalar.value <= 0xdff) {
continue
}
if CharacterSet.letters.contains(scalar) || CharacterSet.decimalDigits.contains(scalar) {
continue
}
return false
}
return true
},
returnKeyAction: {
applyImpl?()
}

View file

@ -752,7 +752,7 @@ final class CountriesMultiselectionScreenComponent: Component {
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .countriesSearch(query: self.navigationTextFieldState.text) {
} else {
self.searchStateDisposable?.dispose()
let searchStateContext = CountriesMultiselectionScreen.StateContext(context: component.context, subject: .countriesSearch(query: self.navigationTextFieldState.text))
let searchStateContext = CountriesMultiselectionScreen.StateContext(context: component.context, subject: .countriesSearch(query: self.navigationTextFieldState.text), showFragment: component.stateContext.showFragment)
var applyState = false
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
@ -789,7 +789,6 @@ final class CountriesMultiselectionScreenComponent: Component {
var sections: [ItemLayout.Section] = []
if let stateValue = self.effectiveStateValue {
var id: Int = 0
for (_, countries) in stateValue.sections {
sections.append(ItemLayout.Section(
@ -1122,6 +1121,7 @@ public extension CountriesMultiselectionScreen {
public let subject: Subject
public let maxCount: Int32?
public let initialSelectedCountries: [String]
public let showFragment: Bool
private var stateDisposable: Disposable?
private let stateSubject = Promise<State>()
@ -1138,16 +1138,22 @@ public extension CountriesMultiselectionScreen {
context: AccountContext,
subject: Subject = .countries,
maxCount: Int32? = nil,
initialSelectedCountries: [String] = []
initialSelectedCountries: [String] = [],
showFragment: Bool = false
) {
self.subject = subject
self.maxCount = maxCount
self.initialSelectedCountries = initialSelectedCountries
self.showFragment = showFragment
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let countries = localizedCountryNamesAndCodes(strings: presentationData.strings).sorted { lhs, rhs in
let countryList = localizedCountryNamesAndCodes(strings: presentationData.strings)
var countries = countryList.sorted { lhs, rhs in
return lhs.0.1.lowercased() < rhs.0.1.lowercased()
}
if showFragment, let index = countries.firstIndex(where: { $0.1 == "FR" }) {
countries.insert((("Fragment", "Fragment"), "FT", [888]), at: index)
}
switch subject {
case .countries:
@ -1156,8 +1162,8 @@ public extension CountriesMultiselectionScreen {
var currentSection: String?
var currentCountries: [CountryItem] = []
for country in countries {
let section = String(country.0.1.prefix(1))
if currentSection != section {
let section = String(country.0.1.prefix(1)).uppercased()
if currentSection != section && country.1 != "FT" {
if let currentSection {
sections.append((currentSection, currentCountries))
}

View file

@ -9385,10 +9385,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard case let .peer(peerId) = self.chatLocation else {
return
}
let navigationTarget = self.effectiveNavigationController.flatMap { navigationController in
resolveChatListNavigationTarget(navigationController: navigationController, excluding: self)
}
self.commitPurposefulAction()
self.chatDisplayNode.historyNode.disconnect()
let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: reportChatSpam).startStandalone()
self.effectiveNavigationController?.popToRoot(animated: true)
if let navigationController = self.effectiveNavigationController {
if let navigationTarget, let popToController = navigationTarget.popToController {
let _ = navigationController.popToViewController(popToController, animated: true)
} else {
navigationController.popToRoot(animated: true)
}
}
let _ = self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: true).startStandalone()
}

View file

@ -1724,27 +1724,32 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH
}
|> distinctUntilChanged
let accountCountry: Signal<String?, NoError> = .single(nil)
|> then(
combineLatest(
accountPeer
|> map { peer -> String? in
if case let .user(user) = peer {
return user.phone
} else {
let accountCountry: Signal<String?, NoError>
if let data = context.currentAppConfiguration.with({ $0 }).data, let country = data["phone_country_iso2"] as? String {
accountCountry = .single(country)
} else {
accountCountry = .single(nil)
|> then(
combineLatest(
accountPeer
|> map { peer -> String? in
if case let .user(user) = peer {
return user.phone
} else {
return nil
}
}
|> distinctUntilChanged,
(context as! AccountContextImpl).countriesConfiguration
)
|> map { phone, countriesConfiguration in
guard let phone, let (country, _) = lookupCountryIdByNumber(phone, configuration: countriesConfiguration) else {
return nil
}
return country.id
}
|> distinctUntilChanged,
(context as! AccountContextImpl).countriesConfiguration
)
|> map { phone, countriesConfiguration in
guard let phone, let (country, _) = lookupCountryIdByNumber(phone, configuration: countriesConfiguration) else {
return nil
}
return country.id
}
)
}
let topicAuthorId: Signal<EnginePeer.Id?, NoError>
if let peerId = chatLocation.peerId, let threadId = chatLocation.threadId {

View file

@ -2310,7 +2310,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
if !poll.countries.isEmpty {
let locale = localeWithStrings(chatPresentationInterfaceState.strings)
let countryNames = poll.countries.map { id in
if let countryName = locale.localizedString(forRegionCode: id) {
if id == "FT" {
return "Fragment"
} else if let countryName = locale.localizedString(forRegionCode: id) {
return countryName
} else {
return id