Comment improvements

This commit is contained in:
Ali 2020-09-15 22:21:35 +04:00
parent c368f569ee
commit 43281e1546
56 changed files with 5184 additions and 4275 deletions

View File

@ -5770,3 +5770,23 @@ Any member of this group will be able to see messages in the channel.";
"Conversation.InputTextAnonymousPlaceholder" = "Send anonymously";
"DialogList.Replies" = "Replies";
"Conversation.ContextViewReplies_1" = "View %@ Reply";
"Conversation.ContextViewReplies_any" = "View %@ Replies";
"Conversation.ContextViewThread" = "View Thread";
"Conversation.ViewReply" = "View Reply";
"Conversation.MessageViewComments_1" = "%@ Comment";
"Conversation.MessageViewComments_any" = "%@ Comments";
"Conversation.MessageLeaveComment" = "Leave a Comment";
"Conversation.MessageLeaveCommentShort" = "Comment";
"Conversation.DiscussionNotStarted" = "No comments here yet...";
"Conversation.DiscussionStarted" = "Discussion started";
"Conversation.InputTextPlaceholderReply" = "Reply";
"Conversation.InputTextPlaceholderComment" = "Comment";
"Conversation.TitleComments_1" = "%@ Comment";
"Conversation.TitleComments_any" = "%@ Comments";
"Conversation.TitleNoComments" = "Comments";

View File

@ -170,7 +170,7 @@ public enum ResolvedUrl {
case botStart(peerId: PeerId, payload: String)
case groupBotStart(peerId: PeerId, payload: String)
case channelMessage(peerId: PeerId, messageId: MessageId)
case replyThreadMessage(replyThreadMessageId: MessageId, isChannelPost: Bool, maxReadMessageId: MessageId?, messageId: MessageId)
case replyThreadMessage(replyThreadMessageId: MessageId, isChannelPost: Bool, maxMessage: ChatReplyThreadMessage.MaxMessage, maxReadMessageId: MessageId?, messageId: MessageId)
case stickerPack(name: String)
case instantView(TelegramMediaWebpage, String?)
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
@ -253,7 +253,7 @@ public enum ChatSearchDomain: Equatable {
public enum ChatLocation: Equatable {
case peer(PeerId)
case replyThread(threadMessageId: MessageId, isChannelPost: Bool, maxReadMessageId: MessageId?)
case replyThread(threadMessageId: MessageId, isChannelPost: Bool, maxMessage: ChatReplyThreadMessage.MaxMessage, maxReadMessageId: MessageId?)
}
public final class NavigateToChatControllerParams {
@ -261,6 +261,7 @@ public final class NavigateToChatControllerParams {
public let chatController: ChatController?
public let context: AccountContext
public let chatLocation: ChatLocation
public let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
public let subject: ChatControllerSubject?
public let botStart: ChatControllerInitialBotStart?
public let updateTextInputState: ChatTextInputState?
@ -277,9 +278,10 @@ public final class NavigateToChatControllerParams {
public let parentGroupId: PeerGroupId?
public let completion: (ChatController) -> Void
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
self.navigationController = navigationController
self.chatController = chatController
self.chatLocationContextHolder = chatLocationContextHolder
self.context = context
self.chatLocation = chatLocation
self.subject = subject

View File

@ -48,7 +48,7 @@ public enum LocationActionListItemIcon: Equatable {
}
private func generateLocationIcon(theme: PresentationTheme) -> UIImage {
return generateImage(CGSize(width: 40.0, height: 40.0)) { size, context in
return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
@ -60,11 +60,11 @@ private func generateLocationIcon(theme: PresentationTheme) -> UIImage {
if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/SendLocationIcon"), color: theme.chat.inputPanel.actionControlForegroundColor) {
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
}
}!
})!
}
private func generateLiveLocationIcon(theme: PresentationTheme) -> UIImage {
return generateImage(CGSize(width: 40.0, height: 40.0)) { size, context in
return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0x6cc139).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
@ -76,7 +76,7 @@ private func generateLiveLocationIcon(theme: PresentationTheme) -> UIImage {
if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/SendLiveLocationIcon"), color: .white) {
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
}
}!
})!
}
final class LocationActionListItem: ListViewItem {

View File

@ -15,7 +15,7 @@ import AccountContext
let locationPinReuseIdentifier = "locationPin"
private func generateSmallBackgroundImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 56.0, height: 56.0)) { size, context in
return generateImage(CGSize(width: 56.0, height: 56.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 4.0, color: UIColor(rgb: 0x000000, alpha: 0.5).cgColor)
@ -25,7 +25,7 @@ private func generateSmallBackgroundImage(color: UIColor) -> UIImage? {
context.setShadow(offset: CGSize(), blur: 0.0, color: nil)
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(x: 17.0 + UIScreenPixel, y: 17.0 + UIScreenPixel, width: 22.0 - 2.0 * UIScreenPixel, height: 22.0 - 2.0 * UIScreenPixel))
}
})
}
class LocationPinAnnotation: NSObject, MKAnnotation {

View File

@ -10,7 +10,7 @@ private let panelSize = CGSize(width: 46.0, height: 90.0)
private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? {
let cornerRadius: CGFloat = 9.0
return generateImage(CGSize(width: (cornerRadius + panelInset) * 2.0, height: (cornerRadius + panelInset) * 2.0)) { size, context in
return generateImage(CGSize(width: (cornerRadius + panelInset) * 2.0, height: (cornerRadius + panelInset) * 2.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
@ -18,11 +18,11 @@ private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? {
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: panelInset, y: panelInset), size: CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)), cornerRadius: cornerRadius)
context.addPath(path.cgPath)
context.fillPath()
}?.stretchableImage(withLeftCapWidth: Int(cornerRadius + panelInset), topCapHeight: Int(cornerRadius + panelInset))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius + panelInset), topCapHeight: Int(cornerRadius + panelInset))
}
private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) -> UIImage? {
return generateImage(CGSize(width: 26.0, height: 14.0)) { size, context in
return generateImage(CGSize(width: 26.0, height: 14.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
@ -30,7 +30,7 @@ private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) ->
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: 26.0, height: 20.0)), cornerRadius: 9.0)
context.addPath(path.cgPath)
context.fillPath()
}?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0)
})?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0)
}
final class LocationMapHeaderNode: ASDisplayNode {

View File

@ -69,7 +69,7 @@ private class LocationMapView: MKMapView, UIGestureRecognizerDelegate {
}
private func generateHeadingArrowImage() -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0)) { size, context in
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
@ -83,7 +83,7 @@ private func generateHeadingArrowImage() -> UIImage? {
context.setBlendMode(.clear)
context.fillEllipse(in: bounds.insetBy(dx: 5.0, dy: 5.0))
}
})
}
final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {

View File

@ -23,5 +23,5 @@ public enum AdditionalMessageHistoryViewDataEntry {
case preferencesEntry(ValueBoxKey, PreferencesEntry?)
case peerIsContact(PeerId, Bool)
case peer(PeerId, Peer?)
case message(MessageId, Message?)
case message(MessageId, [Message])
}

View File

@ -1,6 +1,6 @@
import Foundation
public struct MessageId: Hashable, Comparable, CustomStringConvertible {
public struct MessageId: Hashable, Comparable, CustomStringConvertible, PostboxCoding {
public typealias Namespace = Int32
public typealias Id = Int32
@ -37,6 +37,18 @@ public struct MessageId: Hashable, Comparable, CustomStringConvertible {
buffer.offset += 16
}
public init(decoder: PostboxDecoder) {
self.peerId = PeerId(decoder.decodeInt64ForKey("p", orElse: 0))
self.namespace = decoder.decodeInt32ForKey("n", orElse: 0)
self.id = decoder.decodeInt32ForKey("i", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.peerId.toInt64(), forKey: "p")
encoder.encodeInt32(self.namespace, forKey: "n")
encoder.encodeInt32(self.id, forKey: "i")
}
public func encodeToBuffer(_ buffer: WriteBuffer) {
var peerIdNamespace = self.peerId.namespace
var peerIdId = self.peerId.id

View File

@ -13,6 +13,7 @@ private enum MetadataPrefix: Int8 {
case ShouldReindexUnreadCountsState = 10
case TotalUnreadCountStates = 11
case PeerHistoryTagInitialized = 12
case PeerHistoryThreadHoleIndexInitialized = 13
}
public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable {
@ -45,6 +46,7 @@ final class MessageHistoryMetadataTable: Table {
}
let sharedPeerHistoryInitializedKey = ValueBoxKey(length: 8 + 1)
let sharedPeerThreadHoleIndexInitializedKey = ValueBoxKey(length: 8 + 1 + 8)
let sharedGroupFeedIndexInitializedKey = ValueBoxKey(length: 4 + 1)
let sharedChatListGroupHistoryInitializedKey = ValueBoxKey(length: 4 + 1)
let sharedPeerNextMessageIdByNamespaceKey = ValueBoxKey(length: 8 + 1 + 4)
@ -76,6 +78,12 @@ final class MessageHistoryMetadataTable: Table {
return self.sharedPeerHistoryInitializedKey
}
private func peerThreadHoleIndexInitializedKey(peerId: PeerId, threadId: Int64) -> ValueBoxKey {
self.sharedPeerThreadHoleIndexInitializedKey.setInt64(0, value: peerId.toInt64())
self.sharedPeerThreadHoleIndexInitializedKey.setInt8(8, value: MetadataPrefix.PeerHistoryThreadHoleIndexInitialized.rawValue)
return self.sharedPeerThreadHoleIndexInitializedKey
}
private func peerHistoryInitializedTagKey(id: PeerId, tag: UInt32) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 1 + 4)
key.setInt64(0, value: id.toInt64())
@ -211,6 +219,18 @@ final class MessageHistoryMetadataTable: Table {
}
}
func isThreadHoleIndexInitialized(peerId: PeerId, threadId: Int64) -> Bool {
if self.valueBox.exists(self.table, key: self.peerThreadHoleIndexInitializedKey(peerId: peerId, threadId: threadId)) {
return true
} else {
return false
}
}
func setIsThreadHoleIndexInitialized(peerId: PeerId, threadId: Int64) {
self.valueBox.set(self.table, key: self.peerThreadHoleIndexInitializedKey(peerId: peerId, threadId: threadId), value: MemoryBuffer())
}
func setPeerTagInitialized(peerId: PeerId, tag: MessageTags) {
if self.initializedHistoryPeerIdTags[peerId] == nil {
self.initializedHistoryPeerIdTags[peerId] = Set()

View File

@ -0,0 +1,418 @@
import Foundation
private func decomposeKey(_ key: ValueBoxKey) -> (threadId: Int64, id: MessageId, space: MessageHistoryHoleSpace) {
let tag = MessageTags(rawValue: key.getUInt32(8 + 8 + 4))
let space: MessageHistoryHoleSpace
if tag.rawValue == 0 {
space = .everywhere
} else {
space = .tag(tag)
}
return (key.getInt64(8), MessageId(peerId: PeerId(key.getInt64(0)), namespace: key.getInt32(8 + 8), id: key.getInt32(8 + 8 + 4 + 4)), space)
}
private func decodeValue(value: ReadBuffer, peerId: PeerId, namespace: MessageId.Namespace) -> MessageId {
var id: Int32 = 0
value.read(&id, offset: 0, length: 4)
return MessageId(peerId: peerId, namespace: namespace, id: id)
}
final class MessageHistoryThreadHoleIndexTable: Table {
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
}
let metadataTable: MessageHistoryMetadataTable
let seedConfiguration: SeedConfiguration
init(valueBox: ValueBox, table: ValueBoxTable, metadataTable: MessageHistoryMetadataTable, seedConfiguration: SeedConfiguration) {
self.seedConfiguration = seedConfiguration
self.metadataTable = metadataTable
super.init(valueBox: valueBox, table: table)
}
private func key(threadId: Int64, id: MessageId, space: MessageHistoryHoleSpace) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 8 + 4 + 4 + 4)
key.setInt64(0, value: id.peerId.toInt64())
key.setInt64(8, value: threadId)
key.setInt32(8 + 8, value: id.namespace)
let tagValue: UInt32
switch space {
case .everywhere:
tagValue = 0
case let .tag(tag):
tagValue = tag.rawValue
}
key.setUInt32(8 + 8 + 4, value: tagValue)
key.setInt32(8 + 8 + 4 + 4, value: id.id)
return key
}
private func lowerBound(peerId: PeerId, threadId: Int64) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 8)
key.setInt64(0, value: peerId.toInt64())
key.setInt64(8, value: threadId)
return key
}
private func upperBound(peerId: PeerId, threadId: Int64) -> ValueBoxKey {
return self.lowerBound(peerId: peerId, threadId: threadId).successor
}
private func lowerBound(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 8 + 4 + 4)
key.setInt64(0, value: peerId.toInt64())
key.setInt64(8, value: threadId)
key.setInt32(8 + 8, value: namespace)
let tagValue: UInt32
switch space {
case .everywhere:
tagValue = 0
case let .tag(tag):
tagValue = tag.rawValue
}
key.setUInt32(8 + 8 + 4, value: tagValue)
return key
}
private func upperBound(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 8 + 4 + 4)
key.setInt64(0, value: peerId.toInt64())
key.setInt64(8, value: threadId)
key.setInt32(8 + 4, value: namespace)
let tagValue: UInt32
switch space {
case .everywhere:
tagValue = 0
case let .tag(tag):
tagValue = tag.rawValue
}
key.setUInt32(8 + 8 + 4, value: tagValue)
return key.successor
}
private func namespaceLowerBound(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 8 + 4)
key.setInt64(0, value: peerId.toInt64())
key.setInt64(8, value: threadId)
key.setInt32(8 + 8, value: namespace)
return key
}
private func namespaceUpperBound(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 8 + 4)
key.setInt64(0, value: peerId.toInt64())
key.setInt64(8, value: threadId)
key.setInt32(8 + 8, value: namespace)
return key.successor
}
private func ensureInitialized(peerId: PeerId, threadId: Int64) {
if !self.metadataTable.isThreadHoleIndexInitialized(peerId: peerId, threadId: threadId) {
self.metadataTable.setIsThreadHoleIndexInitialized(peerId: peerId, threadId: threadId)
if let messageNamespaces = self.seedConfiguration.messageThreadHoles[peerId.namespace] {
for namespace in messageNamespaces {
var operations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:]
self.add(peerId: peerId, threadId: threadId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1), operations: &operations)
}
}
}
}
func existingNamespaces(peerId: PeerId, threadId: Int64, holeSpace: MessageHistoryHoleSpace) -> Set<MessageId.Namespace> {
self.ensureInitialized(peerId: peerId, threadId: threadId)
var result = Set<MessageId.Namespace>()
var currentLowerBound = self.lowerBound(peerId: peerId, threadId: threadId)
let upperBound = self.upperBound(peerId: peerId, threadId: threadId)
while true {
var idAndSpace: (Int64, MessageId, MessageHistoryHoleSpace)?
self.valueBox.range(self.table, start: currentLowerBound, end: upperBound, keys: { key in
idAndSpace = decomposeKey(key)
return false
}, limit: 1)
if let (_, id, space) = idAndSpace {
if space == holeSpace {
result.insert(id.namespace)
}
currentLowerBound = self.upperBound(peerId: peerId, threadId: threadId, namespace: id.namespace, space: space)
} else {
break
}
}
return result
}
private func scanSpaces(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace) -> [MessageHistoryHoleSpace] {
self.ensureInitialized(peerId: peerId, threadId: threadId)
var currentLowerBound = self.namespaceLowerBound(peerId: peerId, threadId: threadId, namespace: namespace)
var result: [MessageHistoryHoleSpace] = []
while true {
var found = false
self.valueBox.range(self.table, start: currentLowerBound, end: self.namespaceUpperBound(peerId: peerId, threadId: threadId, namespace: namespace), keys: { key in
let space = decomposeKey(key).space
result.append(space)
currentLowerBound = self.upperBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space)
found = true
return false
}, limit: 1)
if !found {
break
}
}
assert(Set(result).count == result.count)
return result
}
func containing(threadId: Int64, id: MessageId) -> [MessageHistoryHoleSpace: ClosedRange<MessageId.Id>] {
self.ensureInitialized(peerId: id.peerId, threadId: threadId)
var result: [MessageHistoryHoleSpace: ClosedRange<MessageId.Id>] = [:]
for space in self.scanSpaces(peerId: id.peerId, threadId: threadId, namespace: id.namespace) {
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: id, space: space), end: self.upperBound(peerId: id.peerId, threadId: threadId, namespace: id.namespace, space: space), values: { key, value in
let (keyThreadId, upperId, keySpace) = decomposeKey(key)
assert(keyThreadId == threadId)
assert(keySpace == space)
assert(upperId.peerId == id.peerId)
assert(upperId.namespace == id.namespace)
let lowerId = decodeValue(value: value, peerId: id.peerId, namespace: id.namespace)
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
result[space] = holeRange
return false
}, limit: 1)
}
return result
}
func closest(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>) -> IndexSet {
self.ensureInitialized(peerId: peerId, threadId: threadId)
var result = IndexSet()
func processIntersectingRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
let (keyThreadId, upperId, keySpace) = decomposeKey(key)
assert(keyThreadId == threadId)
assert(keySpace == space)
assert(upperId.peerId == peerId)
assert(upperId.namespace == namespace)
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
if holeRange.overlaps(range) {
result.insert(integersIn: Int(holeRange.lowerBound) ... Int(holeRange.upperBound))
}
}
func processEdgeRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
let (keyThreadId, upperId, keySpace) = decomposeKey(key)
assert(keyThreadId == threadId)
assert(keySpace == space)
assert(upperId.peerId == peerId)
assert(upperId.namespace == namespace)
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
result.insert(integersIn: Int(holeRange.lowerBound) ... Int(holeRange.upperBound))
}
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: range.lowerBound), space: space).predecessor, end: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space).successor, values: { key, value in
processIntersectingRange(key, value)
return true
}, limit: 0)
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space), end: self.upperBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space), values: { key, value in
processIntersectingRange(key, value)
return true
}, limit: 1)
if !result.contains(Int(range.lowerBound)) {
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: range.lowerBound), space: space), end: self.lowerBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space), values: { key, value in
processEdgeRange(key, value)
return true
}, limit: 1)
}
if !result.contains(Int(range.upperBound)) {
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space), end: self.upperBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space), values: { key, value in
processEdgeRange(key, value)
return true
}, limit: 1)
}
return result
}
func add(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
self.ensureInitialized(peerId: peerId, threadId: threadId)
self.addInternal(peerId: peerId, threadId: threadId, namespace: namespace, space: space, range: range, operations: &operations)
/*switch space {
case .everywhere:
if let namespaceHoleTags = self.seedConfiguration.messageHoles[peerId.namespace]?[namespace] {
for tag in namespaceHoleTags {
self.addInternal(peerId: peerId, threadId: threadId, namespace: namespace, space: .tag(tag), range: range, operations: &operations)
}
}
case .tag:
break
}*/
}
private func addInternal(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
let clippedLowerBound = max(1, range.lowerBound)
let clippedUpperBound = min(Int32.max - 1, range.upperBound)
if clippedLowerBound > clippedUpperBound {
return
}
let clippedRange = clippedLowerBound ... clippedUpperBound
var removedIndices = IndexSet()
var insertedIndices = IndexSet()
var removeKeys: [Int32] = []
var insertRanges = IndexSet()
var alreadyMapped = false
func processRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
let (keyThreadId, upperId, keySpace) = decomposeKey(key)
assert(keyThreadId == threadId)
assert(keySpace == space)
assert(upperId.peerId == peerId)
assert(upperId.namespace == namespace)
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
let holeRange: ClosedRange<Int32> = lowerId.id ... upperId.id
if clippedRange.lowerBound >= holeRange.lowerBound && clippedRange.upperBound <= holeRange.upperBound {
alreadyMapped = true
return
} else if clippedRange.overlaps(holeRange) || (holeRange.upperBound != Int32.max && clippedRange.lowerBound == holeRange.upperBound + 1) || clippedRange.upperBound == holeRange.lowerBound - 1 {
removeKeys.append(upperId.id)
let unionRange: ClosedRange = min(clippedRange.lowerBound, holeRange.lowerBound) ... max(clippedRange.upperBound, holeRange.upperBound)
insertRanges.insert(integersIn: Int(unionRange.lowerBound) ... Int(unionRange.upperBound))
}
}
let lowerScanBound = max(0, clippedRange.lowerBound - 2)
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: lowerScanBound), space: space), end: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: clippedRange.upperBound), space: space).successor, values: { key, value in
processRange(key, value)
if alreadyMapped {
return false
}
return true
}, limit: 0)
if !alreadyMapped {
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: clippedRange.upperBound), space: space), end: self.upperBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space), values: { key, value in
processRange(key, value)
if alreadyMapped {
return false
}
return true
}, limit: 1)
}
if alreadyMapped {
return
}
insertRanges.insert(integersIn: Int(clippedRange.lowerBound) ... Int(clippedRange.upperBound))
insertedIndices.insert(integersIn: Int(clippedRange.lowerBound) ... Int(clippedRange.upperBound))
for id in removeKeys {
self.valueBox.remove(self.table, key: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: id), space: space), secure: false)
}
for insertRange in insertRanges.rangeView {
let closedRange: ClosedRange<MessageId.Id> = Int32(insertRange.lowerBound) ... Int32(insertRange.upperBound - 1)
var lowerBound: Int32 = closedRange.lowerBound
self.valueBox.set(self.table, key: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: closedRange.upperBound), space: space), value: MemoryBuffer(memory: &lowerBound, capacity: 4, length: 4, freeWhenDone: false))
}
//addOperation(.insert(clippedRange), peerId: peerId, namespace: namespace, space: space, to: &operations)
}
func remove(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
self.ensureInitialized(peerId: peerId, threadId: threadId)
self.removeInternal(peerId: peerId, threadId: threadId, namespace: namespace, space: space, range: range, operations: &operations)
/*switch space {
case .everywhere:
if let namespaceHoleTags = self.seedConfiguration.messageHoles[peerId.namespace]?[namespace] {
for tag in namespaceHoleTags {
self.removeInternal(peerId: peerId, threadId: threadId, namespace: namespace, space: .tag(tag), range: range, operations: &operations)
}
}
case .tag:
break
}*/
}
private func removeInternal(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
var removedIndices = IndexSet()
var insertedIndices = IndexSet()
var removeKeys: [Int32] = []
var insertRanges = IndexSet()
func processRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
let (keyThreadId, upperId, keySpace) = decomposeKey(key)
assert(keyThreadId == threadId)
assert(keySpace == space)
assert(upperId.peerId == peerId)
assert(upperId.namespace == namespace)
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
if range.lowerBound <= holeRange.lowerBound && range.upperBound >= holeRange.upperBound {
removeKeys.append(upperId.id)
} else if range.overlaps(holeRange) {
removeKeys.append(upperId.id)
var holeIndices = IndexSet(integersIn: Int(holeRange.lowerBound) ... Int(holeRange.upperBound))
holeIndices.remove(integersIn: Int(range.lowerBound) ... Int(range.upperBound))
insertRanges.formUnion(holeIndices)
}
}
let lowerScanBound = max(0, range.lowerBound - 2)
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: lowerScanBound), space: space), end: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space).successor, values: { key, value in
processRange(key, value)
return true
}, limit: 0)
self.valueBox.range(self.table, start: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space), end: self.upperBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space), values: { key, value in
processRange(key, value)
return true
}, limit: 1)
for id in removeKeys {
self.valueBox.remove(self.table, key: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: id), space: space), secure: false)
}
for insertRange in insertRanges.rangeView {
let closedRange: ClosedRange<MessageId.Id> = Int32(insertRange.lowerBound) ... Int32(insertRange.upperBound - 1)
var lowerBound: Int32 = closedRange.lowerBound
self.valueBox.set(self.table, key: self.key(threadId: threadId, id: MessageId(peerId: peerId, namespace: namespace, id: closedRange.upperBound), space: space), value: MemoryBuffer(memory: &lowerBound, capacity: 4, length: 4, freeWhenDone: false))
}
/*if !removeKeys.isEmpty {
addOperation(.remove(range), peerId: peerId, namespace: namespace, space: space, to: &operations)
}*/
}
func debugList(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace) -> [ClosedRange<MessageId.Id>] {
var result: [ClosedRange<MessageId.Id>] = []
self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space), end: self.upperBound(peerId: peerId, threadId: threadId, namespace: namespace, space: space), values: { key, value in
let (keyThreadId, upperId, keySpace) = decomposeKey(key)
assert(keyThreadId == threadId)
assert(keySpace == space)
assert(upperId.peerId == peerId)
assert(upperId.namespace == namespace)
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
result.append(holeRange)
return true
}, limit: 0)
return result
}
}

View File

@ -655,35 +655,45 @@ final class MutableMessageHistoryView {
}
case .cachedPeerDataMessages:
break
case let .message(id, _):
case let .message(id, currentMessages):
let currentGroupingKey = currentMessages.first?.groupingKey
var currentIds = [id]
for message in currentMessages {
if message.id != id {
currentIds.append(message.id)
}
}
if let operations = transaction.currentOperationsByPeerId[id.peerId] {
var updateMessage = false
findOperation: for operation in operations {
switch operation {
case let .InsertMessage(message):
if message.id == id {
if message.id == id || (currentGroupingKey != nil && message.groupingKey == currentGroupingKey) {
updateMessage = true
break findOperation
}
case let .Remove(indices):
for (index, _) in indices {
if index.id == id {
if currentIds.contains(index.id) {
updateMessage = true
break findOperation
}
}
case let .UpdateEmbeddedMedia(index, _):
if index.id == id {
if currentIds.contains(index.id) {
updateMessage = true
break findOperation
}
case let .UpdateGroupInfos(dict):
if dict[id] != nil {
updateMessage = true
break findOperation
for id in currentIds {
if dict[id] != nil {
updateMessage = true
break findOperation
}
}
case let .UpdateTimestamp(index, _):
if index.id == id {
if currentIds.contains(index.id) {
updateMessage = true
break findOperation
}
@ -692,10 +702,8 @@ final class MutableMessageHistoryView {
}
}
if updateMessage {
let message = postbox.messageHistoryIndexTable.getIndex(id).flatMap(postbox.messageHistoryTable.getMessage).flatMap { message in
postbox.messageHistoryTable.renderMessage(message, peerTable: postbox.peerTable)
}
self.additionalDatas[i] = .message(id, message)
let messages = postbox.getMessageGroup(at: id) ?? []
self.additionalDatas[i] = .message(id, messages)
hasChanges = true
}
}

View File

@ -68,6 +68,21 @@ public final class Transaction {
return self.postbox?.messageHistoryHoleIndexTable.containing(id: id) ?? [:]
}
public func addThreadIndexHole(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>) {
assert(!self.disposed)
self.postbox?.addThreadIndexHole(peerId: peerId, threadId: threadId, namespace: namespace, space: space, range: range)
}
public func removeThreadIndexHole(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>) {
assert(!self.disposed)
self.postbox?.removeThreadIndexHole(peerId: peerId, threadId: threadId, namespace: namespace, space: space, range: range)
}
public func getThreadIndexHoles(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace) -> IndexSet {
assert(!self.disposed)
return self.postbox!.messageHistoryThreadHoleIndexTable.closest(peerId: peerId, threadId: threadId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1))
}
public func doesChatListGroupContainHoles(groupId: PeerGroupId) -> Bool {
assert(!self.disposed)
return self.postbox?.chatListTable.doesGroupContainHoles(groupId: groupId) ?? false
@ -1237,6 +1252,7 @@ public final class Postbox {
let messageHistoryFailedTable: MessageHistoryFailedTable
let messageHistoryTagsTable: MessageHistoryTagsTable
let messageHistoryThreadsTable: MessageHistoryThreadsTable
let messageHistoryThreadHoleIndexTable: MessageHistoryThreadHoleIndexTable
let globalMessageHistoryTagsTable: GlobalMessageHistoryTagsTable
let localMessageHistoryTagsTable: LocalMessageHistoryTagsTable
let peerChatStateTable: PeerChatStateTable
@ -1320,6 +1336,7 @@ public final class Postbox {
self.pendingMessageActionsTable = PendingMessageActionsTable(valueBox: self.valueBox, table: PendingMessageActionsTable.tableSpec(46), metadataTable: self.pendingMessageActionsMetadataTable)
self.messageHistoryTagsTable = MessageHistoryTagsTable(valueBox: self.valueBox, table: MessageHistoryTagsTable.tableSpec(12), seedConfiguration: self.seedConfiguration, summaryTable: self.messageHistoryTagsSummaryTable)
self.messageHistoryThreadsTable = MessageHistoryThreadsTable(valueBox: self.valueBox, table: MessageHistoryThreadsTable.tableSpec(62))
self.messageHistoryThreadHoleIndexTable = MessageHistoryThreadHoleIndexTable(valueBox: self.valueBox, table: MessageHistoryThreadHoleIndexTable.tableSpec(63), metadataTable: self.messageHistoryMetadataTable, seedConfiguration: self.seedConfiguration)
self.globalMessageHistoryTagsTable = GlobalMessageHistoryTagsTable(valueBox: self.valueBox, table: GlobalMessageHistoryTagsTable.tableSpec(39))
self.localMessageHistoryTagsTable = LocalMessageHistoryTagsTable(valueBox: self.valueBox, table: GlobalMessageHistoryTagsTable.tableSpec(52))
self.messageHistoryIndexTable = MessageHistoryIndexTable(valueBox: self.valueBox, table: MessageHistoryIndexTable.tableSpec(4), messageHistoryHoleIndexTable: self.messageHistoryHoleIndexTable, globalMessageIdsTable: self.globalMessageIdsTable, metadataTable: self.messageHistoryMetadataTable, seedConfiguration: self.seedConfiguration)
@ -1374,6 +1391,7 @@ public final class Postbox {
tables.append(self.messageHistoryFailedTable)
tables.append(self.messageHistoryTagsTable)
tables.append(self.messageHistoryThreadsTable)
tables.append(self.messageHistoryThreadHoleIndexTable)
tables.append(self.globalMessageHistoryTagsTable)
tables.append(self.localMessageHistoryTagsTable)
tables.append(self.messageHistoryIndexTable)
@ -1623,6 +1641,14 @@ public final class Postbox {
self.messageHistoryHoleIndexTable.remove(peerId: peerId, namespace: namespace, space: space, range: range, operations: &self.currentPeerHoleOperations)
}
fileprivate func addThreadIndexHole(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>) {
self.messageHistoryThreadHoleIndexTable.add(peerId: peerId, threadId: threadId, namespace: namespace, space: space, range: range, operations: &self.currentPeerHoleOperations)
}
fileprivate func removeThreadIndexHole(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>) {
self.messageHistoryThreadHoleIndexTable.remove(peerId: peerId, threadId: threadId, namespace: namespace, space: space, range: range, operations: &self.currentPeerHoleOperations)
}
fileprivate func recalculateChatListGroupStats(groupId: PeerGroupId) {
let summary = self.chatListIndexTable.reindexPeerGroupUnreadCounts(postbox: self, groupId: groupId)
self.groupMessageStatsTable.set(groupId: groupId, summary: summary)
@ -2357,12 +2383,14 @@ public final class Postbox {
case let .peer(peerId):
return .single((.peer(peerId), false))
case let .external(_, input):
var isHoleFill = false
return input
|> map { value -> (ResolvedChatLocationInput, Bool) in
let wasHoleFill = isHoleFill
isHoleFill = true
return (.external(value), wasHoleFill)
return Signal { subscriber in
var isHoleFill = false
return (input
|> map { value -> (ResolvedChatLocationInput, Bool) in
let wasHoleFill = isHoleFill
isHoleFill = true
return (.external(value), wasHoleFill)
}).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
}
}
}
@ -2539,8 +2567,8 @@ public final class Postbox {
}
additionalDataEntries.append(.cachedPeerDataMessages(peerId, messages))
case let .message(id):
let message = self.getMessage(id)
additionalDataEntries.append(.message(id, message))
let messages = self.getMessageGroup(at: id)
additionalDataEntries.append(.message(id, messages ?? []))
case let .peerChatState(peerId):
additionalDataEntries.append(.peerChatState(peerId, self.peerChatStateTable.get(peerId) as? PeerChatState))
case .totalUnreadState:
@ -3307,7 +3335,7 @@ public final class Postbox {
return nil
}
fileprivate func getMessageGroup(at id: MessageId) -> [Message]? {
func getMessageGroup(at id: MessageId) -> [Message]? {
guard let index = self.messageHistoryIndexTable.getIndex(id) else {
return nil
}

View File

@ -60,6 +60,7 @@ public final class SeedConfiguration {
public let initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?)
public let messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
public let upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
public let messageThreadHoles: [PeerId.Namespace: [MessageId.Namespace]]
public let messageTagsWithSummary: MessageTags
public let existingGlobalMessageTags: GlobalMessageTags
public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace]
@ -71,11 +72,12 @@ public final class SeedConfiguration {
public let globalNotificationSettingsPreferencesKey: ValueBoxKey
public let defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings
public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set<MessageId.Namespace>, globalNotificationSettingsPreferencesKey: ValueBoxKey, defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings) {
public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], messageThreadHoles: [PeerId.Namespace: [MessageId.Namespace]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set<MessageId.Namespace>, globalNotificationSettingsPreferencesKey: ValueBoxKey, defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings) {
self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces
self.initializeChatListWithHole = initializeChatListWithHole
self.messageHoles = messageHoles
self.upgradedMessageHoles = upgradedMessageHoles
self.messageThreadHoles = messageThreadHoles
self.messageTagsWithSummary = messageTagsWithSummary
self.existingGlobalMessageTags = existingGlobalMessageTags
self.peerNamespacesRequiringMessageTextIndex = peerNamespacesRequiringMessageTextIndex

View File

@ -39,6 +39,7 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara
}
public func setData(_ offset: Int, value: Data) {
assert(offset >= 0 && offset + value.count <= self.length)
let valueLength = value.count
value.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
memcpy(self.memory + offset, bytes, valueLength)
@ -46,66 +47,78 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara
}
public func setInt32(_ offset: Int, value: Int32) {
assert(offset >= 0 && offset + 4 <= self.length)
var bigEndianValue = Int32(bigEndian: value)
memcpy(self.memory + offset, &bigEndianValue, 4)
}
public func setUInt32(_ offset: Int, value: UInt32) {
assert(offset >= 0 && offset + 4 <= self.length)
var bigEndianValue = UInt32(bigEndian: value)
memcpy(self.memory + offset, &bigEndianValue, 4)
}
public func setInt64(_ offset: Int, value: Int64) {
assert(offset >= 0 && offset + 8 <= self.length)
var bigEndianValue = Int64(bigEndian: value)
memcpy(self.memory + offset, &bigEndianValue, 8)
}
public func setInt8(_ offset: Int, value: Int8) {
assert(offset >= 0 && offset + 1 <= self.length)
var varValue = value
memcpy(self.memory + offset, &varValue, 1)
}
public func setUInt8(_ offset: Int, value: UInt8) {
assert(offset >= 0 && offset + 1 <= self.length)
var varValue = value
memcpy(self.memory + offset, &varValue, 1)
}
public func setUInt16(_ offset: Int, value: UInt16) {
assert(offset >= 0 && offset + 2 <= self.length)
var varValue = value
memcpy(self.memory + offset, &varValue, 2)
}
public func getInt32(_ offset: Int) -> Int32 {
assert(offset >= 0 && offset + 4 <= self.length)
var value: Int32 = 0
memcpy(&value, self.memory + offset, 4)
return Int32(bigEndian: value)
}
public func getUInt32(_ offset: Int) -> UInt32 {
assert(offset >= 0 && offset + 4 <= self.length)
var value: UInt32 = 0
memcpy(&value, self.memory + offset, 4)
return UInt32(bigEndian: value)
}
public func getInt64(_ offset: Int) -> Int64 {
assert(offset >= 0 && offset + 8 <= self.length)
var value: Int64 = 0
memcpy(&value, self.memory + offset, 8)
return Int64(bigEndian: value)
}
public func getInt8(_ offset: Int) -> Int8 {
assert(offset >= 0 && offset + 1 <= self.length)
var value: Int8 = 0
memcpy(&value, self.memory + offset, 1)
return value
}
public func getUInt8(_ offset: Int) -> UInt8 {
assert(offset >= 0 && offset + 1 <= self.length)
var value: UInt8 = 0
memcpy(&value, self.memory + offset, 1)
return value
}
public func getUInt16(_ offset: Int) -> UInt16 {
assert(offset >= 0 && offset + 2 <= self.length)
var value: UInt16 = 0
memcpy(&value, self.memory + offset, 2)
return value
@ -206,6 +219,7 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara
}
public func substringValue(_ range: Range<Int>) -> String? {
assert(range.lowerBound >= 0 && range.upperBound <= self.length)
return String(data: Data(bytes: self.memory.advanced(by: range.lowerBound), count: range.count), encoding: .utf8)
}

View File

@ -5,21 +5,27 @@ public class ReplyThreadMessageAttribute: MessageAttribute {
public let count: Int32
public let latestUsers: [PeerId]
public let commentsPeerId: PeerId?
public let maxMessageId: MessageId.Id?
public let maxReadMessageId: MessageId.Id?
public var associatedPeerIds: [PeerId] {
return self.latestUsers
}
public init(count: Int32, latestUsers: [PeerId], commentsPeerId: PeerId?) {
public init(count: Int32, latestUsers: [PeerId], commentsPeerId: PeerId?, maxMessageId: MessageId.Id?, maxReadMessageId: MessageId.Id?) {
self.count = count
self.latestUsers = latestUsers
self.commentsPeerId = commentsPeerId
self.maxMessageId = maxMessageId
self.maxReadMessageId = maxReadMessageId
}
required public init(decoder: PostboxDecoder) {
self.count = decoder.decodeInt32ForKey("c", orElse: 0)
self.latestUsers = decoder.decodeInt64ArrayForKey("u").map(PeerId.init)
self.commentsPeerId = decoder.decodeOptionalInt64ForKey("cp").flatMap(PeerId.init)
self.maxMessageId = decoder.decodeOptionalInt32ForKey("mm")
self.maxReadMessageId = decoder.decodeOptionalInt32ForKey("mrm")
}
public func encode(_ encoder: PostboxEncoder) {
@ -30,5 +36,15 @@ public class ReplyThreadMessageAttribute: MessageAttribute {
} else {
encoder.encodeNil(forKey: "cp")
}
if let maxMessageId = self.maxMessageId {
encoder.encodeInt32(maxMessageId, forKey: "mm")
} else {
encoder.encodeNil(forKey: "mm")
}
if let maxReadMessageId = self.maxReadMessageId {
encoder.encodeInt32(maxReadMessageId, forKey: "mrm")
} else {
encoder.encodeNil(forKey: "mrm")
}
}
}

View File

@ -13,6 +13,13 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
]
}
var messageThreadHoles: [PeerId.Namespace: [MessageId.Namespace]] = [:]
for peerNamespace in peerIdNamespacesWithInitialCloudMessageHoles {
messageThreadHoles[peerNamespace] = [
Namespaces.Message.Cloud
]
}
// To avoid upgrading the database, **new** tags can be added here
// Uninitialized peers will fill the info using messageHoles
var upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]] = [:]
@ -27,7 +34,7 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
globalMessageIdsPeerIdNamespaces.insert(GlobalMessageIdsNamespace(peerIdNamespace: peerIdNamespace, messageIdNamespace: Namespaces.Message.Cloud))
}
return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, upgradedMessageHoles: upgradedMessageHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in
return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, upgradedMessageHoles: upgradedMessageHoles, messageThreadHoles: messageThreadHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in
if let peer = peer as? TelegramUser {
if peer.botInfo != nil {
return .bot

View File

@ -238,7 +238,22 @@ private final class FeaturedStickerPacksContext {
self.disposable.dispose()
}
}
private struct ViewCountContextState {
var timestamp: Int32
var clientId: Int32
func isStillValidFor(_ other: ViewCountContextState) -> Bool {
if other.timestamp > self.timestamp + 30 {
return false
}
if other.clientId > self.clientId {
return false
}
return true
}
}
public final class AccountViewTracker {
weak var account: Account?
private let queue = Queue()
@ -255,7 +270,7 @@ public final class AccountViewTracker {
private var visibleCallListHoleIds: [MessageIndex: Int] = [:]
private var visibleCallListHoleDisposables: [MessageIndex: Disposable] = [:]
private var updatedViewCountMessageIdsAndTimestamps: [MessageId: Int32] = [:]
private var updatedViewCountMessageIdsAndTimestamps: [MessageId: ViewCountContextState] = [:]
private var nextUpdatedViewCountDisposableId: Int32 = 0
private var updatedViewCountDisposables = DisposableDict<Int32>()
@ -576,14 +591,14 @@ public final class AccountViewTracker {
}
}
public func updateViewCountForMessageIds(messageIds: Set<MessageId>) {
public func updateViewCountForMessageIds(messageIds: Set<MessageId>, clientId: Int32) {
self.queue.async {
var addedMessageIds: [MessageId] = []
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
let updatedState = ViewCountContextState(timestamp: Int32(CFAbsoluteTimeGetCurrent()), clientId: clientId)
for messageId in messageIds {
let messageTimestamp = self.updatedViewCountMessageIdsAndTimestamps[messageId]
if messageTimestamp == nil || messageTimestamp! < timestamp - 5 * 60 {
self.updatedViewCountMessageIdsAndTimestamps[messageId] = timestamp
if messageTimestamp == nil || !messageTimestamp!.isStillValidFor(updatedState) {
self.updatedViewCountMessageIdsAndTimestamps[messageId] = updatedState
addedMessageIds.append(messageId)
}
}
@ -601,11 +616,28 @@ public final class AccountViewTracker {
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard case let .messageViews(viewCounts, _)? = result else {
guard case let .messageViews(viewCounts, users)? = result else {
return .complete()
}
return account.postbox.transaction { transaction -> Void in
var peers: [Peer] = []
var peerPresences: [PeerId: PeerPresence] = [:]
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
if let presence = TelegramUserPresence(apiUser: user) {
peerPresences[telegramUser.id] = presence
}
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
for i in 0 ..< messageIds.count {
if i < viewCounts.count {
if case let .messageViews(_, views, forwards, replies) = viewCounts[i] {
@ -616,9 +648,11 @@ public final class AccountViewTracker {
var commentsChannelId: PeerId?
var recentRepliersPeerIds: [PeerId]?
var repliesCount: Int32?
var repliesMaxId: Int32?
var repliesReadMaxId: Int32?
if let replies = replies {
switch replies {
case let .messageReplies(_, repliesCountValue, _, recentRepliers, channelId, _, _):
case let .messageReplies(_, repliesCountValue, _, recentRepliers, channelId, maxId, readMaxId):
if let channelId = channelId {
commentsChannelId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)
}
@ -628,6 +662,8 @@ public final class AccountViewTracker {
} else {
recentRepliersPeerIds = nil
}
repliesMaxId = maxId
repliesReadMaxId = readMaxId
}
}
loop: for j in 0 ..< attributes.count {
@ -642,12 +678,12 @@ public final class AccountViewTracker {
} else if let _ = attributes[j] as? ReplyThreadMessageAttribute {
foundReplies = true
if let repliesCount = repliesCount {
attributes[j] = ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsChannelId)
attributes[j] = ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsChannelId, maxMessageId: repliesMaxId, maxReadMessageId: repliesReadMaxId)
}
}
}
if !foundReplies, let repliesCount = repliesCount {
attributes.append(ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsChannelId))
attributes.append(ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsChannelId, maxMessageId: repliesMaxId, maxReadMessageId: repliesReadMaxId))
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})

View File

@ -233,7 +233,9 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
if message.id.namespace == Namespaces.Message.Local && updatedId.namespace == Namespaces.Message.Cloud && updatedId.peerId.namespace == Namespaces.Peer.CloudChannel {
if let threadId = updatedMessage.threadId {
let messageThreadId = makeThreadIdMessageId(peerId: updatedMessage.id.peerId, threadId: threadId)
updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, difference: 1, addedMessagePeers: [accountPeerId])
if let authorId = updatedMessage.authorId {
updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, difference: 1, addedMessagePeers: [authorId])
}
}
}
}

View File

@ -502,7 +502,9 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH
}
}
if threadId == nil {
if let threadId = threadId {
transaction.removeThreadIndexHole(peerId: peerId, threadId: makeMessageThreadId(threadId), namespace: namespace, space: .everywhere, range: filledRange)
} else {
transaction.removeHole(peerId: peerId, namespace: namespace, space: space, range: filledRange)
}

View File

@ -18,24 +18,54 @@ private class ReplyThreadHistoryContextImpl {
}
let state = Promise<State>()
private var stateValue: State {
private var stateValue: State? {
didSet {
if self.stateValue != oldValue {
self.state.set(.single(self.stateValue))
if let stateValue = self.stateValue {
if stateValue != oldValue {
self.state.set(.single(stateValue))
}
}
}
}
private var initialStateDisposable: Disposable?
private var holesDisposable: Disposable?
private let readDisposable = MetaDisposable()
init(queue: Queue, account: Account, messageId: MessageId, maxReadMessageId: MessageId?) {
init(queue: Queue, account: Account, messageId: MessageId, maxMessage: ChatReplyThreadMessage.MaxMessage, maxReadMessageId: MessageId?) {
self.queue = queue
self.account = account
self.messageId = messageId
self.stateValue = State(messageId: self.messageId, holeIndices: [Namespaces.Message.Cloud: IndexSet(integersIn: 1 ..< Int(Int32.max))], maxReadMessageId: maxReadMessageId)
self.state.set(.single(self.stateValue))
self.initialStateDisposable = (account.postbox.transaction { transaction -> State in
var indices = transaction.getThreadIndexHoles(peerId: messageId.peerId, threadId: makeMessageThreadId(messageId), namespace: Namespaces.Message.Cloud)
switch maxMessage {
case .unknown:
indices.insert(integersIn: 1 ..< Int(Int32.max - 1))
case let .known(maxMessageId):
indices.insert(integersIn: 1 ..< Int(Int32.max - 1))
/*if let maxMessageId = maxMessageId {
let topMessage = transaction.getMessagesWithThreadId(peerId: messageId.peerId, namespace: Namespaces.Message.Cloud, threadId: makeMessageThreadId(messageId), from: MessageIndex.upperBound(peerId: messageId.peerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.lowerBound(peerId: messageId.peerId, namespace: Namespaces.Message.Cloud), limit: 1).first
if let topMessage = topMessage {
if maxMessageId.id < maxMessageId.id {
indices.insert(integersIn: Int(topMessage.id.id + 1) ..< Int(Int32.max - 1))
}
} else {
indices.insert(integersIn: 1 ..< Int(Int32.max - 1))
}
} else {
indices = IndexSet()
}*/
}
return State(messageId: messageId, holeIndices: [Namespaces.Message.Cloud: indices], maxReadMessageId: maxReadMessageId)
}
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.stateValue = state
strongSelf.state.set(.single(state))
})
let threadId = makeMessageThreadId(messageId)
@ -61,6 +91,7 @@ private class ReplyThreadHistoryContextImpl {
}
deinit {
self.initialStateDisposable?.dispose()
self.holesDisposable?.dispose()
self.readDisposable.dispose()
}
@ -73,9 +104,9 @@ private class ReplyThreadHistoryContextImpl {
guard let strongSelf = self else {
return
}
if var currentHoles = strongSelf.stateValue.holeIndices[Namespaces.Message.Cloud] {
if var currentHoles = strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] {
currentHoles.subtract(removedHoleIndices)
strongSelf.stateValue.holeIndices[Namespaces.Message.Cloud] = currentHoles
strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] = currentHoles
}
}))
} else {
@ -101,6 +132,40 @@ private class ReplyThreadHistoryContextImpl {
}
let signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in
if let message = transaction.getMessage(messageId) {
for attribute in message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let sourceMessage = transaction.getMessage(attribute.messageId) {
var updatedAttribute: ReplyThreadMessageAttribute?
for i in 0 ..< sourceMessage.attributes.count {
if let attribute = sourceMessage.attributes[i] as? ReplyThreadMessageAttribute {
if let maxReadMessageId = attribute.maxReadMessageId {
if maxReadMessageId < messageIndex.id.id {
updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id)
}
} else {
updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id)
}
break
}
}
if let updatedAttribute = updatedAttribute {
transaction.updateMessage(sourceMessage.id, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let _ = attributes[j] as? ReplyThreadMessageAttribute {
attributes[j] = updatedAttribute
}
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
break
}
}
}
return transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
@ -153,10 +218,10 @@ public class ReplyThreadHistoryContext {
}
}
public init(account: Account, peerId: PeerId, threadMessageId: MessageId, maxReadMessageId: MessageId?) {
public init(account: Account, peerId: PeerId, threadMessageId: MessageId, maxMessage: ChatReplyThreadMessage.MaxMessage, maxReadMessageId: MessageId?) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return ReplyThreadHistoryContextImpl(queue: queue, account: account, messageId: threadMessageId, maxReadMessageId: maxReadMessageId)
return ReplyThreadHistoryContextImpl(queue: queue, account: account, messageId: threadMessageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
})
}
@ -168,11 +233,18 @@ public class ReplyThreadHistoryContext {
}
public struct ChatReplyThreadMessage {
public enum MaxMessage: Equatable {
case unknown
case known(MessageId?)
}
public var messageId: MessageId
public var maxMessage: MaxMessage
public var maxReadMessageId: MessageId?
public init(messageId: MessageId, maxReadMessageId: MessageId?) {
public init(messageId: MessageId, maxMessage: MaxMessage, maxReadMessageId: MessageId?) {
self.messageId = messageId
self.maxMessage = maxMessage
self.maxReadMessageId = maxReadMessageId
}
}
@ -185,17 +257,46 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
guard let inputPeer = inputPeer else {
return .single(nil)
}
return account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: messageId.id))
let discussionMessage: Signal<Api.messages.DiscussionMessage?, NoError> = account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.DiscussionMessage?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<ChatReplyThreadMessage?, NoError> in
let maxMessage: Signal<Int32?, NoError> = account.network.request(Api.functions.messages.getMessagesViews(peer: inputPeer, id: [messageId.id], increment: .boolFalse))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.MessageViews?, NoError> in
return .single(nil)
}
|> map { result -> Int32? in
guard let result = result else {
return nil
}
var maxId: Int32?
switch result {
case let .messageViews(views, _):
for view in views {
switch view {
case let .messageViews(_, _, _, replies):
if let replies = replies {
switch replies {
case let .messageReplies(_, _, _, _, _, maxIdValue, readMaxIdValue):
maxId = maxIdValue
}
}
}
}
}
return maxId
}
return combineLatest(discussionMessage, maxMessage)
|> mapToSignal { discussionMessage, maxMessage -> Signal<ChatReplyThreadMessage?, NoError> in
guard let discussionMessage = discussionMessage else {
return .single(nil)
}
return account.postbox.transaction { transaction -> ChatReplyThreadMessage? in
switch result {
switch discussionMessage {
case let .discussionMessage(message, readMaxId, chats, users):
guard let parsedMessage = StoreMessage(apiMessage: message), let parsedIndex = parsedMessage.index else {
return nil
@ -225,8 +326,20 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
let resolvedMaxMessage: ChatReplyThreadMessage.MaxMessage
if let maxMessage = maxMessage {
resolvedMaxMessage = .known(MessageId(
peerId: parsedIndex.id.peerId,
namespace: Namespaces.Message.Cloud,
id: maxMessage
))
} else {
resolvedMaxMessage = .known(nil)
}
return ChatReplyThreadMessage(
messageId: parsedIndex.id,
maxMessage: resolvedMaxMessage,
maxReadMessageId: MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
)
}

View File

@ -210,7 +210,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
switch message {
case let .message(flags, _, fromId, chatPeerId, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, chatPeerId, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _):
if let replyTo = replyTo {
let peerId: PeerId = chatPeerId.peerId
@ -221,7 +221,7 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
}
case .messageEmpty:
break
case let .messageService(flags, _, fromId, chatPeerId, replyHeader, _, _):
case let .messageService(_, _, _, chatPeerId, replyHeader, _, _):
if let replyHeader = replyHeader {
switch replyHeader {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, _):
@ -375,6 +375,27 @@ extension StoreMessage {
var attributes: [MessageAttribute] = []
var threadId: Int64?
if let replyTo = replyTo {
var threadMessageId: MessageId?
switch replyTo {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyToTopId):
let replyPeerId = replyToPeerId?.peerId ?? peerId
if let replyToTopId = replyToTopId {
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId)
threadMessageId = threadIdValue
if replyPeerId == peerId {
threadId = makeMessageThreadId(threadIdValue)
}
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId)
threadMessageId = threadIdValue
threadId = makeMessageThreadId(threadIdValue)
}
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId))
}
}
var forwardInfo: StoreMessageForwardInfo?
if let fwdFrom = fwdFrom {
switch fwdFrom {
@ -463,25 +484,6 @@ extension StoreMessage {
attributes.append(InlineBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: viaBotId), title: nil))
}
var threadId: Int64?
if let replyTo = replyTo {
var threadMessageId: MessageId?
switch replyTo {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyToTopId):
let replyPeerId = replyToPeerId?.peerId ?? peerId
if let replyToTopId = replyToTopId {
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId)
threadMessageId = threadIdValue
threadId = makeMessageThreadId(threadIdValue)
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId)
threadMessageId = threadIdValue
threadId = makeMessageThreadId(threadIdValue)
}
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId))
}
}
if namespace != Namespaces.Message.ScheduledCloud {
if let views = views {
attributes.append(ViewCountMessageAttribute(count: Int(views)))
@ -531,7 +533,7 @@ extension StoreMessage {
if let replies = replies {
let recentRepliersPeerIds: [PeerId]?
switch replies {
case let .messageReplies(_, repliesCount, _, recentRepliers, channelId, _, _):
case let .messageReplies(_, repliesCount, _, recentRepliers, channelId, maxId, readMaxId):
if let recentRepliers = recentRepliers {
recentRepliersPeerIds = recentRepliers.map { $0.peerId }
} else {
@ -540,7 +542,7 @@ extension StoreMessage {
let commentsPeerId = channelId.flatMap { PeerId(namespace: Namespaces.Peer.CloudChannel, id: $0) }
attributes.append(ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsPeerId))
attributes.append(ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsPeerId, maxMessageId: maxId, maxReadMessageId: readMaxId))
}
}
@ -589,7 +591,7 @@ extension StoreMessage {
return nil
case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action):
let peerId: PeerId = chatPeerId.peerId
var authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
var attributes: [MessageAttribute] = []
@ -602,11 +604,15 @@ extension StoreMessage {
if let replyToTopId = replyToTopId {
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId)
threadMessageId = threadIdValue
threadId = makeMessageThreadId(threadIdValue)
if replyPeerId == peerId {
threadId = makeMessageThreadId(threadIdValue)
}
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId)
threadMessageId = threadIdValue
threadId = makeMessageThreadId(threadIdValue)
if replyPeerId == peerId {
threadId = makeMessageThreadId(threadIdValue)
}
}
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId))
}

View File

@ -77,7 +77,7 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
let count = max(0, attribute.count + countDifference)
attributes[j] = ReplyThreadMessageAttribute(count: count, latestUsers: mergeLatestUsers(current: attribute.latestUsers, added: addedMessagePeers, isGroup: isGroup, isEmpty: count == 0), commentsPeerId: attribute.commentsPeerId)
attributes[j] = ReplyThreadMessageAttribute(count: count, latestUsers: mergeLatestUsers(current: attribute.latestUsers, added: addedMessagePeers, isGroup: isGroup, isEmpty: count == 0), commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: attribute.maxReadMessageId)
} else if let attribute = attributes[j] as? SourceReferenceMessageAttribute {
channelThreadMessageId = attribute.messageId
}

View File

@ -12,7 +12,7 @@ import AvatarNode
import AccountContext
private let savedMessagesAvatar: UIImage = {
return generateImage(CGSize(width: 60.0, height: 60.0)) { size, context in
return generateImage(CGSize(width: 60.0, height: 60.0), contextGenerator: { size, context in
var locations: [CGFloat] = [1.0, 0.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
@ -28,7 +28,7 @@ private let savedMessagesAvatar: UIImage = {
if let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white) {
context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - savedMessagesIcon.size.width) / 2.0), y: floor((size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size))
}
}!
})!
}()
public enum SendMessageIntentContext {

View File

@ -262,5 +262,6 @@ public enum PresentationResourceParameterKey: Hashable {
case chatMessageCommentsIcon(incoming: Bool)
case chatMessageCommentsArrowIcon(incoming: Bool)
case chatMessageCommentsUnreadDotIcon(incoming: Bool)
case chatMessageRepliesIcon(incoming: Bool)
}

View File

@ -1115,6 +1115,14 @@ public struct PresentationResourcesChat {
})
}
public static func chatMessageCommentsUnreadDotIcon(_ theme: PresentationTheme, incoming: Bool) -> UIImage? {
return theme.image(PresentationResourceParameterKey.chatMessageCommentsUnreadDotIcon(incoming: incoming), { theme in
let messageTheme = incoming ? theme.chat.message.incoming : theme.chat.message.outgoing
return generateFilledCircleImage(diameter: 6.0, color: messageTheme.accentTextColor)
})
}
public static func chatFreeCommentButtonBackground(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? {
return theme.image(PresentationResourceKey.chatFreeCommentButtonBackground.rawValue, { _ in
let strokeColor = bubbleVariableColor(variableColor: theme.chat.message.shareButtonStrokeColor, wallpaper: wallpaper)

View File

@ -303,8 +303,8 @@ public final class AccountContextImpl: AccountContext {
switch location {
case let .peer(peerId):
return .peer(peerId)
case let .replyThread(messageId, _, maxReadMessageId):
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxReadMessageId: maxReadMessageId)
case let .replyThread(messageId, _, maxMessage, maxReadMessageId):
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
return .external(messageId.peerId, context.state)
}
}
@ -313,19 +313,19 @@ public final class AccountContextImpl: AccountContext {
switch location {
case .peer:
let _ = applyMaxReadIndexInteractively(postbox: self.account.postbox, stateManager: self.account.stateManager, index: messageIndex).start()
case let .replyThread(messageId, _, maxReadMessageId):
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxReadMessageId: maxReadMessageId)
case let .replyThread(messageId, _, maxMessage, maxReadMessageId):
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
context.applyMaxReadIndex(messageIndex: messageIndex)
}
}
}
private func chatLocationContext(holder: Atomic<ChatLocationContextHolder?>, account: Account, messageId: MessageId, maxReadMessageId: MessageId?) -> ReplyThreadHistoryContext {
private func chatLocationContext(holder: Atomic<ChatLocationContextHolder?>, account: Account, messageId: MessageId, maxMessage: ChatReplyThreadMessage.MaxMessage, maxReadMessageId: MessageId?) -> ReplyThreadHistoryContext {
let holder = holder.modify { current in
if let current = current as? ChatLocationContextHolderImpl {
return current
} else {
return ChatLocationContextHolderImpl(account: account, messageId: messageId, maxReadMessageId: maxReadMessageId)
return ChatLocationContextHolderImpl(account: account, messageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
}
} as! ChatLocationContextHolderImpl
return holder.context
@ -334,8 +334,8 @@ private func chatLocationContext(holder: Atomic<ChatLocationContextHolder?>, acc
private final class ChatLocationContextHolderImpl: ChatLocationContextHolder {
let context: ReplyThreadHistoryContext
init(account: Account, messageId: MessageId, maxReadMessageId: MessageId?) {
self.context = ReplyThreadHistoryContext(account: account, peerId: messageId.peerId, threadMessageId: messageId, maxReadMessageId: maxReadMessageId)
init(account: Account, messageId: MessageId, maxMessage: ChatReplyThreadMessage.MaxMessage, maxReadMessageId: MessageId?) {
self.context = ReplyThreadHistoryContext(account: account, peerId: messageId.peerId, threadMessageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
}
}

View File

@ -69,7 +69,7 @@ extension ChatLocation {
switch self {
case let .peer(peerId):
return peerId
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
return messageId.peerId
}
}
@ -345,7 +345,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var hasEmbeddedTitleContent = false
private var isEmbeddedTitleContentHidden = false
private let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
public override var customData: Any? {
return self.chatLocation
@ -353,13 +353,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var purposefulAction: (() -> Void)?
public init(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) {
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) {
let _ = ChatControllerCount.modify { value in
return value + 1
}
self.context = context
self.chatLocation = chatLocation
self.chatLocationContextHolder = chatLocationContextHolder
self.subject = subject
self.botStart = botStart
self.peekData = peekData
@ -370,7 +371,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .peer(peerId):
locationBroadcastPanelSource = .peer(peerId)
self.chatLocationInfoData = .peer(Promise())
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
locationBroadcastPanelSource = .none
let promise = Promise<Message?>()
let key = PostboxViewKey.messages([messageId])
@ -1628,7 +1629,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch strongSelf.chatLocation {
case let .peer(peerId):
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil)
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
let peerId = messageId.peerId
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil)
}
@ -2174,13 +2175,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: items, reactionItems: [], gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
})
}, openMessageReplies: { [weak self] messageId in
}, openMessageReplies: { [weak self] messageId, isChannelPost in
guard let strongSelf = self else {
return
}
let foundIndex = Promise<ChatReplyThreadMessage?>()
foundIndex.set(fetchChannelReplyThreadMessage(account: strongSelf.context.account, messageId: messageId))
let foundIndex = Promise<ReplyThreadInfo?>()
foundIndex.set(fetchAndPreloadReplyThreadInfo(context: strongSelf.context, subject: isChannelPost ? .channelPost(messageId) : .groupMessage(messageId)))
var cancelImpl: (() -> Void)?
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: {
@ -2199,7 +2200,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let result = result {
if let navigationController = strongSelf.navigationController as? NavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(threadMessageId: result.messageId, isChannelPost: true, maxReadMessageId: result.maxReadMessageId), activateInput: true, keepStack: .always))
let chatLocation: ChatLocation = .replyThread(threadMessageId: result.message.messageId, isChannelPost: result.isChannelPost, maxMessage: result.message.maxMessage, maxReadMessageId: result.message.maxReadMessageId)
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, activateInput: result.isEmpty, keepStack: .always))
}
}
})
@ -2405,22 +2408,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
let isReplyThread: Bool
let replyThreadType: ChatTitleContent.ReplyThreadType?
switch chatLocation {
case let .peer(peerId):
//TODO:localize
isReplyThread = peerId.isReplies
replyThreadType = nil
case let .replyThread(_, _, readMessageId):
isReplyThread = true
if readMessageId != nil {
replyThreadType = .comments
} else {
replyThreadType = .replies
}
}
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get())
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice in
if let strongSelf = self {
@ -2654,9 +2641,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch chatLocation {
case .peer:
replyThreadType = .replies
case let .replyThread(_, _, readMessageId):
case let .replyThread(_, isChannelPost, _, _):
isReplyThread = true
if readMessageId != nil {
if isChannelPost {
replyThreadType = .comments
} else {
replyThreadType = .replies
@ -2672,7 +2659,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
)
|> deliverOnMainQueue).start(next: { [weak self] peerView, message, onlineMemberCount in
if let strongSelf = self {
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, text: message?.text ?? "")
var count = 0
if let message = message {
for attribute in message.attributes {
if let attribute = attribute as? ReplyThreadMessageAttribute {
count = Int(attribute.count)
break
}
}
}
let text: String
if count == 0 {
text = strongSelf.presentationData.strings.Conversation_TitleNoComments
} else {
text = strongSelf.presentationData.strings.Conversation_TitleComments(Int32(count))
}
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, text: text)
let firstTime = strongSelf.peerView == nil
strongSelf.peerView = peerView
@ -3363,7 +3367,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
|> distinctUntilChanged
self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, hasPendingMessages).start(next: { [weak self] cachedDataAndMessages, hasPendingMessages in
let isTopReplyThreadMessageShown: Signal<Bool, NoError> = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get()
|> distinctUntilChanged
self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown).start(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown in
if let strongSelf = self {
let (cachedData, messages) = cachedDataAndMessages
@ -3391,8 +3398,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if let _ = cachedData as? CachedSecretChatData {
}
if case .replyThread = strongSelf.chatLocation {
pinnedMessageId = nil
if case let .replyThread(messageId, _, _, _) = strongSelf.chatLocation {
if isTopReplyThreadMessageShown {
pinnedMessageId = nil
} else {
pinnedMessageId = messageId
}
}
var pinnedMessage: Message?
@ -5032,7 +5043,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let navigationController = strongSelf.effectiveNavigationController {
let subject: ChatControllerSubject? = sourceMessageId.flatMap(ChatControllerSubject.message)
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(threadMessageId: replyThreadResult.messageId, isChannelPost: false, maxReadMessageId: replyThreadResult.maxReadMessageId), subject: subject, keepStack: .always))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(threadMessageId: replyThreadResult.messageId, isChannelPost: false, maxMessage: replyThreadResult.maxMessage, maxReadMessageId: replyThreadResult.maxReadMessageId), subject: subject, keepStack: .always))
}
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get()))
@ -7488,7 +7499,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch self.chatLocation {
case .peer:
break
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
defaultReplyMessageId = messageId
}
@ -7535,7 +7546,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch self.chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
peerId = messageId.peerId
}
@ -7957,7 +7968,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch self.chatLocation {
case .peer:
break
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
searchTopMsgId = messageId
}
switch search.domain {

View File

@ -112,7 +112,7 @@ public final class ChatControllerInteraction {
let animateDiceSuccess: () -> Void
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
let openMessageReplies: (MessageId) -> Void
let openMessageReplies: (MessageId, Bool) -> Void
let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
@ -195,7 +195,7 @@ public final class ChatControllerInteraction {
animateDiceSuccess: @escaping () -> Void,
greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?,
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
openMessageReplies: @escaping (MessageId) -> Void,
openMessageReplies: @escaping (MessageId, Bool) -> Void,
requestMessageUpdate: @escaping (MessageId) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
@ -315,7 +315,7 @@ public final class ChatControllerInteraction {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _ in
}, openMessageReplies: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -41,11 +41,8 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
let text: String
switch interfaceState.chatLocation {
case .peer:
case .peer, .replyThread:
text = interfaceState.isScheduledMessages ? interfaceState.strings.ScheduledMessages_EmptyPlaceholder : interfaceState.strings.Conversation_EmptyPlaceholder
case .replyThread:
//TODO:localize
text = "No comments here yet"
}
self.textNode.attributedText = NSAttributedString(string: text, font: messageFont, textColor: serviceColor.primaryText)

View File

@ -114,44 +114,43 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
}
var addedThreadHead = false
if case let .replyThread(messageId, isChannelPost, _) = location, view.earlierId == nil, !view.isLoading {
if case let .replyThread(messageId, isChannelPost, _, _) = location, view.earlierId == nil, !view.isLoading {
loop: for entry in view.additionalData {
switch entry {
case let .message(id, message) where id == messageId:
if let message = message {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
selection = .selectable(selected: selectedMessages.contains(message.id))
} else {
selection = .none
}
case let .message(id, messages) where id == messageId:
if !messages.isEmpty {
let selection: ChatHistoryMessageSelection = .none
let topMessage = messages[0]
var adminRank: CachedChannelAdminRank?
if let author = message.author {
if let author = topMessage.author {
adminRank = adminRanks[author.id]
}
var contentTypeHint: ChatMessageEntryContentType = .generic
if presentationData.largeEmoji, message.media.isEmpty {
if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0] {
if presentationData.largeEmoji, topMessage.media.isEmpty {
if stickersEnabled && topMessage.text.count == 1, let _ = associatedData.animatedEmojiStickers[topMessage.text.basicEmoji.0] {
contentTypeHint = .animatedEmoji
} else if message.text.count < 10 && messageIsElligibleForLargeEmoji(message) {
} else if topMessage.text.count < 10 && messageIsElligibleForLargeEmoji(topMessage) {
contentTypeHint = .largeEmoji
}
}
var replyCount = 0
for attribute in message.attributes {
if let attribute = attribute as? ReplyThreadMessageAttribute {
replyCount = Int(attribute.count)
addedThreadHead = true
if messages.count > 1, let groupInfo = messages[0].groupInfo {
var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)] = []
for message in messages {
groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id])))
}
entries.insert(.MessageGroupEntry(groupInfo, groupMessages, presentationData), at: 0)
} else {
entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id])), at: 0)
}
addedThreadHead = true
entries.insert(.MessageEntry(message, presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id])), at: 0)
if view.entries.count > 0 {
entries.insert(.ReplyCountEntry(message.index, isChannelPost, replyCount, presentationData), at: 1)
}
let replyCount = view.entries.isEmpty ? 0 : 1
entries.insert(.ReplyCountEntry(messages[0].index, isChannelPost, replyCount, presentationData), at: 1)
}
break loop
default:

View File

@ -429,6 +429,8 @@ private struct ChatHistoryAnimatedEmojiConfiguration {
}
}
private var nextClientId: Int32 = 1
public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private let context: AccountContext
private let chatLocation: ChatLocation
@ -543,6 +545,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private var loadedMessagesFromCachedDataDisposable: Disposable?
let isTopReplyThreadMessageShown = ValuePromise<Bool>(false, ignoreRepeated: true)
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags?, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles) {
self.context = context
self.chatLocation = chatLocation
@ -566,8 +570,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
//self.debugInfo = true
let clientId = nextClientId
nextClientId += 1
self.messageProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds)
context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId)
}
self.messageReactionsProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds)
@ -620,7 +627,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if !isAuxiliaryChat {
additionalData.append(.totalUnreadState)
}
if case let .replyThread(messageId, _, _) = chatLocation {
if case let .replyThread(messageId, _, _, _) = chatLocation {
additionalData.append(.cachedPeerData(messageId.peerId))
additionalData.append(.peerNotificationSettings(messageId.peerId))
if messageId.peerId.namespace == Namespaces.Peer.CloudChannel {
@ -1052,6 +1059,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
var isTopReplyThreadMessageShownValue = false
if indexRange.0 <= indexRange.1 {
for i in (indexRange.0 ... indexRange.1) {
switch historyView.filteredEntries[i] {
@ -1103,6 +1112,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if hasUnconsumedMention && !hasUnconsumedContent {
messageIdsWithUnseenPersonalMention.append(message.id)
}
if case .replyThread(message.id, _, _, _) = self.chatLocation {
isTopReplyThreadMessageShownValue = true
}
case let .MessageGroupEntry(_, messages, _):
for (message, _, _, _) in messages {
var hasUnconsumedMention = false
@ -1130,6 +1142,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if hasUnconsumedMention && !hasUnconsumedContent {
messageIdsWithUnseenPersonalMention.append(message.id)
}
if case .replyThread(message.id, _, _, _) = self.chatLocation {
isTopReplyThreadMessageShownValue = true
}
}
default:
break
@ -1227,8 +1242,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if readIndexRange.0 <= readIndexRange.1 {
let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView, indexRange: readIndexRange)
if let maxIncomingIndex = maxIncomingIndex {
self.updateMaxVisibleReadIncomingMessageIndex(maxIncomingIndex)
let messageIndex: MessageIndex?
switch self.chatLocation {
case .peer:
messageIndex = maxIncomingIndex
case .replyThread:
messageIndex = maxOverallIndex
}
if let messageIndex = messageIndex {
self.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
}
if let maxOverallIndex = maxOverallIndex, maxOverallIndex != self.maxVisibleMessageIndexReported {
@ -1236,18 +1259,23 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.maxVisibleMessageIndexUpdated?(maxOverallIndex)
}
}
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
}
if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount), id: self.takeNextHistoryLocationId())
let locationInput: ChatHistoryLocation = .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount)
if self.chatHistoryLocationValue?.content != locationInput {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId())
}
} else if loaded.firstIndex < 5, historyView.originalView.laterId == nil, !historyView.originalView.holeLater, let chatHistoryLocationValue = self.chatHistoryLocationValue, !chatHistoryLocationValue.isAtUpperBound, historyView.originalView.anchorIndex != .upperBound {
//TODO:localize
#if !DEBUG
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .upperBound, anchorIndex: .upperBound, count: historyMessageCount), id: self.takeNextHistoryLocationId())
#endif
} else if loaded.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount), id: self.takeNextHistoryLocationId())
let locationInput: ChatHistoryLocation = .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount)
if self.chatHistoryLocationValue?.content != locationInput {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId())
}
}
}
@ -1518,11 +1546,17 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if let _ = visibleRange.loadedRange {
if let visible = visibleRange.visibleRange {
let visibleFirstIndex = visible.firstIndex
/*if !visible.firstIndexFullyVisible {
visibleFirstIndex += 1
}*/
if visibleFirstIndex <= visible.lastIndex {
let (messageIndex, _) = maxMessageIndexForEntries(transition.historyView, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex))
let (incomingIndex, overallIndex) = maxMessageIndexForEntries(transition.historyView, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex))
let messageIndex: MessageIndex?
switch strongSelf.chatLocation {
case .peer:
messageIndex = incomingIndex
case .replyThread:
messageIndex = overallIndex
}
if let messageIndex = messageIndex {
strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
}

View File

@ -9,7 +9,7 @@ import AccountContext
import ChatInterfaceState
func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal<ChatHistoryViewUpdate, NoError> {
return chatHistoryViewForLocation(location, context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: false, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, additionalData: additionalData, orderStatistics: orderStatistics)
return (chatHistoryViewForLocation(location, context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: false, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, additionalData: additionalData, orderStatistics: orderStatistics)
|> castError(Bool.self)
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
switch update {
@ -23,7 +23,7 @@ func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, c
}
}
return .single(update)
}
})
|> restartIfError
}
@ -267,8 +267,8 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL
}
}
}
case let .message(_, message):
if let message = message {
case let .message(_, messages):
for message in messages {
cachedDataMessages[message.id] = message
}
case let .totalUnreadState(totalUnreadState):
@ -289,3 +289,89 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL
return (cachedData, cachedDataMessages, readStateData)
}
struct ReplyThreadInfo {
var message: ChatReplyThreadMessage
var isChannelPost: Bool
var isEmpty: Bool
var contextHolder: Atomic<ChatLocationContextHolder?>
}
enum ReplyThreadSubject {
case channelPost(MessageId)
case groupMessage(MessageId)
}
func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThreadSubject) -> Signal<ReplyThreadInfo?, NoError> {
let message: Signal<ChatReplyThreadMessage?, NoError>
switch subject {
case let .channelPost(messageId):
message = fetchChannelReplyThreadMessage(account: context.account, messageId: messageId)
case let .groupMessage(messageId):
message = .single(ChatReplyThreadMessage(
messageId: messageId,
maxMessage: .unknown,
maxReadMessageId: nil
))
}
return message
|> mapToSignal { message -> Signal<ReplyThreadInfo?, NoError> in
guard let message = message else {
return .single(nil)
}
let isChannelPost: Bool
switch subject {
case .channelPost:
isChannelPost = true
case .groupMessage:
isChannelPost = false
}
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
let preloadSignal = preloadedChatHistoryViewForLocation(
ChatHistoryLocationInput(
content: .Initial(count: 60),
id: 0
),
context: context,
chatLocation: .replyThread(
threadMessageId: message.messageId,
isChannelPost: isChannelPost,
maxMessage: message.maxMessage,
maxReadMessageId: message.maxReadMessageId
),
chatLocationContextHolder: chatLocationContextHolder,
fixedCombinedReadStates: nil,
tagMask: nil,
additionalData: []
)
return preloadSignal
|> map { historyView -> Bool? in
switch historyView {
case .Loading:
return nil
case let .HistoryView(view, _, _, _, _, _, _):
return view.entries.isEmpty
}
}
|> mapToSignal { value -> Signal<Bool, NoError> in
if let value = value {
return .single(value)
} else {
return .complete()
}
}
|> take(1)
|> map { isEmpty -> ReplyThreadInfo? in
return ReplyThreadInfo(
message: message,
isChannelPost: isChannelPost,
isEmpty: isEmpty,
contextHolder: chatLocationContextHolder
)
}
}
}

View File

@ -465,7 +465,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
}
var isReplyThreadHead = false
if case let .replyThread(messageId, _, _) = chatPresentationInterfaceState.chatLocation {
if case let .replyThread(messageId, _, _, _) = chatPresentationInterfaceState.chatLocation {
isReplyThreadHead = messages[0].id == messageId
}
@ -601,16 +601,11 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
if let threadId = threadId {
let replyThreadId = makeThreadIdMessageId(peerId: messages[0].id.peerId, threadId: threadId)
//TODO:localize
let text: String
if threadMessageCount != 0 {
if threadMessageCount == 1 {
text = "View 1 reply"
} else {
text = "View \(threadMessageCount) replies"
}
text = chatPresentationInterfaceState.strings.Conversation_ContextViewReplies(Int32(threadMessageCount))
} else {
text = "View Thread"
text = chatPresentationInterfaceState.strings.Conversation_ContextViewThread
}
actions.append(.action(ContextMenuActionItem(text: text, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replies"), color: theme.actionSheet.primaryTextColor)
@ -622,7 +617,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
c.dismiss(completion: {
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
if case .group = channel.info {
interfaceInteraction.viewReplies(messages[0].id, ChatReplyThreadMessage(messageId: replyThreadId, maxReadMessageId: nil))
interfaceInteraction.viewReplies(messages[0].id, ChatReplyThreadMessage(messageId: replyThreadId, maxMessage: .unknown, maxReadMessageId: nil))
} else {
var cancelImpl: (() -> Void)?
let statusController = OverlayStatusController(theme: chatPresentationInterfaceState.theme, type: .loading(cancelled: {
@ -751,7 +746,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
var threadMessageId: MessageId?
if case let .replyThread(replyThread, _, _) = chatPresentationInterfaceState.chatLocation {
if case let .replyThread(replyThread, _, _, _) = chatPresentationInterfaceState.chatLocation {
threadMessageId = replyThread
}
let _ = (exportMessageLink(account: context.account, peerId: message.id.peerId, messageId: message.id, threadMessageId: threadMessageId)
@ -875,7 +870,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
})))
}
if data.canSelect {
if !isReplyThreadHead, data.canSelect {
if !actions.isEmpty {
actions.append(.separator)
}

View File

@ -124,7 +124,6 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let backgroundLayout = self.filledBackgroundNode.asyncLayout()
return { item, layoutConstants, _, _, _ in

View File

@ -561,7 +561,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else if incoming {
hasAvatar = true
}
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, isChannelPost, _, _):
if messageId.peerId != item.context.account.peerId {
if messageId.peerId.isGroupOrChannel && item.message.author != nil {
var isBroadcastChannel = false
@ -569,6 +569,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
isBroadcastChannel = true
}
if isChannelPost, messageId == item.message.id {
isBroadcastChannel = true
}
if !isBroadcastChannel {
hasAvatar = true
}
@ -742,7 +746,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
if case let .replyThread(replyThreadMessageId, _, _) = item.chatLocation, replyThreadMessageId == replyAttribute.messageId {
if case let .replyThread(replyThreadMessageId, _, _, _) = item.chatLocation, replyThreadMessageId == replyAttribute.messageId {
} else {
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude))
}
@ -1249,7 +1253,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id)
item.controllerInteraction.openMessageReplies(item.message.id, true)
return
}
}

View File

@ -841,7 +841,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
switch item.chatLocation {
case let .peer(peerId):
chatLocationPeerId = peerId
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
chatLocationPeerId = messageId.peerId
}
@ -885,6 +885,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
allowFullWidth = true
}
if case let .replyThread(messageId, isChannelPost, _, _) = item.chatLocation, isChannelPost, messageId == firstMessage.id {
isBroadcastChannel = true
}
if !isBroadcastChannel {
hasAvatar = item.content.firstMessage.effectivelyIncoming(item.context.account.peerId)
}
@ -1044,7 +1048,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
inlineBotNameString = attribute.title
}
} else if let attribute = attribute as? ReplyMessageAttribute {
if case let .replyThread(replyThreadMessageId, _, _) = item.chatLocation, replyThreadMessageId == attribute.messageId {
if case let .replyThread(replyThreadMessageId, _, _, _) = item.chatLocation, replyThreadMessageId == attribute.messageId {
} else {
replyMessage = firstMessage.associatedMessages[attribute.messageId]
}
@ -2570,8 +2574,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
}
}
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
} else if let id = forwardInfo.source?.id ?? forwardInfo.author?.id {
item.controllerInteraction.openPeer(id, .info, nil)
} else if let peer = forwardInfo.source ?? forwardInfo.author {
item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil)
} else if let _ = forwardInfo.authorSignature {
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil)
}

View File

@ -11,10 +11,12 @@ import TelegramPresentationData
final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
private let separatorNode: ASDisplayNode
private let textNode: TextNode
private let alternativeTextNode: TextNode
private let iconNode: ASImageNode
private let arrowNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
private let avatarsNode: MergedAvatarsNode
private let unreadIconNode: ASImageNode
required init() {
self.separatorNode = ASDisplayNode()
@ -26,11 +28,22 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
self.textNode.contentsScale = UIScreenScale
self.textNode.displaysAsynchronously = true
self.alternativeTextNode = TextNode()
self.alternativeTextNode.isUserInteractionEnabled = false
self.alternativeTextNode.contentMode = .topLeft
self.alternativeTextNode.contentsScale = UIScreenScale
self.alternativeTextNode.displaysAsynchronously = true
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.unreadIconNode = ASImageNode()
self.unreadIconNode.displaysAsynchronously = false
self.unreadIconNode.displayWithoutProcessing = true
self.unreadIconNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
@ -45,7 +58,9 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
self.buttonNode.addSubnode(self.separatorNode)
self.buttonNode.addSubnode(self.textNode)
self.buttonNode.addSubnode(self.alternativeTextNode)
self.buttonNode.addSubnode(self.iconNode)
self.buttonNode.addSubnode(self.unreadIconNode)
self.buttonNode.addSubnode(self.arrowNode)
self.buttonNode.addSubnode(self.avatarsNode)
self.addSubnode(self.buttonNode)
@ -54,8 +69,10 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
if let strongSelf = self {
let nodes: [ASDisplayNode] = [
strongSelf.textNode,
strongSelf.alternativeTextNode,
strongSelf.iconNode,
strongSelf.avatarsNode,
strongSelf.unreadIconNode,
strongSelf.arrowNode,
]
for node in nodes {
@ -89,12 +106,13 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
}
}
} else {
item.controllerInteraction.openMessageReplies(item.message.id)
item.controllerInteraction.openMessageReplies(item.message.id, true)
}
}
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let textLayout = TextNode.asyncLayout(self.textNode)
let alternativeTextLayout = TextNode.asyncLayout(self.alternativeTextNode)
return { item, layoutConstants, preparePosition, _, constrainedSize in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -118,28 +136,33 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
var dateReplies = 0
var replyPeers: [Peer] = []
var hasUnseenReplies = false
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyThreadMessageAttribute {
dateReplies = Int(attribute.count)
replyPeers = attribute.latestUsers.compactMap { peerId -> Peer? in
return item.message.peers[peerId]
}
if let maxMessageId = attribute.maxMessageId, let maxReadMessageId = attribute.maxReadMessageId {
hasUnseenReplies = maxMessageId > maxReadMessageId
} else if attribute.maxMessageId != nil {
hasUnseenReplies = true
}
}
}
//TODO:localize
let rawText: String
let rawAlternativeText: String
if item.message.id.peerId.isReplies {
rawText = "View Reply"
rawText = item.presentationData.strings.Conversation_ViewReply
rawAlternativeText = rawText
} else if dateReplies > 0 {
if dateReplies == 1 {
rawText = "1 Comment"
} else {
rawText = "\(dateReplies) Comments"
}
rawText = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
rawAlternativeText = rawText
} else {
rawText = "Leave a Comment"
rawText = item.presentationData.strings.Conversation_MessageLeaveComment
rawAlternativeText = item.presentationData.strings.Conversation_MessageLeaveCommentShort
}
let imageSize: CGFloat = 30.0
@ -151,20 +174,21 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
} else {
textLeftInset = 15.0 + imageSize * min(1.0, CGFloat(replyPeers.count)) + (imageSpacing) * max(0.0, min(2.0, CGFloat(replyPeers.count - 1)))
}
let textRightInset: CGFloat = 33.0
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset - textLeftInset - 28.0), height: constrainedSize.height)
let attributedText: NSAttributedString
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset - textLeftInset - textRightInset), height: constrainedSize.height)
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let textFont = item.presentationData.messageFont
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.accentTextColor)
let attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.accentTextColor)
let alternativeAttributedText = NSAttributedString(string: rawAlternativeText, font: textFont, textColor: messageTheme.accentTextColor)
let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
let (alternativeTextLayout, alternativeTextApply) = alternativeTextLayout(TextNodeLayoutArguments(attributedString: alternativeAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left + textLeftInset, y: -textInsets.top + 5.0 + topOffset), size: textLayout.size)
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
@ -174,7 +198,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
var suggestedBoundingWidth: CGFloat
suggestedBoundingWidth = textFrameWithoutInsets.width
suggestedBoundingWidth += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + textLeftInset + 28.0
suggestedBoundingWidth += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + textLeftInset + textRightInset
let iconImage: UIImage?
let iconOffset: CGPoint
@ -186,6 +210,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
iconOffset = CGPoint(x: 0.0, y: -1.0)
}
let arrowImage = PresentationResourcesChat.chatMessageCommentsArrowIcon(item.presentationData.theme.theme, incoming: incoming)
let unreadIconImage = PresentationResourcesChat.chatMessageCommentsUnreadDotIcon(item.presentationData.theme.theme, incoming: incoming)
return (suggestedBoundingWidth, { boundingWidth in
var boundingSize: CGSize
@ -198,33 +223,26 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
if let strongSelf = self {
strongSelf.item = item
let cachedLayout = strongSelf.textNode.cachedLayout
if case .System = animation {
if let cachedLayout = cachedLayout {
if !cachedLayout.areLinesEqual(to: textLayout) {
if let textContents = strongSelf.textNode.contents {
let fadeNode = ASDisplayNode()
fadeNode.displaysAsynchronously = false
fadeNode.contents = textContents
fadeNode.frame = strongSelf.textNode.frame
fadeNode.isLayerBacked = true
strongSelf.addSubnode(fadeNode)
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
fadeNode?.removeFromSupernode()
})
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
}
}
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.alternativeTextNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.textNode.isHidden = textLayout.truncated
strongSelf.alternativeTextNode.isHidden = !strongSelf.textNode.isHidden
let _ = textApply()
let _ = alternativeTextApply()
let adjustedTextFrame = textFrame
strongSelf.textNode.frame = adjustedTextFrame
strongSelf.alternativeTextNode.frame = CGRect(origin: adjustedTextFrame.origin, size: alternativeTextLayout.size)
let effectiveTextFrame: CGRect
if !strongSelf.alternativeTextNode.isHidden {
effectiveTextFrame = strongSelf.alternativeTextNode.frame
} else {
effectiveTextFrame = strongSelf.textNode.frame
}
if let iconImage = iconImage {
strongSelf.iconNode.image = iconImage
@ -233,8 +251,16 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
if let arrowImage = arrowImage {
strongSelf.arrowNode.image = arrowImage
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: boundingWidth - 33.0, y: 6.0 + topOffset), size: arrowImage.size)
let arrowFrame = CGRect(origin: CGPoint(x: boundingWidth - 33.0, y: 6.0 + topOffset), size: arrowImage.size)
strongSelf.arrowNode.frame = arrowFrame
if let unreadIconImage = unreadIconImage {
strongSelf.unreadIconNode.image = unreadIconImage
strongSelf.unreadIconNode.frame = CGRect(origin: CGPoint(x: effectiveTextFrame.maxX + 4.0, y: effectiveTextFrame.minY + floor((effectiveTextFrame.height - unreadIconImage.size.height) / 2.0) - 1.0), size: unreadIconImage.size)
}
}
strongSelf.unreadIconNode.isHidden = !hasUnseenReplies
strongSelf.iconNode.isHidden = !replyPeers.isEmpty

View File

@ -182,7 +182,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
switch item.chatLocation {
case let .peer(peerId):
messagePeerId = peerId
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
messagePeerId = messageId.peerId
}
@ -194,6 +194,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
isBroadcastChannel = true
}
if case let .replyThread(messageId, isChannelPost, _, _) = item.chatLocation, isChannelPost, messageId == item.message.id {
isBroadcastChannel = true
}
if !isBroadcastChannel {
hasAvatar = true
}
@ -337,7 +341,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
}
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
if case let .replyThread(replyThreadMessageId, _, _) = item.chatLocation, replyThreadMessageId == replyAttribute.messageId {
if case let .replyThread(replyThreadMessageId, _, _, _) = item.chatLocation, replyThreadMessageId == replyAttribute.messageId {
} else {
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
}
@ -712,8 +716,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
}
}
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
} else if let id = forwardInfo.source?.id ?? forwardInfo.author?.id {
item.controllerInteraction.openPeer(id, .info, nil)
} else if let peer = forwardInfo.source ?? forwardInfo.author {
item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil)
} else if let _ = forwardInfo.authorSignature {
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil)
}
@ -742,7 +746,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id)
item.controllerInteraction.openMessageReplies(item.message.id, true)
return
}
}

View File

@ -280,7 +280,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
switch chatLocation {
case let .peer(peerId):
messagePeerId = peerId
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
messagePeerId = messageId.peerId
}
@ -333,6 +333,8 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
isBroadcastChannel = true
}
} else if case let .replyThread(messageId, isChannelPost, _, _) = chatLocation, isChannelPost, messageId == message.id {
isBroadcastChannel = true
}
if !hasActionMedia && !isBroadcastChannel {
if let effectiveAuthor = effectiveAuthor {

View File

@ -249,7 +249,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
} else if incoming {
hasAvatar = true
}
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, isChannelPost, _, _):
if messageId.peerId != item.context.account.peerId {
if messageId.peerId.isGroupOrChannel && item.message.author != nil {
var isBroadcastChannel = false
@ -257,6 +257,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
isBroadcastChannel = true
}
if isChannelPost, messageId == item.message.id {
isBroadcastChannel = true
}
if !isBroadcastChannel {
hasAvatar = true
}
@ -411,7 +415,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
if case let .replyThread(replyThreadMessageId, _, _) = item.chatLocation, replyThreadMessageId == replyAttribute.messageId {
if case let .replyThread(replyThreadMessageId, _, _, _) = item.chatLocation, replyThreadMessageId == replyAttribute.messageId {
} else {
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
}
@ -775,7 +779,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id)
item.controllerInteraction.openMessageReplies(item.message.id, true)
return
}
}

View File

@ -451,7 +451,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _ in
}, openMessageReplies: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
@ -816,9 +816,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(messageId)))
}
case let .replyThreadMessage(replyThreadMessageId, isChannelPost, maxReadMessageId, messageId):
case let .replyThreadMessage(replyThreadMessageId, isChannelPost, maxMessage, maxReadMessageId, messageId):
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(threadMessageId: replyThreadMessageId, isChannelPost: isChannelPost, maxReadMessageId: maxReadMessageId), subject: .message(messageId)))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(threadMessageId: replyThreadMessageId, isChannelPost: isChannelPost, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId), subject: .message(messageId)))
}
case let .stickerPack(name):
let packReference: StickerPackReference = .name(name)

View File

@ -60,25 +60,22 @@ class ChatReplyCountItem: ListViewItem {
class ChatReplyCountItemNode: ListViewItemNode {
var item: ChatReplyCountItem?
let backgroundNode: ASImageNode
let labelNode: TextNode
let filledBackgroundNode: LinkHighlightingNode
private var theme: ChatPresentationThemeData?
private let layoutConstants = ChatMessageItemLayoutConstants.default
init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.filledBackgroundNode = LinkHighlightingNode(color: .clear)
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.filledBackgroundNode)
self.addSubnode(self.labelNode)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
@ -108,53 +105,62 @@ class ChatReplyCountItemNode: ListViewItemNode {
}
func asyncLayout() -> (_ item: ChatReplyCountItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let labelLayout = TextNode.asyncLayout(self.labelNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let backgroundLayout = self.filledBackgroundNode.asyncLayout()
let layoutConstants = self.layoutConstants
let currentTheme = self.theme
return { item, params, dateAtBottom in
var updatedBackgroundImage: UIImage?
if currentTheme != item.presentationData.theme {
updatedBackgroundImage = PresentationResourcesChat.chatUnreadBarBackgroundImage(item.presentationData.theme.theme)
}
let text: String
//TODO:localize
if item.isComments {
if item.count == 0 {
text = "Comments"
} else if item.count == 1 {
text = "1 Comment"
} else {
text = "\(item.count) Comments"
}
if item.count == 0 {
text = item.presentationData.strings.Conversation_DiscussionNotStarted
} else {
if item.count == 0 {
text = "Replies"
} else if item.count == 1 {
text = "1 Reply"
} else {
text = "\(item.count) Replies"
}
text = item.presentationData.strings.Conversation_DiscussionStarted
}
let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.theme.chat.serviceMessage.unreadBarTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let backgroundSize = CGSize(width: params.width, height: 25.0)
let attributedString = NSAttributedString(string: text, font: Font.regular(13.0), textColor: textColor)
return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 25.0), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in
let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var labelRects = labelLayout.linesRects()
if labelRects.count > 1 {
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
for i in 0 ..< sortedIndices.count {
let index = sortedIndices[i]
for j in -1 ... 1 {
if j != 0 && index + j >= 0 && index + j < sortedIndices.count {
if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 {
labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width)
labelRects[index].size.width = labelRects[index + j].size.width
}
}
}
}
}
for i in 0 ..< labelRects.count {
labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0))
labelRects[i].size.height = 20.0
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
}
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0)
let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0)
return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: backgroundSize.height), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.theme = item.presentationData.theme
if let updatedBackgroundImage = updatedBackgroundImage {
strongSelf.backgroundNode.image = updatedBackgroundImage
}
let _ = apply()
let _ = backgroundApply()
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize)
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - size.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0)), size: size.size)
let labelFrame = CGRect(origin: CGPoint(x: floor((params.width - backgroundSize.width) / 2.0) + 8.0, y: floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
}
})
}

View File

@ -792,12 +792,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
}
} else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
} else if case let .replyThread(_, isChannelPost, _) = interfaceState.chatLocation {
//TODO:localize
} else if case let .replyThread(_, isChannelPost, _, _) = interfaceState.chatLocation {
if isChannelPost {
placeholder = "Comment"
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
} else {
placeholder = "Reply"
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
}
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder

View File

@ -108,8 +108,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
switch titleContent {
case let .peer(peerView, _, isScheduledMessages):
if peerView.peerId.isReplies {
//TODO:localize
let typeText: String = "Replies"
let typeText: String = self.strings.DialogList_Replies
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
isEnabled = false
} else if isScheduledMessages {
@ -142,17 +141,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
}
}
case let .replyThread(type, text):
//TODO:localize
let typeText: String
if !text.isEmpty {
typeText = text
} else {
switch type {
case .comments:
typeText = "Comments"
case .replies:
typeText = "Replies"
}
typeText = " "
}
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)

View File

@ -144,7 +144,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _ in
}, openMessageReplies: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -67,7 +67,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
})
}
} else {
controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData)
controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData)
}
controller.purposefulAction = params.purposefulAction
if let search = params.activateMessageSearch {
@ -125,7 +125,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
if message.id.peerId == peerId {
return true
}
case let .replyThread(messageId, _, _):
case let .replyThread(messageId, _, _, _):
if message.id.peerId == messageId.peerId {
return true
}

View File

@ -90,9 +90,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
navigationController?.pushViewController(controller)
case let .channelMessage(peerId, messageId):
openPeer(peerId, .chat(textInputState: nil, subject: .message(messageId), peekData: nil))
case let .replyThreadMessage(replyThreadMessageId, isChannelPost, maxReadMessageId, messageId):
case let .replyThreadMessage(replyThreadMessageId, isChannelPost, maxMessage, maxReadMessageId, messageId):
if let navigationController = navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .replyThread(threadMessageId: replyThreadMessageId, isChannelPost: isChannelPost, maxReadMessageId: maxReadMessageId), subject: .message(messageId)))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .replyThread(threadMessageId: replyThreadMessageId, isChannelPost: isChannelPost, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId), subject: .message(messageId)))
}
case let .stickerPack(name):
dismissInput()

View File

@ -133,7 +133,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _ in
}, openMessageReplies: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))

View File

@ -933,14 +933,18 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
} else if let channel = peer as? TelegramChannel {
var displayLeave = !channel.flags.contains(.isCreator)
var canViewStats = false
var hasDiscussion = false
if let cachedChannelData = cachedData as? CachedChannelData {
canViewStats = cachedChannelData.flags.contains(.canViewStats)
}
switch channel.info {
case .broadcast:
case let .broadcast(info):
if !channel.flags.contains(.isCreator) {
displayLeave = true
}
if info.flags.contains(.hasDiscussionGroup) {
hasDiscussion = true
}
case .group:
displayLeave = false
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
@ -957,6 +961,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
displayLeave = false
}
result.append(.mute)
if hasDiscussion {
result.append(.discussion)
}
result.append(.search)
if displayLeave {
result.append(.leave)

View File

@ -1956,7 +1956,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _ in
}, openMessageReplies: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -1194,7 +1194,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _ in
}, openMessageReplies: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -59,9 +59,9 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate
if let navigationController = controller.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(messageId)))
}
case let .replyThreadMessage(replyThreadMessageId, isChannelPost, maxReadMessageId, messageId):
case let .replyThreadMessage(replyThreadMessageId, isChannelPost, maxMessage, maxReadMessageId, messageId):
if let navigationController = controller.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .replyThread(threadMessageId: replyThreadMessageId, isChannelPost: isChannelPost, maxReadMessageId: maxReadMessageId), subject: .message(messageId)))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .replyThread(threadMessageId: replyThreadMessageId, isChannelPost: isChannelPost, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId), subject: .message(messageId)))
}
case let .stickerPack(name):
let packReference: StickerPackReference = .name(name)

View File

@ -305,7 +305,7 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig
guard let result = result else {
return .channelMessage(peerId: peer.id, messageId: replyThreadMessageId)
}
return .replyThreadMessage(replyThreadMessageId: result.messageId, isChannelPost: true, maxReadMessageId: result.maxReadMessageId, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))
return .replyThreadMessage(replyThreadMessageId: result.messageId, isChannelPost: true, maxMessage: result.maxMessage, maxReadMessageId: result.maxReadMessageId, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))
}
}
} else {

View File

@ -1,4 +1,4 @@
use_gn_build = True
use_gn_build = False
webrtc_libs = [
"libwebrtc.a",