Experimental widget settings

This commit is contained in:
Ali 2020-10-30 20:58:02 +04:00
parent 70f5732f5f
commit 08040c1598
13 changed files with 1691 additions and 1056 deletions

View File

@ -5888,3 +5888,5 @@ Sorry for the inconvenience.";
"Stats.Message.Views" = "Views"; "Stats.Message.Views" = "Views";
"Stats.Message.PublicShares" = "Public Shares"; "Stats.Message.PublicShares" = "Public Shares";
"Stats.Message.PrivateShares" = "Private Shares"; "Stats.Message.PrivateShares" = "Private Shares";
"ChatSettings.WidgetSettings" = "Widget";

View File

@ -208,11 +208,13 @@ private final class LoadingShimmerNode: ASDisplayNode {
public struct ItemListPeerItemEditing: Equatable { public struct ItemListPeerItemEditing: Equatable {
public var editable: Bool public var editable: Bool
public var editing: Bool public var editing: Bool
public var canBeReordered: Bool
public var revealed: Bool? public var revealed: Bool?
public init(editable: Bool, editing: Bool, revealed: Bool?) { public init(editable: Bool, editing: Bool, canBeReordered: Bool = false, revealed: Bool?) {
self.editable = editable self.editable = editable
self.editing = editing self.editing = editing
self.canBeReordered = canBeReordered
self.revealed = revealed self.revealed = revealed
} }
} }
@ -460,6 +462,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors, Bool)? private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors, Bool)?
private var editableControlNode: ItemListEditableControlNode? private var editableControlNode: ItemListEditableControlNode?
private var reorderControlNode: ItemListEditableReorderControlNode?
override public var canBeSelected: Bool { override public var canBeSelected: Bool {
if self.editableControlNode != nil || self.disabledOverlayNode != nil { if self.editableControlNode != nil || self.disabledOverlayNode != nil {
@ -560,6 +563,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
var currentDisabledOverlayNode = self.disabledOverlayNode var currentDisabledOverlayNode = self.disabledOverlayNode
@ -761,12 +765,20 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} }
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
let editingOffset: CGFloat let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
if item.editing.editing { if item.editing.editing {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false) let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0 editingOffset = sizeAndApply.0
if item.editing.canBeReordered {
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = reorderSizeAndApply
reorderInset = reorderSizeAndApply.0
}
} else { } else {
editingOffset = 0.0 editingOffset = 0.0
} }
@ -804,6 +816,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
labelInset += 15.0 labelInset += 15.0
} }
labelInset += reorderInset
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -931,6 +945,23 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}) })
} }
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.addSubnode(reorderControlNode)
reorderControlNode.alpha = 0.0
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
}
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
strongSelf.reorderControlNode?.frame = reorderControlFrame
} else if let reorderControlNode = strongSelf.reorderControlNode {
strongSelf.reorderControlNode = nil
transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in
reorderControlNode?.removeFromSupernode()
})
}
let _ = titleApply() let _ = titleApply()
let _ = statusApply() let _ = statusApply()
let _ = labelApply() let _ = labelApply()
@ -1293,6 +1324,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
shimmerNode.updateAbsoluteRect(rect, within: containerSize) shimmerNode.updateAbsoluteRect(rect, within: containerSize)
} }
} }
override public func isReorderable(at point: CGPoint) -> Bool {
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions {
return true
}
return false
}
} }
public final class ItemListPeerItemHeader: ListViewItemHeader { public final class ItemListPeerItemHeader: ListViewItemHeader {

View File

@ -226,7 +226,7 @@ public func mergeListsStableWithUpdates<T>(leftList: [T], rightList: [T], isLess
} }
var i = 0 var i = 0
while i < rightList.count { while i < rightList.count {
if updatedItems[i].1 != getId(rightList[i]) { if updatedItems.count <= i || updatedItems[i].1 != getId(rightList[i]) {
updatedItems.insert((rightList[i], getId(rightList[i])), at: i) updatedItems.insert((rightList[i], getId(rightList[i])), at: i)
var previousIndex: Int? var previousIndex: Int?
for k in 0 ..< leftList.count { for k in 0 ..< leftList.count {

View File

@ -86,6 +86,7 @@ swift_library(
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI", "//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
"//submodules/AccountUtils:AccountUtils", "//submodules/AccountUtils:AccountUtils",
"//submodules/AuthTransferUI:AuthTransferUI", "//submodules/AuthTransferUI:AuthTransferUI",
"//submodules/WidgetSetupScreen:WidgetSetupScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -12,6 +12,7 @@ import ItemListUI
import PresentationDataUtils import PresentationDataUtils
import AccountContext import AccountContext
import OpenInExternalAppUI import OpenInExternalAppUI
import WidgetSetupScreen
private final class DataAndStorageControllerArguments { private final class DataAndStorageControllerArguments {
let openStorageUsage: () -> Void let openStorageUsage: () -> Void
@ -27,9 +28,10 @@ private final class DataAndStorageControllerArguments {
let toggleDownloadInBackground: (Bool) -> Void let toggleDownloadInBackground: (Bool) -> Void
let openBrowserSelection: () -> Void let openBrowserSelection: () -> Void
let openIntents: () -> Void let openIntents: () -> Void
let openWidgetSettings: () -> Void
let toggleEnableSensitiveContent: (Bool) -> Void let toggleEnableSensitiveContent: (Bool) -> Void
init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, openAutomaticDownloadConnectionType: @escaping (AutomaticDownloadConnectionType) -> Void, resetAutomaticDownload: @escaping () -> Void, openVoiceUseLessData: @escaping () -> Void, openSaveIncomingPhotos: @escaping () -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, toggleAutoplayGifs: @escaping (Bool) -> Void, toggleAutoplayVideos: @escaping (Bool) -> Void, toggleDownloadInBackground: @escaping (Bool) -> Void, openBrowserSelection: @escaping () -> Void, openIntents: @escaping () -> Void, toggleEnableSensitiveContent: @escaping (Bool) -> Void) { init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, openAutomaticDownloadConnectionType: @escaping (AutomaticDownloadConnectionType) -> Void, resetAutomaticDownload: @escaping () -> Void, openVoiceUseLessData: @escaping () -> Void, openSaveIncomingPhotos: @escaping () -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, toggleAutoplayGifs: @escaping (Bool) -> Void, toggleAutoplayVideos: @escaping (Bool) -> Void, toggleDownloadInBackground: @escaping (Bool) -> Void, openBrowserSelection: @escaping () -> Void, openIntents: @escaping () -> Void, openWidgetSettings: @escaping () -> Void, toggleEnableSensitiveContent: @escaping (Bool) -> Void) {
self.openStorageUsage = openStorageUsage self.openStorageUsage = openStorageUsage
self.openNetworkUsage = openNetworkUsage self.openNetworkUsage = openNetworkUsage
self.openProxy = openProxy self.openProxy = openProxy
@ -43,6 +45,7 @@ private final class DataAndStorageControllerArguments {
self.toggleDownloadInBackground = toggleDownloadInBackground self.toggleDownloadInBackground = toggleDownloadInBackground
self.openBrowserSelection = openBrowserSelection self.openBrowserSelection = openBrowserSelection
self.openIntents = openIntents self.openIntents = openIntents
self.openWidgetSettings = openWidgetSettings
self.toggleEnableSensitiveContent = toggleEnableSensitiveContent self.toggleEnableSensitiveContent = toggleEnableSensitiveContent
} }
} }
@ -87,6 +90,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
case useLessVoiceData(PresentationTheme, String, String) case useLessVoiceData(PresentationTheme, String, String)
case otherHeader(PresentationTheme, String) case otherHeader(PresentationTheme, String)
case shareSheet(PresentationTheme, String) case shareSheet(PresentationTheme, String)
case widgetSettings(String)
case saveIncomingPhotos(PresentationTheme, String) case saveIncomingPhotos(PresentationTheme, String)
case saveEditedPhotos(PresentationTheme, String, Bool) case saveEditedPhotos(PresentationTheme, String, Bool)
case openLinksIn(PresentationTheme, String, String) case openLinksIn(PresentationTheme, String, String)
@ -106,7 +110,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return DataAndStorageSection.autoPlay.rawValue return DataAndStorageSection.autoPlay.rawValue
case .voiceCallsHeader, .useLessVoiceData: case .voiceCallsHeader, .useLessVoiceData:
return DataAndStorageSection.voiceCalls.rawValue return DataAndStorageSection.voiceCalls.rawValue
case .otherHeader, .shareSheet, .saveIncomingPhotos, .saveEditedPhotos, .openLinksIn, .downloadInBackground, .downloadInBackgroundInfo: case .otherHeader, .shareSheet, .widgetSettings, .saveIncomingPhotos, .saveEditedPhotos, .openLinksIn, .downloadInBackground, .downloadInBackgroundInfo:
return DataAndStorageSection.other.rawValue return DataAndStorageSection.other.rawValue
case .connectionHeader, .connectionProxy: case .connectionHeader, .connectionProxy:
return DataAndStorageSection.connection.rawValue return DataAndStorageSection.connection.rawValue
@ -143,22 +147,24 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return 11 return 11
case .shareSheet: case .shareSheet:
return 12 return 12
case .saveIncomingPhotos: case .widgetSettings:
return 13 return 13
case .saveEditedPhotos: case .saveIncomingPhotos:
return 14 return 14
case .openLinksIn: case .saveEditedPhotos:
return 15 return 15
case .downloadInBackground: case .openLinksIn:
return 16 return 16
case .downloadInBackgroundInfo: case .downloadInBackground:
return 17 return 17
case .connectionHeader: case .downloadInBackgroundInfo:
return 18 return 18
case .connectionProxy: case .connectionHeader:
return 19 return 19
case .enableSensitiveContent: case .connectionProxy:
return 20 return 20
case .enableSensitiveContent:
return 21
} }
} }
@ -242,6 +248,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .widgetSettings(text):
if case .widgetSettings(text) = rhs {
return true
} else {
return false
}
case let .saveIncomingPhotos(lhsTheme, lhsText): case let .saveIncomingPhotos(lhsTheme, lhsText):
if case let .saveIncomingPhotos(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .saveIncomingPhotos(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -346,6 +358,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openIntents() arguments.openIntents()
}) })
case let .widgetSettings(text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openWidgetSettings()
})
case let .saveIncomingPhotos(theme, text): case let .saveIncomingPhotos(theme, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openSaveIncomingPhotos() arguments.openSaveIncomingPhotos()
@ -489,6 +505,9 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
if #available(iOSApplicationExtension 13.2, iOS 13.2, *) { if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings)) entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
} }
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
entries.append(.widgetSettings(presentationData.strings.ChatSettings_WidgetSettings))
}
entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos)) entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos))
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos))
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
@ -641,6 +660,9 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
}, openIntents: { }, openIntents: {
let controller = intentsSettingsController(context: context) let controller = intentsSettingsController(context: context)
pushControllerImpl?(controller) pushControllerImpl?(controller)
}, openWidgetSettings: {
let controller = widgetSetupScreen(context: context)
pushControllerImpl?(controller)
}, toggleEnableSensitiveContent: { value in }, toggleEnableSensitiveContent: { value in
let _ = (contentSettingsConfiguration.get() let _ = (contentSettingsConfiguration.get()
|> take(1) |> take(1)

View File

@ -57,6 +57,7 @@ private var telegramUIDeclaredEncodables: Void = {
declareEncodable(IntentsSettings.self, f: { IntentsSettings(decoder: $0) }) declareEncodable(IntentsSettings.self, f: { IntentsSettings(decoder: $0) })
declareEncodable(CachedGeocode.self, f: { CachedGeocode(decoder: $0) }) declareEncodable(CachedGeocode.self, f: { CachedGeocode(decoder: $0) })
declareEncodable(ChatListFilterSettings.self, f: { ChatListFilterSettings(decoder: $0) }) declareEncodable(ChatListFilterSettings.self, f: { ChatListFilterSettings(decoder: $0) })
declareEncodable(WidgetSettings.self, f: { WidgetSettings(decoder: $0) })
return return
}() }()

View File

@ -7,6 +7,7 @@ import WidgetItems
import TelegramPresentationData import TelegramPresentationData
import NotificationsPresentationData import NotificationsPresentationData
import WidgetKit import WidgetKit
import TelegramUIPreferences
final class WidgetDataContext { final class WidgetDataContext {
private var currentAccount: Account? private var currentAccount: Account?
@ -24,7 +25,7 @@ final class WidgetDataContext {
return .single(.notAuthorized) return .single(.notAuthorized)
} }
enum RecentPeers { enum CombinedRecentPeers {
struct Unread { struct Unread {
var count: Int32 var count: Int32
var isMuted: Bool var isMuted: Bool
@ -34,19 +35,47 @@ final class WidgetDataContext {
case peers(peers: [Peer], unread: [PeerId: Unread]) case peers(peers: [Peer], unread: [PeerId: Unread])
} }
let recent: Signal<RecentPeers, NoError> = recentPeers(account: account) let preferencesKey: PostboxViewKey = .preferences(keys: Set([
|> mapToSignal { recent -> Signal<RecentPeers, NoError> in 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 {
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
|> mapToSignal { recent -> Signal<CombinedRecentPeers, NoError> in
switch recent { switch recent {
case .disabled: case .disabled:
return .single(.disabled) return .single(.disabled)
case let .peers(peers): case let .peers(peers):
return combineLatest(queue: .mainQueue(), peers.filter { !$0.isDeleted }.map { account.postbox.peerView(id: $0.id)}) |> mapToSignal { peerViews -> Signal<RecentPeers, NoError> in return combineLatest(queue: .mainQueue(), peers.filter { !$0.isDeleted }.map { account.postbox.peerView(id: $0.id)}) |> mapToSignal { peerViews -> Signal<CombinedRecentPeers, NoError> in
return account.postbox.unreadMessageCountsView(items: peerViews.map { return account.postbox.unreadMessageCountsView(items: peerViews.map {
.peer($0.peerId) .peer($0.peerId)
}) })
|> map { values -> RecentPeers in |> map { values -> CombinedRecentPeers in
var peers: [Peer] = [] var peers: [Peer] = []
var unread: [PeerId: RecentPeers.Unread] = [:] var unread: [PeerId: CombinedRecentPeers.Unread] = [:]
for peerView in peerViews { for peerView in peerViews {
if let peer = peerViewMainPeer(peerView) { if let peer = peerViewMainPeer(peerView) {
var isMuted: Bool = false var isMuted: Bool = false
@ -61,7 +90,7 @@ final class WidgetDataContext {
let unreadCount = values.count(for: .peer(peerView.peerId)) let unreadCount = values.count(for: .peer(peerView.peerId))
if let unreadCount = unreadCount, unreadCount > 0 { if let unreadCount = unreadCount, unreadCount > 0 {
unread[peerView.peerId] = RecentPeers.Unread(count: Int32(unreadCount), isMuted: isMuted) unread[peerView.peerId] = CombinedRecentPeers.Unread(count: Int32(unreadCount), isMuted: isMuted)
} }
peers.append(peer) peers.append(peer)
@ -80,13 +109,10 @@ final class WidgetDataContext {
return .disabled return .disabled
case let .peers(peers, unread): case let .peers(peers, unread):
return .peers(WidgetDataPeers(accountPeerId: account.peerId.toInt64(), peers: peers.compactMap { peer -> WidgetDataPeer? in return .peers(WidgetDataPeers(accountPeerId: account.peerId.toInt64(), peers: peers.compactMap { peer -> WidgetDataPeer? in
guard let user = peer as? TelegramUser else {
return nil
}
var name: String = "" var name: String = ""
var lastName: String? var lastName: String?
if let user = peer as? TelegramUser {
if let firstName = user.firstName { if let firstName = user.firstName {
name = firstName name = firstName
lastName = user.lastName lastName = user.lastName
@ -95,6 +121,9 @@ final class WidgetDataContext {
} else if let phone = user.phone, !phone.isEmpty { } else if let phone = user.phone, !phone.isEmpty {
name = phone name = phone
} }
} else {
name = peer.debugDisplayTitle
}
var badge: WidgetDataPeer.Badge? var badge: WidgetDataPeer.Badge?
if let unreadValue = unread[peer.id], unreadValue.count > 0 { if let unreadValue = unread[peer.id], unreadValue.count > 0 {
@ -104,7 +133,7 @@ final class WidgetDataContext {
) )
} }
return WidgetDataPeer(id: user.id.toInt64(), name: name, lastName: lastName, letters: user.displayLetters, avatarPath: smallestImageRepresentation(user.photo).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) }, badge: badge)
})) }))

View File

@ -7,12 +7,14 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 {
case voipDerivedState = 16 case voipDerivedState = 16
case chatArchiveSettings = 17 case chatArchiveSettings = 17
case chatListFilterSettings = 18 case chatListFilterSettings = 18
case widgetSettings = 19
} }
public struct ApplicationSpecificPreferencesKeys { public struct ApplicationSpecificPreferencesKeys {
public static let voipDerivedState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voipDerivedState.rawValue) public static let voipDerivedState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voipDerivedState.rawValue)
public static let chatArchiveSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatArchiveSettings.rawValue) public static let chatArchiveSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatArchiveSettings.rawValue)
public static let chatListFilterSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatListFilterSettings.rawValue) public static let chatListFilterSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatListFilterSettings.rawValue)
public static let widgetSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.widgetSettings.rawValue)
} }
private enum ApplicationSpecificSharedDataKeyValues: Int32 { private enum ApplicationSpecificSharedDataKeyValues: Int32 {

View File

@ -0,0 +1,61 @@
import Foundation
import Postbox
import SwiftSignalKit
public struct WidgetSettings: PreferencesEntry, Equatable {
public var useHints: Bool
public var peers: [PeerId]
public static var `default`: WidgetSettings {
return WidgetSettings(
useHints: true,
peers: []
)
}
public init(
useHints: Bool,
peers: [PeerId]
) {
self.useHints = useHints
self.peers = peers
}
public init(decoder: PostboxDecoder) {
self.useHints = decoder.decodeBoolForKey("useHints", orElse: true)
self.peers = decoder.decodeInt64ArrayForKey("peers").map { PeerId($0) }
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeBool(self.useHints, forKey: "useHints")
encoder.encodeInt64Array(self.peers.map { $0.toInt64() }, forKey: "peers")
}
public func isEqual(to: PreferencesEntry) -> Bool {
if let to = to as? WidgetSettings {
return self == to
} else {
return false
}
}
}
public func updateWidgetSettingsInteractively(postbox: Postbox, _ f: @escaping (WidgetSettings) -> WidgetSettings) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Void in
updateWidgetSettingsInteractively(transaction: transaction, f)
}
|> ignoreValues
}
public func updateWidgetSettingsInteractively(transaction: Transaction, _ f: @escaping (WidgetSettings) -> WidgetSettings) {
transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.widgetSettings, { entry in
let currentSettings: WidgetSettings
if let entry = entry as? WidgetSettings {
currentSettings = entry
} else {
currentSettings = .default
}
return f(currentSettings)
})
}

View File

@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "WidgetSetupScreen",
module_name = "WidgetSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Postbox:Postbox",
"//submodules/SyncCore:SyncCore",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ItemListUI:ItemListUI",
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AccountContext:AccountContext",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,453 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerItem
import ItemListPeerActionItem
private final class Arguments {
let context: AccountContext
let updateUseHints: (Bool) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let removePeer: (PeerId) -> Void
let addPeer: () -> Void
let openPeer: (PeerId) -> Void
init(context: AccountContext, updateUseHints: @escaping (Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) {
self.context = context
self.updateUseHints = updateUseHints
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.removePeer = removePeer
self.addPeer = addPeer
self.openPeer = openPeer
}
}
private enum WidgetSetupScreenEntry: ItemListNodeEntry {
enum Section: Int32 {
case mode
case peers
}
enum StableId: Hashable {
case useHints
case peersHeaderItem
case add
case peer(PeerId)
}
case useHints(String, Bool)
case peersHeaderItem(String)
case peerItem(Int32, PresentationDateTimeFormat, PresentationPersonNameOrder, SelectivePrivacyPeer, ItemListPeerItemEditing, Bool)
case addItem(String, Bool)
var section: ItemListSectionId {
switch self {
case .useHints:
return Section.mode.rawValue
case .peersHeaderItem, .peerItem:
return Section.peers.rawValue
case .addItem:
return Section.peers.rawValue
}
}
var stableId: StableId {
switch self {
case .useHints:
return .useHints
case .peersHeaderItem:
return .peersHeaderItem
case let .peerItem(_, _, _, peer, _, _):
return .peer(peer.peer.id)
case .addItem:
return .add
}
}
var sortIndex: Int32 {
switch self {
case .useHints:
return 0
case .peersHeaderItem:
return 1
case .addItem:
return 2
case let .peerItem(index, _, _, _, _, _):
return 10 + index
}
}
static func ==(lhs: WidgetSetupScreenEntry, rhs: WidgetSetupScreenEntry) -> Bool {
switch lhs {
case let .useHints(text, value):
if case .useHints(text, value) = rhs {
return true
} else {
return false
}
case let .peersHeaderItem(text):
if case .peersHeaderItem(text) = rhs {
return true
} else {
return false
}
case let .peerItem(lhsIndex, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsEditing, lhsEnabled):
if case let .peerItem(rhsIndex, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsEditing, rhsEnabled) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsNameOrder != rhsNameOrder {
return false
}
if lhsPeer != rhsPeer {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
return true
} else {
return false
}
case let .addItem(lhsText, lhsEditing):
if case let .addItem(rhsText, rhsEditing) = rhs, lhsText == rhsText, lhsEditing == rhsEditing {
return true
} else {
return false
}
}
}
static func <(lhs: WidgetSetupScreenEntry, rhs: WidgetSetupScreenEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! Arguments
switch self {
case let .useHints(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateUseHints(value)
})
case let .peersHeaderItem(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peerItem(_, dateTimeFormat, nameOrder, peer, editing, enabled):
var text: ItemListPeerItemText = .none
if let group = peer.peer as? TelegramGroup {
text = .text(presentationData.strings.Conversation_StatusMembers(Int32(group.participantCount)))
} else if let channel = peer.peer as? TelegramChannel {
if let participantCount = peer.participantCount {
text = .text(presentationData.strings.Conversation_StatusMembers(Int32(participantCount)))
} else {
switch channel.info {
case .group:
text = .text(presentationData.strings.Group_Status)
case .broadcast:
text = .text(presentationData.strings.Channel_Status)
}
}
}
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, context: arguments.context, peer: peer.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer.peer.id)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)
})
case let .addItem(text, editing):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, editing: editing, action: {
arguments.addPeer()
})
}
}
}
private struct WidgetSetupScreenControllerState: Equatable {
var editing: Bool = false
var peerIdWithRevealedOptions: PeerId? = nil
}
private func selectivePrivacyPeersControllerEntries(presentationData: PresentationData, state: WidgetSetupScreenControllerState, useHints: Bool, peers: [SelectivePrivacyPeer]) -> [WidgetSetupScreenEntry] {
var entries: [WidgetSetupScreenEntry] = []
entries.append(.useHints("Show Recent Chats", useHints))
if !useHints {
entries.append(.peersHeaderItem(presentationData.strings.Privacy_ChatsTitle))
entries.append(.addItem(presentationData.strings.Privacy_AddNewPeer, state.editing))
var index: Int32 = 0
for peer in peers {
entries.append(.peerItem(index, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, canBeReordered: state.editing, revealed: peer.peer.id == state.peerIdWithRevealedOptions), true))
index += 1
}
}
return entries
}
public func widgetSetupScreen(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(WidgetSetupScreenControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: WidgetSetupScreenControllerState())
let updateState: ((WidgetSetupScreenControllerState) -> WidgetSetupScreenControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let addPeerDisposable = MetaDisposable()
actionsDisposable.add(addPeerDisposable)
let arguments = Arguments(context: context, updateUseHints: { value in
let _ = (updateWidgetSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.useHints = value
return settings
})
|> deliverOnMainQueue).start()
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
var state = state
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
state.peerIdWithRevealedOptions = peerId
}
return state
}
}, removePeer: { memberId in
}, addPeer: {
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: []))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in
var peerIds: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue
}
let _ = (updateWidgetSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
for peerId in peerIds {
switch peerId {
case let .peer(peerId):
settings.peers.removeAll(where: { $0 == peerId })
settings.peers.insert(peerId, at: 0)
case .deviceContact:
break
}
}
return settings
})
|> deliverOnMainQueue).start(completed: {
controller?.dismiss()
})
}))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openPeer: { peerId in
let _ = (context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) else {
return
}
pushControllerImpl?(controller)
})
})
var previousPeers: [SelectivePrivacyPeer]?
var previousState: WidgetSetupScreenControllerState?
struct InputData {
var settings: WidgetSettings
var peers: [SelectivePrivacyPeer]
}
let preferencesKey: PostboxViewKey = .preferences(keys: Set([
ApplicationSpecificPreferencesKeys.widgetSettings
]))
let inputData: Signal<InputData, NoError> = context.account.postbox.combinedView(keys: [
preferencesKey
])
|> mapToSignal { views -> Signal<InputData, 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
}
return context.account.postbox.transaction { transaction -> InputData in
return InputData(
settings: widgetSettings,
peers: widgetSettings.peers.compactMap { peerId -> SelectivePrivacyPeer? in
guard let peer = transaction.getPeer(peerId) else {
return nil
}
return SelectivePrivacyPeer(peer: peer, participantCount: nil)
}
)
}
}
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), inputData)
|> deliverOnMainQueue
|> map { presentationData, state, inputData -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
if !inputData.peers.isEmpty {
if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { state in
var state = state
state.editing = false
return state
}
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
var state = state
state.editing = true
return state
}
})
}
}
let previous = previousPeers
previousPeers = inputData.peers
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Widget"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
var animated = true
if let previous = previous {
if previous.count <= inputData.peers.count {
if Set(previous.map { $0.peer.id }) == Set(inputData.peers.map { $0.peer.id }) && previous.map({ $0.peer.id }) != inputData.peers.map({ $0.peer.id }) {
} else {
animated = false
}
}
} else {
animated = false
}
if let previousState = previousState {
if previousState.editing != state.editing {
animated = true
}
} else {
animated = false
}
previousState = state
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacyPeersControllerEntries(presentationData: presentationData, state: state, useHints: inputData.settings.useHints, peers: inputData.peers), style: .blocks, emptyStateItem: nil, animateChanges: animated)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
if let controller = controller, let navigationController = controller.navigationController as? NavigationController {
navigationController.filterController(controller, animated: true)
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
pushControllerImpl = { [weak controller] c in
if let navigationController = controller?.navigationController as? NavigationController {
navigationController.pushViewController(c)
}
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [WidgetSetupScreenEntry]) -> Signal<Bool, NoError> in
let fromEntry = entries[fromIndex]
guard case let .peerItem(_, _, _, fromPeer, _, _) = fromEntry else {
return .single(false)
}
var referencePeerId: PeerId?
var beforeAll = false
var afterAll = false
if toIndex < entries.count {
switch entries[toIndex] {
case let .peerItem(_, _, _, peer, _, _):
referencePeerId = peer.peer.id
default:
if entries[toIndex] < fromEntry {
beforeAll = true
} else {
afterAll = true
}
}
} else {
afterAll = true
}
return context.account.postbox.transaction { transaction -> Bool in
var updatedOrder = false
updateWidgetSettingsInteractively(transaction: transaction, { settings in
let initialPeers = settings.peers
var settings = settings
if let index = settings.peers.firstIndex(of: fromPeer.peer.id) {
settings.peers.remove(at: index)
}
if let referencePeerId = referencePeerId {
var inserted = false
for i in 0 ..< settings.peers.count {
if settings.peers[i] == referencePeerId {
if fromIndex < toIndex {
settings.peers.insert(fromPeer.peer.id, at: i + 1)
} else {
settings.peers.insert(fromPeer.peer.id, at: i)
}
inserted = true
break
}
}
if !inserted {
settings.peers.append(fromPeer.peer.id)
}
} else if beforeAll {
settings.peers.insert(fromPeer.peer.id, at: 0)
} else if afterAll {
settings.peers.append(fromPeer.peer.id)
}
if initialPeers != settings.peers {
updatedOrder = true
}
return settings
})
return updatedOrder
}
})
return controller
}