mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit 'd25010c1cb'
This commit is contained in:
commit
3188c793bc
30 changed files with 391 additions and 174 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue