[WIP] Message auto-delete and widget

This commit is contained in:
Ali 2021-02-09 22:55:41 +04:00
parent d7538bf131
commit 3880af10ae
38 changed files with 1009 additions and 766 deletions

10
BUILD Normal file
View File

@ -0,0 +1,10 @@
load("@build_bazel_rules_apple//apple:resources.bzl",
"swift_intent_library",
)
swift_intent_library(
name = "GeneratedSources",
src = "Intents.intentdefinition",
visibility = ["//visibility:public"],
)

View File

@ -75,66 +75,6 @@
<string>SelectFriends</string> <string>SelectFriends</string>
<key>INIntentParameters</key> <key>INIntentParameters</key>
<array> <array>
<dict>
<key>INIntentParameterConfigurable</key>
<true/>
<key>INIntentParameterDisplayName</key>
<string>Contents</string>
<key>INIntentParameterDisplayNameID</key>
<string>WAiyZm</string>
<key>INIntentParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentParameterEnumType</key>
<string>Contents</string>
<key>INIntentParameterEnumTypeNamespace</key>
<string>p74MWb</string>
<key>INIntentParameterMetadata</key>
<dict>
<key>INIntentParameterMetadataDefaultValue</key>
<string>recent</string>
</dict>
<key>INIntentParameterName</key>
<string>contents</string>
<key>INIntentParameterPromptDialogs</key>
<array>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogType</key>
<string>Configuration</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogType</key>
<string>Primary</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>There are ${count} options matching ${contents}.</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>oSRWBb</string>
<key>INIntentParameterPromptDialogType</key>
<string>DisambiguationIntroduction</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Just to confirm, you wanted ${contents}?</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>jvYJCG</string>
<key>INIntentParameterPromptDialogType</key>
<string>Confirmation</string>
</dict>
</array>
<key>INIntentParameterTag</key>
<integer>5</integer>
<key>INIntentParameterType</key>
<string>Integer</string>
</dict>
<dict> <dict>
<key>INIntentParameterArraySizes</key> <key>INIntentParameterArraySizes</key>
<array> <array>
@ -160,11 +100,11 @@
<key>INIntentParameterConfigurable</key> <key>INIntentParameterConfigurable</key>
<true/> <true/>
<key>INIntentParameterDisplayName</key> <key>INIntentParameterDisplayName</key>
<string>Chats</string> <string> </string>
<key>INIntentParameterDisplayNameID</key> <key>INIntentParameterDisplayNameID</key>
<string>WIf4LD</string> <string>WIf4LD</string>
<key>INIntentParameterDisplayPriority</key> <key>INIntentParameterDisplayPriority</key>
<integer>2</integer> <integer>1</integer>
<key>INIntentParameterFixedSizeArray</key> <key>INIntentParameterFixedSizeArray</key>
<integer>1</integer> <integer>1</integer>
<key>INIntentParameterName</key> <key>INIntentParameterName</key>
@ -178,6 +118,10 @@
<dict> <dict>
<key>INIntentParameterPromptDialogCustom</key> <key>INIntentParameterPromptDialogCustom</key>
<true/> <true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Search</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>ORCbLf</string>
<key>INIntentParameterPromptDialogType</key> <key>INIntentParameterPromptDialogType</key>
<string>Configuration</string> <string>Configuration</string>
</dict> </dict>
@ -190,8 +134,6 @@
</array> </array>
<key>INIntentParameterRelationship</key> <key>INIntentParameterRelationship</key>
<dict> <dict>
<key>INIntentParameterRelationshipParentName</key>
<string>contents</string>
<key>INIntentParameterRelationshipPredicateName</key> <key>INIntentParameterRelationshipPredicateName</key>
<string>EnumHasExactValue</string> <string>EnumHasExactValue</string>
<key>INIntentParameterRelationshipPredicateValue</key> <key>INIntentParameterRelationshipPredicateValue</key>
@ -201,6 +143,8 @@
<true/> <true/>
<key>INIntentParameterSupportsMultipleValues</key> <key>INIntentParameterSupportsMultipleValues</key>
<true/> <true/>
<key>INIntentParameterSupportsSearch</key>
<true/>
<key>INIntentParameterTag</key> <key>INIntentParameterTag</key>
<integer>19</integer> <integer>19</integer>
<key>INIntentParameterType</key> <key>INIntentParameterType</key>

View File

@ -8,6 +8,10 @@ load("@build_bazel_rules_apple//apple:ios.bzl",
"ios_framework", "ios_framework",
) )
load("@build_bazel_rules_apple//apple:resources.bzl",
"swift_intent_library",
)
load("@build_bazel_rules_apple//apple:watchos.bzl", load("@build_bazel_rules_apple//apple:watchos.bzl",
"watchos_application", "watchos_application",
"watchos_extension", "watchos_extension",
@ -1158,7 +1162,7 @@ plist_fragment(
) )
swift_library( swift_library(
name = "GeneratedSources", name = "GeneratedSources1",
module_name = "GeneratedSources", module_name = "GeneratedSources",
srcs = glob([ srcs = glob([
"Generated/**/*.swift", "Generated/**/*.swift",
@ -1166,6 +1170,12 @@ swift_library(
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )
'''swift_intent_library(
name = "GeneratedSources",
src = "SiriIntents/Intents.intentdefinition",
visibility = ["//visibility:public"],
)'''
swift_library( swift_library(
name = "WidgetExtensionLib", name = "WidgetExtensionLib",
module_name = "WidgetExtensionLib", module_name = "WidgetExtensionLib",
@ -1173,7 +1183,7 @@ swift_library(
"WidgetKitWidget/**/*.swift", "WidgetKitWidget/**/*.swift",
]), ]),
data = [ data = [
"SiriIntents/Intents.intentdefinition", #"SiriIntents/Intents.intentdefinition",
], ],
deps = [ deps = [
"//submodules/BuildConfig:BuildConfig", "//submodules/BuildConfig:BuildConfig",
@ -1185,7 +1195,7 @@ swift_library(
"//submodules/TelegramCore:TelegramCore", "//submodules/TelegramCore:TelegramCore",
"//submodules/SyncCore:SyncCore", "//submodules/SyncCore:SyncCore",
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider", "//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
":GeneratedSources", "//:GeneratedSources",
], ],
) )
@ -1267,7 +1277,7 @@ swift_library(
"SiriIntents/**/*.swift", "SiriIntents/**/*.swift",
]), ]),
data = [ data = [
"SiriIntents/Intents.intentdefinition", #"SiriIntents/Intents.intentdefinition",
], ],
deps = [ deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
@ -1278,7 +1288,7 @@ swift_library(
"//submodules/BuildConfig:BuildConfig", "//submodules/BuildConfig:BuildConfig",
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider", "//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
"//submodules/AppLockState:AppLockState", "//submodules/AppLockState:AppLockState",
":GeneratedSources", "//:GeneratedSources",
], ],
) )

View File

@ -1,36 +0,0 @@
//
// Contents.swift
//
// This file was automatically generated and should not be edited.
//
#if canImport(Intents)
import Intents
@available(iOS 12.0, macOS 10.16, watchOS 5.0, *) @available(tvOS, unavailable)
@objc public enum Contents: Int {
case `unknown` = 0
case `recent` = 1
case `custom` = 2
}
@available(iOS 13.0, macOS 10.16, watchOS 6.0, *) @available(tvOS, unavailable)
@objc(ContentsResolutionResult)
public class ContentsResolutionResult: INEnumResolutionResult {
// This resolution result is for when the app extension wants to tell Siri to proceed, with a given Contents. The resolvedValue can be different than the original Contents. This allows app extensions to apply business logic constraints.
// Use notRequired() to continue with a 'nil' value.
@objc(successWithResolvedContents:)
public class func success(with resolvedValue: Contents) -> Self {
return __success(withResolvedValue: resolvedValue.rawValue)
}
// This resolution result is to ask Siri to confirm if this is the value with which the user wants to continue.
@objc(confirmationRequiredWithContentsToConfirm:)
public class func confirmationRequired(with valueToConfirm: Contents) -> Self {
return __confirmationRequiredWithValue(toConfirm: valueToConfirm.rawValue)
}
}
#endif

View File

@ -1,60 +0,0 @@
//
// Friend.swift
//
// This file was automatically generated and should not be edited.
//
#if canImport(Intents)
import Intents
@available(iOS 12.0, macOS 10.16, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(Friend)
public class Friend: INObject {
@available(iOS 13.0, macOS 10.16, watchOS 6.0, *)
@NSManaged public var subtitle: String?
}
@available(iOS 13.0, macOS 10.16, watchOS 6.0, *) @available(tvOS, unavailable)
@objc(FriendResolutionResult)
public class FriendResolutionResult: INObjectResolutionResult {
// This resolution result is for when the app extension wants to tell Siri to proceed, with a given Friend. The resolvedValue can be different than the original Friend. This allows app extensions to apply business logic constraints.
// Use notRequired() to continue with a 'nil' value.
@objc(successWithResolvedFriend:)
public class func success(with resolvedObject: Friend) -> Self {
return super.success(with: resolvedObject) as! Self
}
// This resolution result is to ask Siri to disambiguate between the provided Friend.
@objc(disambiguationWithFriendsToDisambiguate:)
public class func disambiguation(with objectsToDisambiguate: [Friend]) -> Self {
return super.disambiguation(with: objectsToDisambiguate) as! Self
}
// This resolution result is to ask Siri to confirm if this is the value with which the user wants to continue.
@objc(confirmationRequiredWithFriendToConfirm:)
public class func confirmationRequired(with objectToConfirm: Friend?) -> Self {
return super.confirmationRequired(with: objectToConfirm) as! Self
}
@available(*, unavailable)
override public class func success(with resolvedObject: INObject) -> Self {
fatalError()
}
@available(*, unavailable)
override public class func disambiguation(with objectsToDisambiguate: [INObject]) -> Self {
fatalError()
}
@available(*, unavailable)
override public class func confirmationRequired(with objectToConfirm: INObject?) -> Self {
fatalError()
}
}
#endif

View File

@ -1,120 +0,0 @@
//
// SelectFriendsIntent.swift
//
// This file was automatically generated and should not be edited.
//
#if canImport(Intents)
import Intents
@available(iOS 12.0, macOS 10.16, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(SelectFriendsIntent)
public class SelectFriendsIntent: INIntent {
@NSManaged public var contents: Contents
@NSManaged public var friends: [Friend]?
}
/*!
@abstract Protocol to declare support for handling a SelectFriendsIntent. By implementing this protocol, a class can provide logic for resolving, confirming and handling the intent.
@discussion The minimum requirement for an implementing class is that it should be able to handle the intent. The confirmation method is optional. The handling method is always called last, after confirming the intent.
*/
@available(iOS 12.0, macOS 10.16, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(SelectFriendsIntentHandling)
public protocol SelectFriendsIntentHandling: NSObjectProtocol {
/*!
@abstract Dynamic options methods - provide options for the parameter at runtime
@discussion Called to query dynamic options for the parameter and this intent in its current form.
@param intent The input intent
@param completion The response block contains options for the parameter
*/
@available(iOS 14.0, macOS 10.16, watchOS 7.0, *)
@objc(provideFriendsOptionsCollectionForSelectFriends:withCompletion:)
func provideFriendsOptionsCollection(for intent: SelectFriendsIntent, with completion: @escaping (INObjectCollection<Friend>?, Error?) -> Swift.Void)
/*!
@abstract Confirmation method - Validate that this intent is ready for the next step (i.e. handling)
@discussion Called prior to asking the app to handle the intent. The app should return a response object that contains additional information about the intent, which may be relevant for the system to show the user prior to handling. If unimplemented, the system will assume the intent is valid, and will assume there is no additional information relevant to this intent.
@param intent The input intent
@param completion The response block contains a SelectFriendsIntentResponse containing additional details about the intent that may be relevant for the system to show the user prior to handling.
@see SelectFriendsIntentResponse
*/
@objc(confirmSelectFriends:completion:)
optional func confirm(intent: SelectFriendsIntent, completion: @escaping (SelectFriendsIntentResponse) -> Swift.Void)
/*!
@abstract Handling method - Execute the task represented by the SelectFriendsIntent that's passed in
@discussion Called to actually execute the intent. The app must return a response for this intent.
@param intent The input intent
@param completion The response handling block takes a SelectFriendsIntentResponse containing the details of the result of having executed the intent
@see SelectFriendsIntentResponse
*/
@objc(handleSelectFriends:completion:)
optional func handle(intent: SelectFriendsIntent, completion: @escaping (SelectFriendsIntentResponse) -> Swift.Void)
/*!
@abstract Default values for parameters with dynamic options
@discussion Called to query the parameter default value.
*/
@available(iOS 14.0, macOS 10.16, watchOS 7.0, *)
@objc(defaultFriendsForSelectFriends:)
optional func defaultFriends(for intent: SelectFriendsIntent) -> [Friend]?
/*!
@abstract Deprecated dynamic options methods.
*/
@available(iOS, introduced: 13.0, deprecated: 14.0, message: "")
@available(watchOS, introduced: 6.0, deprecated: 7.0, message: "")
@objc(provideFriendsOptionsForSelectFriends:withCompletion:)
optional func provideFriendsOptions(for intent: SelectFriendsIntent, with completion: @escaping ([Friend]?, Error?) -> Swift.Void)
}
/*!
@abstract Constants indicating the state of the response.
*/
@available(iOS 12.0, macOS 10.16, watchOS 5.0, *) @available(tvOS, unavailable)
@objc public enum SelectFriendsIntentResponseCode: Int {
case unspecified = 0
case ready
case continueInApp
case inProgress
case success
case failure
case failureRequiringAppLaunch
}
@available(iOS 12.0, macOS 10.16, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(SelectFriendsIntentResponse)
public class SelectFriendsIntentResponse: INIntentResponse {
/*!
@abstract The response code indicating your success or failure in confirming or handling the intent.
*/
@objc public fileprivate(set) var code: SelectFriendsIntentResponseCode = .unspecified
/*!
@abstract Initializes the response object with the specified code and user activity object.
@discussion The app extension has the option of capturing its private state as an NSUserActivity and returning it as the 'currentActivity'. If the app is launched, an NSUserActivity will be passed in with the private state. The NSUserActivity may also be used to query the app's UI extension (if provided) for a view controller representing the current intent handling state. In the case of app launch, the NSUserActivity will have its activityType set to the name of the intent. This intent object will also be available in the NSUserActivity.interaction property.
@param code The response code indicating your success or failure in confirming or handling the intent.
@param userActivity The user activity object to use when launching your app. Provide an object if you want to add information that is specific to your app. If you specify nil, the system automatically creates a user activity object for you, sets its type to the class name of the intent being handled, and fills it with an INInteraction object containing the intent and your response.
*/
@objc(initWithCode:userActivity:)
public convenience init(code: SelectFriendsIntentResponseCode, userActivity: NSUserActivity?) {
self.init()
self.code = code
self.userActivity = userActivity
}
}
#endif

View File

@ -56,10 +56,15 @@ enum IntentHandlingError {
@objc(IntentHandler) @objc(IntentHandler)
class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling, INStartAudioCallIntentHandling, INSearchCallHistoryIntentHandling, SelectFriendsIntentHandling { class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling, INStartAudioCallIntentHandling, INSearchCallHistoryIntentHandling, SelectFriendsIntentHandling {
private let accountPromise = Promise<Account?>() private let accountPromise = Promise<Account?>()
private let allAccounts = Promise<[(AccountRecordId, PeerId)]>()
private let resolvePersonsDisposable = MetaDisposable() private let resolvePersonsDisposable = MetaDisposable()
private let actionDisposable = MetaDisposable() private let actionDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
private var rootPath: String?
private var accountManager: AccountManager?
private var encryptionParameters: ValueBoxEncryptionParameters?
private var appGroupUrl: URL? private var appGroupUrl: URL?
override init() { override init() {
@ -88,6 +93,8 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
let rootPath = rootPathForBasePath(appGroupUrl.path) let rootPath = rootPathForBasePath(appGroupUrl.path)
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath) performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
self.rootPath = rootPath
TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: arc4random64()) TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: arc4random64())
let logsPath = rootPath + "/siri-logs" let logsPath = rootPath + "/siri-logs"
@ -97,16 +104,61 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
initializeAccountManagement()
let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata")
self.accountManager = accountManager
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
self.encryptionParameters = encryptionParameters
self.allAccounts.set(accountManager.accountRecords()
|> take(1)
|> map { view -> [(AccountRecordId, PeerId)] in
var result: [(AccountRecordId, Int, PeerId)] = []
for record in view.records {
let isLoggedOut = record.attributes.contains(where: { attribute in
return attribute is LoggedOutAccountAttribute
})
if isLoggedOut {
continue
}
/*let isTestingEnvironment = record.attributes.contains(where: { attribute in
if let attribute = attribute as? AccountEnvironmentAttribute, case .test = attribute.environment {
return true
} else {
return false
}
})*/
var backupData: AccountBackupData?
var sortIndex: Int32 = 0
for attribute in record.attributes {
if let attribute = attribute as? AccountSortOrderAttribute {
sortIndex = attribute.order
} else if let attribute = attribute as? AccountBackupDataAttribute {
backupData = attribute.data
}
}
if let backupData = backupData {
result.append((record.id, Int(sortIndex), PeerId(backupData.peerId)))
}
}
result.sort(by: { lhs, rhs in
if lhs.1 != rhs.1 {
return lhs.1 < rhs.1
} else {
return lhs.0 < rhs.0
}
})
return result.map { record -> (AccountRecordId, PeerId) in
return (record.0, record.2)
}
})
let account: Signal<Account?, NoError> let account: Signal<Account?, NoError>
if let accountCache = accountCache { if let accountCache = accountCache {
account = .single(accountCache) account = .single(accountCache)
} else { } else {
initializeAccountManagement()
let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata")
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters)
|> mapToSignal { account -> Signal<Account?, NoError> in |> mapToSignal { account -> Signal<Account?, NoError> in
if let account = account { if let account = account {
@ -139,6 +191,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
deinit { deinit {
self.resolvePersonsDisposable.dispose() self.resolvePersonsDisposable.dispose()
self.actionDisposable.dispose() self.actionDisposable.dispose()
self.searchDisposable.dispose()
} }
override public func handler(for intent: INIntent) -> Any { override public func handler(for intent: INIntent) -> Any {
@ -701,17 +754,148 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
} }
@available(iOSApplicationExtension 14.0, iOS 14.0, *) @available(iOSApplicationExtension 14.0, iOS 14.0, *)
func provideFriendsOptionsCollection(for intent: SelectFriendsIntent, with completion: @escaping (INObjectCollection<Friend>?, Error?) -> Void) { func provideFriendsOptionsCollection(for intent: SelectFriendsIntent, searchTerm: String?, with completion: @escaping (INObjectCollection<Friend>?, Error?) -> Void) {
let _ = (self.accountPromise.get() guard let rootPath = self.rootPath, let _ = self.accountManager, let encryptionParameters = self.encryptionParameters else {
completion(nil, nil)
return
}
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data) {
if state.isManuallyLocked || state.autolockTimeout != nil {
let error = NSError(domain: "Locked", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Open Telegram and enter passcode to edit widget."
])
completion(nil, error)
return
}
}
self.searchDisposable.set((self.allAccounts.get()
|> castError(Error.self)
|> take(1)
|> mapToSignal { accounts -> Signal<INObjectCollection<Friend>, Error> in
var accountResults: [Signal<INObjectSection<Friend>, Error>] = []
for (accountId, accountPeerId) in accounts {
accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, transaction: { postbox, transaction -> INObjectSection<Friend> in
var accountTitle: String = ""
if let peer = transaction.getPeer(accountPeerId) as? TelegramUser {
if let username = peer.username, !username.isEmpty {
accountTitle = "@\(username)"
} else {
accountTitle = peer.debugDisplayTitle
}
}
var peers: [Peer] = []
if let searchTerm = searchTerm {
if !searchTerm.isEmpty {
for renderedPeer in transaction.searchPeers(query: searchTerm) {
if let peer = renderedPeer.peer, !(peer is TelegramSecretChat) {
peers.append(peer)
}
}
if peers.count > 30 {
peers = Array(peers.dropLast(peers.count - 30))
}
}
} else {
for renderedPeer in transaction.getTopChatListEntries(groupId: .root, count: 50) {
if let peer = renderedPeer.peer, !(peer is TelegramSecretChat) {
peers.append(peer)
}
}
}
var items: [Friend] = []
for peer in peers {
let profileImage = smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
return postbox.mediaBox.resourcePath(representation.resource)
}.flatMap { path -> INImage? in
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
return INImage(imageData: data)
} else {
return nil
}
}
items.append(Friend(identifier: "\(accountId.int64):\(peer.id.toInt64())", display: peer.debugDisplayTitle, subtitle: nil, image: nil))
}
return INObjectSection<Friend>(title: accountTitle, items: items)
})
|> castError(Error.self))
}
return combineLatest(accountResults)
|> map { accountResults -> INObjectCollection<Friend> in
let filteredSections = accountResults.filter { section in
return !section.items.isEmpty
}
if filteredSections.count == 1 {
return INObjectCollection<Friend>(items: filteredSections[0].items)
} else {
return INObjectCollection<Friend>(sections: filteredSections)
}
}
}).start(next: { result in
completion(result, nil)
}, error: { error in
completion(nil, error)
}))
/*let _ = (self.accountPromise.get()
|> take(1) |> take(1)
|> mapToSignal { account -> Signal<[Friend], NoError> in |> mapToSignal { account -> Signal<[Friend], NoError> in
guard let account = account else { guard let account = account else {
return .single([]) return .single([])
} }
if let searchTerm = searchTerm {
if !searchTerm.isEmpty {
return account.postbox.searchPeers(query: searchTerm)
|> map { renderedPeers -> [Friend] in
var peers: [Peer] = []
for renderedPeer in renderedPeers {
if let peer = renderedPeer.peer, !(peer is TelegramSecretChat) {
peers.append(peer)
}
}
peers.sort(by: { lhs, rhs in
return lhs.debugDisplayTitle < rhs.debugDisplayTitle
})
if peers.count > 30 {
peers = Array(peers.dropLast(peers.count - 30))
}
var result: [Friend] = []
for peer in peers {
let profileImage = smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
return account.postbox.mediaBox.resourcePath(representation.resource)
}.flatMap { path -> INImage? in
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
return INImage(imageData: data)
} else {
return nil
}
}
result.append(Friend(identifier: "\(peer.id.toInt64())", display: peer.debugDisplayTitle, subtitle: nil, image: nil))
}
return result
}
} else {
return .single([])
}
} else {
return account.postbox.transaction { transaction -> [Friend] in return account.postbox.transaction { transaction -> [Friend] in
var peers: [Peer] = [] var peers: [Peer] = []
outer: for peerId in transaction.getContactPeerIds() { for peerId in transaction.getContactPeerIds() {
if let peer = transaction.getPeer(peerId) as? TelegramUser { if let peer = transaction.getPeer(peerId) as? TelegramUser {
peers.append(peer) peers.append(peer)
} }
@ -721,6 +905,10 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
return lhs.debugDisplayTitle < rhs.debugDisplayTitle return lhs.debugDisplayTitle < rhs.debugDisplayTitle
}) })
if peers.count > 50 {
peers = Array(peers.dropLast(peers.count - 50))
}
var result: [Friend] = [] var result: [Friend] = []
for peer in peers { for peer in peers {
let profileImage = smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in let profileImage = smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
@ -737,9 +925,10 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
return result return result
} }
} }
}
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { result in
let collection = INObjectCollection(items: result) let collection = INObjectCollection(items: result)
completion(collection, nil) completion(collection, nil)
}) })*/
} }
} }

View File

@ -2187,7 +2187,7 @@ Unused sets are archived when you add more.";
"Widget.AuthRequired" = "Log in to Telegram"; "Widget.AuthRequired" = "Log in to Telegram";
"Widget.NoUsers" = "Start messaging to see your friends here"; "Widget.NoUsers" = "Start messaging to see your friends here";
"Widget.GalleryTitle" = "Telegram"; "Widget.GalleryTitle" = "Telegram";
"Widget.GalleryDescription" = "See your friends here"; "Widget.GalleryDescription" = "Select chats";
"ShareMenu.CopyShareLinkGame" = "Copy link to game"; "ShareMenu.CopyShareLinkGame" = "Copy link to game";

View File

@ -94,11 +94,11 @@ private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId:
private let avatarSize = CGSize(width: 50.0, height: 50.0) private let avatarSize = CGSize(width: 50.0, height: 50.0)
func avatarImage(accountPeerId: Int64, peer: WidgetDataPeer, size: CGSize) -> UIImage { func avatarImage(accountPeerId: Int64?, peer: WidgetDataPeer?, size: CGSize) -> UIImage {
if let path = peer.avatarPath, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { if let path = peer?.avatarPath, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) {
return roundImage return roundImage
} else { } else {
return avatarViewLettersImage(size: size, peerId: peer.id, accountPeerId: accountPeerId, letters: peer.letters)! return avatarViewLettersImage(size: size, peerId: peer?.id ?? 1, accountPeerId: accountPeerId ?? 1, letters: peer?.letters ?? [" "])!
} }
} }

View File

@ -17,6 +17,29 @@ import WidgetItemsUtils
import GeneratedSources import GeneratedSources
struct ParsedPeer {
var accountId: Int64
var accountPeerId: Int64
var peer: WidgetDataPeer
}
struct ParsedPeers {
var peers: [ParsedPeer]
var updateTimestamp: Int32
}
private extension ParsedPeers {
init(accountId: Int64, peers: WidgetDataPeers) {
self.init(peers: peers.peers.map { peer -> ParsedPeer in
return ParsedPeer(
accountId: accountId,
accountPeerId: peers.accountPeerId,
peer: peer
)
}, updateTimestamp: peers.updateTimestamp)
}
}
private var installedSharedLogger = false private var installedSharedLogger = false
private func setupSharedLogger(rootPath: String, path: String) { private func setupSharedLogger(rootPath: String, path: String) {
@ -64,14 +87,7 @@ struct Provider: IntentTimelineProvider {
} }
func getSnapshot(for configuration: SelectFriendsIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { func getSnapshot(for configuration: SelectFriendsIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let contents: SimpleEntry.Contents let entry = SimpleEntry(date: Date(), contents: .peers(ParsedPeers(accountId: 0, peers: WidgetDataPeers(accountPeerId: 0, peers: [], updateTimestamp: 0))))
switch configuration.contents {
case .unknown, .recent:
contents = .recent
case .custom:
contents = .recent
}
let entry = SimpleEntry(date: Date(), contents: contents)
completion(entry) completion(entry)
} }
@ -79,10 +95,6 @@ struct Provider: IntentTimelineProvider {
let currentDate = Date() let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .hour, value: 0, to: currentDate)! let entryDate = Calendar.current.date(byAdding: .hour, value: 0, to: currentDate)!
switch configuration.contents {
case .unknown, .recent:
completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .recent)], policy: .atEnd))
case .custom:
guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .recent)], policy: .atEnd)) completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .recent)], policy: .atEnd))
return return
@ -119,8 +131,111 @@ struct Provider: IntentTimelineProvider {
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
let _ = (accountTransaction(rootPath: rootPath, id: AccountRecordId(rawValue: widgetData.accountId), encryptionParameters: encryptionParameters, transaction: { postbox, transaction -> WidgetDataPeers in var itemsByAccount: [Int64: [(Int64, Friend)]] = [:]
var peers: [WidgetDataPeer] = [] var itemOrder: [(Int64, Int64)] = []
if let friends = configuration.friends {
for item in friends {
guard let identifier = item.identifier else {
continue
}
guard let index = identifier.firstIndex(of: ":") else {
continue
}
guard let accountId = Int64(identifier[identifier.startIndex ..< index]) else {
continue
}
guard let peerId = Int64(identifier[identifier.index(after: index)...]) else {
continue
}
if itemsByAccount[accountId] == nil {
itemsByAccount[accountId] = []
}
itemsByAccount[accountId]?.append((peerId, item))
itemOrder.append((accountId, peerId))
}
}
var friendsByAccount: [Signal<[ParsedPeer], NoError>] = []
for (accountId, items) in itemsByAccount {
friendsByAccount.append(accountTransaction(rootPath: rootPath, id: AccountRecordId(rawValue: accountId), encryptionParameters: encryptionParameters, transaction: { postbox, transaction -> [ParsedPeer] in
guard let state = transaction.getState() as? AuthorizedAccountState else {
return []
}
var result: [ParsedPeer] = []
for (peerId, _) in items {
guard let peer = transaction.getPeer(PeerId(peerId)) else {
continue
}
var name: String = ""
var lastName: String?
if let user = peer as? TelegramUser {
if let firstName = user.firstName {
name = firstName
lastName = user.lastName
} else if let lastName = user.lastName {
name = lastName
} else if let phone = user.phone, !phone.isEmpty {
name = phone
}
} else {
name = peer.debugDisplayTitle
}
var badge: WidgetDataPeer.Badge?
if let readState = transaction.getCombinedPeerReadState(peer.id), readState.count > 0 {
var isMuted = false
if let notificationSettings = transaction.getPeerNotificationSettings(peer.id) as? TelegramPeerNotificationSettings {
isMuted = notificationSettings.isRemovedFromTotalUnreadCount(default: false)
}
badge = WidgetDataPeer.Badge(
count: Int(readState.count),
isMuted: isMuted
)
}
var mappedMessage: WidgetDataPeer.Message?
if let index = transaction.getTopPeerMessageIndex(peerId: peer.id) {
if let message = transaction.getMessage(index.id) {
mappedMessage = WidgetDataPeer.Message(message: message)
}
}
let widgetPeer = WidgetDataPeer(id: peer.id.toInt64(), name: name, lastName: lastName, letters: peer.displayLetters, avatarPath: smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
return postbox.mediaBox.resourcePath(representation.resource)
}, badge: badge, message: mappedMessage)
result.append(ParsedPeer(accountId: accountId, accountPeerId: state.peerId.toInt64(), peer: widgetPeer))
}
return result
}))
}
let _ = combineLatest(friendsByAccount).start(next: { allPeers in
var orderedPeers: [ParsedPeer] = []
outer: for (accountId, peerId) in itemOrder {
for peerSet in allPeers {
for peer in peerSet {
if peer.accountId == accountId && peer.peer.id == peerId {
orderedPeers.append(peer)
continue outer
}
}
}
}
let result = ParsedPeers(peers: orderedPeers, updateTimestamp: Int32(Date().timeIntervalSince1970))
completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .peers(result))], policy: .atEnd))
})
/*let _ = (accountTransaction(rootPath: rootPath, id: AccountRecordId(rawValue: widgetData.accountId), encryptionParameters: encryptionParameters, transaction: { postbox, transaction -> ParsedPeers in
var peers: [ParsedPeer] = []
if let items = configuration.friends { if let items = configuration.friends {
for item in items { for item in items {
guard let identifier = item.identifier, let peerIdValue = Int64(identifier) else { guard let identifier = item.identifier, let peerIdValue = Int64(identifier) else {
@ -171,19 +286,18 @@ struct Provider: IntentTimelineProvider {
}, badge: badge, message: mappedMessage)) }, badge: badge, message: mappedMessage))
} }
} }
return WidgetDataPeers(accountPeerId: widgetPeers.accountPeerId, peers: peers, updateTimestamp: Int32(Date().timeIntervalSince1970)) return ParsedPeers(peers: peers, updateTimestamp: Int32(Date().timeIntervalSince1970))
}) })
|> deliverOnMainQueue).start(next: { peers in |> deliverOnMainQueue).start(next: { peers in
completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .peers(peers))], policy: .atEnd)) completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .peers(peers))], policy: .atEnd))
}) })*/
}
} }
} }
struct SimpleEntry: TimelineEntry { struct SimpleEntry: TimelineEntry {
enum Contents { enum Contents {
case recent case recent
case peers(WidgetDataPeers) case peers(ParsedPeers)
} }
let date: Date let date: Date
@ -191,27 +305,32 @@ struct SimpleEntry: TimelineEntry {
} }
enum PeersWidgetData { enum PeersWidgetData {
case placeholder
case empty case empty
case locked case peers(ParsedPeers)
case peers(WidgetDataPeers)
} }
extension PeersWidgetData { extension PeersWidgetData {
static let previewData = PeersWidgetData.placeholder static let previewData = PeersWidgetData.empty
} }
struct AvatarItemView: View { struct AvatarItemView: View {
var accountPeerId: Int64 var peer: ParsedPeer?
var peer: WidgetDataPeer
var itemSize: CGFloat var itemSize: CGFloat
var displayBadge: Bool = true var displayBadge: Bool = true
var body: some View { var body: some View {
return ZStack { return ZStack {
Image(uiImage: avatarImage(accountPeerId: accountPeerId, peer: peer, size: CGSize(width: itemSize, height: itemSize))) if let peer = peer {
Image(uiImage: avatarImage(accountPeerId: peer.accountPeerId, peer: peer.peer, size: CGSize(width: itemSize, height: itemSize)))
.clipShape(Circle()) .clipShape(Circle())
if displayBadge, let badge = peer.badge, badge.count > 0 { } else {
Image(uiImage: avatarImage(accountPeerId: nil, peer: nil, size: CGSize(width: itemSize, height: itemSize)))
//Rectangle()
.frame(width: itemSize, height: itemSize)
.clipShape(Circle())
.redacted(reason: .placeholder)
}
/*if let peer = peer, displayBadge, let badge = peer.badge, badge.count > 0 {
Text("\(badge.count)") Text("\(badge.count)")
.font(Font.system(size: 16.0)) .font(Font.system(size: 16.0))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -223,7 +342,7 @@ struct AvatarItemView: View {
.frame(minWidth: 20, idealWidth: 20, maxWidth: .infinity, minHeight: 20, idealHeight: 20, maxHeight: 20.0, alignment: .center) .frame(minWidth: 20, idealWidth: 20, maxWidth: .infinity, minHeight: 20, idealHeight: 20, maxHeight: 20.0, alignment: .center)
) )
.position(x: floor(0.84 * itemSize), y: floor(0.16 * itemSize)) .position(x: floor(0.84 * itemSize), y: floor(0.16 * itemSize))
} }*/
} }
} }
} }
@ -259,7 +378,7 @@ struct WidgetView: View {
} }
} }
func peersView(geometry: GeometryProxy, peers: WidgetDataPeers) -> some View { func peersView(geometry: GeometryProxy, peers: ParsedPeers) -> some View {
let columnCount: Int let columnCount: Int
let rowCount: Int let rowCount: Int
@ -308,9 +427,8 @@ struct WidgetView: View {
return ZStack { return ZStack {
ForEach(0 ..< min(peers.peers.count, columnCount * rowCount), content: { i in ForEach(0 ..< min(peers.peers.count, columnCount * rowCount), content: { i in
Link(destination: URL(string: linkForPeer(id: peers.peers[i].id))!, label: { Link(destination: URL(string: linkForPeer(id: peers.peers[i].peer.id))!, label: {
AvatarItemView( AvatarItemView(
accountPeerId: peers.accountPeerId,
peer: peers.peers[i], peer: peers.peers[i],
itemSize: itemSize itemSize: itemSize
).frame(width: itemSize, height: itemSize) ).frame(width: itemSize, height: itemSize)
@ -322,18 +440,10 @@ struct WidgetView: View {
func peerViews() -> AnyView { func peerViews() -> AnyView {
switch data { switch data {
case .placeholder: case .empty:
return AnyView(GeometryReader { geometry in return AnyView(GeometryReader { geometry in
placeholder(geometry: geometry) placeholder(geometry: geometry)
}) })
case .empty:
return AnyView(VStack {
Text(presentationData.applicationStartRequiredString)
})
case .locked:
return AnyView(VStack {
Text(presentationData.applicationLockedString)
})
case let .peers(peers): case let .peers(peers):
return AnyView(GeometryReader { geometry in return AnyView(GeometryReader { geometry in
peersView(geometry: geometry, peers: peers) peersView(geometry: geometry, peers: peers)
@ -348,23 +458,50 @@ struct WidgetView: View {
.padding(0.0) .padding(0.0)
} }
func chatTopLine(_ peer: WidgetDataPeer) -> some View { func chatTopLine(_ peer: ParsedPeer?) -> some View {
let dateText: String let dateText: String
if let message = peer.message {
let chatTitle: Text
let date: Text
if let peer = peer {
if let message = peer.peer.message {
dateText = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .none, timeStyle: .short) dateText = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .none, timeStyle: .short)
} else { } else {
dateText = "" dateText = ""
} }
chatTitle = Text(peer.peer.name).font(Font.system(size: 16.0, weight: .medium, design: .default)).foregroundColor(.primary)
date = Text(dateText)
.font(Font.system(size: 14.0, weight: .regular, design: .default)).foregroundColor(.secondary)
} else {
dateText = "10:00"
chatTitle = Text("Chat Title").font(Font.system(size: 16.0, weight: .medium, design: .default)).foregroundColor(.primary)
date = Text(dateText)
.font(Font.system(size: 14.0, weight: .regular, design: .default)).foregroundColor(.secondary)
}
return HStack(alignment: .center, spacing: 0.0, content: { return HStack(alignment: .center, spacing: 0.0, content: {
Text(peer.name).font(Font.system(size: 16.0, weight: .medium, design: .default)).foregroundColor(.primary) if peer != nil {
chatTitle
} else {
chatTitle.redacted(reason: .placeholder)
}
Spacer() Spacer()
Text(dateText).font(Font.system(size: 14.0, weight: .regular, design: .default)).foregroundColor(.secondary) if peer != nil {
date
} else {
date.redacted(reason: .placeholder)
}
}) })
.padding(0.0)
} }
func chatBottomLine(_ peer: WidgetDataPeer) -> some View { func chatBottomLine(_ peer: ParsedPeer?) -> AnyView {
var text = peer.message?.text ?? "" var text = peer?.peer.message?.text ?? ""
if let message = peer.message { text += "\n"
if peer == nil {
text = "First Line Of Text Here\nSecond line fwqefeqwfqwef qwef wq"
}
if let message = peer?.peer.message {
//TODO:localize //TODO:localize
switch message.content { switch message.content {
case .text: case .text:
@ -406,17 +543,47 @@ struct WidgetView: View {
case let .poll(poll): case let .poll(poll):
text = "📊 \(poll.title)" text = "📊 \(poll.title)"
} }
if let author = message.author {
if author.isMe {
text = "You: \(text)"
} else {
text = "\(author.title): \(text)"
}
}
} }
var hasBadge = false var hasBadge = false
if let badge = peer.badge, badge.count > 0 { if let peer = peer, let badge = peer.peer.badge, badge.count > 0 {
hasBadge = true hasBadge = true
} }
return HStack(alignment: .center, spacing: hasBadge ? 6.0 : 0.0, content: { let textView = Text(text)
Text(text).lineLimit(nil).font(Font.system(size: 15.0, weight: .regular, design: .default)).foregroundColor(.secondary).multilineTextAlignment(.leading).frame(maxHeight: .infinity, alignment: .topLeading) .lineLimit(2)
Spacer() .font(Font.system(size: 15.0, weight: .regular, design: .default))
if let badge = peer.badge, badge.count > 0 { .foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.padding(0.0)
//.frame(maxHeight: .infinity, alignment: .topLeading)
//.background(Rectangle().foregroundColor(.gray))
if peer != nil {
return AnyView(textView)
} else {
return AnyView(
textView
.redacted(reason: .placeholder)
)
}
/*return HStack(alignment: .center, spacing: hasBadge ? 6.0 : 0.0, content: {
if peer != nil {
textView
} else {
textView.redacted(reason: .placeholder)
}
//Spacer()
/*if let peer = peer, let badge = peer.badge, badge.count > 0 {
VStack { VStack {
Spacer() Spacer()
Text("\(badge.count)") Text("\(badge.count)")
@ -431,55 +598,86 @@ struct WidgetView: View {
) )
.padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 6.0, trailing: 3.0)) .padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 6.0, trailing: 3.0))
} }
} }*/
}) })*/
} }
func chatContent(_ peer: WidgetDataPeer) -> some View { func chatContent(_ peer: ParsedPeer?) -> some View {
return VStack(alignment: .leading, spacing: 2.0, content: { return VStack(alignment: .leading, spacing: 0.0, content: {
chatTopLine(peer) chatTopLine(peer)
chatBottomLine(peer).frame(maxHeight: .infinity) chatBottomLine(peer)
}) })
} }
func chatContentView(_ index: Int, size: CGSize) -> AnyView { func chatContentView(_ index: Int, size: CGSize) -> AnyView {
let peers: WidgetDataPeers let peers: ParsedPeers?
switch data { switch data {
case let .peers(peersValue): case let .peers(peersValue):
if peersValue.peers.count <= index {
peers = nil
} else {
peers = peersValue peers = peersValue
if peers.peers.count <= index {
return AnyView(Spacer())
} }
default: default:
return AnyView(Spacer()) peers = nil
} }
let itemHeight = (size.height - 22.0) / 2.0 let itemHeight = (size.height - 22.0) / 2.0
let url: URL
if let peers = peers {
url = URL(string: linkForPeer(id: peers.peers[index].peer.id))!
} else {
url = URL(string: "\(buildConfig.appSpecificUrlScheme)://")!
}
return AnyView( return AnyView(
Link(destination: URL(string: linkForPeer(id: peers.peers[index].id))!, label: { Link(destination: url, label: {
HStack(alignment: .center, spacing: 0.0, content: { HStack(alignment: .center, spacing: 0.0, content: {
AvatarItemView(accountPeerId: peers.accountPeerId, peer: peers.peers[index], itemSize: 54.0, displayBadge: false).frame(width: 54.0, height: 54.0, alignment: .leading).padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0)) AvatarItemView(peer: peers?.peers[index], itemSize: 54.0, displayBadge: false).frame(width: 54.0, height: 54.0, alignment: .leading).padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
chatContent(peers.peers[index]).frame(maxWidth: .infinity).padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 10.0)) chatContent(peers?.peers[index]).frame(maxWidth: .infinity).padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 10.0))
}) })
}).position(x: size.width / 2.0, y: (itemHeight * 2.0) / 4.0 + CGFloat(index) * itemHeight).frame(width: size.width, height: itemHeight, alignment: .leading) })
.frame(width: size.width, height: itemHeight, alignment: .leading)
) )
} }
func chatSeparatorView(size: CGSize) -> some View { func chatSeparatorView(size: CGSize) -> some View {
let separatorWidth = size.width - 54.0 - 20.0 return HStack(alignment: .center, spacing: 0.0, content: {
Spacer()
Rectangle()
.foregroundColor(getSeparatorColor())
.frame(width: size.width - 54.0 - 20.0, height: 0.5, alignment: .leading)
})
.frame(width: size.width, height: 1.0, alignment: .leading)
/*let separatorWidth = size.width - 54.0 - 20.0
let itemHeight = (size.height - 22.0) / 2.0 let itemHeight = (size.height - 22.0) / 2.0
return Rectangle().foregroundColor(getSeparatorColor()).position(x: (54.0 + 20.0 + separatorWidth) / 2.0, y: itemHeight / 2.0).frame(width: separatorWidth, height: 0.33, alignment: .leading) return Rectangle().foregroundColor(getSeparatorColor())
//.position(x: (54.0 + 20.0 + separatorWidth) / 2.0, y: itemHeight / 2.0)
.frame(width: size.width, height: 1.0, alignment: .leading)*/
} }
func chatsUpdateBackgroundView(size: CGSize) -> some View { func chatsUpdateBackgroundView(size: CGSize) -> some View {
return Rectangle().foregroundColor(getUpdatedBackgroundColor()).position(x: size.width / 2.0, y: size.height - 22.0 - 11.0).frame(width: size.width, height: 22.0, alignment: .leading) return Rectangle().foregroundColor(getUpdatedBackgroundColor())
//.position(x: size.width / 2.0, y: size.height - 11.0)
.frame(width: size.width, height: 22.0, alignment: .center)
}
func chatUpdateView(size: CGSize) -> some View {
return ZStack(alignment: Alignment(horizontal: .center, vertical: .center), content: {
Rectangle().foregroundColor(getUpdatedBackgroundColor())
chatsUpdateTimestampView(size: size)
})
.frame(width: size.width, height: 22.0 - 1.0, alignment: .center)
} }
func chatsUpdateTimestampView(size: CGSize) -> some View { func chatsUpdateTimestampView(size: CGSize) -> some View {
let text: String let text: String
switch data { switch data {
case let .peers(peersValue): case let .peers(peersValue):
if peersValue.peers.isEmpty {
text = "Long tap to edit widget"
} else {
let date = Date(timeIntervalSince1970: Double(peersValue.updateTimestamp)) let date = Date(timeIntervalSince1970: Double(peersValue.updateTimestamp))
let calendar = Calendar.current let calendar = Calendar.current
//TODO:localize //TODO:localize
@ -494,15 +692,20 @@ struct WidgetView: View {
formatter.timeStyle = .short formatter.timeStyle = .short
text = "updated at \(formatter.string(from: date))" text = "updated at \(formatter.string(from: date))"
} }
}
default: default:
text = "" text = ""
} }
return HStack(alignment: .center, spacing: 0.0, content: { return Text(text)
.font(Font.system(size: 12.0))
.foregroundColor(getUpdatedTextColor())
/*return HStack(alignment: .center, spacing: 0.0, content: {
Text(text) Text(text)
.font(Font.system(size: 12.0)) .font(Font.system(size: 12.0))
.foregroundColor(getUpdatedTextColor()) .foregroundColor(getUpdatedTextColor())
}).position(x: size.width / 2.0, y: size.height - 22.0 - 11.0).frame(width: size.width, height: 22.0, alignment: .leading) }).position(x: size.width / 2.0, y: size.height - 11.0).frame(width: size.width, height: 22.0, alignment: .leading)*/
} }
func getSeparatorColor() -> Color { func getSeparatorColor() -> Color {
@ -540,13 +743,12 @@ struct WidgetView: View {
var body: some View { var body: some View {
GeometryReader(content: { geometry in GeometryReader(content: { geometry in
return ZStack { return VStack(alignment: .center, spacing: 0.0, content: {
chatContentView(0, size: geometry.size) chatContentView(0, size: geometry.size)
chatContentView(1, size: geometry.size)
chatSeparatorView(size: geometry.size) chatSeparatorView(size: geometry.size)
chatsUpdateBackgroundView(size: geometry.size) chatContentView(1, size: geometry.size)
chatsUpdateTimestampView(size: geometry.size) chatUpdateView(size: geometry.size)
} })
}) })
.padding(0.0) .padding(0.0)
} }
@ -600,7 +802,7 @@ func getWidgetData(contents: SimpleEntry.Contents) -> PeersWidgetData {
case .recent: case .recent:
let appBundleIdentifier = Bundle.main.bundleIdentifier! let appBundleIdentifier = Bundle.main.bundleIdentifier!
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
return .placeholder return .empty
} }
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound]) let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
@ -608,28 +810,28 @@ func getWidgetData(contents: SimpleEntry.Contents) -> PeersWidgetData {
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else { guard let appGroupUrl = maybeAppGroupUrl else {
return .placeholder return .empty
} }
let rootPath = rootPathForBasePath(appGroupUrl.path) let rootPath = rootPathForBasePath(appGroupUrl.path)
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { /*if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data) {
return .locked if state.isManuallyLocked || state.autolockTimeout != nil {
return .empty
} }
}*/
let dataPath = rootPath + "/widget-data" let dataPath = rootPath + "/widget-data"
if let data = try? Data(contentsOf: URL(fileURLWithPath: dataPath)), let widgetData = try? JSONDecoder().decode(WidgetData.self, from: data) { if let data = try? Data(contentsOf: URL(fileURLWithPath: dataPath)), let widgetData = try? JSONDecoder().decode(WidgetData.self, from: data) {
switch widgetData.content { switch widgetData.content {
case let .peers(peers): case let .peers(peers):
return .peers(peers) return .peers(ParsedPeers(accountId: widgetData.accountId, peers: peers))
case .disabled: case .empty:
return .placeholder return .empty
case .notAuthorized:
return .locked
} }
} else { } else {
return .placeholder return .empty
} }
case let .peers(peers): case let .peers(peers):
return .peers(peers) return .peers(peers)

@ -1 +1 @@
Subproject commit f7f2b6d7c952f3cf6bdcedce6a0a2a40a27ff596 Subproject commit c1f83903e864d753477e51d66d3ada6c2c6d096f

@ -1 +1 @@
Subproject commit e2b1e1e4399bd168a00d1c3125eaa3ae52835340 Subproject commit 49ef5bc098f68e40f730518f1c795d642e5c11a2

@ -1 +1 @@
Subproject commit bfb54953cee1bc985ba8113dd2e635e1d294abdb Subproject commit e80b9795db5e35f86d643e0fba7776f1b1f71066

View File

@ -40,6 +40,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
_startValue = 0.0f; _startValue = 0.0f;
_value = _startValue; _value = _startValue;
_dotSize = 10.5f; _dotSize = 10.5f;
_minimumUndottedValue = -1;
_lineSize = TGPhotoEditorSliderViewLineSize; _lineSize = TGPhotoEditorSliderViewLineSize;
_knobPadding = TGPhotoEditorSliderViewInternalMargin; _knobPadding = TGPhotoEditorSliderViewInternalMargin;
@ -174,7 +175,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
CGContextSetBlendMode(context, kCGBlendModeCopy); CGContextSetBlendMode(context, kCGBlendModeCopy);
} }
if (_minimumUndottedValue > 0 && self.positionsCount > 1) { if (_minimumUndottedValue > -1 && self.positionsCount > 1) {
CGContextSetLineWidth(context, backFrame.size.height); CGContextSetLineWidth(context, backFrame.size.height);
CGContextSetLineCap(context, kCGLineCapRound); CGContextSetLineCap(context, kCGLineCapRound);
@ -222,7 +223,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
CGContextSetBlendMode(context, kCGBlendModeNormal); CGContextSetBlendMode(context, kCGBlendModeNormal);
if (_minimumUndottedValue > 0) { if (_minimumUndottedValue > -1) {
} else { } else {
CGContextSetFillColorWithColor(context, _trackColor.CGColor); CGContextSetFillColorWithColor(context, _trackColor.CGColor);
[self drawRectangle:trackFrame cornerRadius:self.trackCornerRadius context:context]; [self drawRectangle:trackFrame cornerRadius:self.trackCornerRadius context:context];
@ -359,6 +360,13 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
[self setNeedsDisplay]; [self setNeedsDisplay];
} }
- (void)setMinimumUndottedValue:(int)minimumUndottedValue {
if (_minimumUndottedValue != minimumUndottedValue) {
_minimumUndottedValue = minimumUndottedValue;
[self setNeedsDisplay];
}
}
#pragma mark - Properties #pragma mark - Properties
- (bool)isTracking - (bool)isTracking

View File

@ -147,9 +147,13 @@ private func peerAutoremoveSetupEntries(peer: Peer?, presentationData: Presentat
entries.append(.timeValue(resolvedValue, resolvedMaxValue)) entries.append(.timeValue(resolvedValue, resolvedMaxValue))
if let channel = peer as? TelegramChannel, case .broadcast = channel.info { if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
entries.append(.timeComment("Automatically delete messages sent in this channel after a certain period of time.")) entries.append(.timeComment("Automatically delete messages sent in this channel after a certain period of time."))
} else {
if resolvedMaxValue != Int32.max {
entries.append(.timeComment("\(peer?.compactDisplayTitle ?? "") has set messages to auto-delete in \(timeIntervalString(strings: presentationData.strings, value: resolvedMaxValue)). You can't cancel it or make this interval longer."))
} else { } else {
entries.append(.timeComment("Automatically delete messages sent in this chat after a certain period of time.")) entries.append(.timeComment("Automatically delete messages sent in this chat after a certain period of time."))
} }
}
if let user = peer as? TelegramUser { if let user = peer as? TelegramUser {
entries.append(.globalSwitch("Also auto-delete for \(user.compactDisplayTitle)", globalValue)) entries.append(.globalSwitch("Also auto-delete for \(user.compactDisplayTitle)", globalValue))
} }
@ -158,8 +162,13 @@ private func peerAutoremoveSetupEntries(peer: Peer?, presentationData: Presentat
} }
public enum PeerAutoremoveSetupScreenResult { public enum PeerAutoremoveSetupScreenResult {
public struct Updated {
public var myValue: Int32?
public var limitedByValue: Int32?
}
case unchanged case unchanged
case updated(Int32?) case updated(Updated)
} }
public func peerAutoremoveSetupScreen(context: AccountContext, peerId: PeerId, completion: @escaping (PeerAutoremoveSetupScreenResult) -> Void = { _ in }) -> ViewController { public func peerAutoremoveSetupScreen(context: AccountContext, peerId: PeerId, completion: @escaping (PeerAutoremoveSetupScreenResult) -> Void = { _ in }) -> ViewController {
@ -257,6 +266,13 @@ public func peerAutoremoveSetupScreen(context: AccountContext, peerId: PeerId, c
resolvedValue = nil resolvedValue = nil
} }
let resolvedMaxValue: Int32
if peer is TelegramUser {
resolvedMaxValue = peerValue
} else {
resolvedMaxValue = Int32.max
}
let resolvedGlobalValue = globalValue ?? defaultGlobalValue let resolvedGlobalValue = globalValue ?? defaultGlobalValue
let signal = setChatMessageAutoremoveTimeoutInteractively(account: context.account, peerId: peerId, timeout: resolvedValue, isGlobal: resolvedGlobalValue) let signal = setChatMessageAutoremoveTimeoutInteractively(account: context.account, peerId: peerId, timeout: resolvedValue, isGlobal: resolvedGlobalValue)
@ -267,7 +283,10 @@ public func peerAutoremoveSetupScreen(context: AccountContext, peerId: PeerId, c
}, completed: { }, completed: {
dismissImpl?() dismissImpl?()
if resolvedValue != resolvedDefaultValue { if resolvedValue != resolvedDefaultValue {
completion(.updated(changedValue)) completion(.updated(PeerAutoremoveSetupScreenResult.Updated(
myValue: resolvedValue,
limitedByValue: resolvedMaxValue == Int32.max ? nil : resolvedMaxValue
)))
} else { } else {
completion(.unchanged) completion(.unchanged)
} }

View File

@ -90,7 +90,7 @@ class PeerRemoveTimeoutItem: ListViewItem, ItemListItem {
private func generateKnobImage() -> UIImage? { private func generateKnobImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.25).cgColor) context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 8.0, color: UIColor(white: 0.0, alpha: 0.15).cgColor)
context.setFillColor(UIColor.white.cgColor) context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
}) })
@ -160,7 +160,7 @@ class PeerRemoveTimeoutItemNode: ListViewItemNode, ItemListItemNode {
sliderView.value = mapTimeoutToSliderValue(item.value) sliderView.value = mapTimeoutToSliderValue(item.value)
sliderView.minimumUndottedValue = 2 - Int32(mapTimeoutToSliderValue(item.maxValue)) sliderView.minimumUndottedValue = Int32(mapTimeoutToSliderValue(item.maxValue))
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor sliderView.backColor = item.theme.list.disclosureArrowColor
@ -287,6 +287,7 @@ class PeerRemoveTimeoutItemNode: ListViewItemNode, ItemListItemNode {
if let sliderView = strongSelf.sliderView { if let sliderView = strongSelf.sliderView {
sliderView.isUserInteractionEnabled = item.enabled sliderView.isUserInteractionEnabled = item.enabled
sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
sliderView.minimumUndottedValue = Int32(mapTimeoutToSliderValue(item.maxValue))
if themeUpdated { if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor

View File

@ -79,6 +79,17 @@ public enum PostboxAccessChallengeData: PostboxCoding, Equatable, Codable {
return true return true
} }
} }
public var lockId: String? {
switch self {
case .none:
return nil
case let .numericalPassword(value):
return "numericalPassword:\(value)"
case let .plaintextPassword(value):
return "plaintextPassword:\(value)"
}
}
} }
public struct AuthAccountRecord: PostboxCoding, Codable { public struct AuthAccountRecord: PostboxCoding, Codable {

View File

@ -559,31 +559,31 @@ final class MutableChatListView {
return self.sampledState.hole return self.sampledState.hole
} }
private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) -> MutableChatListEntry? { private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox) -> MutableChatListEntry? {
switch entry { switch entry {
case let .IntermediateMessageEntry(index, messageIndex): case let .IntermediateMessageEntry(index, messageIndex):
var renderedMessages: [Message] = [] var renderedMessages: [Message] = []
if let messageIndex = messageIndex { if let messageIndex = messageIndex {
if let messageGroup = postbox.messageHistoryTable.getMessageGroup(at: messageIndex, limit: 10) { if let messageGroup = postbox.messageHistoryTable.getMessageGroup(at: messageIndex, limit: 10) {
renderedMessages.append(contentsOf: messageGroup.compactMap(renderMessage)) renderedMessages.append(contentsOf: messageGroup.compactMap(postbox.renderIntermediateMessage))
} }
} }
var peers = SimpleDictionary<PeerId, Peer>() var peers = SimpleDictionary<PeerId, Peer>()
var notificationSettings: PeerNotificationSettings? var notificationSettings: PeerNotificationSettings?
var presence: PeerPresence? var presence: PeerPresence?
var isContact: Bool = false var isContact: Bool = false
if let peer = getPeer(index.messageIndex.id.peerId) { if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
peers[peer.id] = peer peers[peer.id] = peer
if let associatedPeerId = peer.associatedPeerId { if let associatedPeerId = peer.associatedPeerId {
if let associatedPeer = getPeer(associatedPeerId) { if let associatedPeer = postbox.peerTable.get(associatedPeerId) {
peers[associatedPeer.id] = associatedPeer peers[associatedPeer.id] = associatedPeer
} }
notificationSettings = getPeerNotificationSettings(associatedPeerId) notificationSettings = postbox.peerNotificationSettingsTable.getEffective(associatedPeerId)
presence = getPeerPresence(associatedPeerId) presence = postbox.peerPresenceTable.get(associatedPeerId)
isContact = postbox.contactsTable.isContact(peerId: associatedPeerId) isContact = postbox.contactsTable.isContact(peerId: associatedPeerId)
} else { } else {
notificationSettings = getPeerNotificationSettings(index.messageIndex.id.peerId) notificationSettings = postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId)
presence = getPeerPresence(index.messageIndex.id.peerId) presence = postbox.peerPresenceTable.get(index.messageIndex.id.peerId)
isContact = postbox.contactsTable.isContact(peerId: peer.id) isContact = postbox.contactsTable.isContact(peerId: peer.id)
} }
} }
@ -597,9 +597,9 @@ final class MutableChatListView {
} }
} }
func render(postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) { func render(postbox: Postbox) {
for i in 0 ..< self.additionalItemEntries.count { for i in 0 ..< self.additionalItemEntries.count {
if let updatedEntry = self.renderEntry(self.additionalItemEntries[i].entry, postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) { if let updatedEntry = self.renderEntry(self.additionalItemEntries[i].entry, postbox: postbox) {
self.additionalItemEntries[i].entry = updatedEntry self.additionalItemEntries[i].entry = updatedEntry
} }
} }

View File

@ -15,7 +15,7 @@ final class MutableContactPeersView {
self.includePresences = includePresences self.includePresences = includePresences
} }
func replay(replacePeerIds: Set<PeerId>?, updatedPeerPresences: [PeerId: PeerPresence], getPeer: (PeerId) -> Peer?, getPeerPresence: (PeerId) -> PeerPresence?) -> Bool { func replay(postbox: Postbox, replacePeerIds: Set<PeerId>?, updatedPeerPresences: [PeerId: PeerPresence]) -> Bool {
var updated = false var updated = false
if let replacePeerIds = replacePeerIds { if let replacePeerIds = replacePeerIds {
let removedPeerIds = self.peerIds.subtracting(replacePeerIds) let removedPeerIds = self.peerIds.subtracting(replacePeerIds)
@ -29,11 +29,11 @@ final class MutableContactPeersView {
} }
for peerId in addedPeerIds { for peerId in addedPeerIds {
if let peer = getPeer(peerId) { if let peer = postbox.peerTable.get(peerId) {
self.peers[peerId] = peer self.peers[peerId] = peer
} }
if self.includePresences { if self.includePresences {
if let presence = getPeerPresence(peerId) { if let presence = postbox.peerPresenceTable.get(peerId) {
self.peerPresences[peerId] = presence self.peerPresences[peerId] = presence
} }
} }

View File

@ -11,7 +11,7 @@ final class MutableMessageView {
self.stableId = message?.stableId self.stableId = message?.stableId
} }
func replay(_ operations: [MessageHistoryOperation], updatedMedia: [MediaId: Media?], renderIntermediateMessage: (IntermediateMessage) -> Message) -> Bool { func replay(postbox: Postbox, operations: [MessageHistoryOperation], updatedMedia: [MediaId: Media?]) -> Bool {
var updated = false var updated = false
for operation in operations { for operation in operations {
switch operation { switch operation {
@ -28,7 +28,7 @@ final class MutableMessageView {
} }
case let .InsertMessage(message): case let .InsertMessage(message):
if message.id == self.messageId || message.stableId == self.stableId { if message.id == self.messageId || message.stableId == self.stableId {
self.message = renderIntermediateMessage(message) self.message = postbox.renderIntermediateMessage(message)
self.stableId = message.stableId self.stableId = message.stableId
updated = true updated = true
} }

View File

@ -6,14 +6,14 @@ final class MutablePeerMergedOperationLogView {
var tailIndex: Int32? var tailIndex: Int32?
let limit: Int let limit: Int
init(tag: PeerOperationLogTag, limit: Int, getOperations: (PeerOperationLogTag, Int32, Int) -> [PeerMergedOperationLogEntry], getTailIndex: (PeerOperationLogTag) -> Int32?) { init(postbox: Postbox, tag: PeerOperationLogTag, limit: Int) {
self.tag = tag self.tag = tag
self.entries = getOperations(tag, 0, limit) self.entries = postbox.peerOperationLogTable.getMergedEntries(tag: tag, fromIndex: 0, limit: limit)
self.tailIndex = getTailIndex(tag) self.tailIndex = postbox.peerMergedOperationLogIndexTable.tailIndex(tag: tag)
self.limit = limit self.limit = limit
} }
func replay(operations: [PeerMergedOperationLogOperation], getOperations: (PeerOperationLogTag, Int32, Int) -> [PeerMergedOperationLogEntry], getTailIndex: (PeerOperationLogTag) -> Int32?) -> Bool { func replay(postbox: Postbox, operations: [PeerMergedOperationLogOperation]) -> Bool {
var updated = false var updated = false
var invalidatedTail = false var invalidatedTail = false
@ -65,7 +65,7 @@ final class MutablePeerMergedOperationLogView {
if updated { if updated {
if invalidatedTail { if invalidatedTail {
self.tailIndex = getTailIndex(self.tag) self.tailIndex = postbox.peerMergedOperationLogIndexTable.tailIndex(tag: self.tag)
} }
if self.entries.count < self.limit { if self.entries.count < self.limit {
if let tailIndex = self.tailIndex { if let tailIndex = self.tailIndex {
@ -74,7 +74,7 @@ final class MutablePeerMergedOperationLogView {
if !self.entries.isEmpty { if !self.entries.isEmpty {
fromIndex = self.entries.last!.mergedIndex + 1 fromIndex = self.entries.last!.mergedIndex + 1
} }
for entry in getOperations(self.tag, fromIndex, self.limit - self.entries.count) { for entry in postbox.peerOperationLogTable.getMergedEntries(tag: self.tag, fromIndex: fromIndex, limit: self.limit - self.entries.count) {
self.entries.append(entry) self.entries.append(entry)
} }
for i in 0 ..< self.entries.count { for i in 0 ..< self.entries.count {

View File

@ -1058,6 +1058,25 @@ public final class Transaction {
return postbox.chatListTable.getNamespaceEntries(groupId: groupId, namespace: namespace, summaryTag: summaryTag, messageIndexTable: postbox.messageHistoryIndexTable, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, readStateTable: postbox.readStateTable, summaryTable: postbox.messageHistoryTagsSummaryTable) return postbox.chatListTable.getNamespaceEntries(groupId: groupId, namespace: namespace, summaryTag: summaryTag, messageIndexTable: postbox.messageHistoryIndexTable, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, readStateTable: postbox.readStateTable, summaryTable: postbox.messageHistoryTagsSummaryTable)
} }
public func getTopChatListEntries(groupId: PeerGroupId, count: Int) -> [RenderedPeer] {
assert(!self.disposed)
guard let postbox = self.postbox else {
return []
}
return postbox.chatListTable.earlierEntryInfos(groupId: groupId, index: nil, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: count).compactMap { entry -> RenderedPeer? in
switch entry {
case let .message(index, _):
if let peer = self.getPeer(index.messageIndex.id.peerId) {
return RenderedPeer(peer: peer)
} else {
return nil
}
case .hole:
return nil
}
}
}
public func addHolesEverywhere(peerNamespaces: [PeerId.Namespace], holeNamespace: MessageId.Namespace) { public func addHolesEverywhere(peerNamespaces: [PeerId.Namespace], holeNamespace: MessageId.Namespace) {
assert(!self.disposed) assert(!self.disposed)
self.postbox?.addHolesEverywhere(peerNamespaces: peerNamespaces, holeNamespace: holeNamespace) self.postbox?.addHolesEverywhere(peerNamespaces: peerNamespaces, holeNamespace: holeNamespace)
@ -1067,6 +1086,11 @@ public final class Transaction {
assert(!self.disposed) assert(!self.disposed)
self.postbox?.reindexUnreadCounters() self.postbox?.reindexUnreadCounters()
} }
public func searchPeers(query: String) -> [RenderedPeer] {
assert(!self.disposed)
return self.postbox?.searchPeers(query: query) ?? []
}
} }
public enum PostboxResult { public enum PostboxResult {
@ -1481,27 +1505,13 @@ public final class Postbox {
self.transactionStateVersion = self.metadataTable.transactionStateVersion() self.transactionStateVersion = self.metadataTable.transactionStateVersion()
self.viewTracker = ViewTracker(queue: self.queue, renderMessage: self.renderIntermediateMessage, getPeer: { peerId in self.viewTracker = ViewTracker(
return self.peerTable.get(peerId) queue: self.queue,
}, getPeerNotificationSettings: { peerId in unsentMessageIds: self.messageHistoryUnsentTable.get(),
return self.peerNotificationSettingsTable.getEffective(peerId) synchronizePeerReadStateOperations: self.synchronizeReadStateTable.get(getCombinedPeerReadState: { peerId in
}, getCachedPeerData: { peerId in
return self.cachedPeerDataTable.get(peerId)
}, getPeerPresence: { peerId in
return self.peerPresenceTable.get(peerId)
}, getPeerReadState: { peerId in
return self.readStateTable.getCombinedState(peerId) return self.readStateTable.getCombinedState(peerId)
}, operationLogGetOperations: { tag, fromIndex, limit in })
return self.peerOperationLogTable.getMergedEntries(tag: tag, fromIndex: fromIndex, limit: limit) )
}, operationLogGetTailIndex: { tag in
return self.peerMergedOperationLogIndexTable.tailIndex(tag: tag)
}, getTimestampBasedMessageAttributesHead: { tag in
return self.timestampBasedMessageAttributesTable.head(tag: tag)
}, getPreferencesEntry: { key in
return self.preferencesTable.get(key: key)
}, unsentMessageIds: self.messageHistoryUnsentTable.get(), synchronizePeerReadStateOperations: self.synchronizeReadStateTable.get(getCombinedPeerReadState: { peerId in
return self.readStateTable.getCombinedState(peerId)
}))
print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
@ -2784,13 +2794,7 @@ public final class Postbox {
public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate? = nil, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> { public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate? = nil, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> {
return self.transactionSignal(userInteractive: userInteractive, { subscriber, transaction in return self.transactionSignal(userInteractive: userInteractive, { subscriber, transaction in
let mutableView = MutableChatListView(postbox: self, groupId: groupId, filterPredicate: filterPredicate, aroundIndex: index, count: count, summaryComponents: summaryComponents) let mutableView = MutableChatListView(postbox: self, groupId: groupId, filterPredicate: filterPredicate, aroundIndex: index, count: count, summaryComponents: summaryComponents)
mutableView.render(postbox: self, renderMessage: self.renderIntermediateMessage, getPeer: { id in mutableView.render(postbox: self)
return self.peerTable.get(id)
}, getPeerNotificationSettings: {
self.peerNotificationSettingsTable.getEffective($0)
}, getPeerPresence: {
self.peerPresenceTable.get($0)
})
let (index, signal) = self.viewTracker.addChatListView(mutableView) let (index, signal) = self.viewTracker.addChatListView(mutableView)
@ -2890,6 +2894,11 @@ public final class Postbox {
public func searchPeers(query: String) -> Signal<[RenderedPeer], NoError> { public func searchPeers(query: String) -> Signal<[RenderedPeer], NoError> {
return self.transaction { transaction -> Signal<[RenderedPeer], NoError> in return self.transaction { transaction -> Signal<[RenderedPeer], NoError> in
return .single(transaction.searchPeers(query: query))
} |> switchToLatest
}
fileprivate func searchPeers(query: String) -> [RenderedPeer] {
var peerIds = Set<PeerId>() var peerIds = Set<PeerId>()
var chatPeers: [RenderedPeer] = [] var chatPeers: [RenderedPeer] = []
@ -2934,8 +2943,7 @@ public final class Postbox {
contactPeers.sort(by: { lhs, rhs in contactPeers.sort(by: { lhs, rhs in
lhs.peers[lhs.peerId]!.indexName.indexName(.lastNameFirst) < rhs.peers[rhs.peerId]!.indexName.indexName(.lastNameFirst) lhs.peers[lhs.peerId]!.indexName.indexName(.lastNameFirst) < rhs.peers[rhs.peerId]!.indexName.indexName(.lastNameFirst)
}) })
return .single(chatPeers + contactPeers) return chatPeers + contactPeers
} |> switchToLatest
} }
public func peerView(id: PeerId) -> Signal<PeerView, NoError> { public func peerView(id: PeerId) -> Signal<PeerView, NoError> {
@ -3128,11 +3136,7 @@ public final class Postbox {
public func mergedOperationLogView(tag: PeerOperationLogTag, limit: Int) -> Signal<PeerMergedOperationLogView, NoError> { public func mergedOperationLogView(tag: PeerOperationLogTag, limit: Int) -> Signal<PeerMergedOperationLogView, NoError> {
return self.transactionSignal { subscriber, transaction in return self.transactionSignal { subscriber, transaction in
let view = MutablePeerMergedOperationLogView(tag: tag, limit: limit, getOperations: { tag, fromIndex, limit in let view = MutablePeerMergedOperationLogView(postbox: self, tag: tag, limit: limit)
return self.peerOperationLogTable.getMergedEntries(tag: tag, fromIndex: fromIndex, limit: limit)
}, getTailIndex: { tag in
return self.peerMergedOperationLogIndexTable.tailIndex(tag: tag)
})
subscriber.putNext(PeerMergedOperationLogView(view)) subscriber.putNext(PeerMergedOperationLogView(view))
@ -3155,9 +3159,7 @@ public final class Postbox {
public func timestampBasedMessageAttributesView(tag: UInt16) -> Signal<TimestampBasedMessageAttributesView, NoError> { public func timestampBasedMessageAttributesView(tag: UInt16) -> Signal<TimestampBasedMessageAttributesView, NoError> {
return self.transactionSignal { subscriber, transaction in return self.transactionSignal { subscriber, transaction in
let view = MutableTimestampBasedMessageAttributesView(tag: tag, getHead: { tag in let view = MutableTimestampBasedMessageAttributesView(postbox: self, tag: tag)
return self.timestampBasedMessageAttributesTable.head(tag: tag)
})
let (index, signal) = self.viewTracker.addTimestampBasedMessageAttributesView(view) let (index, signal) = self.viewTracker.addTimestampBasedMessageAttributesView(view)
subscriber.putNext(TimestampBasedMessageAttributesView(view)) subscriber.putNext(TimestampBasedMessageAttributesView(view))

View File

@ -4,12 +4,12 @@ final class MutableTimestampBasedMessageAttributesView {
let tag: UInt16 let tag: UInt16
var head: TimestampBasedMessageAttributesEntry? var head: TimestampBasedMessageAttributesEntry?
init(tag: UInt16, getHead: (UInt16) -> TimestampBasedMessageAttributesEntry?) { init(postbox: Postbox, tag: UInt16) {
self.tag = tag self.tag = tag
self.head = getHead(tag) self.head = postbox.timestampBasedMessageAttributesTable.head(tag: tag)
} }
func replay(operations: [TimestampBasedMessageAttributesOperation], getHead: (UInt16) -> TimestampBasedMessageAttributesEntry?) -> Bool { func replay(postbox: Postbox, operations: [TimestampBasedMessageAttributesOperation]) -> Bool {
var updated = false var updated = false
var invalidatedHead = false var invalidatedHead = false
for operation in operations { for operation in operations {
@ -37,7 +37,7 @@ final class MutableTimestampBasedMessageAttributesView {
} }
} }
if invalidatedHead { if invalidatedHead {
self.head = getHead(self.tag) self.head = postbox.timestampBasedMessageAttributesTable.head(tag: self.tag)
} }
return updated return updated
} }

View File

@ -11,15 +11,6 @@ public enum ViewUpdateType {
final class ViewTracker { final class ViewTracker {
private let queue: Queue private let queue: Queue
private let renderMessage: (IntermediateMessage) -> Message
private let getPeer: (PeerId) -> Peer?
private let getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?
private let getCachedPeerData: (PeerId) -> CachedPeerData?
private let getPeerPresence: (PeerId) -> PeerPresence?
private let getPeerReadState: (PeerId) -> CombinedPeerReadState?
private let operationLogGetOperations: (PeerOperationLogTag, Int32, Int) -> [PeerMergedOperationLogEntry]
private let operationLogGetTailIndex: (PeerOperationLogTag) -> Int32?
private let getPreferencesEntry: (ValueBoxKey) -> PreferencesEntry?
private var chatListViews = Bag<(MutableChatListView, ValuePipe<(ChatListView, ViewUpdateType)>)>() private var chatListViews = Bag<(MutableChatListView, ValuePipe<(ChatListView, ViewUpdateType)>)>()
private var messageHistoryViews = Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>() private var messageHistoryViews = Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>()
@ -46,7 +37,6 @@ final class ViewTracker {
private var unreadMessageCountsViews = Bag<(MutableUnreadMessageCountsView, ValuePipe<UnreadMessageCountsView>)>() private var unreadMessageCountsViews = Bag<(MutableUnreadMessageCountsView, ValuePipe<UnreadMessageCountsView>)>()
private var peerMergedOperationLogViews = Bag<(MutablePeerMergedOperationLogView, ValuePipe<PeerMergedOperationLogView>)>() private var peerMergedOperationLogViews = Bag<(MutablePeerMergedOperationLogView, ValuePipe<PeerMergedOperationLogView>)>()
private let getTimestampBasedMessageAttributesHead: (UInt16) -> TimestampBasedMessageAttributesEntry?
private var timestampBasedMessageAttributesViews = Bag<(MutableTimestampBasedMessageAttributesView, ValuePipe<TimestampBasedMessageAttributesView>)>() private var timestampBasedMessageAttributesViews = Bag<(MutableTimestampBasedMessageAttributesView, ValuePipe<TimestampBasedMessageAttributesView>)>()
private var combinedViews = Bag<(CombinedMutableView, ValuePipe<CombinedView>)>() private var combinedViews = Bag<(CombinedMutableView, ValuePipe<CombinedView>)>()
@ -56,23 +46,14 @@ final class ViewTracker {
private var preferencesViews = Bag<(MutablePreferencesView, ValuePipe<PreferencesView>)>() private var preferencesViews = Bag<(MutablePreferencesView, ValuePipe<PreferencesView>)>()
private var multiplePeersViews = Bag<(MutableMultiplePeersView, ValuePipe<MultiplePeersView>)>() private var multiplePeersViews = Bag<(MutableMultiplePeersView, ValuePipe<MultiplePeersView>)>()
private var itemCollectionsViews = Bag<(MutableItemCollectionsView, ValuePipe<ItemCollectionsView>)>() private var itemCollectionsViews = Bag<(MutableItemCollectionsView, ValuePipe<ItemCollectionsView>)>()
private var failedMessageIdsViews = Bag<(MutableFailedMessageIdsView, ValuePipe<FailedMessageIdsView>)>() private var failedMessageIdsViews = Bag<(MutableFailedMessageIdsView, ValuePipe<FailedMessageIdsView>)>()
init(
init(queue: Queue, renderMessage: @escaping (IntermediateMessage) -> Message, getPeer: @escaping (PeerId) -> Peer?, getPeerNotificationSettings: @escaping (PeerId) -> PeerNotificationSettings?, getCachedPeerData: @escaping (PeerId) -> CachedPeerData?, getPeerPresence: @escaping (PeerId) -> PeerPresence?, getPeerReadState: @escaping (PeerId) -> CombinedPeerReadState?, operationLogGetOperations: @escaping (PeerOperationLogTag, Int32, Int) -> [PeerMergedOperationLogEntry], operationLogGetTailIndex: @escaping (PeerOperationLogTag) -> Int32?, getTimestampBasedMessageAttributesHead: @escaping (UInt16) -> TimestampBasedMessageAttributesEntry?, getPreferencesEntry: @escaping (ValueBoxKey) -> PreferencesEntry?, unsentMessageIds: [MessageId], synchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation]) { queue: Queue,
unsentMessageIds: [MessageId],
synchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation]
) {
self.queue = queue self.queue = queue
self.renderMessage = renderMessage
self.getPeer = getPeer
self.getPeerNotificationSettings = getPeerNotificationSettings
self.getCachedPeerData = getCachedPeerData
self.getPeerPresence = getPeerPresence
self.getPeerReadState = getPeerReadState
self.operationLogGetOperations = operationLogGetOperations
self.operationLogGetTailIndex = operationLogGetTailIndex
self.getTimestampBasedMessageAttributesHead = getTimestampBasedMessageAttributesHead
self.getPreferencesEntry = getPreferencesEntry
self.unsentMessageView = UnsentMessageHistoryView(ids: unsentMessageIds) self.unsentMessageView = UnsentMessageHistoryView(ids: unsentMessageIds)
self.synchronizeReadStatesView = MutableSynchronizePeerReadStatesView(operations: synchronizePeerReadStateOperations) self.synchronizeReadStatesView = MutableSynchronizePeerReadStatesView(operations: synchronizePeerReadStateOperations)
@ -254,9 +235,7 @@ final class ViewTracker {
for (mutableView, pipe) in self.chatListViews.copyItems() { for (mutableView, pipe) in self.chatListViews.copyItems() {
if mutableView.refreshDueToExternalTransaction(postbox: postbox) { if mutableView.refreshDueToExternalTransaction(postbox: postbox) {
mutableView.render(postbox: postbox, renderMessage: self.renderMessage, getPeer: { id in mutableView.render(postbox: postbox)
return self.getPeer(id)
}, getPeerNotificationSettings: self.getPeerNotificationSettings, getPeerPresence: self.getPeerPresence)
pipe.putNext((ChatListView(mutableView), .Generic)) pipe.putNext((ChatListView(mutableView), .Generic))
} }
} }
@ -350,7 +329,7 @@ final class ViewTracker {
for (mutableView, pipe) in self.messageViews.copyItems() { for (mutableView, pipe) in self.messageViews.copyItems() {
let operations = transaction.currentOperationsByPeerId[mutableView.messageId.peerId] let operations = transaction.currentOperationsByPeerId[mutableView.messageId.peerId]
if operations != nil || !transaction.updatedMedia.isEmpty || !transaction.currentUpdatedCachedPeerData.isEmpty { if operations != nil || !transaction.updatedMedia.isEmpty || !transaction.currentUpdatedCachedPeerData.isEmpty {
if mutableView.replay(operations ?? [], updatedMedia: transaction.updatedMedia, renderIntermediateMessage: self.renderMessage) { if mutableView.replay(postbox: postbox, operations: operations ?? [], updatedMedia: transaction.updatedMedia) {
pipe.putNext(MessageView(mutableView)) pipe.putNext(MessageView(mutableView))
} }
} }
@ -360,9 +339,7 @@ final class ViewTracker {
let context = MutableChatListViewReplayContext() let context = MutableChatListViewReplayContext()
if mutableView.replay(postbox: postbox, operations: transaction.chatListOperations, updatedPeerNotificationSettings: transaction.currentUpdatedPeerNotificationSettings, updatedPeers: transaction.currentUpdatedPeers, updatedPeerPresences: transaction.currentUpdatedPeerPresences, transaction: transaction, context: context) { if mutableView.replay(postbox: postbox, operations: transaction.chatListOperations, updatedPeerNotificationSettings: transaction.currentUpdatedPeerNotificationSettings, updatedPeers: transaction.currentUpdatedPeers, updatedPeerPresences: transaction.currentUpdatedPeerPresences, transaction: transaction, context: context) {
mutableView.complete(postbox: postbox, context: context) mutableView.complete(postbox: postbox, context: context)
mutableView.render(postbox: postbox, renderMessage: self.renderMessage, getPeer: { id in mutableView.render(postbox: postbox)
return self.getPeer(id)
}, getPeerNotificationSettings: self.getPeerNotificationSettings, getPeerPresence: self.getPeerPresence)
pipe.putNext((ChatListView(mutableView), .Generic)) pipe.putNext((ChatListView(mutableView), .Generic))
} }
} }
@ -396,7 +373,7 @@ final class ViewTracker {
} }
for (mutableView, pipe) in self.contactPeersViews.copyItems() { for (mutableView, pipe) in self.contactPeersViews.copyItems() {
if mutableView.replay(replacePeerIds: transaction.replaceContactPeerIds, updatedPeerPresences: transaction.currentUpdatedPeerPresences, getPeer: self.getPeer, getPeerPresence: self.getPeerPresence) { if mutableView.replay(postbox: postbox, replacePeerIds: transaction.replaceContactPeerIds, updatedPeerPresences: transaction.currentUpdatedPeerPresences) {
pipe.putNext(ContactPeersView(mutableView)) pipe.putNext(ContactPeersView(mutableView))
} }
} }
@ -408,13 +385,13 @@ final class ViewTracker {
} }
for (mutableView, pipe) in self.peerMergedOperationLogViews.copyItems() { for (mutableView, pipe) in self.peerMergedOperationLogViews.copyItems() {
if mutableView.replay(operations: transaction.currentPeerMergedOperationLogOperations, getOperations: self.operationLogGetOperations, getTailIndex: self.operationLogGetTailIndex) { if mutableView.replay(postbox: postbox, operations: transaction.currentPeerMergedOperationLogOperations) {
pipe.putNext(PeerMergedOperationLogView(mutableView)) pipe.putNext(PeerMergedOperationLogView(mutableView))
} }
} }
for (mutableView, pipe) in self.timestampBasedMessageAttributesViews.copyItems() { for (mutableView, pipe) in self.timestampBasedMessageAttributesViews.copyItems() {
if mutableView.replay(operations: transaction.currentTimestampBasedMessageAttributesOperations, getHead: self.getTimestampBasedMessageAttributesHead) { if mutableView.replay(postbox: postbox, operations: transaction.currentTimestampBasedMessageAttributesOperations) {
pipe.putNext(TimestampBasedMessageAttributesView(mutableView)) pipe.putNext(TimestampBasedMessageAttributesView(mutableView))
} }
} }

View File

@ -571,7 +571,7 @@ public final class VoiceChatController: ViewController {
private let dimNode: ASDisplayNode private let dimNode: ASDisplayNode
private let contentContainer: ASDisplayNode private let contentContainer: ASDisplayNode
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let mainVideoContainer: MainVideoContainerNode private var mainVideoContainer: MainVideoContainerNode?
private let listNode: ListView private let listNode: ListView
private let topPanelNode: ASDisplayNode private let topPanelNode: ASDisplayNode
private let topPanelEdgeNode: ASDisplayNode private let topPanelEdgeNode: ASDisplayNode
@ -672,7 +672,9 @@ public final class VoiceChatController: ViewController {
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor
self.backgroundNode.clipsToBounds = false self.backgroundNode.clipsToBounds = false
if false {
self.mainVideoContainer = MainVideoContainerNode(context: call.accountContext, call: call) self.mainVideoContainer = MainVideoContainerNode(context: call.accountContext, call: call)
}
self.listNode = ListView() self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
@ -763,11 +765,11 @@ public final class VoiceChatController: ViewController {
if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != source { if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != source {
strongSelf.currentDominantSpeakerWithVideo = (peerId, source) strongSelf.currentDominantSpeakerWithVideo = (peerId, source)
strongSelf.call.setFullSizeVideo(peerId: peerId) strongSelf.call.setFullSizeVideo(peerId: peerId)
strongSelf.mainVideoContainer.updatePeer(peer: (peerId: peerId, source: source)) strongSelf.mainVideoContainer?.updatePeer(peer: (peerId: peerId, source: source))
} else { } else {
strongSelf.currentDominantSpeakerWithVideo = nil strongSelf.currentDominantSpeakerWithVideo = nil
strongSelf.call.setFullSizeVideo(peerId: nil) strongSelf.call.setFullSizeVideo(peerId: nil)
strongSelf.mainVideoContainer.updatePeer(peer: nil) strongSelf.mainVideoContainer?.updatePeer(peer: nil)
} }
} }
default: default:
@ -1029,7 +1031,7 @@ public final class VoiceChatController: ViewController {
}), true)) }), true))
} }
items.append(.action(ContextMenuActionItem(text: "Toggle Full Screen", icon: { theme in /*items.append(.action(ContextMenuActionItem(text: "Toggle Full Screen", icon: { theme in
return nil return nil
}, action: { _, f in }, action: { _, f in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1038,7 +1040,7 @@ public final class VoiceChatController: ViewController {
strongSelf.itemInteraction?.openPeer(peer.id) strongSelf.itemInteraction?.openPeer(peer.id)
f(.default) f(.default)
}))) })))*/
if peer.id != strongSelf.context.account.peerId { if peer.id != strongSelf.context.account.peerId {
if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) { if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) {
@ -1203,7 +1205,7 @@ public final class VoiceChatController: ViewController {
self.bottomPanelNode.addSubnode(self.bottomCornersNode) self.bottomPanelNode.addSubnode(self.bottomCornersNode)
self.bottomPanelNode.addSubnode(self.bottomPanelBackgroundNode) self.bottomPanelNode.addSubnode(self.bottomPanelBackgroundNode)
self.bottomPanelNode.addSubnode(self.audioOutputNode) self.bottomPanelNode.addSubnode(self.audioOutputNode)
self.bottomPanelNode.addSubnode(self.cameraButtonNode) //self.bottomPanelNode.addSubnode(self.cameraButtonNode)
self.bottomPanelNode.addSubnode(self.leaveNode) self.bottomPanelNode.addSubnode(self.leaveNode)
self.bottomPanelNode.addSubnode(self.actionButton) self.bottomPanelNode.addSubnode(self.actionButton)
@ -1212,7 +1214,9 @@ public final class VoiceChatController: ViewController {
self.contentContainer.addSubnode(self.backgroundNode) self.contentContainer.addSubnode(self.backgroundNode)
self.contentContainer.addSubnode(self.listNode) self.contentContainer.addSubnode(self.listNode)
self.contentContainer.addSubnode(self.mainVideoContainer) if let mainVideoContainer = self.mainVideoContainer {
self.contentContainer.addSubnode(mainVideoContainer)
}
self.contentContainer.addSubnode(self.topPanelNode) self.contentContainer.addSubnode(self.topPanelNode)
self.contentContainer.addSubnode(self.leftBorderNode) self.contentContainer.addSubnode(self.leftBorderNode)
self.contentContainer.addSubnode(self.rightBorderNode) self.contentContainer.addSubnode(self.rightBorderNode)
@ -1347,7 +1351,7 @@ public final class VoiceChatController: ViewController {
if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != source { if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != source {
strongSelf.currentDominantSpeakerWithVideo = (peerId, source) strongSelf.currentDominantSpeakerWithVideo = (peerId, source)
strongSelf.call.setFullSizeVideo(peerId: peerId) strongSelf.call.setFullSizeVideo(peerId: peerId)
strongSelf.mainVideoContainer.updatePeer(peer: (peerId: peerId, source: source)) strongSelf.mainVideoContainer?.updatePeer(peer: (peerId: peerId, source: source))
} }
} }
@ -1578,7 +1582,7 @@ public final class VoiceChatController: ViewController {
if !validSources.contains(source) { if !validSources.contains(source) {
strongSelf.currentDominantSpeakerWithVideo = nil strongSelf.currentDominantSpeakerWithVideo = nil
strongSelf.call.setFullSizeVideo(peerId: nil) strongSelf.call.setFullSizeVideo(peerId: nil)
strongSelf.mainVideoContainer.updatePeer(peer: nil) strongSelf.mainVideoContainer?.updatePeer(peer: nil)
} }
} }
@ -1589,8 +1593,8 @@ public final class VoiceChatController: ViewController {
} }
})) }))
self.isFullscreen = true //self.isFullscreen = true
self.isExpanded = true //self.isExpanded = true
} }
deinit { deinit {
@ -1862,9 +1866,11 @@ public final class VoiceChatController: ViewController {
let panelOffset = max(layoutTopInset, rawPanelOffset) let panelOffset = max(layoutTopInset, rawPanelOffset)
let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: size.width, height: topPanelHeight)) let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: size.width, height: topPanelHeight))
if let mainVideoContainer = self.mainVideoContainer {
let videoContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: layout.size.width, height: 200.0)) let videoContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: layout.size.width, height: 200.0))
transition.updateFrameAdditive(node: self.mainVideoContainer, frame: videoContainerFrame) transition.updateFrameAdditive(node: mainVideoContainer, frame: videoContainerFrame)
self.mainVideoContainer.update(size: videoContainerFrame.size, transition: transition) mainVideoContainer.update(size: videoContainerFrame.size, transition: transition)
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: size.width, height: layout.size.height)) let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: size.width, height: layout.size.height))
let sideInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0
@ -2132,7 +2138,10 @@ public final class VoiceChatController: ViewController {
} }
let bottomPanelHeight = bottomAreaHeight + layout.intrinsicInsets.bottom let bottomPanelHeight = bottomAreaHeight + layout.intrinsicInsets.bottom
let listTopInset = layoutTopInset + topPanelHeight + 200.0 var listTopInset = layoutTopInset + topPanelHeight
if self.mainVideoContainer != nil {
listTopInset += 200.0
}
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight) let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
let topInset: CGFloat let topInset: CGFloat
@ -2237,15 +2246,21 @@ public final class VoiceChatController: ViewController {
let sideButtonOrigin = max(sideButtonMinimalInset, floor((size.width - 144.0) / 2.0) - sideButtonOffset - sideButtonSize.width) let sideButtonOrigin = max(sideButtonMinimalInset, floor((size.width - 144.0) / 2.0) - sideButtonOffset - sideButtonSize.width)
if self.audioOutputNode.supernode === self.bottomPanelNode { if self.audioOutputNode.supernode === self.bottomPanelNode {
if true {
let audioOutputFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)
transition.updateFrame(node: self.audioOutputNode, frame: audioOutputFrame)
} else {
let cameraButtonDistance: CGFloat = 4.0 let cameraButtonDistance: CGFloat = 4.0
let audioOutputFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((bottomAreaHeight - sideButtonSize.height - cameraButtonDistance - cameraButtonSize.height) / 2.0) + cameraButtonDistance + cameraButtonSize.height), size: sideButtonSize) let audioOutputFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((bottomAreaHeight - sideButtonSize.height - cameraButtonDistance - cameraButtonSize.height) / 2.0) + cameraButtonDistance + cameraButtonSize.height), size: sideButtonSize)
transition.updateFrame(node: self.audioOutputNode, frame: audioOutputFrame) transition.updateFrame(node: self.audioOutputNode, frame: audioOutputFrame)
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: size.width - sideButtonOrigin - sideButtonSize.width, y: floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
transition.updateFrame(node: self.cameraButtonNode, frame: CGRect(origin: CGPoint(x: floor(audioOutputFrame.midX - cameraButtonSize.width / 2.0), y: audioOutputFrame.minY - cameraButtonDistance - cameraButtonSize.height), size: cameraButtonSize)) transition.updateFrame(node: self.cameraButtonNode, frame: CGRect(origin: CGPoint(x: floor(audioOutputFrame.midX - cameraButtonSize.width / 2.0), y: audioOutputFrame.minY - cameraButtonDistance - cameraButtonSize.height), size: cameraButtonSize))
} }
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: size.width - sideButtonOrigin - sideButtonSize.width, y: floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
}
if isFirstTime { if isFirstTime {
while !self.enqueuedTransitions.isEmpty { while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition() self.dequeueTransition()
@ -2276,7 +2291,7 @@ public final class VoiceChatController: ViewController {
self.cameraButtonNode.layer.removeAllAnimations() self.cameraButtonNode.layer.removeAllAnimations()
self.leaveNode.layer.removeAllAnimations() self.leaveNode.layer.removeAllAnimations()
self.bottomPanelNode.addSubnode(self.audioOutputNode) self.bottomPanelNode.addSubnode(self.audioOutputNode)
self.bottomPanelNode.addSubnode(self.cameraButtonNode) //self.bottomPanelNode.addSubnode(self.cameraButtonNode)
self.bottomPanelNode.addSubnode(self.leaveNode) self.bottomPanelNode.addSubnode(self.leaveNode)
self.bottomPanelNode.addSubnode(self.actionButton) self.bottomPanelNode.addSubnode(self.actionButton)
self.containerLayoutUpdated(layout, navigationHeight :navigationHeight, transition: .immediate) self.containerLayoutUpdated(layout, navigationHeight :navigationHeight, transition: .immediate)

View File

@ -371,7 +371,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
extension StoreMessage { extension StoreMessage {
convenience init?(apiMessage: Api.Message, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { convenience init?(apiMessage: Api.Message, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
switch apiMessage { switch apiMessage {
case let .message(flags, id, fromId, chatPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, restrictionReason, _): case let .message(flags, id, fromId, chatPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, restrictionReason, ttlPeriod):
let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId
let peerId: PeerId let peerId: PeerId
@ -468,19 +468,29 @@ extension StoreMessage {
var consumableContent: (Bool, Bool)? = nil var consumableContent: (Bool, Bool)? = nil
var resolvedTtlPeriod: Int32? = ttlPeriod
if let media = media { if let media = media {
let (mediaValue, expirationTimer) = textMediaAndExpirationTimerFromApiMedia(media, peerId) let (mediaValue, expirationTimer) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let mediaValue = mediaValue { if let mediaValue = mediaValue {
medias.append(mediaValue) medias.append(mediaValue)
if let expirationTimer = expirationTimer, expirationTimer > 0 { if let expirationTimer = expirationTimer, expirationTimer > 0 {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil)) if let resolvedTtlPeriodValue = resolvedTtlPeriod {
resolvedTtlPeriod = min(resolvedTtlPeriodValue, expirationTimer)
} else {
resolvedTtlPeriod = expirationTimer
}
consumableContent = (true, false) consumableContent = (true, false)
} }
} }
} }
if let resolvedTtlPeriod = resolvedTtlPeriod {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: resolvedTtlPeriod, countdownBeginTime: ttlPeriod == nil ? nil : date))
}
if let postAuthor = postAuthor { if let postAuthor = postAuthor {
attributes.append(AuthorSignatureMessageAttribute(signature: postAuthor)) attributes.append(AuthorSignatureMessageAttribute(signature: postAuthor))
} }

View File

@ -212,7 +212,7 @@ swift_library(
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode", "//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
"//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode", "//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode",
"//submodules/AudioBlob:AudioBlob", "//submodules/AudioBlob:AudioBlob",
"//Telegram:GeneratedSources", "//:GeneratedSources",
"//third-party/ZipArchive:ZipArchive", "//third-party/ZipArchive:ZipArchive",
"//submodules/ChatImportUI:ChatImportUI", "//submodules/ChatImportUI:ChatImportUI",
"//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks", "//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks",

View File

@ -5505,10 +5505,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if canSetupAutoremoveTimeout { if canSetupAutoremoveTimeout {
strongSelf.chatDisplayNode.dismissInput() strongSelf.presentAutoremoveSetup()
let controller = peerAutoremoveSetupScreen(context: strongSelf.context, peerId: peerId)
strongSelf.push(controller)
} else if let currentAutoremoveTimeout = currentAutoremoveTimeout, let rect = strongSelf.chatDisplayNode.frameForInputPanelAccessoryButton(.messageAutoremoveTimeout(currentAutoremoveTimeout)) { } else if let currentAutoremoveTimeout = currentAutoremoveTimeout, let rect = strongSelf.chatDisplayNode.frameForInputPanelAccessoryButton(.messageAutoremoveTimeout(currentAutoremoveTimeout)) {
//TODO:localize //TODO:localize
@ -7535,13 +7532,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
var isClearCache = false
let text: String let text: String
if peerId == self.context.account.peerId { if peerId == self.context.account.peerId {
text = self.presentationData.strings.Conversation_ClearSelfHistory text = self.presentationData.strings.Conversation_ClearSelfHistory
} else if peerId.namespace == Namespaces.Peer.SecretChat { } else if peerId.namespace == Namespaces.Peer.SecretChat {
text = self.presentationData.strings.Conversation_ClearSecretHistory text = self.presentationData.strings.Conversation_ClearSecretHistory
} else if peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { } else if peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel {
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
isClearCache = true
text = self.presentationData.strings.Conversation_ClearCache
} else {
text = self.presentationData.strings.Conversation_ClearGroupHistory text = self.presentationData.strings.Conversation_ClearGroupHistory
}
} else { } else {
text = self.presentationData.strings.Conversation_ClearPrivateHistory text = self.presentationData.strings.Conversation_ClearPrivateHistory
} }
@ -7631,13 +7634,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})) }))
} else { } else {
items.append(ActionSheetTextItem(title: text)) items.append(ActionSheetTextItem(title: text))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak self, weak actionSheet] in items.append(ActionSheetButtonItem(title: isClearCache ? self.presentationData.strings.Conversation_ClearCache : self.presentationData.strings.Conversation_ClearAll, color: isClearCache ? .accent : .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated() actionSheet?.dismissAnimated()
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if isClearCache {
strongSelf.navigationButtonAction(.clearCache)
} else {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}), }),
@ -7645,6 +7650,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
beginClear(.forLocalPeer) beginClear(.forLocalPeer)
}) })
], parseMarkdown: true), in: .window(.root)) ], parseMarkdown: true), in: .window(.root))
}
})) }))
} }
@ -7691,19 +7697,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
actionSheet.dismissAnimated() actionSheet.dismissAnimated()
let controller = peerAutoremoveSetupScreen(context: strongSelf.context, peerId: peer.id, completion: { updatedValue in strongSelf.presentAutoremoveSetup()
if case let .updated(value) = updatedValue {
guard let strongSelf = self else {
return
}
if let value = value {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_AutoremoveChanged("\(timeIntervalString(strings: strongSelf.presentationData.strings, value: value))").0), elevatedLayout: false, action: { _ in return false }), in: .current)
}
}
})
strongSelf.chatDisplayNode.dismissInput()
strongSelf.push(controller)
})) }))
} }
} }
@ -11856,6 +11850,36 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false return false
} }
private func presentAutoremoveSetup() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let controller = peerAutoremoveSetupScreen(context: self.context, peerId: peer.id, completion: { [weak self] updatedValue in
if case let .updated(value) = updatedValue {
guard let strongSelf = self else {
return
}
var text: String?
if let myValue = value.myValue {
if let limitedByValue = value.limitedByValue, limitedByValue < myValue {
text = "\(peer.compactDisplayTitle) has set messages to auto-delete in \(timeIntervalString(strings: strongSelf.presentationData.strings, value: limitedByValue)). You can't cancel it or make this interval longer."
} else {
text = strongSelf.presentationData.strings.Conversation_AutoremoveChanged("\(timeIntervalString(strings: strongSelf.presentationData.strings, value: myValue))").0
}
} else if let limitedByValue = value.limitedByValue {
text = "\(peer.compactDisplayTitle) has set messages to auto-delete in \(timeIntervalString(strings: strongSelf.presentationData.strings, value: limitedByValue)). You can't cancel it or make this interval longer."
}
if let text = text {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
}
}
})
self.chatDisplayNode.dismissInput()
self.push(controller)
}
private var effectiveNavigationController: NavigationController? { private var effectiveNavigationController: NavigationController? {
if let navigationController = self.navigationController as? NavigationController { if let navigationController = self.navigationController as? NavigationController {
return navigationController return navigationController

View File

@ -1436,7 +1436,8 @@ private func stringForRemainingTime(_ duration: Int32, strings: PresentationStri
let seconds = duration % 60 let seconds = duration % 60
let durationString: String let durationString: String
if days > 0 { if days > 0 {
return strings.Conversation_AutoremoveRemainingDays(days) let roundDays = round(Double(duration) / (3600.0 * 24.0))
return strings.Conversation_AutoremoveRemainingDays(Int32(roundDays))
} else if hours > 0 { } else if hours > 0 {
durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds) durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else { } else {

View File

@ -44,6 +44,16 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha
canClear = true canClear = true
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.addressName == nil && presentationInterfaceState.peerGeoLocation == nil { } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.addressName == nil && presentationInterfaceState.peerGeoLocation == nil {
canClear = true canClear = true
} else if let peer = peer as? TelegramChannel {
if case .broadcast = peer.info {
//TODO:localize
title = "Clear Channel"
}
if peer.hasPermission(.changeInfo) {
canClear = true
} else {
canClear = false
}
} else { } else {
canClear = false canClear = false
} }

View File

@ -263,7 +263,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
repliesImage = graphics.incomingDateAndStatusPinnedIcon repliesImage = graphics.incomingDateAndStatusPinnedIcon
} }
if hasAutoremove { if hasAutoremove {
selfExpiringImage = graphics.incomingDateAndStatusSelfExpiringIcon //selfExpiringImage = graphics.incomingDateAndStatusSelfExpiringIcon
} }
case let .BubbleOutgoing(status): case let .BubbleOutgoing(status):
dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
@ -282,7 +282,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
repliesImage = graphics.outgoingDateAndStatusPinnedIcon repliesImage = graphics.outgoingDateAndStatusPinnedIcon
} }
if hasAutoremove { if hasAutoremove {
selfExpiringImage = graphics.outgoingDateAndStatusSelfExpiringIcon //selfExpiringImage = graphics.outgoingDateAndStatusSelfExpiringIcon
} }
case .ImageIncoming: case .ImageIncoming:
dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
@ -301,7 +301,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
repliesImage = graphics.mediaPinnedIcon repliesImage = graphics.mediaPinnedIcon
} }
if hasAutoremove { if hasAutoremove {
selfExpiringImage = graphics.mediaSelfExpiringIcon //selfExpiringImage = graphics.mediaSelfExpiringIcon
} }
case let .ImageOutgoing(status): case let .ImageOutgoing(status):
dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
@ -321,7 +321,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
repliesImage = graphics.mediaPinnedIcon repliesImage = graphics.mediaPinnedIcon
} }
if hasAutoremove { if hasAutoremove {
selfExpiringImage = graphics.mediaSelfExpiringIcon //selfExpiringImage = graphics.mediaSelfExpiringIcon
} }
case .FreeIncoming: case .FreeIncoming:
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
@ -341,7 +341,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
repliesImage = graphics.freePinnedIcon repliesImage = graphics.freePinnedIcon
} }
if hasAutoremove { if hasAutoremove {
selfExpiringImage = graphics.freeSelfExpiringIcon //selfExpiringImage = graphics.freeSelfExpiringIcon
} }
case let .FreeOutgoing(status): case let .FreeOutgoing(status):
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
@ -362,7 +362,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
repliesImage = graphics.freePinnedIcon repliesImage = graphics.freePinnedIcon
} }
if hasAutoremove { if hasAutoremove {
selfExpiringImage = graphics.freeSelfExpiringIcon //selfExpiringImage = graphics.freeSelfExpiringIcon
} }
} }

View File

@ -23,6 +23,7 @@ import OverlayStatusController
import AlertUI import AlertUI
import PresentationDataUtils import PresentationDataUtils
import LocationUI import LocationUI
import AppLock
private final class AccountUserInterfaceInUseContext { private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>() let subscribers = Bag<(Bool) -> Void>()
@ -742,7 +743,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.widgetDataContext = WidgetDataContext(basePath: self.basePath, activeAccount: self.activeAccounts self.widgetDataContext = WidgetDataContext(basePath: self.basePath, activeAccount: self.activeAccounts
|> map { primary, _, _ in |> map { primary, _, _ in
return primary return primary
}, presentationData: self.presentationData) }, presentationData: self.presentationData, appLockContext: self.appLockContext as! AppLockContextImpl)
let enableSpotlight = accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.intentsSettings])) let enableSpotlight = accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.intentsSettings]))
|> map { sharedData -> Bool in |> map { sharedData -> Bool in

View File

@ -9,23 +9,40 @@ import NotificationsPresentationData
import WidgetKit import WidgetKit
import TelegramUIPreferences import TelegramUIPreferences
import WidgetItemsUtils import WidgetItemsUtils
import AccountContext
import AppLock
import GeneratedSources import GeneratedSources
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
private extension SelectFriendsIntent {
var configurationHash: String {
var result = "widget"
if let items = self.friends {
for item in items {
if let identifier = item.identifier {
result.append("+\(identifier)")
}
}
}
return result
}
}
final class WidgetDataContext { final class WidgetDataContext {
private var currentAccount: Account? private var currentAccount: Account?
private var currentAccountDisposable: Disposable? private var currentAccountDisposable: Disposable?
private var widgetPresentationDataDisposable: Disposable? private var widgetPresentationDataDisposable: Disposable?
private var notificationPresentationDataDisposable: Disposable? private var notificationPresentationDataDisposable: Disposable?
init(basePath: String, activeAccount: Signal<Account?, NoError>, presentationData: Signal<PresentationData, NoError>) { init(basePath: String, activeAccount: Signal<Account?, NoError>, presentationData: Signal<PresentationData, NoError>, appLockContext: AppLockContextImpl) {
self.currentAccountDisposable = (activeAccount self.currentAccountDisposable = (activeAccount
|> distinctUntilChanged(isEqual: { lhs, rhs in |> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs === rhs return lhs === rhs
}) })
|> mapToSignal { account -> Signal<WidgetData, NoError> in |> mapToSignal { account -> Signal<WidgetData, NoError> in
guard let account = account else { guard let account = account else {
return .single(WidgetData(accountId: 0, content: .notAuthorized)) return .single(WidgetData(accountId: 0, content: .empty, unlockedForLockId: nil))
} }
enum CombinedRecentPeers { enum CombinedRecentPeers {
@ -38,11 +55,12 @@ final class WidgetDataContext {
case peers(peers: [Peer], unread: [PeerId: Unread], messages: [PeerId: WidgetDataPeer.Message]) case peers(peers: [Peer], unread: [PeerId: Unread], messages: [PeerId: WidgetDataPeer.Message])
} }
let updatedAdditionalPeerIds: Signal<Set<PeerId>, NoError> = Signal { subscriber in let updatedAdditionalPeerIds: Signal<(Set<PeerId>, Set<String>), NoError> = Signal { subscriber in
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
#if arch(arm64) || arch(i386) || arch(x86_64) #if arch(arm64) || arch(i386) || arch(x86_64)
WidgetCenter.shared.getCurrentConfigurations({ result in WidgetCenter.shared.getCurrentConfigurations({ result in
var peerIds = Set<PeerId>() var peerIds = Set<PeerId>()
var configurationHashes = Set<String>()
if case let .success(infos) = result { if case let .success(infos) = result {
for info in infos { for info in infos {
if let configuration = info.configuration as? SelectFriendsIntent { if let configuration = info.configuration as? SelectFriendsIntent {
@ -54,19 +72,20 @@ final class WidgetDataContext {
peerIds.insert(PeerId(peerIdValue)) peerIds.insert(PeerId(peerIdValue))
} }
} }
configurationHashes.insert(configuration.configurationHash)
} }
} }
} }
subscriber.putNext(peerIds) subscriber.putNext((peerIds, configurationHashes))
subscriber.putCompletion() subscriber.putCompletion()
}) })
#else #else
subscriber.putNext(Set()) subscriber.putNext((Set(), Set()))
subscriber.putCompletion() subscriber.putCompletion()
#endif #endif
} else { } else {
subscriber.putNext(Set()) subscriber.putNext((Set(), Set()))
subscriber.putCompletion() subscriber.putCompletion()
} }
@ -74,33 +93,9 @@ final class WidgetDataContext {
} }
|> runOn(.mainQueue()) |> runOn(.mainQueue())
let preferencesKey: PostboxViewKey = .preferences(keys: Set([ let unlockedForLockId: Signal<String?, NoError> = .single(nil)
ApplicationSpecificPreferencesKeys.widgetSettings
]))
let sourcePeers: Signal<RecentPeers, NoError> = account.postbox.combinedView(keys: [
preferencesKey
])
|> mapToSignal { views -> Signal<RecentPeers, NoError> in
let widgetSettings: WidgetSettings
if let view = views.views[preferencesKey] as? PreferencesView, let value = view.values[ApplicationSpecificPreferencesKeys.widgetSettings] as? WidgetSettings {
widgetSettings = value
} else {
widgetSettings = .default
}
if widgetSettings.useHints { let sourcePeers: Signal<RecentPeers, NoError> = recentPeers(account: account)
return recentPeers(account: account)
} else {
return account.postbox.transaction { transaction -> RecentPeers in
return .peers(widgetSettings.peers.compactMap { peerId -> Peer? in
guard let peer = transaction.getPeer(peerId) else {
return nil
}
return peer
})
}
}
}
let recent: Signal<CombinedRecentPeers, NoError> = sourcePeers let recent: Signal<CombinedRecentPeers, NoError> = sourcePeers
|> mapToSignal { recent -> Signal<CombinedRecentPeers, NoError> in |> mapToSignal { recent -> Signal<CombinedRecentPeers, NoError> in
@ -160,7 +155,7 @@ final class WidgetDataContext {
|> map { result -> WidgetData in |> map { result -> WidgetData in
switch result { switch result {
case .disabled: case .disabled:
return WidgetData(accountId: account.id.int64, content: .disabled) return WidgetData(accountId: account.id.int64, content: .empty, unlockedForLockId: nil)
case let .peers(peers, unread, messages): case let .peers(peers, unread, messages):
return WidgetData(accountId: account.id.int64, content: .peers(WidgetDataPeers(accountPeerId: account.peerId.toInt64(), peers: peers.compactMap { peer -> WidgetDataPeer? in return WidgetData(accountId: account.id.int64, content: .peers(WidgetDataPeers(accountPeerId: account.peerId.toInt64(), peers: peers.compactMap { peer -> WidgetDataPeer? in
var name: String = "" var name: String = ""
@ -192,15 +187,23 @@ final class WidgetDataContext {
return WidgetDataPeer(id: peer.id.toInt64(), name: name, lastName: lastName, letters: peer.displayLetters, avatarPath: smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in return WidgetDataPeer(id: peer.id.toInt64(), name: name, lastName: lastName, letters: peer.displayLetters, avatarPath: smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
return account.postbox.mediaBox.resourcePath(representation.resource) return account.postbox.mediaBox.resourcePath(representation.resource)
}, badge: badge, message: message) }, badge: badge, message: message)
}, updateTimestamp: Int32(Date().timeIntervalSince1970)))) }, updateTimestamp: Int32(Date().timeIntervalSince1970))), unlockedForLockId: nil)
} }
} }
|> distinctUntilChanged |> distinctUntilChanged
let additionalPeerIds = Signal<Set<PeerId>, NoError>.single(Set()) |> then(updatedAdditionalPeerIds) let additionalPeerIds = Signal<(Set<PeerId>, Set<String>), NoError>.complete() |> then(updatedAdditionalPeerIds)
let processedCustom: Signal<WidgetData, NoError> = additionalPeerIds let processedCustom: Signal<WidgetData, NoError> = additionalPeerIds
|> distinctUntilChanged |> distinctUntilChanged(isEqual: { lhs, rhs in
|> mapToSignal { additionalPeerIds -> Signal<CombinedRecentPeers, NoError> in if lhs.0 != rhs.0 {
return false
}
if lhs.1 != rhs.1 {
return false
}
return true
})
|> mapToSignal { additionalPeerIds, _ -> Signal<CombinedRecentPeers, NoError> in
return combineLatest(queue: .mainQueue(), additionalPeerIds.map { account.postbox.peerView(id: $0) }) return combineLatest(queue: .mainQueue(), additionalPeerIds.map { account.postbox.peerView(id: $0) })
|> mapToSignal { peerViews -> Signal<CombinedRecentPeers, NoError> in |> mapToSignal { peerViews -> Signal<CombinedRecentPeers, NoError> in
let topMessagesKey: PostboxViewKey = .topChatMessage(peerIds: peerViews.map { let topMessagesKey: PostboxViewKey = .topChatMessage(peerIds: peerViews.map {
@ -250,7 +253,7 @@ final class WidgetDataContext {
|> map { result -> WidgetData in |> map { result -> WidgetData in
switch result { switch result {
case .disabled: case .disabled:
return WidgetData(accountId: account.id.int64, content: .disabled) return WidgetData(accountId: account.id.int64, content: .empty, unlockedForLockId: nil)
case let .peers(peers, unread, messages): case let .peers(peers, unread, messages):
return WidgetData(accountId: account.id.int64, content: .peers(WidgetDataPeers(accountPeerId: account.peerId.toInt64(), peers: peers.compactMap { peer -> WidgetDataPeer? in return WidgetData(accountId: account.id.int64, content: .peers(WidgetDataPeers(accountPeerId: account.peerId.toInt64(), peers: peers.compactMap { peer -> WidgetDataPeer? in
var name: String = "" var name: String = ""
@ -282,13 +285,15 @@ final class WidgetDataContext {
return WidgetDataPeer(id: peer.id.toInt64(), name: name, lastName: lastName, letters: peer.displayLetters, avatarPath: smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in return WidgetDataPeer(id: peer.id.toInt64(), name: name, lastName: lastName, letters: peer.displayLetters, avatarPath: smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
return account.postbox.mediaBox.resourcePath(representation.resource) return account.postbox.mediaBox.resourcePath(representation.resource)
}, badge: badge, message: message) }, badge: badge, message: message)
}, updateTimestamp: Int32(Date().timeIntervalSince1970)))) }, updateTimestamp: Int32(Date().timeIntervalSince1970))), unlockedForLockId: nil)
} }
} }
|> distinctUntilChanged |> distinctUntilChanged
return combineLatest(processedRecent, processedCustom) return combineLatest(processedRecent, processedCustom, unlockedForLockId)
|> map { processedRecent, _ -> WidgetData in |> map { processedRecent, _, unlockedForLockId -> WidgetData in
var processedRecent = processedRecent
processedRecent.unlockedForLockId = unlockedForLockId
return processedRecent return processedRecent
} }
}).start(next: { widgetData in }).start(next: { widgetData in

@ -1 +1 @@
Subproject commit ef3baff23ea69ff11673266b62996b88397b260b Subproject commit 28aa4cb94cd150f825fefbf1400bf2eb6500a9f7

View File

@ -147,7 +147,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2 self.textNode.maximumNumberOfLines = 5
displayUndo = false displayUndo = false
self.originalRemainingSeconds = 3 self.originalRemainingSeconds = 3
case let .info(text): case let .info(text):

View File

@ -16,6 +16,16 @@ public struct WidgetDataPeer: Codable, Equatable {
} }
public struct Message: Codable, Equatable { public struct Message: Codable, Equatable {
public struct Author: Codable, Equatable {
public var isMe: Bool
public var title: String
public init(isMe: Bool, title: String) {
self.isMe = isMe
self.title = title
}
}
public enum Content: Codable, Equatable { public enum Content: Codable, Equatable {
public enum DecodingError: Error { public enum DecodingError: Error {
case generic case generic
@ -205,11 +215,13 @@ public struct WidgetDataPeer: Codable, Equatable {
} }
} }
public var author: Author?
public var text: String public var text: String
public var content: Content public var content: Content
public var timestamp: Int32 public var timestamp: Int32
public init(text: String, content: Content, timestamp: Int32) { public init(author: Author?, text: String, content: Content, timestamp: Int32) {
self.author = author
self.text = text self.text = text
self.content = content self.content = content
self.timestamp = timestamp self.timestamp = timestamp
@ -273,23 +285,19 @@ public struct WidgetData: Codable, Equatable {
} }
private enum Cases: Int32, Codable { private enum Cases: Int32, Codable {
case notAuthorized case empty
case disabled
case peers case peers
} }
case notAuthorized case empty
case disabled
case peers(WidgetDataPeers) case peers(WidgetDataPeers)
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
let discriminator = try container.decode(Cases.self, forKey: .discriminator) let discriminator = try container.decode(Cases.self, forKey: .discriminator)
switch discriminator { switch discriminator {
case .notAuthorized: case .empty:
self = .notAuthorized self = .empty
case .disabled:
self = .disabled
case .peers: case .peers:
self = .peers(try container.decode(WidgetDataPeers.self, forKey: .peers)) self = .peers(try container.decode(WidgetDataPeers.self, forKey: .peers))
} }
@ -298,10 +306,8 @@ public struct WidgetData: Codable, Equatable {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
switch self { switch self {
case .notAuthorized: case .empty:
try container.encode(Cases.notAuthorized, forKey: .discriminator) try container.encode(Cases.empty, forKey: .discriminator)
case .disabled:
try container.encode(Cases.disabled, forKey: .discriminator)
case let .peers(peers): case let .peers(peers):
try container.encode(Cases.peers, forKey: .discriminator) try container.encode(Cases.peers, forKey: .discriminator)
try container.encode(peers, forKey: .peers) try container.encode(peers, forKey: .peers)
@ -311,9 +317,11 @@ public struct WidgetData: Codable, Equatable {
public var accountId: Int64 public var accountId: Int64
public var content: Content public var content: Content
public var unlockedForLockId: String?
public init(accountId: Int64, content: Content) { public init(accountId: Int64, content: Content, unlockedForLockId: String?) {
self.accountId = accountId self.accountId = accountId
self.content = content self.content = content
self.unlockedForLockId = unlockedForLockId
} }
} }

View File

@ -54,6 +54,18 @@ public extension WidgetDataPeer.Message {
break break
} }
} }
self.init(text: message.text, content: content, timestamp: message.timestamp)
var author: Author?
if let _ = message.peers[message.id.peerId] as? TelegramGroup {
if let authorPeer = message.author {
author = Author(isMe: false, title: authorPeer.debugDisplayTitle)
}
} else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info {
if let authorPeer = message.author {
author = Author(isMe: false, title: authorPeer.debugDisplayTitle)
}
}
self.init(author: author, text: message.text, content: content, timestamp: message.timestamp)
} }
} }

@ -1 +1 @@
Subproject commit 5746d4b64798196743aaf083b0c07a3cc58a713f Subproject commit e42b4630117498504692fbac80ec6dbb970b2314