User Info screen

This commit is contained in:
Ali 2020-02-07 14:23:25 +00:00
parent f9ccae936a
commit e2071301c2
28 changed files with 7682 additions and 4769 deletions

View File

@ -262,6 +262,11 @@
"State.Updating" = "Updating..."; "State.Updating" = "Updating...";
"State.WaitingForNetwork" = "Waiting for network"; "State.WaitingForNetwork" = "Waiting for network";
"ChatState.Connecting" = "connecting...";
"ChatState.ConnectingToProxy" = "connecting to proxy...";
"ChatState.Updating" = "updating...";
"ChatState.WaitingForNetwork" = "waiting for network...";
// Presence // Presence
"Presence.online" = "online"; "Presence.online" = "online";

View File

@ -351,7 +351,7 @@ public extension ContainedViewLayoutTransition {
func animatePositionAdditive(node: ASDisplayNode, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { func animatePositionAdditive(node: ASDisplayNode, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) {
switch self { switch self {
case .immediate: case .immediate:
break completion(true)
case let .animated(duration, curve): case let .animated(duration, curve):
node.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) node.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
} }
@ -360,7 +360,7 @@ public extension ContainedViewLayoutTransition {
func animatePositionAdditive(layer: CALayer, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { func animatePositionAdditive(layer: CALayer, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) {
switch self { switch self {
case .immediate: case .immediate:
break completion(true)
case let .animated(duration, curve): case let .animated(duration, curve):
layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
} }
@ -369,7 +369,7 @@ public extension ContainedViewLayoutTransition {
func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) {
switch self { switch self {
case .immediate: case .immediate:
break completion?()
case let .animated(duration, curve): case let .animated(duration, curve):
node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in
completion?() completion?()
@ -380,7 +380,7 @@ public extension ContainedViewLayoutTransition {
func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) {
switch self { switch self {
case .immediate: case .immediate:
break completion?()
case let .animated(duration, curve): case let .animated(duration, curve):
layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in
completion?() completion?()

View File

@ -1188,4 +1188,25 @@ open class NavigationBar: ASDisplayNode {
} }
} }
} }
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
if self.backButtonNode.supernode != nil && !self.backButtonNode.isHidden {
let effectiveBackButtonRect = CGRect(origin: CGPoint(), size: CGSize(width: self.backButtonNode.frame.maxX + 20.0, height: self.bounds.height))
if effectiveBackButtonRect.contains(point) {
return self.backButtonNode.internalHitTest(self.view.convert(point, to: self.backButtonNode.view), with: event)
}
}
}
guard let result = super.hitTest(point, with: event) else {
return nil
}
if result == self.view || result == self.clippingNode.view {
return nil
}
return result
}
} }

View File

@ -241,7 +241,7 @@ private final class NavigationButtonItemNode: ASTextNode {
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event) super.touchesMoved(touches, with: event)
self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true) //self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true)
} }
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
@ -251,7 +251,7 @@ private final class NavigationButtonItemNode: ASTextNode {
let previousTouchCount = self.touchCount let previousTouchCount = self.touchCount
self.touchCount = max(0, self.touchCount - touches.count) self.touchCount = max(0, self.touchCount - touches.count)
if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && self.touchInsideApparentBounds(touches.first!) { if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled {
self.pressed() self.pressed()
} }
} }
@ -455,4 +455,12 @@ public final class NavigationButtonNode: ASDisplayNode {
} }
return totalSize return totalSize
} }
func internalHitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.nodes.count == 1 {
return self.nodes[0].view
} else {
return super.hitTest(point, with: event)
}
}
} }

View File

@ -424,7 +424,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
} }
} }
private let avatarFont = avatarPlaceholderFont(size: 15.0) private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
private let badgeFont = Font.regular(15.0) private let badgeFont = Font.regular(15.0)
public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNode { public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNode {

View File

@ -304,6 +304,9 @@ public func notificationSoundSelectionController(context: AccountContext, isModa
playSoundDisposable.dispose() playSoundDisposable.dispose()
}) })
controller.enableInteractiveDismiss = true controller.enableInteractiveDismiss = true
if isModal {
controller.navigationPresentation = .modal
}
completeImpl = { [weak controller] in completeImpl = { [weak controller] in
let sound = stateValue.with { state in let sound = stateValue.with { state in

View File

@ -77,7 +77,7 @@ public class MemoryBuffer: Equatable, CustomStringConvertible {
data.copyBytes(to: self.memory.assumingMemoryBound(to: UInt8.self), count: data.count) data.copyBytes(to: self.memory.assumingMemoryBound(to: UInt8.self), count: data.count)
self.capacity = data.count self.capacity = data.count
self.length = data.count self.length = data.count
self.freeWhenDone = false self.freeWhenDone = true
} }
} }

View File

@ -0,0 +1,76 @@
import Foundation
final class MutableHistoryTagInfoView: MutablePostboxView {
fileprivate let peerId: PeerId
fileprivate let tag: MessageTags
fileprivate var currentIndex: MessageIndex?
init(postbox: Postbox, peerId: PeerId, tag: MessageTags) {
self.peerId = peerId
self.tag = tag
for namespace in postbox.messageHistoryIndexTable.existingNamespaces(peerId: self.peerId) {
if let index = postbox.messageHistoryTagsTable.latestIndex(tag: self.tag, peerId: self.peerId, namespace: namespace) {
self.currentIndex = index
break
}
}
}
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
if let operations = transaction.currentOperationsByPeerId[self.peerId] {
var updated = false
var refresh = false
for operation in operations {
switch operation {
case let .InsertMessage(message):
if self.currentIndex == nil {
if message.tags.contains(self.tag) {
self.currentIndex = message.index
updated = true
}
}
case let .Remove(indicesAndTags):
if self.currentIndex != nil {
for (index, tags) in indicesAndTags {
if tags.contains(self.tag) {
if index == self.currentIndex {
self.currentIndex = nil
updated = true
refresh = true
}
}
}
}
default:
break
}
}
if refresh {
for namespace in postbox.messageHistoryIndexTable.existingNamespaces(peerId: self.peerId) {
if let index = postbox.messageHistoryTagsTable.latestIndex(tag: self.tag, peerId: self.peerId, namespace: namespace) {
self.currentIndex = index
break
}
}
}
return updated
} else {
return false
}
}
func immutableView() -> PostboxView {
return HistoryTagInfoView(self)
}
}
public final class HistoryTagInfoView: PostboxView {
public let isEmpty: Bool
init(_ view: MutableHistoryTagInfoView) {
self.isEmpty = view.currentIndex == nil
}
}

View File

@ -132,6 +132,15 @@ class MessageHistoryTagsTable: Table {
return Int(self.valueBox.count(self.table, start: lowerBoundKey, end: upperBoundKey)) return Int(self.valueBox.count(self.table, start: lowerBoundKey, end: upperBoundKey))
} }
func latestIndex(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace) -> MessageIndex? {
var result: MessageIndex?
self.valueBox.range(self.table, start: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), end: self.upperBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in
result = extractKey(key)
return true
}, limit: 1)
return result
}
func findRandomIndex(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, ignoreIds: ([MessageId], Set<MessageId>), isMessage: (MessageIndex) -> Bool) -> MessageIndex? { func findRandomIndex(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, ignoreIds: ([MessageId], Set<MessageId>), isMessage: (MessageIndex) -> Bool) -> MessageIndex? {
var indices: [MessageIndex] = [] var indices: [MessageIndex] = []
self.valueBox.range(self.table, start: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), end: self.upperBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in self.valueBox.range(self.table, start: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), end: self.upperBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in

View File

@ -28,289 +28,300 @@ public enum PostboxViewKey: Hashable {
case peerChatInclusion(PeerId) case peerChatInclusion(PeerId)
case basicPeer(PeerId) case basicPeer(PeerId)
case allChatListHoles(PeerGroupId) case allChatListHoles(PeerGroupId)
case historyTagInfo(peerId: PeerId, tag: MessageTags)
public var hashValue: Int { public var hashValue: Int {
switch self { switch self {
case .itemCollectionInfos: case .itemCollectionInfos:
return 0 return 0
case .itemCollectionIds: case .itemCollectionIds:
return 1 return 1
case let .peerChatState(peerId): case let .peerChatState(peerId):
return peerId.hashValue return peerId.hashValue
case let .itemCollectionInfo(id): case let .itemCollectionInfo(id):
return id.hashValue return id.hashValue
case let .orderedItemList(id): case let .orderedItemList(id):
return id.hashValue return id.hashValue
case .preferences: case .preferences:
return 3 return 3
case .globalMessageTags: case .globalMessageTags:
return 4 return 4
case let .peer(peerId, _): case let .peer(peerId, _):
return peerId.hashValue return peerId.hashValue
case let .pendingMessageActions(type): case let .pendingMessageActions(type):
return type.hashValue return type.hashValue
case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace): case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace):
return tagMask.rawValue.hashValue ^ namespace.hashValue return tagMask.rawValue.hashValue ^ namespace.hashValue
case let .pendingMessageActionsSummary(type, peerId, namespace): case let .pendingMessageActionsSummary(type, peerId, namespace):
return type.hashValue ^ peerId.hashValue ^ namespace.hashValue return type.hashValue ^ peerId.hashValue ^ namespace.hashValue
case let .historyTagSummaryView(tag, peerId, namespace): case let .historyTagSummaryView(tag, peerId, namespace):
return tag.rawValue.hashValue ^ peerId.hashValue ^ namespace.hashValue return tag.rawValue.hashValue ^ peerId.hashValue ^ namespace.hashValue
case let .cachedPeerData(peerId): case let .cachedPeerData(peerId):
return peerId.hashValue return peerId.hashValue
case .unreadCounts: case .unreadCounts:
return 5 return 5
case .peerNotificationSettings: case .peerNotificationSettings:
return 6 return 6
case .pendingPeerNotificationSettings: case .pendingPeerNotificationSettings:
return 7 return 7
case let .messageOfInterestHole(location, namespace, count): case let .messageOfInterestHole(location, namespace, count):
return 8 &+ 31 &* location.hashValue &+ 31 &* namespace.hashValue &+ 31 &* count.hashValue return 8 &+ 31 &* location.hashValue &+ 31 &* namespace.hashValue &+ 31 &* count.hashValue
case let .localMessageTag(tag): case let .localMessageTag(tag):
return tag.hashValue return tag.hashValue
case .messages: case .messages:
return 10 return 10
case .additionalChatListItems: case .additionalChatListItems:
return 11 return 11
case let .cachedItem(id): case let .cachedItem(id):
return id.hashValue return id.hashValue
case .peerPresences: case .peerPresences:
return 13 return 13
case .synchronizeGroupMessageStats: case .synchronizeGroupMessageStats:
return 14 return 14
case .peerNotificationSettingsBehaviorTimestampView: case .peerNotificationSettingsBehaviorTimestampView:
return 15 return 15
case let .peerChatInclusion(peerId): case let .peerChatInclusion(peerId):
return peerId.hashValue return peerId.hashValue
case let .basicPeer(peerId): case let .basicPeer(peerId):
return peerId.hashValue return peerId.hashValue
case let .allChatListHoles(groupId): case let .allChatListHoles(groupId):
return groupId.hashValue return groupId.hashValue
case let .historyTagInfo(peerId, tag):
return peerId.hashValue ^ tag.hashValue
} }
} }
public static func ==(lhs: PostboxViewKey, rhs: PostboxViewKey) -> Bool { public static func ==(lhs: PostboxViewKey, rhs: PostboxViewKey) -> Bool {
switch lhs { switch lhs {
case let .itemCollectionInfos(lhsNamespaces): case let .itemCollectionInfos(lhsNamespaces):
if case let .itemCollectionInfos(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces { if case let .itemCollectionInfos(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces {
return true return true
} else { } else {
return false return false
} }
case let .itemCollectionIds(lhsNamespaces): case let .itemCollectionIds(lhsNamespaces):
if case let .itemCollectionIds(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces { if case let .itemCollectionIds(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces {
return true return true
} else { } else {
return false return false
} }
case let .itemCollectionInfo(id): case let .itemCollectionInfo(id):
if case .itemCollectionInfo(id) = rhs { if case .itemCollectionInfo(id) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .peerChatState(peerId): case let .peerChatState(peerId):
if case .peerChatState(peerId) = rhs { if case .peerChatState(peerId) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .orderedItemList(id): case let .orderedItemList(id):
if case .orderedItemList(id) = rhs { if case .orderedItemList(id) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .preferences(lhsKeys): case let .preferences(lhsKeys):
if case let .preferences(rhsKeys) = rhs, lhsKeys == rhsKeys { if case let .preferences(rhsKeys) = rhs, lhsKeys == rhsKeys {
return true return true
} else { } else {
return false return false
} }
case let .globalMessageTags(globalTag, position, count, _): case let .globalMessageTags(globalTag, position, count, _):
if case .globalMessageTags(globalTag, position, count, _) = rhs { if case .globalMessageTags(globalTag, position, count, _) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .peer(peerId, components): case let .peer(peerId, components):
if case .peer(peerId, components) = rhs { if case .peer(peerId, components) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .pendingMessageActions(type): case let .pendingMessageActions(type):
if case .pendingMessageActions(type) = rhs { if case .pendingMessageActions(type) = rhs {
return true return true
} else { } else {
return false return false
} }
case .invalidatedMessageHistoryTagSummaries: case .invalidatedMessageHistoryTagSummaries:
if case .invalidatedMessageHistoryTagSummaries = rhs { if case .invalidatedMessageHistoryTagSummaries = rhs {
return true return true
} else { } else {
return false return false
} }
case let .pendingMessageActionsSummary(type, peerId, namespace): case let .pendingMessageActionsSummary(type, peerId, namespace):
if case .pendingMessageActionsSummary(type, peerId, namespace) = rhs { if case .pendingMessageActionsSummary(type, peerId, namespace) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .historyTagSummaryView(tag, peerId, namespace): case let .historyTagSummaryView(tag, peerId, namespace):
if case .historyTagSummaryView(tag, peerId, namespace) = rhs { if case .historyTagSummaryView(tag, peerId, namespace) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .cachedPeerData(peerId): case let .cachedPeerData(peerId):
if case .cachedPeerData(peerId) = rhs { if case .cachedPeerData(peerId) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .unreadCounts(lhsItems): case let .unreadCounts(lhsItems):
if case let .unreadCounts(rhsItems) = rhs, lhsItems == rhsItems { if case let .unreadCounts(rhsItems) = rhs, lhsItems == rhsItems {
return true return true
} else { } else {
return false return false
} }
case let .peerNotificationSettings(peerIds): case let .peerNotificationSettings(peerIds):
if case .peerNotificationSettings(peerIds) = rhs { if case .peerNotificationSettings(peerIds) = rhs {
return true return true
} else { } else {
return false return false
} }
case .pendingPeerNotificationSettings: case .pendingPeerNotificationSettings:
if case .pendingPeerNotificationSettings = rhs { if case .pendingPeerNotificationSettings = rhs {
return true return true
} else { } else {
return false return false
} }
case let .messageOfInterestHole(peerId, namespace, count): case let .messageOfInterestHole(peerId, namespace, count):
if case .messageOfInterestHole(peerId, namespace, count) = rhs { if case .messageOfInterestHole(peerId, namespace, count) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .localMessageTag(tag): case let .localMessageTag(tag):
if case .localMessageTag(tag) = rhs { if case .localMessageTag(tag) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .messages(ids): case let .messages(ids):
if case .messages(ids) = rhs { if case .messages(ids) = rhs {
return true return true
} else { } else {
return false return false
} }
case .additionalChatListItems: case .additionalChatListItems:
if case .additionalChatListItems = rhs { if case .additionalChatListItems = rhs {
return true return true
} else { } else {
return false return false
} }
case let .cachedItem(id): case let .cachedItem(id):
if case .cachedItem(id) = rhs { if case .cachedItem(id) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .peerPresences(ids): case let .peerPresences(ids):
if case .peerPresences(ids) = rhs { if case .peerPresences(ids) = rhs {
return true return true
} else { } else {
return false return false
} }
case .synchronizeGroupMessageStats: case .synchronizeGroupMessageStats:
if case .synchronizeGroupMessageStats = rhs { if case .synchronizeGroupMessageStats = rhs {
return true return true
} else { } else {
return false return false
} }
case .peerNotificationSettingsBehaviorTimestampView: case .peerNotificationSettingsBehaviorTimestampView:
if case .peerNotificationSettingsBehaviorTimestampView = rhs { if case .peerNotificationSettingsBehaviorTimestampView = rhs {
return true return true
} else { } else {
return false return false
} }
case let .peerChatInclusion(id): case let .peerChatInclusion(id):
if case .peerChatInclusion(id) = rhs { if case .peerChatInclusion(id) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .basicPeer(id): case let .basicPeer(id):
if case .basicPeer(id) = rhs { if case .basicPeer(id) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .allChatListHoles(groupId): case let .allChatListHoles(groupId):
if case .allChatListHoles(groupId) = rhs { if case .allChatListHoles(groupId) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .historyTagInfo(peerId, tag):
if case .historyTagInfo(peerId, tag) = rhs {
return true
} else {
return false
}
} }
} }
} }
func postboxViewForKey(postbox: Postbox, key: PostboxViewKey) -> MutablePostboxView { func postboxViewForKey(postbox: Postbox, key: PostboxViewKey) -> MutablePostboxView {
switch key { switch key {
case let .itemCollectionInfos(namespaces): case let .itemCollectionInfos(namespaces):
return MutableItemCollectionInfosView(postbox: postbox, namespaces: namespaces) return MutableItemCollectionInfosView(postbox: postbox, namespaces: namespaces)
case let .itemCollectionIds(namespaces): case let .itemCollectionIds(namespaces):
return MutableItemCollectionIdsView(postbox: postbox, namespaces: namespaces) return MutableItemCollectionIdsView(postbox: postbox, namespaces: namespaces)
case let .itemCollectionInfo(id): case let .itemCollectionInfo(id):
return MutableItemCollectionInfoView(postbox: postbox, id: id) return MutableItemCollectionInfoView(postbox: postbox, id: id)
case let .peerChatState(peerId): case let .peerChatState(peerId):
return MutablePeerChatStateView(postbox: postbox, peerId: peerId) return MutablePeerChatStateView(postbox: postbox, peerId: peerId)
case let .orderedItemList(id): case let .orderedItemList(id):
return MutableOrderedItemListView(postbox: postbox, collectionId: id) return MutableOrderedItemListView(postbox: postbox, collectionId: id)
case let .preferences(keys): case let .preferences(keys):
return MutablePreferencesView(postbox: postbox, keys: keys) return MutablePreferencesView(postbox: postbox, keys: keys)
case let .globalMessageTags(globalTag, position, count, groupingPredicate): case let .globalMessageTags(globalTag, position, count, groupingPredicate):
return MutableGlobalMessageTagsView(postbox: postbox, globalTag: globalTag, position: position, count: count, groupingPredicate: groupingPredicate) return MutableGlobalMessageTagsView(postbox: postbox, globalTag: globalTag, position: position, count: count, groupingPredicate: groupingPredicate)
case let .peer(peerId, components): case let .peer(peerId, components):
return MutablePeerView(postbox: postbox, peerId: peerId, components: components) return MutablePeerView(postbox: postbox, peerId: peerId, components: components)
case let .pendingMessageActions(type): case let .pendingMessageActions(type):
return MutablePendingMessageActionsView(postbox: postbox, type: type) return MutablePendingMessageActionsView(postbox: postbox, type: type)
case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace): case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace):
return MutableInvalidatedMessageHistoryTagSummariesView(postbox: postbox, tagMask: tagMask, namespace: namespace) return MutableInvalidatedMessageHistoryTagSummariesView(postbox: postbox, tagMask: tagMask, namespace: namespace)
case let .pendingMessageActionsSummary(type, peerId, namespace): case let .pendingMessageActionsSummary(type, peerId, namespace):
return MutablePendingMessageActionsSummaryView(postbox: postbox, type: type, peerId: peerId, namespace: namespace) return MutablePendingMessageActionsSummaryView(postbox: postbox, type: type, peerId: peerId, namespace: namespace)
case let .historyTagSummaryView(tag, peerId, namespace): case let .historyTagSummaryView(tag, peerId, namespace):
return MutableMessageHistoryTagSummaryView(postbox: postbox, tag: tag, peerId: peerId, namespace: namespace) return MutableMessageHistoryTagSummaryView(postbox: postbox, tag: tag, peerId: peerId, namespace: namespace)
case let .cachedPeerData(peerId): case let .cachedPeerData(peerId):
return MutableCachedPeerDataView(postbox: postbox, peerId: peerId) return MutableCachedPeerDataView(postbox: postbox, peerId: peerId)
case let .unreadCounts(items): case let .unreadCounts(items):
return MutableUnreadMessageCountsView(postbox: postbox, items: items) return MutableUnreadMessageCountsView(postbox: postbox, items: items)
case let .peerNotificationSettings(peerIds): case let .peerNotificationSettings(peerIds):
return MutablePeerNotificationSettingsView(postbox: postbox, peerIds: peerIds) return MutablePeerNotificationSettingsView(postbox: postbox, peerIds: peerIds)
case .pendingPeerNotificationSettings: case .pendingPeerNotificationSettings:
return MutablePendingPeerNotificationSettingsView(postbox: postbox) return MutablePendingPeerNotificationSettingsView(postbox: postbox)
case let .messageOfInterestHole(location, namespace, count): case let .messageOfInterestHole(location, namespace, count):
return MutableMessageOfInterestHolesView(postbox: postbox, location: location, namespace: namespace, count: count) return MutableMessageOfInterestHolesView(postbox: postbox, location: location, namespace: namespace, count: count)
case let .localMessageTag(tag): case let .localMessageTag(tag):
return MutableLocalMessageTagsView(postbox: postbox, tag: tag) return MutableLocalMessageTagsView(postbox: postbox, tag: tag)
case let .messages(ids): case let .messages(ids):
return MutableMessagesView(postbox: postbox, ids: ids) return MutableMessagesView(postbox: postbox, ids: ids)
case .additionalChatListItems: case .additionalChatListItems:
return MutableAdditionalChatListItemsView(postbox: postbox) return MutableAdditionalChatListItemsView(postbox: postbox)
case let .cachedItem(id): case let .cachedItem(id):
return MutableCachedItemView(postbox: postbox, id: id) return MutableCachedItemView(postbox: postbox, id: id)
case let .peerPresences(ids): case let .peerPresences(ids):
return MutablePeerPresencesView(postbox: postbox, ids: ids) return MutablePeerPresencesView(postbox: postbox, ids: ids)
case .synchronizeGroupMessageStats: case .synchronizeGroupMessageStats:
return MutableSynchronizeGroupMessageStatsView(postbox: postbox) return MutableSynchronizeGroupMessageStatsView(postbox: postbox)
case .peerNotificationSettingsBehaviorTimestampView: case .peerNotificationSettingsBehaviorTimestampView:
return MutablePeerNotificationSettingsBehaviorTimestampView(postbox: postbox) return MutablePeerNotificationSettingsBehaviorTimestampView(postbox: postbox)
case let .peerChatInclusion(peerId): case let .peerChatInclusion(peerId):
return MutablePeerChatInclusionView(postbox: postbox, peerId: peerId) return MutablePeerChatInclusionView(postbox: postbox, peerId: peerId)
case let .basicPeer(peerId): case let .basicPeer(peerId):
return MutableBasicPeerView(postbox: postbox, peerId: peerId) return MutableBasicPeerView(postbox: postbox, peerId: peerId)
case let .allChatListHoles(groupId): case let .allChatListHoles(groupId):
return MutableAllChatListHolesView(postbox: postbox, groupId: groupId) return MutableAllChatListHolesView(postbox: postbox, groupId: groupId)
case let .historyTagInfo(peerId, tag):
return MutableHistoryTagInfoView(postbox: postbox, peerId: peerId, tag: tag)
} }
} }

View File

@ -99,7 +99,7 @@ public final class SearchDisplayController {
self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarFrame.maxY, transition: transition) self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarFrame.maxY, transition: transition)
} }
public func activate(insertSubnode: (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode) { public func activate(insertSubnode: (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode?) {
guard let (layout, navigationBarHeight) = self.containerLayout else { guard let (layout, navigationBarHeight) = self.containerLayout else {
return return
} }
@ -110,19 +110,20 @@ public final class SearchDisplayController {
self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate) self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate)
let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil)
let contentNodePosition = self.contentNode.layer.position
var contentNavigationBarHeight = navigationBarHeight var contentNavigationBarHeight = navigationBarHeight
if layout.statusBarHeight == nil { if layout.statusBarHeight == nil {
contentNavigationBarHeight += 28.0 contentNavigationBarHeight += 28.0
} }
self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) if let placeholder = placeholder {
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil)
self.searchBar.placeholderString = placeholder.placeholderString let contentNodePosition = self.contentNode.layer.position
self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
self.searchBar.placeholderString = placeholder.placeholderString
}
let navigationBarFrame: CGRect let navigationBarFrame: CGRect
switch self.mode { switch self.mode {
@ -149,18 +150,26 @@ public final class SearchDisplayController {
self.searchBar.layout() self.searchBar.layout()
self.searchBar.activate() self.searchBar.activate()
self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) if let placeholder = placeholder {
self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
} else {
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
} }
public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) { public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) {
self.searchBar.deactivate() self.searchBar.deactivate()
let searchBar = self.searchBar
if let placeholder = placeholder { if let placeholder = placeholder {
let searchBar = self.searchBar
searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: {
[weak searchBar] in [weak searchBar] in
searchBar?.removeFromSupernode() searchBar?.removeFromSupernode()
}) })
} else {
searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] _ in
searchBar?.removeFromSupernode()
})
} }
let contentNode = self.contentNode let contentNode = self.contentNode

View File

@ -1064,7 +1064,7 @@ public final class AccountViewTracker {
} }
} }
public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags? = nil, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, clipHoles: Bool = true, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags? = nil, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> {
if let account = self.account { if let account = self.account {
let inputAnchor: HistoryViewInputAnchor let inputAnchor: HistoryViewInputAnchor
switch index { switch index {
@ -1075,7 +1075,7 @@ public final class AccountViewTracker {
case let .message(index): case let .message(index):
inputAnchor = .index(index) inputAnchor = .index(index)
} }
let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, count: count, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, count: count, clipHoles: clipHoles, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData))
return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, addHoleIfNeeded: false) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, addHoleIfNeeded: false)
} else { } else {
return .never() return .never()

View File

@ -1892,7 +1892,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.presentInGlobalOverlay(contextController) strongSelf.presentInGlobalOverlay(contextController)
} }
avatarNode.tapped = { [weak self] in avatarNode.tapped = { [weak self] in
self?.navigationButtonAction(.openChatInfo(expandAvatar: true)) guard let strongSelf = self else {
return
}
var expandAvatar = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, peer.smallProfileImage != nil {
expandAvatar = true
}
strongSelf.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar))
} }
} }
self.navigationItem.titleView = self.chatTitleView self.navigationItem.titleView = self.chatTitleView

View File

@ -87,7 +87,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
return self.measureNode.measure(CGSize(width: 240.0, height: 160.0)).width return self.measureNode.measure(CGSize(width: 240.0, height: 160.0)).width
} }
func update(theme: PresentationTheme, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, alignment: NSTextAlignment = .left, animated: Bool, badgeAnimated: Bool = true) { func update(theme: PresentationTheme?, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, alignment: NSTextAlignment = .left, animated: Bool, badgeAnimated: Bool = true) {
var transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate var transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
let previousContentSize = self.previousContentSize let previousContentSize = self.previousContentSize
@ -263,7 +263,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
var originY: CGFloat = 5.0 var originY: CGFloat = 5.0
switch mediaDownloadState { switch mediaDownloadState {
case .remote: case .remote:
if let image = PresentationResourcesChat.chatBubbleFileCloudFetchMediaIcon(theme) { if let theme = theme, let image = PresentationResourcesChat.chatBubbleFileCloudFetchMediaIcon(theme) {
state = .customIcon(image) state = .customIcon(image)
} else { } else {
state = .none state = .none

View File

@ -60,7 +60,7 @@ private final class ChatTitleNetworkStatusNode: ASDisplayNode {
func updateTheme(theme: PresentationTheme) { func updateTheme(theme: PresentationTheme) {
self.theme = theme self.theme = theme
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.medium(24.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
self.activityIndicator.type = .custom(self.theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5, false) self.activityIndicator.type = .custom(self.theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5, false)
} }
@ -109,7 +109,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
private var titleRightIcon: ChatTitleIcon = .none private var titleRightIcon: ChatTitleIcon = .none
private var titleScamIcon = false private var titleScamIcon = false
private var networkStatusNode: ChatTitleNetworkStatusNode? //private var networkStatusNode: ChatTitleNetworkStatusNode?
private var presenceManager: PeerPresenceStatusManager? private var presenceManager: PeerPresenceStatusManager?
@ -125,7 +125,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
isOnline = true isOnline = true
} }
if isOnline || layout?.metrics.widthClass == .regular { /*if isOnline || layout?.metrics.widthClass == .regular {
self.contentContainer.isHidden = false self.contentContainer.isHidden = false
if let networkStatusNode = self.networkStatusNode { if let networkStatusNode = self.networkStatusNode {
self.networkStatusNode = nil self.networkStatusNode = nil
@ -155,7 +155,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
case .online: case .online:
break break
} }
} }*/
self.setNeedsLayout() self.setNeedsLayout()
} }
@ -164,6 +164,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
didSet { didSet {
if self.networkState != oldValue { if self.networkState != oldValue {
updateNetworkStatusNode(networkState: self.networkState, layout: self.layout) updateNetworkStatusNode(networkState: self.networkState, layout: self.layout)
self.updateStatus()
} }
} }
} }
@ -277,175 +278,191 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
} }
var state = ChatTitleActivityNodeState.none var state = ChatTitleActivityNodeState.none
if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed { switch self.networkState {
var stringValue = "" case .waitingForNetwork, .connecting, .updating:
var first = true var infoText: String
var mergedActivity = inputActivities[0].1 switch self.networkState {
for (_, activity) in inputActivities { case .waitingForNetwork:
if activity != mergedActivity { infoText = self.strings.ChatState_WaitingForNetwork
mergedActivity = .typingText case let .connecting(proxy):
break infoText = self.strings.ChatState_Connecting
} case .updating:
infoText = self.strings.ChatState_Updating
case .online:
infoText = ""
} }
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { state = .info(NSAttributedString(string: infoText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor), .generic)
switch mergedActivity { case .online:
case .typingText: if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed {
stringValue = strings.Conversation_typing var stringValue = ""
case .uploadingFile: var first = true
stringValue = strings.Activity_UploadingDocument var mergedActivity = inputActivities[0].1
case .recordingVoice: for (_, activity) in inputActivities {
stringValue = strings.Activity_RecordingAudio if activity != mergedActivity {
case .uploadingPhoto: mergedActivity = .typingText
stringValue = strings.Activity_UploadingPhoto break
case .uploadingVideo:
stringValue = strings.Activity_UploadingVideo
case .playingGame:
stringValue = strings.Activity_PlayingGame
case .recordingInstantVideo:
stringValue = strings.Activity_RecordingVideoMessage
case .uploadingInstantVideo:
stringValue = strings.Activity_UploadingVideoMessage
}
} else {
for (peer, _) in inputActivities {
let title = peer.compactDisplayTitle
if !title.isEmpty {
if first {
first = false
} else {
stringValue += ", "
}
stringValue += title
} }
} }
} if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat {
let color = self.theme.rootController.navigationBar.accentTextColor switch mergedActivity {
let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: color) case .typingText:
switch mergedActivity { stringValue = strings.Conversation_typing
case .typingText: case .uploadingFile:
state = .typingText(string, color) stringValue = strings.Activity_UploadingDocument
case .recordingVoice: case .recordingVoice:
state = .recordingVoice(string, color) stringValue = strings.Activity_RecordingAudio
case .recordingInstantVideo: case .uploadingPhoto:
state = .recordingVideo(string, color) stringValue = strings.Activity_UploadingPhoto
case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo: case .uploadingVideo:
state = .uploading(string, color) stringValue = strings.Activity_UploadingVideo
case .playingGame: case .playingGame:
state = .playingGame(string, color) stringValue = strings.Activity_PlayingGame
} case .recordingInstantVideo:
} else { stringValue = strings.Activity_RecordingVideoMessage
if let titleContent = self.titleContent { case .uploadingInstantVideo:
switch titleContent { stringValue = strings.Activity_UploadingVideoMessage
case let .peer(peerView, onlineMemberCount, isScheduledMessages): }
if let peer = peerViewMainPeer(peerView) { } else {
let servicePeer = isServicePeer(peer) for (peer, _) in inputActivities {
if peer.id == self.account.peerId || isScheduledMessages { let title = peer.compactDisplayTitle
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if !title.isEmpty {
state = .info(string, .generic) if first {
} else if let user = peer as? TelegramUser { first = false
if servicePeer { } else {
stringValue += ", "
}
stringValue += title
}
}
}
let color = self.theme.rootController.navigationBar.accentTextColor
let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: color)
switch mergedActivity {
case .typingText:
state = .typingText(string, color)
case .recordingVoice:
state = .recordingVoice(string, color)
case .recordingInstantVideo:
state = .recordingVideo(string, color)
case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo:
state = .uploading(string, color)
case .playingGame:
state = .playingGame(string, color)
}
} else {
if let titleContent = self.titleContent {
switch titleContent {
case let .peer(peerView, onlineMemberCount, isScheduledMessages):
if let peer = peerViewMainPeer(peerView) {
let servicePeer = isServicePeer(peer)
if peer.id == self.account.peerId || isScheduledMessages {
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic) state = .info(string, .generic)
} else if user.flags.contains(.isSupport) { } else if let user = peer as? TelegramUser {
let statusText = self.strings.Bot_GenericSupportStatus if servicePeer {
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic)
state = .info(string, .generic) } else if user.flags.contains(.isSupport) {
} else if let _ = user.botInfo { let statusText = self.strings.Bot_GenericSupportStatus
let statusText = self.strings.Bot_GenericBotStatus
let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic)
state = .info(string, .generic) } else if let _ = user.botInfo {
} else if let peer = peerViewMainPeer(peerView) { let statusText = self.strings.Bot_GenericBotStatus
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let userPresence: TelegramUserPresence let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { state = .info(string, .generic)
userPresence = presence } else if let peer = peerViewMainPeer(peerView) {
self.presenceManager?.reset(presence: presence) let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let userPresence: TelegramUserPresence
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence {
userPresence = presence
self.presenceManager?.reset(presence: presence)
} else {
userPresence = TelegramUserPresence(status: .none, lastActivity: 0)
}
let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, dateTimeFormat: self.dateTimeFormat, presence: userPresence, relativeTo: Int32(timestamp))
let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor)
state = .info(attributedString, activity ? .online : .lastSeenTime)
} else { } else {
userPresence = TelegramUserPresence(status: .none, lastActivity: 0) let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic)
} }
let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, dateTimeFormat: self.dateTimeFormat, presence: userPresence, relativeTo: Int32(timestamp)) } else if let group = peer as? TelegramGroup {
let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) var onlineCount = 0
state = .info(attributedString, activity ? .online : .lastSeenTime) if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants {
} else { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) for participant in participants.participants {
state = .info(string, .generic) if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence {
} let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp))
} else if let group = peer as? TelegramGroup { switch relativeStatus {
var onlineCount = 0 case .online:
if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { onlineCount += 1
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 default:
for participant in participants.participants { break
if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { }
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp))
switch relativeStatus {
case .online:
onlineCount += 1
default:
break
} }
} }
} }
} if onlineCount > 1 {
if onlineCount > 1 { let string = NSMutableAttributedString()
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
state = .info(string, .generic)
} else {
let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic)
}
} else if let channel = peer as? TelegramChannel {
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
if memberCount == 0 {
let string: NSAttributedString
if case .group = channel.info {
string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
} else {
string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
}
state = .info(string, .generic) state = .info(string, .generic)
} else { } else {
if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
let string = NSMutableAttributedString() state = .info(string, .generic)
}
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) } else if let channel = peer as? TelegramChannel {
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
if memberCount == 0 {
let string: NSAttributedString
if case .group = channel.info {
string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
} else {
string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
}
state = .info(string, .generic) state = .info(string, .generic)
} else { } else {
let membersString: String if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 {
if case .group = channel.info { let string = NSMutableAttributedString()
membersString = strings.Conversation_StatusMembers(memberCount)
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
state = .info(string, .generic)
} else { } else {
membersString = strings.Conversation_StatusSubscribers(memberCount) let membersString: String
if case .group = channel.info {
membersString = strings.Conversation_StatusMembers(memberCount)
} else {
membersString = strings.Conversation_StatusSubscribers(memberCount)
}
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic)
} }
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic)
} }
} } else {
} else { switch channel.info {
switch channel.info { case .group:
case .group: let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic)
state = .info(string, .generic) case .broadcast:
case .broadcast: let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic)
state = .info(string, .generic) }
} }
} }
} }
} default:
default: break
break }
self.accessibilityLabel = self.titleNode.attributedText?.string
self.accessibilityValue = state.string
} else {
self.accessibilityLabel = nil
} }
self.accessibilityLabel = self.titleNode.attributedText?.string
self.accessibilityValue = state.string
} else {
self.accessibilityLabel = nil
} }
} }
@ -552,7 +569,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.networkStatusNode?.updateTheme(theme: theme) //self.networkStatusNode?.updateTheme(theme: theme)
let titleContent = self.titleContent let titleContent = self.titleContent
self.titleContent = titleContent self.titleContent = titleContent
self.updateStatus() self.updateStatus()
@ -612,7 +629,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
let titleSideInset: CGFloat = 3.0 let titleSideInset: CGFloat = 3.0
if size.height > 40.0 { if size.height > 40.0 {
var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height)) var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0 - leftInset, height: size.height))
titleSize.width += credibilityIconWidth titleSize.width += credibilityIconWidth
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .left) let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .left)
let titleInfoSpacing: CGFloat = 0.0 let titleInfoSpacing: CGFloat = 0.0
@ -663,10 +680,10 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
} }
} }
if let networkStatusNode = self.networkStatusNode { /*if let networkStatusNode = self.networkStatusNode {
transition.updateFrame(node: networkStatusNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: networkStatusNode, frame: CGRect(origin: CGPoint(), size: size))
networkStatusNode.updateLayout(size: size, transition: transition) networkStatusNode.updateLayout(size: size, transition: transition)
} }*/
} }
@objc func buttonPressed() { @objc func buttonPressed() {

View File

@ -33,7 +33,7 @@ func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMedia
} }
} }
func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool) -> Signal<FileMediaResourceStatus, NoError> { func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions) |> map { status -> MediaPlayerPlaybackStatus? in let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions) |> map { status -> MediaPlayerPlaybackStatus? in
return status?.status return status?.status
} }

View File

@ -15,6 +15,7 @@ import AccountContext
import RadialStatusNode import RadialStatusNode
import PhotoResources import PhotoResources
import MusicAlbumArtResources import MusicAlbumArtResources
import UniversalMediaPlayer
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
@ -124,6 +125,7 @@ private struct FetchControls {
private enum FileIconImage: Equatable { private enum FileIconImage: Equatable {
case imageRepresentation(TelegramMediaFile, TelegramMediaImageRepresentation) case imageRepresentation(TelegramMediaFile, TelegramMediaImageRepresentation)
case albumArt(TelegramMediaFile, SharedMediaPlaybackAlbumArt) case albumArt(TelegramMediaFile, SharedMediaPlaybackAlbumArt)
case roundVideo(TelegramMediaFile)
static func ==(lhs: FileIconImage, rhs: FileIconImage) -> Bool { static func ==(lhs: FileIconImage, rhs: FileIconImage) -> Bool {
switch lhs { switch lhs {
@ -139,6 +141,12 @@ private enum FileIconImage: Equatable {
} else { } else {
return false return false
} }
case let .roundVideo(file):
if case .roundVideo(file) = rhs {
return true
} else {
return false
}
} }
} }
} }
@ -159,6 +167,10 @@ final class ListMessageFileItemNode: ListMessageNode {
private let statusButtonNode: HighlightTrackingButtonNode private let statusButtonNode: HighlightTrackingButtonNode
private let statusNode: RadialStatusNode private let statusNode: RadialStatusNode
private var waveformNode: AudioWaveformNode?
private var waveformForegroundNode: AudioWaveformNode?
private var waveformScrubbingNode: MediaPlayerScrubbingNode?
private var currentIconImage: FileIconImage? private var currentIconImage: FileIconImage?
private var currentMedia: Media? private var currentMedia: Media?
@ -167,6 +179,8 @@ final class ListMessageFileItemNode: ListMessageNode {
private var fetchStatus: MediaResourceStatus? private var fetchStatus: MediaResourceStatus?
private var resourceStatus: FileMediaResourceMediaStatus? private var resourceStatus: FileMediaResourceMediaStatus?
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
private let playbackStatusDisposable = MetaDisposable()
private let playbackStatus = Promise<MediaPlayerStatus>()
private var downloadStatusIconNode: ASImageNode private var downloadStatusIconNode: ASImageNode
private var linearProgressNode: ASDisplayNode private var linearProgressNode: ASDisplayNode
@ -226,7 +240,7 @@ final class ListMessageFileItemNode: ListMessageNode {
self.downloadStatusIconNode.displayWithoutProcessing = true self.downloadStatusIconNode.displayWithoutProcessing = true
self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: .black, foregroundColor: .white, icon: nil)) self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: .black, foregroundColor: .white, icon: nil))
self.progressNode.isLayerBacked = true //self.progressNode.isLayerBacked = true
self.linearProgressNode = ASDisplayNode() self.linearProgressNode = ASDisplayNode()
self.linearProgressNode.isLayerBacked = true self.linearProgressNode.isLayerBacked = true
@ -235,6 +249,7 @@ final class ListMessageFileItemNode: ListMessageNode {
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.progressNode)
self.addSubnode(self.descriptionNode) self.addSubnode(self.descriptionNode)
self.addSubnode(self.descriptionProgressNode) self.addSubnode(self.descriptionProgressNode)
self.addSubnode(self.extensionIconNode) self.addSubnode(self.extensionIconNode)
@ -335,9 +350,13 @@ final class ListMessageFileItemNode: ListMessageNode {
var iconImage: FileIconImage? var iconImage: FileIconImage?
var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>? var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>?
var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>?
var updatedFetchControls: FetchControls? var updatedFetchControls: FetchControls?
var waveform: AudioWaveform?
var isAudio = false var isAudio = false
var isVoice = false
var isInstantVideo = false
let message = item.message let message = item.message
@ -346,9 +365,12 @@ final class ListMessageFileItemNode: ListMessageNode {
if let file = media as? TelegramMediaFile { if let file = media as? TelegramMediaFile {
selectedMedia = file selectedMedia = file
isInstantVideo = file.isInstantVideo
for attribute in file.attributes { for attribute in file.attributes {
if case let .Audio(voice, _, title, performer, _) = attribute { if case let .Audio(voice, _, title, performer, waveformValue) = attribute {
isAudio = true isAudio = true
isVoice = voice
titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor) titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
@ -365,11 +387,21 @@ final class ListMessageFileItemNode: ListMessageNode {
if !voice { if !voice {
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false))) iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false)))
} else {
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
waveformValue?.withDataNoCopy { data in
waveform = AudioWaveform(bitstream: data, bitsPerSample: 5)
}
} }
} }
} }
if !isAudio { if isInstantVideo {
titleText = NSAttributedString(string: item.strings.Message_VideoMessage, font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
iconImage = .roundVideo(file)
} else if !isAudio {
let fileName: String = file.fileName ?? "" let fileName: String = file.fileName ?? ""
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
@ -402,7 +434,7 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
if isAudio { if isAudio && !isVoice {
leftInset += 14.0 leftInset += 14.0
} }
@ -435,9 +467,9 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
if statusUpdated { if statusUpdated {
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false) updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false, isSharedMedia: true)
if isAudio { if isAudio || isInstantVideo {
if let currentUpdatedStatusSignal = updatedStatusSignal { if let currentUpdatedStatusSignal = updatedStatusSignal {
updatedStatusSignal = currentUpdatedStatusSignal updatedStatusSignal = currentUpdatedStatusSignal
|> map { status in |> map { status in
@ -450,6 +482,9 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
} }
if isVoice {
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false)
}
} }
} }
@ -472,6 +507,11 @@ final class ListMessageFileItemNode: ListMessageNode {
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments) iconImageApply = iconImageLayout(arguments)
case let .roundVideo(file):
let iconSize = CGSize(width: 42.0, height: 42.0)
let imageCorners = ImageCorners(topLeft: .Corner(iconSize.width / 2.0), topRight: .Corner(iconSize.width / 2.0), bottomLeft: .Corner(iconSize.width / 2.0), bottomRight: .Corner(iconSize.width / 2.0))
let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments)
} }
} }
@ -482,7 +522,8 @@ final class ListMessageFileItemNode: ListMessageNode {
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation) updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation)
case let .albumArt(file, albumArt): case let .albumArt(file, albumArt):
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true) updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true)
case let .roundVideo(file):
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true)
} }
} else { } else {
updateIconImageSignal = .complete() updateIconImageSignal = .complete()
@ -581,6 +622,48 @@ final class ListMessageFileItemNode: ListMessageNode {
strongSelf.currentIconImage = iconImage strongSelf.currentIconImage = iconImage
if isVoice {
let waveformNode: AudioWaveformNode
let waveformForegroundNode: AudioWaveformNode
let waveformScrubbingNode: MediaPlayerScrubbingNode
if let current = strongSelf.waveformNode {
waveformNode = current
} else {
waveformNode = AudioWaveformNode()
waveformNode.isLayerBacked = true
strongSelf.waveformNode = waveformNode
strongSelf.addSubnode(waveformNode)
}
if let current = strongSelf.waveformForegroundNode {
waveformForegroundNode = current
} else {
waveformForegroundNode = AudioWaveformNode()
waveformForegroundNode.isLayerBacked = true
strongSelf.waveformForegroundNode = waveformForegroundNode
strongSelf.addSubnode(waveformForegroundNode)
}
if let current = strongSelf.waveformScrubbingNode {
waveformScrubbingNode = current
} else {
waveformScrubbingNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: waveformNode, foregroundContentNode: waveformForegroundNode))
waveformScrubbingNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: 0.0, bottom: -10.0, right: 0.0)
waveformScrubbingNode.seek = { timestamp in
if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(message) {
context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type)
}
}
waveformScrubbingNode.enableScrubbing = false
waveformScrubbingNode.status = strongSelf.playbackStatus.get()
strongSelf.waveformScrubbingNode = waveformScrubbingNode
strongSelf.addSubnode(waveformScrubbingNode)
}
strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - (leftOffset + leftInset) - 16.0, height: 12.0))
waveformNode.setup(color: item.theme.list.controlSecondaryColor, waveform: waveform)
waveformForegroundNode.setup(color: item.theme.list.itemAccentColor, waveform: waveform)
}
if let iconImageApply = iconImageApply { if let iconImageApply = iconImageApply {
if let updateImageSignal = updateIconImageSignal { if let updateImageSignal = updateIconImageSignal {
strongSelf.iconImageNode.setSignal(updateImageSignal) strongSelf.iconImageNode.setSignal(updateImageSignal)
@ -632,10 +715,24 @@ final class ListMessageFileItemNode: ListMessageNode {
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 11.0) / 2.0)), size: CGSize(width: 11.0, height: 11.0))) transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 11.0) / 2.0)), size: CGSize(width: 11.0, height: 11.0)))
let progressSize: CGFloat = 40.0
transition.updateFrame(node: strongSelf.progressNode, frame: CGRect(origin: CGPoint(x: leftOffset + params.leftInset + floor((leftInset - params.leftInset - progressSize) / 2.0), y: floor((nodeLayout.contentSize.height - progressSize) / 2.0)), size: CGSize(width: progressSize, height: progressSize)))
if let updatedFetchControls = updatedFetchControls { if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls) let _ = strongSelf.fetchControls.swap(updatedFetchControls)
} }
if let updatedPlaybackStatusSignal = updatedPlaybackStatusSignal {
strongSelf.playbackStatus.set(updatedPlaybackStatusSignal)
/*strongSelf.playbackStatusDisposable.set((updatedPlaybackStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf {
strongSelf.playerStatus = status
}
}
}))*/
}
strongSelf.updateStatus(transition: transition) strongSelf.updateStatus(transition: transition)
} }
}) })
@ -648,25 +745,34 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
var isAudio = false var isAudio = false
var isVoice = false
var isInstantVideo = false
if let file = media as? TelegramMediaFile { if let file = media as? TelegramMediaFile {
isAudio = file.isMusic || file.isVoice isAudio = file.isMusic || file.isVoice
isVoice = file.isVoice
isInstantVideo = file.isInstantVideo
} }
self.progressNode.isHidden = !isVoice
var enableScrubbing = false
var musicIsPlaying: Bool? var musicIsPlaying: Bool?
var statusState: RadialStatusNodeState = .none var statusState: RadialStatusNodeState = .none
if !isAudio { if !isAudio && !isInstantVideo {
self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate) self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
} else { } else {
switch fetchStatus { if !isVoice && !isInstantVideo {
case let .Fetching(_, progress): switch fetchStatus {
let adjustedProgress = max(progress, 0.027) case let .Fetching(_, progress):
statusState = .cloudProgress(color: item.theme.list.itemAccentColor, strokeBackgroundColor: item.theme.list.itemAccentColor.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress)) let adjustedProgress = max(progress, 0.027)
case .Local: statusState = .cloudProgress(color: item.theme.list.itemAccentColor, strokeBackgroundColor: item.theme.list.itemAccentColor.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress))
break case .Local:
case .Remote: break
if let image = PresentationResourcesItemList.cloudFetchIcon(item.theme) { case .Remote:
statusState = .customIcon(image) if let image = PresentationResourcesItemList.cloudFetchIcon(item.theme) {
} statusState = .customIcon(image)
}
}
} }
self.statusNode.transitionToState(statusState, completion: {}) self.statusNode.transitionToState(statusState, completion: {})
self.statusButtonNode.isUserInteractionEnabled = statusState != .none self.statusButtonNode.isUserInteractionEnabled = statusState != .none
@ -691,6 +797,7 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
case let .playbackStatus(playbackStatus): case let .playbackStatus(playbackStatus):
enableScrubbing = true
switch playbackStatus { switch playbackStatus {
case .playing: case .playing:
musicIsPlaying = true musicIsPlaying = true
@ -701,7 +808,8 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
} }
if let musicIsPlaying = musicIsPlaying { self.waveformScrubbingNode?.enableScrubbing = enableScrubbing
if let musicIsPlaying = musicIsPlaying, !isVoice {
if self.playbackOverlayNode == nil { if self.playbackOverlayNode == nil {
let playbackOverlayNode = ListMessagePlaybackOverlayNode() let playbackOverlayNode = ListMessagePlaybackOverlayNode()
playbackOverlayNode.frame = self.iconImageNode.frame playbackOverlayNode.frame = self.iconImageNode.frame

View File

@ -13,6 +13,8 @@ import TelegramUIPreferences
final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext private let context: AccountContext
private let peerId: PeerId private let peerId: PeerId
private let paneInteraction: PeerInfoPaneInteraction
private let controllerInteraction: ChatControllerInteraction
private let listNode: ChatHistoryListNode private let listNode: ChatHistoryListNode
@ -24,14 +26,25 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
return self.ready.get() return self.ready.get()
} }
private let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
private var selectedMessages: Set<MessageId>? {
didSet {
if self.selectedMessages != oldValue {
self.selectedMessagesPromise.set(.single(self.selectedMessages))
}
}
}
private var hiddenMediaDisposable: Disposable? private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, tagMask: MessageTags) { init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, tagMask: MessageTags, interaction: PeerInfoPaneInteraction) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.paneInteraction = interaction
var openMessageImpl: ((MessageId) -> Bool)? var openMessageImpl: ((MessageId) -> Bool)?
let controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in var toggleMessageSelectionImpl: (([MessageId]) -> Void)?
self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in
return openMessageImpl?(message.id) ?? false return openMessageImpl?(message.id) ?? false
}, openPeer: { _, _, _ in }, openPeer: { _, _, _ in
}, openPeerMention: { _ in }, openPeerMention: { _ in
@ -39,7 +52,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
}, openMessageContextActions: { _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _ in }, navigateToMessage: { _, _ in
}, tapMessage: nil, clickThroughMessage: { }, tapMessage: nil, clickThroughMessage: {
}, toggleMessagesSelection: { _, _ in }, toggleMessagesSelection: { ids, _ in
toggleMessageSelectionImpl?(ids)
}, sendCurrentMessage: { _ in }, sendCurrentMessage: { _ in
}, sendMessage: { _ in }, sendMessage: { _ in
}, sendSticker: { _, _, _, _ in }, sendSticker: { _, _, _, _ in
@ -97,8 +111,13 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
self.controllerInteraction.selectionState = self.paneInteraction.selectedMessageIds.flatMap { ids in
return ChatInterfaceSelectionState(selectedIds: ids)
}
self.selectedMessages = self.paneInteraction.selectedMessageIds
self.selectedMessagesPromise.set(.single(self.selectedMessages))
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false))
super.init() super.init()
@ -106,6 +125,12 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
return openMessage(id) return openMessage(id)
} }
toggleMessageSelectionImpl = { [weak self] ids in
for id in ids {
self?.paneInteraction.toggleMessageSelected(id)
}
}
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -116,7 +141,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
hiddenMedia[messageId] = [media] hiddenMedia[messageId] = [media]
} }
} }
controllerInteraction.hiddenMedia = hiddenMedia strongSelf.controllerInteraction.hiddenMedia = hiddenMedia
strongSelf.listNode.forEachItemNode { itemNode in strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode { if let itemNode = itemNode as? ListMessageNode {
itemNode.updateHiddenMedia() itemNode.updateHiddenMedia()
@ -171,4 +196,16 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
} }
return transitionNode return transitionNode
} }
func updateSelectedMessages(animated: Bool) {
self.controllerInteraction.selectionState = self.paneInteraction.selectedMessageIds.flatMap { ids in
return ChatInterfaceSelectionState(selectedIds: ids)
}
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated)
}
}
self.selectedMessages = self.paneInteraction.selectedMessageIds
}
} }

View File

@ -0,0 +1,165 @@
import AsyncDisplayKit
import Display
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
import ItemListPeerItem
import MergeLists
import ItemListUI
private struct GroupsInCommonListTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private struct GroupsInCommonListEntry: Comparable, Identifiable {
var index: Int
var peer: Peer
var stableId: PeerId {
return self.peer.id
}
static func ==(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool {
return lhs.peer.isEqual(rhs.peer)
}
static func <(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> ListViewItem {
let peer = self.peer
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: self.peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, contextAction: { node, gesture in
//arguments.contextAction(peer, node, gesture)
}, hasTopStripe: false, noInsets: true)
}
}
private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> GroupsInCommonListTransaction {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) }
return GroupsInCommonListTransaction(deletions: deletions, insertions: insertions, updates: updates)
}
final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let peerId: PeerId
private let paneInteraction: PeerInfoPaneInteraction
private let listNode: ListView
private var peers: [Peer] = []
private var currentEntries: [GroupsInCommonListEntry] = []
private var enqueuedTransactions: [GroupsInCommonListTransaction] = []
private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
init(context: AccountContext, peerId: PeerId, interaction: PeerInfoPaneInteraction, peers: [Peer]) {
self.context = context
self.peerId = peerId
self.paneInteraction = interaction
self.listNode = ListView()
super.init()
self.listNode.preloadPages = true
self.addSubnode(self.listNode)
self.peers = peers
}
deinit {
}
func scrollToTop() -> Bool {
if !self.listNode.scrollToOffsetFromTop(0.0) {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
return true
} else {
return false
}
}
func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), headerInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.scrollEnabled = !isScrollingLockedAtTop
if isFirstLayout {
self.updatePeers(peers: self.peers, presentationData: presentationData)
}
}
private func updatePeers(peers: [Peer], presentationData: PresentationData) {
var entries: [GroupsInCommonListEntry] = []
for peer in peers {
entries.append(GroupsInCommonListEntry(index: entries.count, peer: peer))
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in
self?.paneInteraction.openPeer(peer)
})
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
}
private func dequeueTransaction() {
guard let (layout, _, _) = self.currentParams, let transaction = self.enqueuedTransactions.first else {
return
}
self.enqueuedTransactions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf.ready.set(.single(true))
}
})
}
func findLoadedMessage(id: MessageId) -> Message? {
return nil
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func updateSelectedMessages(animated: Bool) {
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,98 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
enum PeerInfoScreenActionColor {
case accent
case destructive
}
final class PeerInfoScreenActionItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let color: PeerInfoScreenActionColor
let action: (() -> Void)?
init(id: AnyHashable, text: String, color: PeerInfoScreenActionColor = .accent, action: (() -> Void)?) {
self.id = id
self.text = text
self.color = color
self.action = action
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenActionItemNode()
}
}
private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let textNode: ImmediateTextNode
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenActionItem?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.textNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenActionItem else {
return 10.0
}
self.item = item
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor
switch item.color {
case .accent:
textColorValue = presentationData.theme.list.itemAccentColor
case .destructive:
textColorValue = presentationData.theme.list.itemDestructiveColor
}
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize)
let height = textSize.height + 22.0
transition.updateFrame(node: self.textNode, frame: textFrame)
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}

View File

@ -0,0 +1,115 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
let id: AnyHashable
let label: String
let text: String
let action: (() -> Void)?
init(id: AnyHashable, label: String, text: String, action: (() -> Void)?) {
self.id = id
self.label = label
self.text = text
self.action = action
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenDisclosureItemNode()
}
}
private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let labelNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let arrowNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenDisclosureItem?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.labelNode = ImmediateTextNode()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.isLayerBacked = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenDisclosureItem else {
return 10.0
}
self.item = item
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor
let labelColorValue: UIColor = presentationData.theme.list.itemSecondaryTextColor
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(17.0), textColor: labelColorValue)
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let labelSize = self.labelNode.updateLayout(CGSize(width: width - textSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let arrowInset: CGFloat = 18.0
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize)
let labelFrame = CGRect(origin: CGPoint(x: width - sideInset - arrowInset - labelSize.width, y: 11.0), size: labelSize)
let height = textSize.height + 22.0
if let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) {
self.arrowNode.image = arrowImage
let arrowFrame = CGRect(origin: CGPoint(x: width - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
transition.updateFrame(node: self.labelNode, frame: labelFrame)
transition.updateFrame(node: self.textNode, frame: textFrame)
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}

View File

@ -0,0 +1,121 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
final class PeerInfoScreenSwitchItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let value: Bool
let toggled: ((Bool) -> Void)?
init(id: AnyHashable, text: String, value: Bool, toggled: ((Bool) -> Void)?) {
self.id = id
self.text = text
self.value = value
self.toggled = toggled
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenSwitchItemNode()
}
}
private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let textNode: ImmediateTextNode
private let switchNode: SwitchNode
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenSwitchItem?
private var theme: PresentationTheme?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.switchNode = SwitchNode()
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.textNode)
self.addSubnode(self.switchNode)
self.switchNode.valueUpdated = { [weak self] value in
self?.item?.toggled?(value)
}
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenSwitchItem else {
return 10.0
}
let firstTime = self.item == nil
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.switchNode.frameColor = presentationData.theme.list.itemSwitchColors.frameColor
self.switchNode.contentColor = presentationData.theme.list.itemSwitchColors.contentColor
self.switchNode.handleColor = presentationData.theme.list.itemSwitchColors.handleColor
}
self.item = item
self.selectionNode.pressed = nil
let sideInset: CGFloat = 16.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - 56.0, height: .greatestFiniteMagnitude))
let arrowInset: CGFloat = 18.0
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize)
let height = textSize.height + 22.0
transition.updateFrame(node: self.textNode, frame: textFrame)
if let switchView = self.switchNode.view as? UISwitch {
if self.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
self.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0, y: floor((height - switchSize.height) / 2.0)), size: switchSize)
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: !firstTime)
}
}
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}

View File

@ -8,13 +8,26 @@ import TelegramPresentationData
import AccountContext import AccountContext
import ContextUI import ContextUI
import PhotoResources import PhotoResources
import RadialStatusNode
import TelegramStringFormatting
import GridMessageSelectionNode
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white
private final class VisualMediaItemInteraction { private final class VisualMediaItemInteraction {
let openMessage: (MessageId) -> Void let openMessage: (MessageId) -> Void
var hiddenMedia: [MessageId: [Media]] = [:] let toggleSelection: (MessageId) -> Void
init(openMessage: @escaping (MessageId) -> Void) { var hiddenMedia: [MessageId: [Media]] = [:]
var selectedMessageIds: Set<MessageId>?
init(
openMessage: @escaping (MessageId) -> Void,
toggleSelection: @escaping (MessageId) -> Void
) {
self.openMessage = openMessage self.openMessage = openMessage
self.toggleSelection = toggleSelection
} }
} }
@ -24,12 +37,16 @@ private final class VisualMediaItemNode: ASDisplayNode {
private let containerNode: ContextControllerSourceNode private let containerNode: ContextControllerSourceNode
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private var statusNode: RadialStatusNode
private let mediaBadgeNode: ChatMessageInteractiveMediaBadge
private var selectionNode: GridMessageSelectionNode?
private let fetchStatusDisposable = MetaDisposable() private let fetchStatusDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
private var resourceStatus: MediaResourceStatus? private var resourceStatus: MediaResourceStatus?
private var item: (VisualMediaItem, Media?, CGSize, CGSize?)? private var item: (VisualMediaItem, Media?, CGSize, CGSize?)?
private var theme: PresentationTheme?
init(context: AccountContext, interaction: VisualMediaItemInteraction) { init(context: AccountContext, interaction: VisualMediaItemInteraction) {
self.context = context self.context = context
@ -37,11 +54,19 @@ private final class VisualMediaItemNode: ASDisplayNode {
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
let progressDiameter: CGFloat = 40.0
self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter)
self.statusNode.isUserInteractionEnabled = false
self.mediaBadgeNode = ChatMessageInteractiveMediaBadge()
self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 50.0, height: 50.0))
super.init() super.init()
self.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode) self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.mediaBadgeNode)
self.containerNode.isGestureEnabled = false self.containerNode.isGestureEnabled = false
} }
@ -69,6 +94,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
if item === self.item?.0 && size == self.item?.2 { if item === self.item?.0 && size == self.item?.2 {
return return
} }
self.theme = theme
var media: Media? var media: Media?
for value in item.message.media { for value in item.message.media {
if let image = value as? TelegramMediaImage { if let image = value as? TelegramMediaImage {
@ -88,20 +114,20 @@ private final class VisualMediaItemNode: ASDisplayNode {
self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(item.message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(item.message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true)
self.fetchStatusDisposable.set(nil) self.fetchStatusDisposable.set(nil)
/*self.statusNode.transitionToState(.none, completion: { [weak self] in self.statusNode.transitionToState(.none, completion: { [weak self] in
self?.statusNode.isHidden = true self?.statusNode.isHidden = true
})*/ })
//self.mediaBadgeNode.isHidden = true self.mediaBadgeNode.isHidden = true
self.resourceStatus = nil self.resourceStatus = nil
} else if let file = media as? TelegramMediaFile, file.isVideo { } else if let file = media as? TelegramMediaFile, file.isVideo {
mediaDimensions = file.dimensions?.cgSize mediaDimensions = file.dimensions?.cgSize
self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad)
/*self.mediaBadgeNode.isHidden = false self.mediaBadgeNode.isHidden = false
self.resourceStatus = nil self.resourceStatus = nil
self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: messageId, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: item.message.id, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, let item = strongSelf.item { if let strongSelf = self, let (item, _, _, _) = strongSelf.item {
strongSelf.resourceStatus = status strongSelf.resourceStatus = status
let isStreamable = isMediaStreamable(message: item.message, media: file) let isStreamable = isMediaStreamable(message: item.message, media: file)
@ -158,18 +184,25 @@ private final class VisualMediaItemNode: ASDisplayNode {
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
} }
strongSelf.mediaBadgeNode.update(theme: item.theme, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false) strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false)
} }
} }
})) }))
if self.statusNode.supernode == nil { if self.statusNode.supernode == nil {
self.imageNode.addSubnode(self.statusNode) self.imageNode.addSubnode(self.statusNode)
}*/ }
} else { } else {
//self.mediaBadgeNode.isHidden = true self.mediaBadgeNode.isHidden = true
} }
self.item = (item, media, size, mediaDimensions) self.item = (item, media, size, mediaDimensions)
let progressDiameter: CGFloat = 40.0
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - progressDiameter) / 2.0), y: floor((size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter))
self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 18.0 - 3.0), size: CGSize(width: 50.0, height: 50.0))
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: size)
self.updateHiddenMedia() self.updateHiddenMedia()
} }
@ -185,6 +218,46 @@ private final class VisualMediaItemNode: ASDisplayNode {
let imageSize = mediaDimensions.aspectFilled(imageFrame.size) let imageSize = mediaDimensions.aspectFilled(imageFrame.size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))()
} }
self.updateSelectionState(animated: false)
}
}
func updateSelectionState(animated: Bool) {
if let (item, media, _, mediaDimensions) = self.item, let theme = self.theme {
if let selectedIds = self.interaction.selectedMessageIds {
let selected = selectedIds.contains(item.message.id)
if let selectionNode = self.selectionNode {
selectionNode.updateSelected(selected, animated: animated)
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
} else {
let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in
if let strongSelf = self, let messageId = strongSelf.item?.0.message.id {
strongSelf.interaction.toggleSelection(messageId)
}
})
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.containerNode.addSubnode(selectionNode)
self.selectionNode = selectionNode
selectionNode.updateSelected(selected, animated: false)
if animated {
selectionNode.animateIn()
}
}
} else {
if let selectionNode = self.selectionNode {
self.selectionNode = nil
if animated {
selectionNode.animateOut { [weak selectionNode] in
selectionNode?.removeFromSupernode()
}
} else {
selectionNode.removeFromSupernode()
}
}
}
} }
} }
@ -194,15 +267,15 @@ private final class VisualMediaItemNode: ASDisplayNode {
var statusNodeHidden = false var statusNodeHidden = false
var accessoryHidden = false var accessoryHidden = false
if let strongSelf = self { if let strongSelf = self {
//statusNodeHidden = strongSelf.statusNode.isHidden statusNodeHidden = strongSelf.statusNode.isHidden
//accessoryHidden = strongSelf.mediaBadgeNode.isHidden accessoryHidden = strongSelf.mediaBadgeNode.isHidden
//strongSelf.statusNode.isHidden = true strongSelf.statusNode.isHidden = true
//strongSelf.mediaBadgeNode.isHidden = true strongSelf.mediaBadgeNode.isHidden = true
} }
let view = imageNode?.view.snapshotContentTree(unhide: true) let view = imageNode?.view.snapshotContentTree(unhide: true)
if let strongSelf = self { if let strongSelf = self {
//strongSelf.statusNode.isHidden = statusNodeHidden strongSelf.statusNode.isHidden = statusNodeHidden
//strongSelf.mediaBadgeNode.isHidden = accessoryHidden strongSelf.mediaBadgeNode.isHidden = accessoryHidden
} }
return (view, nil) return (view, nil)
}) })
@ -232,6 +305,8 @@ private final class VisualMediaItem {
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
private let context: AccountContext private let context: AccountContext
private let peerId: PeerId private let peerId: PeerId
private let interaction: PeerInfoPaneInteraction
private let scrollNode: ASScrollNode private let scrollNode: ASScrollNode
private var _itemInteraction: VisualMediaItemInteraction? private var _itemInteraction: VisualMediaItemInteraction?
@ -257,17 +332,24 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private var isRequestingView: Bool = false private var isRequestingView: Bool = false
private var isFirstHistoryView: Bool = true private var isFirstHistoryView: Bool = true
init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId) { init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, interaction: PeerInfoPaneInteraction) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.interaction = interaction
self.scrollNode = ASScrollNode() self.scrollNode = ASScrollNode()
super.init() super.init()
self._itemInteraction = VisualMediaItemInteraction(openMessage: { id in self._itemInteraction = VisualMediaItemInteraction(
openMessage(id) openMessage: { id in
}) openMessage(id)
},
toggleSelection: { id in
interaction.toggleMessageSelected(id)
}
)
self.itemInteraction.selectedMessageIds = self.interaction.selectedMessageIds
self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false
if #available(iOS 11.0, *) { if #available(iOS 11.0, *) {
@ -372,6 +454,13 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return nil return nil
} }
func updateSelectedMessages(animated: Bool) {
self.itemInteraction.selectedMessageIds = self.interaction.selectedMessageIds
for (_, itemNode) in self.visibleMediaItems {
itemNode.updateSelectionState(animated: animated)
}
}
func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, isScrollingLockedAtTop, presentationData) self.currentParams = (size, isScrollingLockedAtTop, presentationData)
@ -420,10 +509,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
maxVisibleRow = min(rowCount - 1, maxVisibleRow) maxVisibleRow = min(rowCount - 1, maxVisibleRow)
let minVisibleIndex = minVisibleRow * itemsInRow let minVisibleIndex = minVisibleRow * itemsInRow
let maxVisibleIndex = min(self.mediaItems.count - 1, maxVisibleRow * itemsInRow - 1) let maxVisibleIndex = min(self.mediaItems.count - 1, (maxVisibleRow + 1) * itemsInRow - 1)
var validIds = Set<UInt32>() var validIds = Set<UInt32>()
if minVisibleIndex < maxVisibleIndex { if minVisibleIndex <= maxVisibleIndex {
for i in minVisibleIndex ... maxVisibleIndex { for i in minVisibleIndex ... maxVisibleIndex {
let stableId = self.mediaItems[i].message.stableId let stableId = self.mediaItems[i].message.stableId
validIds.insert(stableId) validIds.insert(stableId)

View File

@ -448,12 +448,12 @@ public final class WalletStrings: Equatable {
public var Wallet_SecureStorageReset_Title: String { return self._s[218]! } public var Wallet_SecureStorageReset_Title: String { return self._s[218]! }
public var Wallet_Receive_CommentHeader: String { return self._s[219]! } public var Wallet_Receive_CommentHeader: String { return self._s[219]! }
public var Wallet_Info_ReceiveGrams: String { return self._s[220]! } public var Wallet_Info_ReceiveGrams: String { return self._s[220]! }
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String { public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value) let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator) let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue) return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
} }
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String { public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value) let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator) let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue) return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)