mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 01:10:09 +00:00
Comment improvements
This commit is contained in:
parent
c368f569ee
commit
43281e1546
@ -5770,3 +5770,23 @@ Any member of this group will be able to see messages in the channel.";
|
|||||||
"Conversation.InputTextAnonymousPlaceholder" = "Send anonymously";
|
"Conversation.InputTextAnonymousPlaceholder" = "Send anonymously";
|
||||||
|
|
||||||
"DialogList.Replies" = "Replies";
|
"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";
|
||||||
|
|||||||
@ -170,7 +170,7 @@ public enum ResolvedUrl {
|
|||||||
case botStart(peerId: PeerId, payload: String)
|
case botStart(peerId: PeerId, payload: String)
|
||||||
case groupBotStart(peerId: PeerId, payload: String)
|
case groupBotStart(peerId: PeerId, payload: String)
|
||||||
case channelMessage(peerId: PeerId, messageId: MessageId)
|
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 stickerPack(name: String)
|
||||||
case instantView(TelegramMediaWebpage, String?)
|
case instantView(TelegramMediaWebpage, String?)
|
||||||
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
|
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
|
||||||
@ -253,7 +253,7 @@ public enum ChatSearchDomain: Equatable {
|
|||||||
|
|
||||||
public enum ChatLocation: Equatable {
|
public enum ChatLocation: Equatable {
|
||||||
case peer(PeerId)
|
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 {
|
public final class NavigateToChatControllerParams {
|
||||||
@ -261,6 +261,7 @@ public final class NavigateToChatControllerParams {
|
|||||||
public let chatController: ChatController?
|
public let chatController: ChatController?
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
public let chatLocation: ChatLocation
|
public let chatLocation: ChatLocation
|
||||||
|
public let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
|
||||||
public let subject: ChatControllerSubject?
|
public let subject: ChatControllerSubject?
|
||||||
public let botStart: ChatControllerInitialBotStart?
|
public let botStart: ChatControllerInitialBotStart?
|
||||||
public let updateTextInputState: ChatTextInputState?
|
public let updateTextInputState: ChatTextInputState?
|
||||||
@ -277,9 +278,10 @@ public final class NavigateToChatControllerParams {
|
|||||||
public let parentGroupId: PeerGroupId?
|
public let parentGroupId: PeerGroupId?
|
||||||
public let completion: (ChatController) -> Void
|
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.navigationController = navigationController
|
||||||
self.chatController = chatController
|
self.chatController = chatController
|
||||||
|
self.chatLocationContextHolder = chatLocationContextHolder
|
||||||
self.context = context
|
self.context = context
|
||||||
self.chatLocation = chatLocation
|
self.chatLocation = chatLocation
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
|
|||||||
@ -48,7 +48,7 @@ public enum LocationActionListItemIcon: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func generateLocationIcon(theme: PresentationTheme) -> UIImage {
|
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.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor)
|
context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor)
|
||||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
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) {
|
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))
|
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 {
|
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.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.setFillColor(UIColor(rgb: 0x6cc139).cgColor)
|
context.setFillColor(UIColor(rgb: 0x6cc139).cgColor)
|
||||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
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) {
|
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))
|
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 {
|
final class LocationActionListItem: ListViewItem {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import AccountContext
|
|||||||
let locationPinReuseIdentifier = "locationPin"
|
let locationPinReuseIdentifier = "locationPin"
|
||||||
|
|
||||||
private func generateSmallBackgroundImage(color: UIColor) -> UIImage? {
|
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.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.setShadow(offset: CGSize(), blur: 4.0, color: UIColor(rgb: 0x000000, alpha: 0.5).cgColor)
|
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.setShadow(offset: CGSize(), blur: 0.0, color: nil)
|
||||||
context.setFillColor(color.cgColor)
|
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))
|
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 {
|
class LocationPinAnnotation: NSObject, MKAnnotation {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ private let panelSize = CGSize(width: 46.0, height: 90.0)
|
|||||||
|
|
||||||
private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? {
|
private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? {
|
||||||
let cornerRadius: CGFloat = 9.0
|
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.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
|
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)
|
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.addPath(path.cgPath)
|
||||||
context.fillPath()
|
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? {
|
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.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
|
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)
|
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.addPath(path.cgPath)
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
}?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0)
|
})?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class LocationMapHeaderNode: ASDisplayNode {
|
final class LocationMapHeaderNode: ASDisplayNode {
|
||||||
|
|||||||
@ -69,7 +69,7 @@ private class LocationMapView: MKMapView, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func generateHeadingArrowImage() -> UIImage? {
|
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)
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
context.clear(bounds)
|
context.clear(bounds)
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ private func generateHeadingArrowImage() -> UIImage? {
|
|||||||
|
|
||||||
context.setBlendMode(.clear)
|
context.setBlendMode(.clear)
|
||||||
context.fillEllipse(in: bounds.insetBy(dx: 5.0, dy: 5.0))
|
context.fillEllipse(in: bounds.insetBy(dx: 5.0, dy: 5.0))
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||||
|
|||||||
@ -23,5 +23,5 @@ public enum AdditionalMessageHistoryViewDataEntry {
|
|||||||
case preferencesEntry(ValueBoxKey, PreferencesEntry?)
|
case preferencesEntry(ValueBoxKey, PreferencesEntry?)
|
||||||
case peerIsContact(PeerId, Bool)
|
case peerIsContact(PeerId, Bool)
|
||||||
case peer(PeerId, Peer?)
|
case peer(PeerId, Peer?)
|
||||||
case message(MessageId, Message?)
|
case message(MessageId, [Message])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct MessageId: Hashable, Comparable, CustomStringConvertible {
|
public struct MessageId: Hashable, Comparable, CustomStringConvertible, PostboxCoding {
|
||||||
public typealias Namespace = Int32
|
public typealias Namespace = Int32
|
||||||
public typealias Id = Int32
|
public typealias Id = Int32
|
||||||
|
|
||||||
@ -37,6 +37,18 @@ public struct MessageId: Hashable, Comparable, CustomStringConvertible {
|
|||||||
buffer.offset += 16
|
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) {
|
public func encodeToBuffer(_ buffer: WriteBuffer) {
|
||||||
var peerIdNamespace = self.peerId.namespace
|
var peerIdNamespace = self.peerId.namespace
|
||||||
var peerIdId = self.peerId.id
|
var peerIdId = self.peerId.id
|
||||||
|
|||||||
@ -13,6 +13,7 @@ private enum MetadataPrefix: Int8 {
|
|||||||
case ShouldReindexUnreadCountsState = 10
|
case ShouldReindexUnreadCountsState = 10
|
||||||
case TotalUnreadCountStates = 11
|
case TotalUnreadCountStates = 11
|
||||||
case PeerHistoryTagInitialized = 12
|
case PeerHistoryTagInitialized = 12
|
||||||
|
case PeerHistoryThreadHoleIndexInitialized = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable {
|
public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable {
|
||||||
@ -45,6 +46,7 @@ final class MessageHistoryMetadataTable: Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sharedPeerHistoryInitializedKey = ValueBoxKey(length: 8 + 1)
|
let sharedPeerHistoryInitializedKey = ValueBoxKey(length: 8 + 1)
|
||||||
|
let sharedPeerThreadHoleIndexInitializedKey = ValueBoxKey(length: 8 + 1 + 8)
|
||||||
let sharedGroupFeedIndexInitializedKey = ValueBoxKey(length: 4 + 1)
|
let sharedGroupFeedIndexInitializedKey = ValueBoxKey(length: 4 + 1)
|
||||||
let sharedChatListGroupHistoryInitializedKey = ValueBoxKey(length: 4 + 1)
|
let sharedChatListGroupHistoryInitializedKey = ValueBoxKey(length: 4 + 1)
|
||||||
let sharedPeerNextMessageIdByNamespaceKey = ValueBoxKey(length: 8 + 1 + 4)
|
let sharedPeerNextMessageIdByNamespaceKey = ValueBoxKey(length: 8 + 1 + 4)
|
||||||
@ -76,6 +78,12 @@ final class MessageHistoryMetadataTable: Table {
|
|||||||
return self.sharedPeerHistoryInitializedKey
|
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 {
|
private func peerHistoryInitializedTagKey(id: PeerId, tag: UInt32) -> ValueBoxKey {
|
||||||
let key = ValueBoxKey(length: 8 + 1 + 4)
|
let key = ValueBoxKey(length: 8 + 1 + 4)
|
||||||
key.setInt64(0, value: id.toInt64())
|
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) {
|
func setPeerTagInitialized(peerId: PeerId, tag: MessageTags) {
|
||||||
if self.initializedHistoryPeerIdTags[peerId] == nil {
|
if self.initializedHistoryPeerIdTags[peerId] == nil {
|
||||||
self.initializedHistoryPeerIdTags[peerId] = Set()
|
self.initializedHistoryPeerIdTags[peerId] = Set()
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -655,35 +655,45 @@ final class MutableMessageHistoryView {
|
|||||||
}
|
}
|
||||||
case .cachedPeerDataMessages:
|
case .cachedPeerDataMessages:
|
||||||
break
|
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] {
|
if let operations = transaction.currentOperationsByPeerId[id.peerId] {
|
||||||
var updateMessage = false
|
var updateMessage = false
|
||||||
findOperation: for operation in operations {
|
findOperation: for operation in operations {
|
||||||
switch operation {
|
switch operation {
|
||||||
case let .InsertMessage(message):
|
case let .InsertMessage(message):
|
||||||
if message.id == id {
|
if message.id == id || (currentGroupingKey != nil && message.groupingKey == currentGroupingKey) {
|
||||||
updateMessage = true
|
updateMessage = true
|
||||||
break findOperation
|
break findOperation
|
||||||
}
|
}
|
||||||
case let .Remove(indices):
|
case let .Remove(indices):
|
||||||
for (index, _) in indices {
|
for (index, _) in indices {
|
||||||
if index.id == id {
|
if currentIds.contains(index.id) {
|
||||||
updateMessage = true
|
updateMessage = true
|
||||||
break findOperation
|
break findOperation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .UpdateEmbeddedMedia(index, _):
|
case let .UpdateEmbeddedMedia(index, _):
|
||||||
if index.id == id {
|
if currentIds.contains(index.id) {
|
||||||
updateMessage = true
|
updateMessage = true
|
||||||
break findOperation
|
break findOperation
|
||||||
}
|
}
|
||||||
case let .UpdateGroupInfos(dict):
|
case let .UpdateGroupInfos(dict):
|
||||||
if dict[id] != nil {
|
for id in currentIds {
|
||||||
updateMessage = true
|
if dict[id] != nil {
|
||||||
break findOperation
|
updateMessage = true
|
||||||
|
break findOperation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case let .UpdateTimestamp(index, _):
|
case let .UpdateTimestamp(index, _):
|
||||||
if index.id == id {
|
if currentIds.contains(index.id) {
|
||||||
updateMessage = true
|
updateMessage = true
|
||||||
break findOperation
|
break findOperation
|
||||||
}
|
}
|
||||||
@ -692,10 +702,8 @@ final class MutableMessageHistoryView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if updateMessage {
|
if updateMessage {
|
||||||
let message = postbox.messageHistoryIndexTable.getIndex(id).flatMap(postbox.messageHistoryTable.getMessage).flatMap { message in
|
let messages = postbox.getMessageGroup(at: id) ?? []
|
||||||
postbox.messageHistoryTable.renderMessage(message, peerTable: postbox.peerTable)
|
self.additionalDatas[i] = .message(id, messages)
|
||||||
}
|
|
||||||
self.additionalDatas[i] = .message(id, message)
|
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,21 @@ public final class Transaction {
|
|||||||
return self.postbox?.messageHistoryHoleIndexTable.containing(id: id) ?? [:]
|
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 {
|
public func doesChatListGroupContainHoles(groupId: PeerGroupId) -> Bool {
|
||||||
assert(!self.disposed)
|
assert(!self.disposed)
|
||||||
return self.postbox?.chatListTable.doesGroupContainHoles(groupId: groupId) ?? false
|
return self.postbox?.chatListTable.doesGroupContainHoles(groupId: groupId) ?? false
|
||||||
@ -1237,6 +1252,7 @@ public final class Postbox {
|
|||||||
let messageHistoryFailedTable: MessageHistoryFailedTable
|
let messageHistoryFailedTable: MessageHistoryFailedTable
|
||||||
let messageHistoryTagsTable: MessageHistoryTagsTable
|
let messageHistoryTagsTable: MessageHistoryTagsTable
|
||||||
let messageHistoryThreadsTable: MessageHistoryThreadsTable
|
let messageHistoryThreadsTable: MessageHistoryThreadsTable
|
||||||
|
let messageHistoryThreadHoleIndexTable: MessageHistoryThreadHoleIndexTable
|
||||||
let globalMessageHistoryTagsTable: GlobalMessageHistoryTagsTable
|
let globalMessageHistoryTagsTable: GlobalMessageHistoryTagsTable
|
||||||
let localMessageHistoryTagsTable: LocalMessageHistoryTagsTable
|
let localMessageHistoryTagsTable: LocalMessageHistoryTagsTable
|
||||||
let peerChatStateTable: PeerChatStateTable
|
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.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.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.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.globalMessageHistoryTagsTable = GlobalMessageHistoryTagsTable(valueBox: self.valueBox, table: GlobalMessageHistoryTagsTable.tableSpec(39))
|
||||||
self.localMessageHistoryTagsTable = LocalMessageHistoryTagsTable(valueBox: self.valueBox, table: GlobalMessageHistoryTagsTable.tableSpec(52))
|
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)
|
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.messageHistoryFailedTable)
|
||||||
tables.append(self.messageHistoryTagsTable)
|
tables.append(self.messageHistoryTagsTable)
|
||||||
tables.append(self.messageHistoryThreadsTable)
|
tables.append(self.messageHistoryThreadsTable)
|
||||||
|
tables.append(self.messageHistoryThreadHoleIndexTable)
|
||||||
tables.append(self.globalMessageHistoryTagsTable)
|
tables.append(self.globalMessageHistoryTagsTable)
|
||||||
tables.append(self.localMessageHistoryTagsTable)
|
tables.append(self.localMessageHistoryTagsTable)
|
||||||
tables.append(self.messageHistoryIndexTable)
|
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)
|
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) {
|
fileprivate func recalculateChatListGroupStats(groupId: PeerGroupId) {
|
||||||
let summary = self.chatListIndexTable.reindexPeerGroupUnreadCounts(postbox: self, groupId: groupId)
|
let summary = self.chatListIndexTable.reindexPeerGroupUnreadCounts(postbox: self, groupId: groupId)
|
||||||
self.groupMessageStatsTable.set(groupId: groupId, summary: summary)
|
self.groupMessageStatsTable.set(groupId: groupId, summary: summary)
|
||||||
@ -2357,12 +2383,14 @@ public final class Postbox {
|
|||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
return .single((.peer(peerId), false))
|
return .single((.peer(peerId), false))
|
||||||
case let .external(_, input):
|
case let .external(_, input):
|
||||||
var isHoleFill = false
|
return Signal { subscriber in
|
||||||
return input
|
var isHoleFill = false
|
||||||
|> map { value -> (ResolvedChatLocationInput, Bool) in
|
return (input
|
||||||
let wasHoleFill = isHoleFill
|
|> map { value -> (ResolvedChatLocationInput, Bool) in
|
||||||
isHoleFill = true
|
let wasHoleFill = isHoleFill
|
||||||
return (.external(value), wasHoleFill)
|
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))
|
additionalDataEntries.append(.cachedPeerDataMessages(peerId, messages))
|
||||||
case let .message(id):
|
case let .message(id):
|
||||||
let message = self.getMessage(id)
|
let messages = self.getMessageGroup(at: id)
|
||||||
additionalDataEntries.append(.message(id, message))
|
additionalDataEntries.append(.message(id, messages ?? []))
|
||||||
case let .peerChatState(peerId):
|
case let .peerChatState(peerId):
|
||||||
additionalDataEntries.append(.peerChatState(peerId, self.peerChatStateTable.get(peerId) as? PeerChatState))
|
additionalDataEntries.append(.peerChatState(peerId, self.peerChatStateTable.get(peerId) as? PeerChatState))
|
||||||
case .totalUnreadState:
|
case .totalUnreadState:
|
||||||
@ -3307,7 +3335,7 @@ public final class Postbox {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func getMessageGroup(at id: MessageId) -> [Message]? {
|
func getMessageGroup(at id: MessageId) -> [Message]? {
|
||||||
guard let index = self.messageHistoryIndexTable.getIndex(id) else {
|
guard let index = self.messageHistoryIndexTable.getIndex(id) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,7 @@ public final class SeedConfiguration {
|
|||||||
public let initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?)
|
public let initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?)
|
||||||
public let messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
|
public let messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
|
||||||
public let upgradedMessageHoles: [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 messageTagsWithSummary: MessageTags
|
||||||
public let existingGlobalMessageTags: GlobalMessageTags
|
public let existingGlobalMessageTags: GlobalMessageTags
|
||||||
public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace]
|
public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace]
|
||||||
@ -71,11 +72,12 @@ public final class SeedConfiguration {
|
|||||||
public let globalNotificationSettingsPreferencesKey: ValueBoxKey
|
public let globalNotificationSettingsPreferencesKey: ValueBoxKey
|
||||||
public let defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings
|
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.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces
|
||||||
self.initializeChatListWithHole = initializeChatListWithHole
|
self.initializeChatListWithHole = initializeChatListWithHole
|
||||||
self.messageHoles = messageHoles
|
self.messageHoles = messageHoles
|
||||||
self.upgradedMessageHoles = upgradedMessageHoles
|
self.upgradedMessageHoles = upgradedMessageHoles
|
||||||
|
self.messageThreadHoles = messageThreadHoles
|
||||||
self.messageTagsWithSummary = messageTagsWithSummary
|
self.messageTagsWithSummary = messageTagsWithSummary
|
||||||
self.existingGlobalMessageTags = existingGlobalMessageTags
|
self.existingGlobalMessageTags = existingGlobalMessageTags
|
||||||
self.peerNamespacesRequiringMessageTextIndex = peerNamespacesRequiringMessageTextIndex
|
self.peerNamespacesRequiringMessageTextIndex = peerNamespacesRequiringMessageTextIndex
|
||||||
|
|||||||
@ -39,6 +39,7 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func setData(_ offset: Int, value: Data) {
|
public func setData(_ offset: Int, value: Data) {
|
||||||
|
assert(offset >= 0 && offset + value.count <= self.length)
|
||||||
let valueLength = value.count
|
let valueLength = value.count
|
||||||
value.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
value.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
||||||
memcpy(self.memory + offset, bytes, valueLength)
|
memcpy(self.memory + offset, bytes, valueLength)
|
||||||
@ -46,66 +47,78 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func setInt32(_ offset: Int, value: Int32) {
|
public func setInt32(_ offset: Int, value: Int32) {
|
||||||
|
assert(offset >= 0 && offset + 4 <= self.length)
|
||||||
var bigEndianValue = Int32(bigEndian: value)
|
var bigEndianValue = Int32(bigEndian: value)
|
||||||
memcpy(self.memory + offset, &bigEndianValue, 4)
|
memcpy(self.memory + offset, &bigEndianValue, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setUInt32(_ offset: Int, value: UInt32) {
|
public func setUInt32(_ offset: Int, value: UInt32) {
|
||||||
|
assert(offset >= 0 && offset + 4 <= self.length)
|
||||||
var bigEndianValue = UInt32(bigEndian: value)
|
var bigEndianValue = UInt32(bigEndian: value)
|
||||||
memcpy(self.memory + offset, &bigEndianValue, 4)
|
memcpy(self.memory + offset, &bigEndianValue, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setInt64(_ offset: Int, value: Int64) {
|
public func setInt64(_ offset: Int, value: Int64) {
|
||||||
|
assert(offset >= 0 && offset + 8 <= self.length)
|
||||||
var bigEndianValue = Int64(bigEndian: value)
|
var bigEndianValue = Int64(bigEndian: value)
|
||||||
memcpy(self.memory + offset, &bigEndianValue, 8)
|
memcpy(self.memory + offset, &bigEndianValue, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setInt8(_ offset: Int, value: Int8) {
|
public func setInt8(_ offset: Int, value: Int8) {
|
||||||
|
assert(offset >= 0 && offset + 1 <= self.length)
|
||||||
var varValue = value
|
var varValue = value
|
||||||
memcpy(self.memory + offset, &varValue, 1)
|
memcpy(self.memory + offset, &varValue, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setUInt8(_ offset: Int, value: UInt8) {
|
public func setUInt8(_ offset: Int, value: UInt8) {
|
||||||
|
assert(offset >= 0 && offset + 1 <= self.length)
|
||||||
var varValue = value
|
var varValue = value
|
||||||
memcpy(self.memory + offset, &varValue, 1)
|
memcpy(self.memory + offset, &varValue, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setUInt16(_ offset: Int, value: UInt16) {
|
public func setUInt16(_ offset: Int, value: UInt16) {
|
||||||
|
assert(offset >= 0 && offset + 2 <= self.length)
|
||||||
var varValue = value
|
var varValue = value
|
||||||
memcpy(self.memory + offset, &varValue, 2)
|
memcpy(self.memory + offset, &varValue, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getInt32(_ offset: Int) -> Int32 {
|
public func getInt32(_ offset: Int) -> Int32 {
|
||||||
|
assert(offset >= 0 && offset + 4 <= self.length)
|
||||||
var value: Int32 = 0
|
var value: Int32 = 0
|
||||||
memcpy(&value, self.memory + offset, 4)
|
memcpy(&value, self.memory + offset, 4)
|
||||||
return Int32(bigEndian: value)
|
return Int32(bigEndian: value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getUInt32(_ offset: Int) -> UInt32 {
|
public func getUInt32(_ offset: Int) -> UInt32 {
|
||||||
|
assert(offset >= 0 && offset + 4 <= self.length)
|
||||||
var value: UInt32 = 0
|
var value: UInt32 = 0
|
||||||
memcpy(&value, self.memory + offset, 4)
|
memcpy(&value, self.memory + offset, 4)
|
||||||
return UInt32(bigEndian: value)
|
return UInt32(bigEndian: value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getInt64(_ offset: Int) -> Int64 {
|
public func getInt64(_ offset: Int) -> Int64 {
|
||||||
|
assert(offset >= 0 && offset + 8 <= self.length)
|
||||||
var value: Int64 = 0
|
var value: Int64 = 0
|
||||||
memcpy(&value, self.memory + offset, 8)
|
memcpy(&value, self.memory + offset, 8)
|
||||||
return Int64(bigEndian: value)
|
return Int64(bigEndian: value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getInt8(_ offset: Int) -> Int8 {
|
public func getInt8(_ offset: Int) -> Int8 {
|
||||||
|
assert(offset >= 0 && offset + 1 <= self.length)
|
||||||
var value: Int8 = 0
|
var value: Int8 = 0
|
||||||
memcpy(&value, self.memory + offset, 1)
|
memcpy(&value, self.memory + offset, 1)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getUInt8(_ offset: Int) -> UInt8 {
|
public func getUInt8(_ offset: Int) -> UInt8 {
|
||||||
|
assert(offset >= 0 && offset + 1 <= self.length)
|
||||||
var value: UInt8 = 0
|
var value: UInt8 = 0
|
||||||
memcpy(&value, self.memory + offset, 1)
|
memcpy(&value, self.memory + offset, 1)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getUInt16(_ offset: Int) -> UInt16 {
|
public func getUInt16(_ offset: Int) -> UInt16 {
|
||||||
|
assert(offset >= 0 && offset + 2 <= self.length)
|
||||||
var value: UInt16 = 0
|
var value: UInt16 = 0
|
||||||
memcpy(&value, self.memory + offset, 2)
|
memcpy(&value, self.memory + offset, 2)
|
||||||
return value
|
return value
|
||||||
@ -206,6 +219,7 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func substringValue(_ range: Range<Int>) -> String? {
|
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)
|
return String(data: Data(bytes: self.memory.advanced(by: range.lowerBound), count: range.count), encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,21 +5,27 @@ public class ReplyThreadMessageAttribute: MessageAttribute {
|
|||||||
public let count: Int32
|
public let count: Int32
|
||||||
public let latestUsers: [PeerId]
|
public let latestUsers: [PeerId]
|
||||||
public let commentsPeerId: PeerId?
|
public let commentsPeerId: PeerId?
|
||||||
|
public let maxMessageId: MessageId.Id?
|
||||||
|
public let maxReadMessageId: MessageId.Id?
|
||||||
|
|
||||||
public var associatedPeerIds: [PeerId] {
|
public var associatedPeerIds: [PeerId] {
|
||||||
return self.latestUsers
|
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.count = count
|
||||||
self.latestUsers = latestUsers
|
self.latestUsers = latestUsers
|
||||||
self.commentsPeerId = commentsPeerId
|
self.commentsPeerId = commentsPeerId
|
||||||
|
self.maxMessageId = maxMessageId
|
||||||
|
self.maxReadMessageId = maxReadMessageId
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(decoder: PostboxDecoder) {
|
required public init(decoder: PostboxDecoder) {
|
||||||
self.count = decoder.decodeInt32ForKey("c", orElse: 0)
|
self.count = decoder.decodeInt32ForKey("c", orElse: 0)
|
||||||
self.latestUsers = decoder.decodeInt64ArrayForKey("u").map(PeerId.init)
|
self.latestUsers = decoder.decodeInt64ArrayForKey("u").map(PeerId.init)
|
||||||
self.commentsPeerId = decoder.decodeOptionalInt64ForKey("cp").flatMap(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) {
|
public func encode(_ encoder: PostboxEncoder) {
|
||||||
@ -30,5 +36,15 @@ public class ReplyThreadMessageAttribute: MessageAttribute {
|
|||||||
} else {
|
} else {
|
||||||
encoder.encodeNil(forKey: "cp")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
// To avoid upgrading the database, **new** tags can be added here
|
||||||
// Uninitialized peers will fill the info using messageHoles
|
// Uninitialized peers will fill the info using messageHoles
|
||||||
var upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]] = [:]
|
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))
|
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 let peer = peer as? TelegramUser {
|
||||||
if peer.botInfo != nil {
|
if peer.botInfo != nil {
|
||||||
return .bot
|
return .bot
|
||||||
|
|||||||
@ -238,7 +238,22 @@ private final class FeaturedStickerPacksContext {
|
|||||||
self.disposable.dispose()
|
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 {
|
public final class AccountViewTracker {
|
||||||
weak var account: Account?
|
weak var account: Account?
|
||||||
private let queue = Queue()
|
private let queue = Queue()
|
||||||
@ -255,7 +270,7 @@ public final class AccountViewTracker {
|
|||||||
private var visibleCallListHoleIds: [MessageIndex: Int] = [:]
|
private var visibleCallListHoleIds: [MessageIndex: Int] = [:]
|
||||||
private var visibleCallListHoleDisposables: [MessageIndex: Disposable] = [:]
|
private var visibleCallListHoleDisposables: [MessageIndex: Disposable] = [:]
|
||||||
|
|
||||||
private var updatedViewCountMessageIdsAndTimestamps: [MessageId: Int32] = [:]
|
private var updatedViewCountMessageIdsAndTimestamps: [MessageId: ViewCountContextState] = [:]
|
||||||
private var nextUpdatedViewCountDisposableId: Int32 = 0
|
private var nextUpdatedViewCountDisposableId: Int32 = 0
|
||||||
private var updatedViewCountDisposables = DisposableDict<Int32>()
|
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 {
|
self.queue.async {
|
||||||
var addedMessageIds: [MessageId] = []
|
var addedMessageIds: [MessageId] = []
|
||||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
|
let updatedState = ViewCountContextState(timestamp: Int32(CFAbsoluteTimeGetCurrent()), clientId: clientId)
|
||||||
for messageId in messageIds {
|
for messageId in messageIds {
|
||||||
let messageTimestamp = self.updatedViewCountMessageIdsAndTimestamps[messageId]
|
let messageTimestamp = self.updatedViewCountMessageIdsAndTimestamps[messageId]
|
||||||
if messageTimestamp == nil || messageTimestamp! < timestamp - 5 * 60 {
|
if messageTimestamp == nil || !messageTimestamp!.isStillValidFor(updatedState) {
|
||||||
self.updatedViewCountMessageIdsAndTimestamps[messageId] = timestamp
|
self.updatedViewCountMessageIdsAndTimestamps[messageId] = updatedState
|
||||||
addedMessageIds.append(messageId)
|
addedMessageIds.append(messageId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -601,11 +616,28 @@ public final class AccountViewTracker {
|
|||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||||
guard case let .messageViews(viewCounts, _)? = result else {
|
guard case let .messageViews(viewCounts, users)? = result else {
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
return account.postbox.transaction { transaction -> Void in
|
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 {
|
for i in 0 ..< messageIds.count {
|
||||||
if i < viewCounts.count {
|
if i < viewCounts.count {
|
||||||
if case let .messageViews(_, views, forwards, replies) = viewCounts[i] {
|
if case let .messageViews(_, views, forwards, replies) = viewCounts[i] {
|
||||||
@ -616,9 +648,11 @@ public final class AccountViewTracker {
|
|||||||
var commentsChannelId: PeerId?
|
var commentsChannelId: PeerId?
|
||||||
var recentRepliersPeerIds: [PeerId]?
|
var recentRepliersPeerIds: [PeerId]?
|
||||||
var repliesCount: Int32?
|
var repliesCount: Int32?
|
||||||
|
var repliesMaxId: Int32?
|
||||||
|
var repliesReadMaxId: Int32?
|
||||||
if let replies = replies {
|
if let replies = replies {
|
||||||
switch replies {
|
switch replies {
|
||||||
case let .messageReplies(_, repliesCountValue, _, recentRepliers, channelId, _, _):
|
case let .messageReplies(_, repliesCountValue, _, recentRepliers, channelId, maxId, readMaxId):
|
||||||
if let channelId = channelId {
|
if let channelId = channelId {
|
||||||
commentsChannelId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)
|
commentsChannelId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)
|
||||||
}
|
}
|
||||||
@ -628,6 +662,8 @@ public final class AccountViewTracker {
|
|||||||
} else {
|
} else {
|
||||||
recentRepliersPeerIds = nil
|
recentRepliersPeerIds = nil
|
||||||
}
|
}
|
||||||
|
repliesMaxId = maxId
|
||||||
|
repliesReadMaxId = readMaxId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loop: for j in 0 ..< attributes.count {
|
loop: for j in 0 ..< attributes.count {
|
||||||
@ -642,12 +678,12 @@ public final class AccountViewTracker {
|
|||||||
} else if let _ = attributes[j] as? ReplyThreadMessageAttribute {
|
} else if let _ = attributes[j] as? ReplyThreadMessageAttribute {
|
||||||
foundReplies = true
|
foundReplies = true
|
||||||
if let repliesCount = repliesCount {
|
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 {
|
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))
|
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))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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 message.id.namespace == Namespaces.Message.Local && updatedId.namespace == Namespaces.Message.Cloud && updatedId.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
if let threadId = updatedMessage.threadId {
|
if let threadId = updatedMessage.threadId {
|
||||||
let messageThreadId = makeThreadIdMessageId(peerId: updatedMessage.id.peerId, threadId: 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])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
transaction.removeHole(peerId: peerId, namespace: namespace, space: space, range: filledRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,24 +18,54 @@ private class ReplyThreadHistoryContextImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let state = Promise<State>()
|
let state = Promise<State>()
|
||||||
private var stateValue: State {
|
private var stateValue: State? {
|
||||||
didSet {
|
didSet {
|
||||||
if self.stateValue != oldValue {
|
if let stateValue = self.stateValue {
|
||||||
self.state.set(.single(self.stateValue))
|
if stateValue != oldValue {
|
||||||
|
self.state.set(.single(stateValue))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var initialStateDisposable: Disposable?
|
||||||
private var holesDisposable: Disposable?
|
private var holesDisposable: Disposable?
|
||||||
private let readDisposable = MetaDisposable()
|
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.queue = queue
|
||||||
self.account = account
|
self.account = account
|
||||||
self.messageId = messageId
|
self.messageId = messageId
|
||||||
|
|
||||||
self.stateValue = State(messageId: self.messageId, holeIndices: [Namespaces.Message.Cloud: IndexSet(integersIn: 1 ..< Int(Int32.max))], maxReadMessageId: maxReadMessageId)
|
self.initialStateDisposable = (account.postbox.transaction { transaction -> State in
|
||||||
self.state.set(.single(self.stateValue))
|
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)
|
let threadId = makeMessageThreadId(messageId)
|
||||||
|
|
||||||
@ -61,6 +91,7 @@ private class ReplyThreadHistoryContextImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
self.initialStateDisposable?.dispose()
|
||||||
self.holesDisposable?.dispose()
|
self.holesDisposable?.dispose()
|
||||||
self.readDisposable.dispose()
|
self.readDisposable.dispose()
|
||||||
}
|
}
|
||||||
@ -73,9 +104,9 @@ private class ReplyThreadHistoryContextImpl {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if var currentHoles = strongSelf.stateValue.holeIndices[Namespaces.Message.Cloud] {
|
if var currentHoles = strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] {
|
||||||
currentHoles.subtract(removedHoleIndices)
|
currentHoles.subtract(removedHoleIndices)
|
||||||
strongSelf.stateValue.holeIndices[Namespaces.Message.Cloud] = currentHoles
|
strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] = currentHoles
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
@ -101,6 +132,40 @@ private class ReplyThreadHistoryContextImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
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)
|
return transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer)
|
||||||
}
|
}
|
||||||
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
|> 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
|
let queue = self.queue
|
||||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
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 struct ChatReplyThreadMessage {
|
||||||
|
public enum MaxMessage: Equatable {
|
||||||
|
case unknown
|
||||||
|
case known(MessageId?)
|
||||||
|
}
|
||||||
|
|
||||||
public var messageId: MessageId
|
public var messageId: MessageId
|
||||||
|
public var maxMessage: MaxMessage
|
||||||
public var maxReadMessageId: MessageId?
|
public var maxReadMessageId: MessageId?
|
||||||
|
|
||||||
public init(messageId: MessageId, maxReadMessageId: MessageId?) {
|
public init(messageId: MessageId, maxMessage: MaxMessage, maxReadMessageId: MessageId?) {
|
||||||
self.messageId = messageId
|
self.messageId = messageId
|
||||||
|
self.maxMessage = maxMessage
|
||||||
self.maxReadMessageId = maxReadMessageId
|
self.maxReadMessageId = maxReadMessageId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,17 +257,46 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
|
|||||||
guard let inputPeer = inputPeer else {
|
guard let inputPeer = inputPeer else {
|
||||||
return .single(nil)
|
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)
|
|> map(Optional.init)
|
||||||
|> `catch` { _ -> Signal<Api.messages.DiscussionMessage?, NoError> in
|
|> `catch` { _ -> Signal<Api.messages.DiscussionMessage?, NoError> in
|
||||||
return .single(nil)
|
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 {
|
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 .single(nil)
|
||||||
}
|
}
|
||||||
return account.postbox.transaction { transaction -> ChatReplyThreadMessage? in
|
return account.postbox.transaction { transaction -> ChatReplyThreadMessage? in
|
||||||
switch result {
|
switch discussionMessage {
|
||||||
case let .discussionMessage(message, readMaxId, chats, users):
|
case let .discussionMessage(message, readMaxId, chats, users):
|
||||||
guard let parsedMessage = StoreMessage(apiMessage: message), let parsedIndex = parsedMessage.index else {
|
guard let parsedMessage = StoreMessage(apiMessage: message), let parsedIndex = parsedMessage.index else {
|
||||||
return nil
|
return nil
|
||||||
@ -225,8 +326,20 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
|
|||||||
|
|
||||||
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
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(
|
return ChatReplyThreadMessage(
|
||||||
messageId: parsedIndex.id,
|
messageId: parsedIndex.id,
|
||||||
|
maxMessage: resolvedMaxMessage,
|
||||||
maxReadMessageId: MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
|
maxReadMessageId: MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -210,7 +210,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
|||||||
|
|
||||||
func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
|
func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
|
||||||
switch message {
|
switch message {
|
||||||
case let .message(flags, _, fromId, chatPeerId, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _):
|
case let .message(_, _, _, chatPeerId, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||||
if let replyTo = replyTo {
|
if let replyTo = replyTo {
|
||||||
let peerId: PeerId = chatPeerId.peerId
|
let peerId: PeerId = chatPeerId.peerId
|
||||||
|
|
||||||
@ -221,7 +221,7 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
|
|||||||
}
|
}
|
||||||
case .messageEmpty:
|
case .messageEmpty:
|
||||||
break
|
break
|
||||||
case let .messageService(flags, _, fromId, chatPeerId, replyHeader, _, _):
|
case let .messageService(_, _, _, chatPeerId, replyHeader, _, _):
|
||||||
if let replyHeader = replyHeader {
|
if let replyHeader = replyHeader {
|
||||||
switch replyHeader {
|
switch replyHeader {
|
||||||
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, _):
|
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, _):
|
||||||
@ -375,6 +375,27 @@ extension StoreMessage {
|
|||||||
|
|
||||||
var attributes: [MessageAttribute] = []
|
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?
|
var forwardInfo: StoreMessageForwardInfo?
|
||||||
if let fwdFrom = fwdFrom {
|
if let fwdFrom = fwdFrom {
|
||||||
switch fwdFrom {
|
switch fwdFrom {
|
||||||
@ -463,25 +484,6 @@ extension StoreMessage {
|
|||||||
attributes.append(InlineBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: viaBotId), title: nil))
|
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 namespace != Namespaces.Message.ScheduledCloud {
|
||||||
if let views = views {
|
if let views = views {
|
||||||
attributes.append(ViewCountMessageAttribute(count: Int(views)))
|
attributes.append(ViewCountMessageAttribute(count: Int(views)))
|
||||||
@ -531,7 +533,7 @@ extension StoreMessage {
|
|||||||
if let replies = replies {
|
if let replies = replies {
|
||||||
let recentRepliersPeerIds: [PeerId]?
|
let recentRepliersPeerIds: [PeerId]?
|
||||||
switch replies {
|
switch replies {
|
||||||
case let .messageReplies(_, repliesCount, _, recentRepliers, channelId, _, _):
|
case let .messageReplies(_, repliesCount, _, recentRepliers, channelId, maxId, readMaxId):
|
||||||
if let recentRepliers = recentRepliers {
|
if let recentRepliers = recentRepliers {
|
||||||
recentRepliersPeerIds = recentRepliers.map { $0.peerId }
|
recentRepliersPeerIds = recentRepliers.map { $0.peerId }
|
||||||
} else {
|
} else {
|
||||||
@ -540,7 +542,7 @@ extension StoreMessage {
|
|||||||
|
|
||||||
let commentsPeerId = channelId.flatMap { PeerId(namespace: Namespaces.Peer.CloudChannel, id: $0) }
|
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
|
return nil
|
||||||
case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action):
|
case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action):
|
||||||
let peerId: PeerId = chatPeerId.peerId
|
let peerId: PeerId = chatPeerId.peerId
|
||||||
var authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
|
let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
|
||||||
|
|
||||||
var attributes: [MessageAttribute] = []
|
var attributes: [MessageAttribute] = []
|
||||||
|
|
||||||
@ -602,11 +604,15 @@ extension StoreMessage {
|
|||||||
if let replyToTopId = replyToTopId {
|
if let replyToTopId = replyToTopId {
|
||||||
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId)
|
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId)
|
||||||
threadMessageId = threadIdValue
|
threadMessageId = threadIdValue
|
||||||
threadId = makeMessageThreadId(threadIdValue)
|
if replyPeerId == peerId {
|
||||||
|
threadId = makeMessageThreadId(threadIdValue)
|
||||||
|
}
|
||||||
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
|
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId)
|
let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId)
|
||||||
threadMessageId = threadIdValue
|
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))
|
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,7 +77,7 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe
|
|||||||
loop: for j in 0 ..< attributes.count {
|
loop: for j in 0 ..< attributes.count {
|
||||||
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
|
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
|
||||||
let count = max(0, attribute.count + countDifference)
|
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 {
|
} else if let attribute = attributes[j] as? SourceReferenceMessageAttribute {
|
||||||
channelThreadMessageId = attribute.messageId
|
channelThreadMessageId = attribute.messageId
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import AvatarNode
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
private let savedMessagesAvatar: UIImage = {
|
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]
|
var locations: [CGFloat] = [1.0, 0.0]
|
||||||
|
|
||||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
@ -28,7 +28,7 @@ private let savedMessagesAvatar: UIImage = {
|
|||||||
if let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white) {
|
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))
|
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 {
|
public enum SendMessageIntentContext {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -262,5 +262,6 @@ public enum PresentationResourceParameterKey: Hashable {
|
|||||||
|
|
||||||
case chatMessageCommentsIcon(incoming: Bool)
|
case chatMessageCommentsIcon(incoming: Bool)
|
||||||
case chatMessageCommentsArrowIcon(incoming: Bool)
|
case chatMessageCommentsArrowIcon(incoming: Bool)
|
||||||
|
case chatMessageCommentsUnreadDotIcon(incoming: Bool)
|
||||||
case chatMessageRepliesIcon(incoming: Bool)
|
case chatMessageRepliesIcon(incoming: Bool)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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? {
|
public static func chatFreeCommentButtonBackground(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.chatFreeCommentButtonBackground.rawValue, { _ in
|
return theme.image(PresentationResourceKey.chatFreeCommentButtonBackground.rawValue, { _ in
|
||||||
let strokeColor = bubbleVariableColor(variableColor: theme.chat.message.shareButtonStrokeColor, wallpaper: wallpaper)
|
let strokeColor = bubbleVariableColor(variableColor: theme.chat.message.shareButtonStrokeColor, wallpaper: wallpaper)
|
||||||
|
|||||||
Binary file not shown.
@ -303,8 +303,8 @@ public final class AccountContextImpl: AccountContext {
|
|||||||
switch location {
|
switch location {
|
||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
return .peer(peerId)
|
return .peer(peerId)
|
||||||
case let .replyThread(messageId, _, maxReadMessageId):
|
case let .replyThread(messageId, _, maxMessage, maxReadMessageId):
|
||||||
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxReadMessageId: maxReadMessageId)
|
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
|
||||||
return .external(messageId.peerId, context.state)
|
return .external(messageId.peerId, context.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,19 +313,19 @@ public final class AccountContextImpl: AccountContext {
|
|||||||
switch location {
|
switch location {
|
||||||
case .peer:
|
case .peer:
|
||||||
let _ = applyMaxReadIndexInteractively(postbox: self.account.postbox, stateManager: self.account.stateManager, index: messageIndex).start()
|
let _ = applyMaxReadIndexInteractively(postbox: self.account.postbox, stateManager: self.account.stateManager, index: messageIndex).start()
|
||||||
case let .replyThread(messageId, _, maxReadMessageId):
|
case let .replyThread(messageId, _, maxMessage, maxReadMessageId):
|
||||||
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxReadMessageId: maxReadMessageId)
|
let context = chatLocationContext(holder: contextHolder, account: self.account, messageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
|
||||||
context.applyMaxReadIndex(messageIndex: messageIndex)
|
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
|
let holder = holder.modify { current in
|
||||||
if let current = current as? ChatLocationContextHolderImpl {
|
if let current = current as? ChatLocationContextHolderImpl {
|
||||||
return current
|
return current
|
||||||
} else {
|
} else {
|
||||||
return ChatLocationContextHolderImpl(account: account, messageId: messageId, maxReadMessageId: maxReadMessageId)
|
return ChatLocationContextHolderImpl(account: account, messageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
|
||||||
}
|
}
|
||||||
} as! ChatLocationContextHolderImpl
|
} as! ChatLocationContextHolderImpl
|
||||||
return holder.context
|
return holder.context
|
||||||
@ -334,8 +334,8 @@ private func chatLocationContext(holder: Atomic<ChatLocationContextHolder?>, acc
|
|||||||
private final class ChatLocationContextHolderImpl: ChatLocationContextHolder {
|
private final class ChatLocationContextHolderImpl: ChatLocationContextHolder {
|
||||||
let context: ReplyThreadHistoryContext
|
let context: ReplyThreadHistoryContext
|
||||||
|
|
||||||
init(account: Account, messageId: MessageId, maxReadMessageId: MessageId?) {
|
init(account: Account, messageId: MessageId, maxMessage: ChatReplyThreadMessage.MaxMessage, maxReadMessageId: MessageId?) {
|
||||||
self.context = ReplyThreadHistoryContext(account: account, peerId: messageId.peerId, threadMessageId: messageId, maxReadMessageId: maxReadMessageId)
|
self.context = ReplyThreadHistoryContext(account: account, peerId: messageId.peerId, threadMessageId: messageId, maxMessage: maxMessage, maxReadMessageId: maxReadMessageId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ extension ChatLocation {
|
|||||||
switch self {
|
switch self {
|
||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
return peerId
|
return peerId
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
return messageId.peerId
|
return messageId.peerId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,7 +345,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
private var hasEmbeddedTitleContent = false
|
private var hasEmbeddedTitleContent = false
|
||||||
private var isEmbeddedTitleContentHidden = false
|
private var isEmbeddedTitleContentHidden = false
|
||||||
|
|
||||||
private let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
|
||||||
|
|
||||||
public override var customData: Any? {
|
public override var customData: Any? {
|
||||||
return self.chatLocation
|
return self.chatLocation
|
||||||
@ -353,13 +353,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
var purposefulAction: (() -> Void)?
|
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
|
let _ = ChatControllerCount.modify { value in
|
||||||
return value + 1
|
return value + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
self.context = context
|
self.context = context
|
||||||
self.chatLocation = chatLocation
|
self.chatLocation = chatLocation
|
||||||
|
self.chatLocationContextHolder = chatLocationContextHolder
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.botStart = botStart
|
self.botStart = botStart
|
||||||
self.peekData = peekData
|
self.peekData = peekData
|
||||||
@ -370,7 +371,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
locationBroadcastPanelSource = .peer(peerId)
|
locationBroadcastPanelSource = .peer(peerId)
|
||||||
self.chatLocationInfoData = .peer(Promise())
|
self.chatLocationInfoData = .peer(Promise())
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
locationBroadcastPanelSource = .none
|
locationBroadcastPanelSource = .none
|
||||||
let promise = Promise<Message?>()
|
let promise = Promise<Message?>()
|
||||||
let key = PostboxViewKey.messages([messageId])
|
let key = PostboxViewKey.messages([messageId])
|
||||||
@ -1628,7 +1629,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
switch strongSelf.chatLocation {
|
switch strongSelf.chatLocation {
|
||||||
case let .peer(peerId):
|
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)
|
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
|
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)
|
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)
|
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)
|
strongSelf.presentInGlobalOverlay(contextController)
|
||||||
})
|
})
|
||||||
}, openMessageReplies: { [weak self] messageId in
|
}, openMessageReplies: { [weak self] messageId, isChannelPost in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let foundIndex = Promise<ChatReplyThreadMessage?>()
|
let foundIndex = Promise<ReplyThreadInfo?>()
|
||||||
foundIndex.set(fetchChannelReplyThreadMessage(account: strongSelf.context.account, messageId: messageId))
|
foundIndex.set(fetchAndPreloadReplyThreadInfo(context: strongSelf.context, subject: isChannelPost ? .channelPost(messageId) : .groupMessage(messageId)))
|
||||||
|
|
||||||
var cancelImpl: (() -> Void)?
|
var cancelImpl: (() -> Void)?
|
||||||
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: {
|
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 result = result {
|
||||||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
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())
|
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get())
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice in
|
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -2654,9 +2641,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
switch chatLocation {
|
switch chatLocation {
|
||||||
case .peer:
|
case .peer:
|
||||||
replyThreadType = .replies
|
replyThreadType = .replies
|
||||||
case let .replyThread(_, _, readMessageId):
|
case let .replyThread(_, isChannelPost, _, _):
|
||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
if readMessageId != nil {
|
if isChannelPost {
|
||||||
replyThreadType = .comments
|
replyThreadType = .comments
|
||||||
} else {
|
} else {
|
||||||
replyThreadType = .replies
|
replyThreadType = .replies
|
||||||
@ -2672,7 +2659,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
)
|
)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peerView, message, onlineMemberCount in
|
|> deliverOnMainQueue).start(next: { [weak self] peerView, message, onlineMemberCount in
|
||||||
if let strongSelf = self {
|
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
|
let firstTime = strongSelf.peerView == nil
|
||||||
strongSelf.peerView = peerView
|
strongSelf.peerView = peerView
|
||||||
@ -3363,7 +3367,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
|> distinctUntilChanged
|
|> 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 {
|
if let strongSelf = self {
|
||||||
let (cachedData, messages) = cachedDataAndMessages
|
let (cachedData, messages) = cachedDataAndMessages
|
||||||
|
|
||||||
@ -3391,8 +3398,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
} else if let _ = cachedData as? CachedSecretChatData {
|
} else if let _ = cachedData as? CachedSecretChatData {
|
||||||
}
|
}
|
||||||
|
|
||||||
if case .replyThread = strongSelf.chatLocation {
|
if case let .replyThread(messageId, _, _, _) = strongSelf.chatLocation {
|
||||||
pinnedMessageId = nil
|
if isTopReplyThreadMessageShown {
|
||||||
|
pinnedMessageId = nil
|
||||||
|
} else {
|
||||||
|
pinnedMessageId = messageId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pinnedMessage: Message?
|
var pinnedMessage: Message?
|
||||||
@ -5032,7 +5043,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
if let navigationController = strongSelf.effectiveNavigationController {
|
if let navigationController = strongSelf.effectiveNavigationController {
|
||||||
let subject: ChatControllerSubject? = sourceMessageId.flatMap(ChatControllerSubject.message)
|
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()))
|
}, 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 {
|
switch self.chatLocation {
|
||||||
case .peer:
|
case .peer:
|
||||||
break
|
break
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
defaultReplyMessageId = messageId
|
defaultReplyMessageId = messageId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7535,7 +7546,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
switch self.chatLocation {
|
switch self.chatLocation {
|
||||||
case let .peer(peerIdValue):
|
case let .peer(peerIdValue):
|
||||||
peerId = peerIdValue
|
peerId = peerIdValue
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
peerId = messageId.peerId
|
peerId = messageId.peerId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7957,7 +7968,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
switch self.chatLocation {
|
switch self.chatLocation {
|
||||||
case .peer:
|
case .peer:
|
||||||
break
|
break
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
searchTopMsgId = messageId
|
searchTopMsgId = messageId
|
||||||
}
|
}
|
||||||
switch search.domain {
|
switch search.domain {
|
||||||
|
|||||||
@ -112,7 +112,7 @@ public final class ChatControllerInteraction {
|
|||||||
let animateDiceSuccess: () -> Void
|
let animateDiceSuccess: () -> Void
|
||||||
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?
|
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?
|
||||||
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||||
let openMessageReplies: (MessageId) -> Void
|
let openMessageReplies: (MessageId, Bool) -> Void
|
||||||
|
|
||||||
let requestMessageUpdate: (MessageId) -> Void
|
let requestMessageUpdate: (MessageId) -> Void
|
||||||
let cancelInteractiveKeyboardGestures: () -> Void
|
let cancelInteractiveKeyboardGestures: () -> Void
|
||||||
@ -195,7 +195,7 @@ public final class ChatControllerInteraction {
|
|||||||
animateDiceSuccess: @escaping () -> Void,
|
animateDiceSuccess: @escaping () -> Void,
|
||||||
greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?,
|
greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?,
|
||||||
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
||||||
openMessageReplies: @escaping (MessageId) -> Void,
|
openMessageReplies: @escaping (MessageId, Bool) -> Void,
|
||||||
requestMessageUpdate: @escaping (MessageId) -> Void,
|
requestMessageUpdate: @escaping (MessageId) -> Void,
|
||||||
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
||||||
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
|
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
|
||||||
@ -315,7 +315,7 @@ public final class ChatControllerInteraction {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _ in
|
}, openMessageReplies: { _, _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
|
|||||||
@ -41,11 +41,8 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
|
|||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
switch interfaceState.chatLocation {
|
switch interfaceState.chatLocation {
|
||||||
case .peer:
|
case .peer, .replyThread:
|
||||||
text = interfaceState.isScheduledMessages ? interfaceState.strings.ScheduledMessages_EmptyPlaceholder : interfaceState.strings.Conversation_EmptyPlaceholder
|
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)
|
self.textNode.attributedText = NSAttributedString(string: text, font: messageFont, textColor: serviceColor.primaryText)
|
||||||
|
|||||||
@ -114,44 +114,43 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var addedThreadHead = false
|
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 {
|
loop: for entry in view.additionalData {
|
||||||
switch entry {
|
switch entry {
|
||||||
case let .message(id, message) where id == messageId:
|
case let .message(id, messages) where id == messageId:
|
||||||
if let message = message {
|
if !messages.isEmpty {
|
||||||
let selection: ChatHistoryMessageSelection
|
let selection: ChatHistoryMessageSelection = .none
|
||||||
if let selectedMessages = selectedMessages {
|
|
||||||
selection = .selectable(selected: selectedMessages.contains(message.id))
|
let topMessage = messages[0]
|
||||||
} else {
|
|
||||||
selection = .none
|
|
||||||
}
|
|
||||||
|
|
||||||
var adminRank: CachedChannelAdminRank?
|
var adminRank: CachedChannelAdminRank?
|
||||||
if let author = message.author {
|
if let author = topMessage.author {
|
||||||
adminRank = adminRanks[author.id]
|
adminRank = adminRanks[author.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentTypeHint: ChatMessageEntryContentType = .generic
|
var contentTypeHint: ChatMessageEntryContentType = .generic
|
||||||
if presentationData.largeEmoji, message.media.isEmpty {
|
if presentationData.largeEmoji, topMessage.media.isEmpty {
|
||||||
if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0] {
|
if stickersEnabled && topMessage.text.count == 1, let _ = associatedData.animatedEmojiStickers[topMessage.text.basicEmoji.0] {
|
||||||
contentTypeHint = .animatedEmoji
|
contentTypeHint = .animatedEmoji
|
||||||
} else if message.text.count < 10 && messageIsElligibleForLargeEmoji(message) {
|
} else if topMessage.text.count < 10 && messageIsElligibleForLargeEmoji(topMessage) {
|
||||||
contentTypeHint = .largeEmoji
|
contentTypeHint = .largeEmoji
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var replyCount = 0
|
addedThreadHead = true
|
||||||
for attribute in message.attributes {
|
if messages.count > 1, let groupInfo = messages[0].groupInfo {
|
||||||
if let attribute = attribute as? ReplyThreadMessageAttribute {
|
var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)] = []
|
||||||
replyCount = Int(attribute.count)
|
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
|
let replyCount = view.entries.isEmpty ? 0 : 1
|
||||||
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(messages[0].index, isChannelPost, replyCount, presentationData), at: 1)
|
||||||
entries.insert(.ReplyCountEntry(message.index, isChannelPost, replyCount, presentationData), at: 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break loop
|
break loop
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -429,6 +429,8 @@ private struct ChatHistoryAnimatedEmojiConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var nextClientId: Int32 = 1
|
||||||
|
|
||||||
public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let chatLocation: ChatLocation
|
private let chatLocation: ChatLocation
|
||||||
@ -543,6 +545,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
|
|
||||||
private var loadedMessagesFromCachedDataDisposable: Disposable?
|
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) {
|
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.context = context
|
||||||
self.chatLocation = chatLocation
|
self.chatLocation = chatLocation
|
||||||
@ -566,8 +570,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
|
|
||||||
//self.debugInfo = true
|
//self.debugInfo = true
|
||||||
|
|
||||||
|
let clientId = nextClientId
|
||||||
|
nextClientId += 1
|
||||||
|
|
||||||
self.messageProcessingManager.process = { [weak context] messageIds in
|
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
|
self.messageReactionsProcessingManager.process = { [weak context] messageIds in
|
||||||
context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds)
|
context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds)
|
||||||
@ -620,7 +627,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
if !isAuxiliaryChat {
|
if !isAuxiliaryChat {
|
||||||
additionalData.append(.totalUnreadState)
|
additionalData.append(.totalUnreadState)
|
||||||
}
|
}
|
||||||
if case let .replyThread(messageId, _, _) = chatLocation {
|
if case let .replyThread(messageId, _, _, _) = chatLocation {
|
||||||
additionalData.append(.cachedPeerData(messageId.peerId))
|
additionalData.append(.cachedPeerData(messageId.peerId))
|
||||||
additionalData.append(.peerNotificationSettings(messageId.peerId))
|
additionalData.append(.peerNotificationSettings(messageId.peerId))
|
||||||
if messageId.peerId.namespace == Namespaces.Peer.CloudChannel {
|
if messageId.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
@ -1052,6 +1059,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
|
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
|
||||||
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
|
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
|
||||||
|
|
||||||
|
var isTopReplyThreadMessageShownValue = false
|
||||||
|
|
||||||
if indexRange.0 <= indexRange.1 {
|
if indexRange.0 <= indexRange.1 {
|
||||||
for i in (indexRange.0 ... indexRange.1) {
|
for i in (indexRange.0 ... indexRange.1) {
|
||||||
switch historyView.filteredEntries[i] {
|
switch historyView.filteredEntries[i] {
|
||||||
@ -1103,6 +1112,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
if hasUnconsumedMention && !hasUnconsumedContent {
|
if hasUnconsumedMention && !hasUnconsumedContent {
|
||||||
messageIdsWithUnseenPersonalMention.append(message.id)
|
messageIdsWithUnseenPersonalMention.append(message.id)
|
||||||
}
|
}
|
||||||
|
if case .replyThread(message.id, _, _, _) = self.chatLocation {
|
||||||
|
isTopReplyThreadMessageShownValue = true
|
||||||
|
}
|
||||||
case let .MessageGroupEntry(_, messages, _):
|
case let .MessageGroupEntry(_, messages, _):
|
||||||
for (message, _, _, _) in messages {
|
for (message, _, _, _) in messages {
|
||||||
var hasUnconsumedMention = false
|
var hasUnconsumedMention = false
|
||||||
@ -1130,6 +1142,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
if hasUnconsumedMention && !hasUnconsumedContent {
|
if hasUnconsumedMention && !hasUnconsumedContent {
|
||||||
messageIdsWithUnseenPersonalMention.append(message.id)
|
messageIdsWithUnseenPersonalMention.append(message.id)
|
||||||
}
|
}
|
||||||
|
if case .replyThread(message.id, _, _, _) = self.chatLocation {
|
||||||
|
isTopReplyThreadMessageShownValue = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@ -1227,8 +1242,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
if readIndexRange.0 <= readIndexRange.1 {
|
if readIndexRange.0 <= readIndexRange.1 {
|
||||||
let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView, indexRange: readIndexRange)
|
let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView, indexRange: readIndexRange)
|
||||||
|
|
||||||
if let maxIncomingIndex = maxIncomingIndex {
|
let messageIndex: MessageIndex?
|
||||||
self.updateMaxVisibleReadIncomingMessageIndex(maxIncomingIndex)
|
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 {
|
if let maxOverallIndex = maxOverallIndex, maxOverallIndex != self.maxVisibleMessageIndexReported {
|
||||||
@ -1236,18 +1259,23 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
self.maxVisibleMessageIndexUpdated?(maxOverallIndex)
|
self.maxVisibleMessageIndexUpdated?(maxOverallIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
|
if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
|
||||||
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {
|
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 {
|
} 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())
|
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 {
|
} 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 _ = visibleRange.loadedRange {
|
||||||
if let visible = visibleRange.visibleRange {
|
if let visible = visibleRange.visibleRange {
|
||||||
let visibleFirstIndex = visible.firstIndex
|
let visibleFirstIndex = visible.firstIndex
|
||||||
/*if !visible.firstIndexFullyVisible {
|
|
||||||
visibleFirstIndex += 1
|
|
||||||
}*/
|
|
||||||
if visibleFirstIndex <= visible.lastIndex {
|
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 {
|
if let messageIndex = messageIndex {
|
||||||
strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
|
strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import AccountContext
|
|||||||
import ChatInterfaceState
|
import ChatInterfaceState
|
||||||
|
|
||||||
func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal<ChatHistoryViewUpdate, NoError> {
|
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)
|
|> castError(Bool.self)
|
||||||
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
|
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
|
||||||
switch update {
|
switch update {
|
||||||
@ -23,7 +23,7 @@ func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return .single(update)
|
return .single(update)
|
||||||
}
|
})
|
||||||
|> restartIfError
|
|> restartIfError
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,8 +267,8 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .message(_, message):
|
case let .message(_, messages):
|
||||||
if let message = message {
|
for message in messages {
|
||||||
cachedDataMessages[message.id] = message
|
cachedDataMessages[message.id] = message
|
||||||
}
|
}
|
||||||
case let .totalUnreadState(totalUnreadState):
|
case let .totalUnreadState(totalUnreadState):
|
||||||
@ -289,3 +289,89 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL
|
|||||||
|
|
||||||
return (cachedData, cachedDataMessages, readStateData)
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -465,7 +465,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isReplyThreadHead = false
|
var isReplyThreadHead = false
|
||||||
if case let .replyThread(messageId, _, _) = chatPresentationInterfaceState.chatLocation {
|
if case let .replyThread(messageId, _, _, _) = chatPresentationInterfaceState.chatLocation {
|
||||||
isReplyThreadHead = messages[0].id == messageId
|
isReplyThreadHead = messages[0].id == messageId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,16 +601,11 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||||||
|
|
||||||
if let threadId = threadId {
|
if let threadId = threadId {
|
||||||
let replyThreadId = makeThreadIdMessageId(peerId: messages[0].id.peerId, threadId: threadId)
|
let replyThreadId = makeThreadIdMessageId(peerId: messages[0].id.peerId, threadId: threadId)
|
||||||
//TODO:localize
|
|
||||||
let text: String
|
let text: String
|
||||||
if threadMessageCount != 0 {
|
if threadMessageCount != 0 {
|
||||||
if threadMessageCount == 1 {
|
text = chatPresentationInterfaceState.strings.Conversation_ContextViewReplies(Int32(threadMessageCount))
|
||||||
text = "View 1 reply"
|
|
||||||
} else {
|
|
||||||
text = "View \(threadMessageCount) replies"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
text = "View Thread"
|
text = chatPresentationInterfaceState.strings.Conversation_ContextViewThread
|
||||||
}
|
}
|
||||||
actions.append(.action(ContextMenuActionItem(text: text, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: text, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replies"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replies"), color: theme.actionSheet.primaryTextColor)
|
||||||
@ -622,7 +617,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||||||
c.dismiss(completion: {
|
c.dismiss(completion: {
|
||||||
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
|
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
|
||||||
if case .group = channel.info {
|
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 {
|
} else {
|
||||||
var cancelImpl: (() -> Void)?
|
var cancelImpl: (() -> Void)?
|
||||||
let statusController = OverlayStatusController(theme: chatPresentationInterfaceState.theme, type: .loading(cancelled: {
|
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)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
||||||
}, action: { _, f in
|
}, action: { _, f in
|
||||||
var threadMessageId: MessageId?
|
var threadMessageId: MessageId?
|
||||||
if case let .replyThread(replyThread, _, _) = chatPresentationInterfaceState.chatLocation {
|
if case let .replyThread(replyThread, _, _, _) = chatPresentationInterfaceState.chatLocation {
|
||||||
threadMessageId = replyThread
|
threadMessageId = replyThread
|
||||||
}
|
}
|
||||||
let _ = (exportMessageLink(account: context.account, peerId: message.id.peerId, messageId: message.id, threadMessageId: threadMessageId)
|
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 {
|
if !actions.isEmpty {
|
||||||
actions.append(.separator)
|
actions.append(.separator)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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))) {
|
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 makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||||
|
|
||||||
let backgroundLayout = self.filledBackgroundNode.asyncLayout()
|
let backgroundLayout = self.filledBackgroundNode.asyncLayout()
|
||||||
|
|
||||||
return { item, layoutConstants, _, _, _ in
|
return { item, layoutConstants, _, _, _ in
|
||||||
|
|||||||
@ -561,7 +561,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
} else if incoming {
|
} else if incoming {
|
||||||
hasAvatar = true
|
hasAvatar = true
|
||||||
}
|
}
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, isChannelPost, _, _):
|
||||||
if messageId.peerId != item.context.account.peerId {
|
if messageId.peerId != item.context.account.peerId {
|
||||||
if messageId.peerId.isGroupOrChannel && item.message.author != nil {
|
if messageId.peerId.isGroupOrChannel && item.message.author != nil {
|
||||||
var isBroadcastChannel = false
|
var isBroadcastChannel = false
|
||||||
@ -569,6 +569,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
isBroadcastChannel = true
|
isBroadcastChannel = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isChannelPost, messageId == item.message.id {
|
||||||
|
isBroadcastChannel = true
|
||||||
|
}
|
||||||
|
|
||||||
if !isBroadcastChannel {
|
if !isBroadcastChannel {
|
||||||
hasAvatar = true
|
hasAvatar = true
|
||||||
}
|
}
|
||||||
@ -742,7 +746,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
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 {
|
} else {
|
||||||
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude))
|
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 {
|
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? ReplyThreadMessageAttribute {
|
if let _ = attribute as? ReplyThreadMessageAttribute {
|
||||||
item.controllerInteraction.openMessageReplies(item.message.id)
|
item.controllerInteraction.openMessageReplies(item.message.id, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -841,7 +841,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
switch item.chatLocation {
|
switch item.chatLocation {
|
||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
chatLocationPeerId = peerId
|
chatLocationPeerId = peerId
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
chatLocationPeerId = messageId.peerId
|
chatLocationPeerId = messageId.peerId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -885,6 +885,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
allowFullWidth = true
|
allowFullWidth = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if case let .replyThread(messageId, isChannelPost, _, _) = item.chatLocation, isChannelPost, messageId == firstMessage.id {
|
||||||
|
isBroadcastChannel = true
|
||||||
|
}
|
||||||
|
|
||||||
if !isBroadcastChannel {
|
if !isBroadcastChannel {
|
||||||
hasAvatar = item.content.firstMessage.effectivelyIncoming(item.context.account.peerId)
|
hasAvatar = item.content.firstMessage.effectivelyIncoming(item.context.account.peerId)
|
||||||
}
|
}
|
||||||
@ -1044,7 +1048,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
inlineBotNameString = attribute.title
|
inlineBotNameString = attribute.title
|
||||||
}
|
}
|
||||||
} else if let attribute = attribute as? ReplyMessageAttribute {
|
} 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 {
|
} else {
|
||||||
replyMessage = firstMessage.associatedMessages[attribute.messageId]
|
replyMessage = firstMessage.associatedMessages[attribute.messageId]
|
||||||
}
|
}
|
||||||
@ -2570,8 +2574,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
|
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
|
||||||
} else if let id = forwardInfo.source?.id ?? forwardInfo.author?.id {
|
} else if let peer = forwardInfo.source ?? forwardInfo.author {
|
||||||
item.controllerInteraction.openPeer(id, .info, nil)
|
item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil)
|
||||||
} else if let _ = forwardInfo.authorSignature {
|
} else if let _ = forwardInfo.authorSignature {
|
||||||
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil)
|
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,12 @@ import TelegramPresentationData
|
|||||||
final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
||||||
private let separatorNode: ASDisplayNode
|
private let separatorNode: ASDisplayNode
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
|
private let alternativeTextNode: TextNode
|
||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
private let arrowNode: ASImageNode
|
private let arrowNode: ASImageNode
|
||||||
private let buttonNode: HighlightTrackingButtonNode
|
private let buttonNode: HighlightTrackingButtonNode
|
||||||
private let avatarsNode: MergedAvatarsNode
|
private let avatarsNode: MergedAvatarsNode
|
||||||
|
private let unreadIconNode: ASImageNode
|
||||||
|
|
||||||
required init() {
|
required init() {
|
||||||
self.separatorNode = ASDisplayNode()
|
self.separatorNode = ASDisplayNode()
|
||||||
@ -26,11 +28,22 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.textNode.contentsScale = UIScreenScale
|
self.textNode.contentsScale = UIScreenScale
|
||||||
self.textNode.displaysAsynchronously = true
|
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 = ASImageNode()
|
||||||
self.iconNode.displaysAsynchronously = false
|
self.iconNode.displaysAsynchronously = false
|
||||||
self.iconNode.displayWithoutProcessing = true
|
self.iconNode.displayWithoutProcessing = true
|
||||||
self.iconNode.isUserInteractionEnabled = false
|
self.iconNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.unreadIconNode = ASImageNode()
|
||||||
|
self.unreadIconNode.displaysAsynchronously = false
|
||||||
|
self.unreadIconNode.displayWithoutProcessing = true
|
||||||
|
self.unreadIconNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.arrowNode = ASImageNode()
|
self.arrowNode = ASImageNode()
|
||||||
self.arrowNode.displaysAsynchronously = false
|
self.arrowNode.displaysAsynchronously = false
|
||||||
self.arrowNode.displayWithoutProcessing = true
|
self.arrowNode.displayWithoutProcessing = true
|
||||||
@ -45,7 +58,9 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
self.buttonNode.addSubnode(self.separatorNode)
|
self.buttonNode.addSubnode(self.separatorNode)
|
||||||
self.buttonNode.addSubnode(self.textNode)
|
self.buttonNode.addSubnode(self.textNode)
|
||||||
|
self.buttonNode.addSubnode(self.alternativeTextNode)
|
||||||
self.buttonNode.addSubnode(self.iconNode)
|
self.buttonNode.addSubnode(self.iconNode)
|
||||||
|
self.buttonNode.addSubnode(self.unreadIconNode)
|
||||||
self.buttonNode.addSubnode(self.arrowNode)
|
self.buttonNode.addSubnode(self.arrowNode)
|
||||||
self.buttonNode.addSubnode(self.avatarsNode)
|
self.buttonNode.addSubnode(self.avatarsNode)
|
||||||
self.addSubnode(self.buttonNode)
|
self.addSubnode(self.buttonNode)
|
||||||
@ -54,8 +69,10 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let nodes: [ASDisplayNode] = [
|
let nodes: [ASDisplayNode] = [
|
||||||
strongSelf.textNode,
|
strongSelf.textNode,
|
||||||
|
strongSelf.alternativeTextNode,
|
||||||
strongSelf.iconNode,
|
strongSelf.iconNode,
|
||||||
strongSelf.avatarsNode,
|
strongSelf.avatarsNode,
|
||||||
|
strongSelf.unreadIconNode,
|
||||||
strongSelf.arrowNode,
|
strongSelf.arrowNode,
|
||||||
]
|
]
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
@ -89,12 +106,13 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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))) {
|
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 textLayout = TextNode.asyncLayout(self.textNode)
|
||||||
|
let alternativeTextLayout = TextNode.asyncLayout(self.alternativeTextNode)
|
||||||
|
|
||||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
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 dateReplies = 0
|
||||||
var replyPeers: [Peer] = []
|
var replyPeers: [Peer] = []
|
||||||
|
var hasUnseenReplies = false
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? ReplyThreadMessageAttribute {
|
if let attribute = attribute as? ReplyThreadMessageAttribute {
|
||||||
dateReplies = Int(attribute.count)
|
dateReplies = Int(attribute.count)
|
||||||
replyPeers = attribute.latestUsers.compactMap { peerId -> Peer? in
|
replyPeers = attribute.latestUsers.compactMap { peerId -> Peer? in
|
||||||
return item.message.peers[peerId]
|
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 rawText: String
|
||||||
|
let rawAlternativeText: String
|
||||||
|
|
||||||
if item.message.id.peerId.isReplies {
|
if item.message.id.peerId.isReplies {
|
||||||
rawText = "View Reply"
|
rawText = item.presentationData.strings.Conversation_ViewReply
|
||||||
|
rawAlternativeText = rawText
|
||||||
} else if dateReplies > 0 {
|
} else if dateReplies > 0 {
|
||||||
if dateReplies == 1 {
|
rawText = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
|
||||||
rawText = "1 Comment"
|
rawAlternativeText = rawText
|
||||||
} else {
|
|
||||||
rawText = "\(dateReplies) Comments"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
rawText = "Leave a Comment"
|
rawText = item.presentationData.strings.Conversation_MessageLeaveComment
|
||||||
|
rawAlternativeText = item.presentationData.strings.Conversation_MessageLeaveCommentShort
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageSize: CGFloat = 30.0
|
let imageSize: CGFloat = 30.0
|
||||||
@ -151,20 +174,21 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
} else {
|
} else {
|
||||||
textLeftInset = 15.0 + imageSize * min(1.0, CGFloat(replyPeers.count)) + (imageSpacing) * max(0.0, min(2.0, CGFloat(replyPeers.count - 1)))
|
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 textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset - textLeftInset - textRightInset), height: constrainedSize.height)
|
||||||
|
|
||||||
let attributedText: NSAttributedString
|
|
||||||
|
|
||||||
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
|
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
|
||||||
|
|
||||||
let textFont = item.presentationData.messageFont
|
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 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 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))
|
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
|
var suggestedBoundingWidth: CGFloat
|
||||||
suggestedBoundingWidth = textFrameWithoutInsets.width
|
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 iconImage: UIImage?
|
||||||
let iconOffset: CGPoint
|
let iconOffset: CGPoint
|
||||||
@ -186,6 +210,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
iconOffset = CGPoint(x: 0.0, y: -1.0)
|
iconOffset = CGPoint(x: 0.0, y: -1.0)
|
||||||
}
|
}
|
||||||
let arrowImage = PresentationResourcesChat.chatMessageCommentsArrowIcon(item.presentationData.theme.theme, incoming: incoming)
|
let arrowImage = PresentationResourcesChat.chatMessageCommentsArrowIcon(item.presentationData.theme.theme, incoming: incoming)
|
||||||
|
let unreadIconImage = PresentationResourcesChat.chatMessageCommentsUnreadDotIcon(item.presentationData.theme.theme, incoming: incoming)
|
||||||
|
|
||||||
return (suggestedBoundingWidth, { boundingWidth in
|
return (suggestedBoundingWidth, { boundingWidth in
|
||||||
var boundingSize: CGSize
|
var boundingSize: CGSize
|
||||||
@ -198,33 +223,26 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.item = item
|
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.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 _ = textApply()
|
||||||
|
let _ = alternativeTextApply()
|
||||||
|
|
||||||
let adjustedTextFrame = textFrame
|
let adjustedTextFrame = textFrame
|
||||||
|
|
||||||
strongSelf.textNode.frame = adjustedTextFrame
|
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 {
|
if let iconImage = iconImage {
|
||||||
strongSelf.iconNode.image = iconImage
|
strongSelf.iconNode.image = iconImage
|
||||||
@ -233,8 +251,16 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
if let arrowImage = arrowImage {
|
if let arrowImage = arrowImage {
|
||||||
strongSelf.arrowNode.image = 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
|
strongSelf.iconNode.isHidden = !replyPeers.isEmpty
|
||||||
|
|
||||||
|
|||||||
@ -182,7 +182,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
|||||||
switch item.chatLocation {
|
switch item.chatLocation {
|
||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
messagePeerId = peerId
|
messagePeerId = peerId
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
messagePeerId = messageId.peerId
|
messagePeerId = messageId.peerId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,6 +194,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
|||||||
isBroadcastChannel = true
|
isBroadcastChannel = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if case let .replyThread(messageId, isChannelPost, _, _) = item.chatLocation, isChannelPost, messageId == item.message.id {
|
||||||
|
isBroadcastChannel = true
|
||||||
|
}
|
||||||
|
|
||||||
if !isBroadcastChannel {
|
if !isBroadcastChannel {
|
||||||
hasAvatar = true
|
hasAvatar = true
|
||||||
}
|
}
|
||||||
@ -337,7 +341,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
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 {
|
} else {
|
||||||
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
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)
|
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
|
||||||
} else if let id = forwardInfo.source?.id ?? forwardInfo.author?.id {
|
} else if let peer = forwardInfo.source ?? forwardInfo.author {
|
||||||
item.controllerInteraction.openPeer(id, .info, nil)
|
item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil)
|
||||||
} else if let _ = forwardInfo.authorSignature {
|
} else if let _ = forwardInfo.authorSignature {
|
||||||
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil)
|
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 {
|
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? ReplyThreadMessageAttribute {
|
if let _ = attribute as? ReplyThreadMessageAttribute {
|
||||||
item.controllerInteraction.openMessageReplies(item.message.id)
|
item.controllerInteraction.openMessageReplies(item.message.id, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -280,7 +280,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
|
|||||||
switch chatLocation {
|
switch chatLocation {
|
||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
messagePeerId = peerId
|
messagePeerId = peerId
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
messagePeerId = messageId.peerId
|
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 {
|
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
||||||
isBroadcastChannel = true
|
isBroadcastChannel = true
|
||||||
}
|
}
|
||||||
|
} else if case let .replyThread(messageId, isChannelPost, _, _) = chatLocation, isChannelPost, messageId == message.id {
|
||||||
|
isBroadcastChannel = true
|
||||||
}
|
}
|
||||||
if !hasActionMedia && !isBroadcastChannel {
|
if !hasActionMedia && !isBroadcastChannel {
|
||||||
if let effectiveAuthor = effectiveAuthor {
|
if let effectiveAuthor = effectiveAuthor {
|
||||||
|
|||||||
@ -249,7 +249,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
} else if incoming {
|
} else if incoming {
|
||||||
hasAvatar = true
|
hasAvatar = true
|
||||||
}
|
}
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, isChannelPost, _, _):
|
||||||
if messageId.peerId != item.context.account.peerId {
|
if messageId.peerId != item.context.account.peerId {
|
||||||
if messageId.peerId.isGroupOrChannel && item.message.author != nil {
|
if messageId.peerId.isGroupOrChannel && item.message.author != nil {
|
||||||
var isBroadcastChannel = false
|
var isBroadcastChannel = false
|
||||||
@ -257,6 +257,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
isBroadcastChannel = true
|
isBroadcastChannel = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isChannelPost, messageId == item.message.id {
|
||||||
|
isBroadcastChannel = true
|
||||||
|
}
|
||||||
|
|
||||||
if !isBroadcastChannel {
|
if !isBroadcastChannel {
|
||||||
hasAvatar = true
|
hasAvatar = true
|
||||||
}
|
}
|
||||||
@ -411,7 +415,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
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 {
|
} else {
|
||||||
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
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 {
|
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? ReplyThreadMessageAttribute {
|
if let _ = attribute as? ReplyThreadMessageAttribute {
|
||||||
item.controllerInteraction.openMessageReplies(item.message.id)
|
item.controllerInteraction.openMessageReplies(item.message.id, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -451,7 +451,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _ in
|
}, openMessageReplies: { _, _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||||
@ -816,9 +816,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||||||
if let navigationController = strongSelf.getNavigationController() {
|
if let navigationController = strongSelf.getNavigationController() {
|
||||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(messageId)))
|
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() {
|
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):
|
case let .stickerPack(name):
|
||||||
let packReference: StickerPackReference = .name(name)
|
let packReference: StickerPackReference = .name(name)
|
||||||
|
|||||||
@ -60,25 +60,22 @@ class ChatReplyCountItem: ListViewItem {
|
|||||||
|
|
||||||
class ChatReplyCountItemNode: ListViewItemNode {
|
class ChatReplyCountItemNode: ListViewItemNode {
|
||||||
var item: ChatReplyCountItem?
|
var item: ChatReplyCountItem?
|
||||||
let backgroundNode: ASImageNode
|
|
||||||
let labelNode: TextNode
|
let labelNode: TextNode
|
||||||
|
let filledBackgroundNode: LinkHighlightingNode
|
||||||
|
|
||||||
private var theme: ChatPresentationThemeData?
|
private var theme: ChatPresentationThemeData?
|
||||||
|
|
||||||
private let layoutConstants = ChatMessageItemLayoutConstants.default
|
private let layoutConstants = ChatMessageItemLayoutConstants.default
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.backgroundNode = ASImageNode()
|
|
||||||
self.backgroundNode.isLayerBacked = true
|
|
||||||
self.backgroundNode.displayWithoutProcessing = true
|
|
||||||
|
|
||||||
self.labelNode = TextNode()
|
self.labelNode = TextNode()
|
||||||
self.labelNode.isUserInteractionEnabled = false
|
self.labelNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.filledBackgroundNode = LinkHighlightingNode(color: .clear)
|
||||||
|
|
||||||
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
|
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
|
||||||
|
|
||||||
self.addSubnode(self.backgroundNode)
|
self.addSubnode(self.filledBackgroundNode)
|
||||||
|
|
||||||
self.addSubnode(self.labelNode)
|
self.addSubnode(self.labelNode)
|
||||||
|
|
||||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
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) {
|
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 layoutConstants = self.layoutConstants
|
||||||
let currentTheme = self.theme
|
|
||||||
|
|
||||||
return { item, params, dateAtBottom in
|
return { item, params, dateAtBottom in
|
||||||
var updatedBackgroundImage: UIImage?
|
|
||||||
if currentTheme != item.presentationData.theme {
|
|
||||||
updatedBackgroundImage = PresentationResourcesChat.chatUnreadBarBackgroundImage(item.presentationData.theme.theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
//TODO:localize
|
if item.count == 0 {
|
||||||
if item.isComments {
|
text = item.presentationData.strings.Conversation_DiscussionNotStarted
|
||||||
if item.count == 0 {
|
|
||||||
text = "Comments"
|
|
||||||
} else if item.count == 1 {
|
|
||||||
text = "1 Comment"
|
|
||||||
} else {
|
|
||||||
text = "\(item.count) Comments"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if item.count == 0 {
|
text = item.presentationData.strings.Conversation_DiscussionStarted
|
||||||
text = "Replies"
|
|
||||||
} else if item.count == 1 {
|
|
||||||
text = "1 Reply"
|
|
||||||
} else {
|
|
||||||
text = "\(item.count) Replies"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if let strongSelf = self {
|
||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
strongSelf.theme = item.presentationData.theme
|
strongSelf.theme = item.presentationData.theme
|
||||||
|
|
||||||
if let updatedBackgroundImage = updatedBackgroundImage {
|
|
||||||
strongSelf.backgroundNode.image = updatedBackgroundImage
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = apply()
|
let _ = apply()
|
||||||
|
let _ = backgroundApply()
|
||||||
|
|
||||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize)
|
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 = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - size.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0)), size: size.size)
|
strongSelf.labelNode.frame = labelFrame
|
||||||
|
strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -792,12 +792,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
}
|
}
|
||||||
} else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
|
} else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
|
||||||
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
|
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
|
||||||
} else if case let .replyThread(_, isChannelPost, _) = interfaceState.chatLocation {
|
} else if case let .replyThread(_, isChannelPost, _, _) = interfaceState.chatLocation {
|
||||||
//TODO:localize
|
|
||||||
if isChannelPost {
|
if isChannelPost {
|
||||||
placeholder = "Comment"
|
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
|
||||||
} else {
|
} else {
|
||||||
placeholder = "Reply"
|
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
|
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
|
||||||
|
|||||||
@ -108,8 +108,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
switch titleContent {
|
switch titleContent {
|
||||||
case let .peer(peerView, _, isScheduledMessages):
|
case let .peer(peerView, _, isScheduledMessages):
|
||||||
if peerView.peerId.isReplies {
|
if peerView.peerId.isReplies {
|
||||||
//TODO:localize
|
let typeText: String = self.strings.DialogList_Replies
|
||||||
let typeText: String = "Replies"
|
|
||||||
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
} else if isScheduledMessages {
|
} else if isScheduledMessages {
|
||||||
@ -142,17 +141,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .replyThread(type, text):
|
case let .replyThread(type, text):
|
||||||
//TODO:localize
|
|
||||||
let typeText: String
|
let typeText: String
|
||||||
if !text.isEmpty {
|
if !text.isEmpty {
|
||||||
typeText = text
|
typeText = text
|
||||||
} else {
|
} else {
|
||||||
switch type {
|
typeText = " "
|
||||||
case .comments:
|
|
||||||
typeText = "Comments"
|
|
||||||
case .replies:
|
|
||||||
typeText = "Replies"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||||
|
|||||||
@ -144,7 +144,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _ in
|
}, openMessageReplies: { _, _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
|
|||||||
@ -67,7 +67,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
controller.purposefulAction = params.purposefulAction
|
||||||
if let search = params.activateMessageSearch {
|
if let search = params.activateMessageSearch {
|
||||||
@ -125,7 +125,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
|||||||
if message.id.peerId == peerId {
|
if message.id.peerId == peerId {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case let .replyThread(messageId, _, _):
|
case let .replyThread(messageId, _, _, _):
|
||||||
if message.id.peerId == messageId.peerId {
|
if message.id.peerId == messageId.peerId {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,9 +90,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
|||||||
navigationController?.pushViewController(controller)
|
navigationController?.pushViewController(controller)
|
||||||
case let .channelMessage(peerId, messageId):
|
case let .channelMessage(peerId, messageId):
|
||||||
openPeer(peerId, .chat(textInputState: nil, subject: .message(messageId), peekData: nil))
|
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 {
|
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):
|
case let .stickerPack(name):
|
||||||
dismissInput()
|
dismissInput()
|
||||||
|
|||||||
@ -133,7 +133,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _ in
|
}, openMessageReplies: { _, _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
|
||||||
|
|||||||
@ -933,14 +933,18 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||||||
} else if let channel = peer as? TelegramChannel {
|
} else if let channel = peer as? TelegramChannel {
|
||||||
var displayLeave = !channel.flags.contains(.isCreator)
|
var displayLeave = !channel.flags.contains(.isCreator)
|
||||||
var canViewStats = false
|
var canViewStats = false
|
||||||
|
var hasDiscussion = false
|
||||||
if let cachedChannelData = cachedData as? CachedChannelData {
|
if let cachedChannelData = cachedData as? CachedChannelData {
|
||||||
canViewStats = cachedChannelData.flags.contains(.canViewStats)
|
canViewStats = cachedChannelData.flags.contains(.canViewStats)
|
||||||
}
|
}
|
||||||
switch channel.info {
|
switch channel.info {
|
||||||
case .broadcast:
|
case let .broadcast(info):
|
||||||
if !channel.flags.contains(.isCreator) {
|
if !channel.flags.contains(.isCreator) {
|
||||||
displayLeave = true
|
displayLeave = true
|
||||||
}
|
}
|
||||||
|
if info.flags.contains(.hasDiscussionGroup) {
|
||||||
|
hasDiscussion = true
|
||||||
|
}
|
||||||
case .group:
|
case .group:
|
||||||
displayLeave = false
|
displayLeave = false
|
||||||
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
|
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
|
||||||
@ -957,6 +961,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||||||
displayLeave = false
|
displayLeave = false
|
||||||
}
|
}
|
||||||
result.append(.mute)
|
result.append(.mute)
|
||||||
|
if hasDiscussion {
|
||||||
|
result.append(.discussion)
|
||||||
|
}
|
||||||
result.append(.search)
|
result.append(.search)
|
||||||
if displayLeave {
|
if displayLeave {
|
||||||
result.append(.leave)
|
result.append(.leave)
|
||||||
|
|||||||
@ -1956,7 +1956,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _ in
|
}, openMessageReplies: { _, _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
|
|||||||
@ -1194,7 +1194,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _ in
|
}, openMessageReplies: { _, _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
|
|||||||
@ -59,9 +59,9 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate
|
|||||||
if let navigationController = controller.navigationController as? NavigationController {
|
if let navigationController = controller.navigationController as? NavigationController {
|
||||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(messageId)))
|
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 {
|
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):
|
case let .stickerPack(name):
|
||||||
let packReference: StickerPackReference = .name(name)
|
let packReference: StickerPackReference = .name(name)
|
||||||
|
|||||||
@ -305,7 +305,7 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig
|
|||||||
guard let result = result else {
|
guard let result = result else {
|
||||||
return .channelMessage(peerId: peer.id, messageId: replyThreadMessageId)
|
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 {
|
} else {
|
||||||
|
|||||||
2
third-party/webrtc/BUILD
vendored
2
third-party/webrtc/BUILD
vendored
@ -1,4 +1,4 @@
|
|||||||
use_gn_build = True
|
use_gn_build = False
|
||||||
|
|
||||||
webrtc_libs = [
|
webrtc_libs = [
|
||||||
"libwebrtc.a",
|
"libwebrtc.a",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user